스프링/미션

[Spring] lotto - API 서버 구현(1)

문상휘파람 2024. 10. 5. 14:21

이번에는 API 서버 구현입니다! 

 

중간에 exception 설명하느라 길어졌네요 ㅋㅋ


 

바로 시작합니다! 

 

1. 로또 사용자 등록

API

 

*MemberController

@PostMapping("/members")
public ResponseEntity<Void> createUser(@RequestBody CreateRequest createRequest) {
    CreateResponse createResponse = MemberMapper.toCreatedResponse(memberService.createMember(createRequest));
    URI location = URI.create("/api/members/" + createResponse.id());
    return ResponseEntity.created(location).build();
}

 

저번 racing 에서는 Mapper 를 사용하지 않았는데, 이번에는 사용해 보았습니다! 

확실히 사용하니까 직접 값을 넣음으로써 발생하는 휴먼 에러가 많이 줄더라고요.

다른 부분에서는 모르겠지만, 가독성이나 사용자가 발생시키는 에러 부분에서는 쓸만 하다고 생각했습니다!

 

 

*CreateRequest

public record CreateRequest(String name, int money) {
}

 

사용자 생성 값을 전달받는 dto는 record 클래스로 구현하였습니다.

 

*CreateResponse

public record CreateResponse(Long id, String name, int money) {
}

 

response 역시 마찬가지입니다.

 

 

*MemberEntity

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String name;
@Column
private int money;

public Member(String name, int money) {
    this.name = name;
    this.money = money;
}

protected Member() {
}

 

MemberEntity 입니다! 

 

 

*MemberService

public Member createMember(CreateRequest createRequest) {
    Member member = MemberMapper.toMember(createRequest);
    memberRepository.save(member);
    return member;
}

 

유저를 생성하는 서비스 코드입니다. 여기서도 역시 Mapper 를 사용하였습니다. 코드 가독성 굿굿...

 

 

*결과

결과

postman 결과입니다! 

 

컨트롤러에서 설정했던 urI 도 헤더에 잘 담기고, 유저도 잘 생성된 것을 보실 수 있습니다.

 


 

2. 로또 n장 구매

API

 

*LottoController

@PostMapping("/lottos")
public ResponseEntity<Void> buyLottos(@RequestBody LottoRequest lottoRequest) {
    lottoService.buyLotto(lottoRequest);
    return ResponseEntity.status(HttpStatus.CREATED).build();
}

 

로또를 구매하는 로직이기에 Lotto 패키지를 하나 파서 진행하였습니다! 

 

*LottoService

public void buyLotto(LottoRequest lottoRequest) {
    LottoAnswer lottoAnswer = getLottoAnswer();
    memberService.buyLotto(lottoRequest.userId(), lottoRequest.count());
    saveLottos(lottoRequest, lottoAnswer.getLottoAnswer());
}

 

로또를 구매할 때, 당첨 번호도 같이 생성해주도록 구현하였습니다.

로또를 구매하면, 유저의 금액도 차감해주어야 하기에 Memberservice를 참조하였습니다.(이 경우 문제가 많더라고요 따로 다루겠습니다.)

 

private LottoAnswer getLottoAnswer() {
    return lottoAnswerRepository.findFirstByOrderById().orElseGet(this::createLottoAnswer);
}

private LottoAnswer createLottoAnswer() {
    Lotto lotto = new Lotto(createRandomNumber);
    LottoAnswer lottoAnswer = new LottoAnswer(lotto.getLotto().toString());
    lottoAnswerRepository.save(lottoAnswer);
    return lottoAnswer;
}

 

당첨 번호는 한 번만 생성해야 하기 때문에, 이미 당첨번호가 있으면 가져오고 없으면 당첨번호를 생성해서 db에 저장해주도록 했습니다!

getLottoAnswer() 메서드에서 한 줄에 점 하나만 찍는, 객체지향 생활체조 규칙을 지키지 않았네요... 왜 그랬지... 아쉽습니다.

 

private void saveLottos(LottoRequest lottoRequest, String lottoAnswer) {
    Member member = memberRepository.findById(lottoRequest.userId())
            .orElseThrow(NotFoundMemberException::new);
    LottoNumberParser parsedLottoAnswer = new LottoNumberParser(lottoAnswer);
    for (int i = 0; i < lottoRequest.count(); i++) {
        Lotto lotto = new Lotto(createRandomNumber);
        int count = getLottoRank(lotto.getLotto(), parsedLottoAnswer.getLottoNumber());
        LottoEntity lottoEntity = LottoMapper.toLottoEntity(member, lotto.getLotto().toString(), count);
        lottoRepository.save(lottoEntity);
        memberService.saveWinning(member, count);
    }
}

private int getLottoRank(List<Integer> lotto, List<Integer> lottoAnswer) {
    LottoRank lottoRank = new LottoRank(lotto, lottoAnswer);
    return lottoRank.getCount();
}

 

로또를 저장하는 로직입니다. saveLottos() 메서드가 이렇게 보니 참 아쉬운 것 같습니다. 캡슐화도 덜 되있고, 책임이 다른 부분이 많아서 여러 메서드로 쪼개서 책임을 분할하여도 될 것 같습니다.

 

그래도 주요 로직을 설명드리자면,

1. 당첨 번호를 가져와 LottoNumberParser 도메인에서 리스트 형태로 반환해줍니다.

2. 구입한 로또 개수만큼 로또를 생성합니다.

3. 로또 생성과 동시에 getLottoRank() 메서드를 통해, 맞은 개수를 도출합니다.

4. 참조한 MemberService의 saveWinning 메서드를 이용해서, 유저 테이블에 당첨금을 저장합니다.

 

 

*LottoAnswer

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column
private String lottoAnswer;

 

당첨 번호 엔티티입니다. 당첨 번호만 필드로 두었습니다.

 

 

*LottoEntity

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "memberId")
private Member member;
@Column
private String lottoNumber;
@Column
private boolean win;

 

로또 엔티티 입니다.

누가 구매한 로또인지 알기 위해 Member를 직접 참조하였고, 로또 번호와 당첨 여부를 필드로 두었습니다.

 

 

*MemberService

public void buyLotto(Long id, int count) {
    Member member = memberRepository.findById(id)
            .orElseThrow(NotFoundMemberException::new);
    member.updateMoney(count);
    MemberLotto memberLotto = new MemberLotto(member);
    memberLottoRepository.save(memberLotto);
    memberLotto.updateLottoCount(count);
}

 

로또를 구매하였을 때, 유저의 금액을 차감해주는 로직입니다.

 

 

*Member

public void updateMoney(int count) {
    validateLottoMoney(count);
    this.money -= count * LOTTO_TICKET_PRICE;
}

private void validateLottoMoney(int count) {
    if (money < count * LOTTO_TICKET_PRICE) {
        throw new CustomException(CustomErrorCode.MONEY_NOT_FOUNT_EXCEPTION);
    }
}

 

위에 보이는 MemberService에서 금액을 차감하는 로직을 엔티티에 구현하였습니다. 

이로써 저는 엔티티 + 도메인 형식으로 Member를 구현하였습니다.

 

유저가 가지고 있는 금액보다 로또 구매 금액이 클 경우 예외처리를 하도록 하였습니다.

 

 

*결과

결과

 

Lotto 구매 로직이 제대로 작동하는 것을 보실 수 있습니다!


 

오늘 포스팅에서는 API 1번(사용자 등록)과 2번(로또 구매) 구현 과정을 정리해보았습니다! 

 

다음 포스팅에서는 3,4,5번을 함께 진행해보겠습니다!

 

코드가 어색하거나 다른 방식이 더 좋은 경우가 있다면, 댓글로 알려주시면 감사하겠습니다 :)