본문 바로가기
Memo/우테코 4기

[우테코] 블랙잭 미션 2단계 학습 로그

by 연로그 2022. 3. 20.
반응형

목차

1. GitHub 저장소

2. 새로운 요구사항

3. 리팩터링

4. 생각하기

5. 셀프 회고


 

 


1. GitHub 저장소

GitHub: https://github.com/yeon-06/java-blackjack/tree/step2

Pull Request: https://github.com/woowacourse/java-blackjack/pull/321

1단계 학습 로그: https://yeonyeon.tistory.com/207

 


2. 새로운 요구사항

img: pixabay

 

  • 배팅 금액 입력 받기
    • 1000원 단위로 입력 가능
    • 음수 입력 불가
    • (딜러를 제외한) 플레이어는 배팅 금액을 필수로 입력받아야 함
  • 수입 금액
    • 플레이어 버스트: 배팅 * 0
    • 플레이어 블랙잭: 배팅 * 1.5
    • 딜러 + 플레이어 블랙잭: 배팅 금액 돌려받기
    • 딜러 버스트: 배팅 금액 돌려받기
    • 그 외 플레이어 승리: 배팅 * 1
  • 결과 출력
    • "참가자 이름: 수입 금액" 형식으로 출력

 


3. 리팩터링

 

3-1. 참가자에게 배팅 금액 설정하기

  • AS-IS
     단순하게 생성자에 List<BettingAmount>를 추가했다. List가 여러개 들어오자 로직상 항상 같은 개수가 들어옴에도 불구하고 size()를 비교하는 등등 검증이나 객체 변환에 대한 로직들이 필요했다.
// AS-IS - BlackjackGame.java
public BlackjackGame(List<Name> names, List<BettingAmount> bettingAmounts, DeckGenerator deckGenerator) {
    cardDistributor = new CardDistributor(deckGenerator);
    this.participants = new Participants(names, bettingAmounts);
}

// AS-IS - Participants.java
public Participants(List<Name> names, List<BettingAmount> bettingAmounts) {
    this.dealer = new Dealer();
    this.players = initPlayers(new ArrayList<>(names), new ArrayList<>(bettingAmounts));
    validatePlayers(players);
}

private List<Player> initPlayers(List<Name> names, List<BettingAmount> bettingAmounts) {
    // 복잡한 로직!!
}

 

  • TO-DO
     List 2개 대신 Map으로 가져왔다. 불필요한 로직들이 사라졌다.
// TO-DO - BlackjackGame.java
public BlackjackGame(Map<Name, BettingAmount> participantInfos, DeckGenerator deckGenerator) {
    cardDistributor = new CardDistributor(deckGenerator);
    this.participants = new Participants(participantInfos);
}

// TO-DO - Participants.java
public Participants(Map<Name, BettingAmount> participantInfos) {
    this.dealer = new Dealer();
    this.players = initPlayers(participantInfos);
    validatePlayers(players);
}

 

 

 

3-2. 메서드 이름 다시 짓기

결과를 도출하다는 이름을 지어야했는데 어떤 이름을 지을까 고민이 됐다.

도와줘요 구글!

 

구글은 날 도와주지 않았다 ㅡㅡ

 

getter도 아니고 안에 많은 로직을 거치는데 메서드 이름에 get을 붙이기 꺼려졌다.

도출하다를 검색해서 elicit이라는 이름을 붙였는데 대체 이게 뭔 단어인가.. 싶어서 리뷰어님께 도움을 구했다.

 

갓로운

 

3-3. MatchResult Enum 생성

승패 결과에 따른 이익 금액을 계산하기 위해 MatchResult를 생성.

본래는 GameResult에 해당 로직이 존재했으나 승패에 따라 1.5, 0, 1 이런 상수를 곱해야하는데 이를 저장하고 활용하기에는 enum이 더 편하다고 생각했다.

 

 


4. 생각하기

 

4-1. 테스트 코드는 메서드 사용법의 레퍼런스가 된다.

 다양한 테스트 케이스를 테스트하기 위해 @ParameterizedTest와 @MethodSource를 이용하고 있다.

원래는 Player 목록과 Dealer를 통해 Participants를 만들 수 있었는데 3-1에서 생성자에 받는 타입이 변경되면서 테스트 코드를 갈아엎어야 하는 상황이 왔다.

 

처음에는 세팅할 수 있게끔 테스트 코드 내부에 테스트용 클래스를 생성했다.

로직은 안봐도 된다. 대충 슥 보기만 해도 복잡하지 않은가? 😂

public static class ParticipantsTest {
    private final CardDistributor cardDistributor;
    private final Participants participants;

    public ParticipantsTest(Participants participants, int playerCardCount, int dealerCardCount, List<Card> cards) {
        this.cardDistributor = new CardDistributor(new TestGenerator(cards));
        this.participants = participants;
        init(dealerCardCount, playerCardCount);
    }

    public void init(int dealerCardCount, int playerCardCount) {
        // 복잡... 아무튼 매우 복잡한 로직!!111
    }

    public Participants getParticipants() {
        return participants;
    }
}

 

테스트를 위해 클래스를 생성하고 개발자는 이걸 사용하기 위해 매번 로직을 분석해야 했다.

너무너무너무 복잡하고 번거롭고 심지어 내 의도대로 돌아가지도 않았다.

 

이 부분에서 많은 고민을 하고 있었는데 2기 크루셨던 라면과 대화하며 인사이트를 얻었다. (감사해요 라면😊👍)

테스트 코드는 레퍼런스의 기능을 할 수도 있다, 이 말을 듣고 테스트 목적에 대해 생각해보기 시작했다.

 

현재 문제는 Player의 Cards를 마음대로 세팅하는게 불가능한거였다.

그래서 그냥 Cards를 파라미터로 받아온 후 Player에 해당 Cards를 넣었다.

테스트용 클래스를 생성할 필요가 없으며 로직도 이해가 더 잘된다.

@ParameterizedTest(name = "{0}")
@MethodSource("provideParameters")
@DisplayName("수익 구하기")
void bettingAmount(String comment, Cards playerCards, Cards dealerCards, long expect) {
    // given
    Player player = participants.getPlayers().get(0);
    drawCards(player, playerCards);

    Dealer dealer = participants.getDealer();
    drawCards(dealer, dealerCards);

    GameResult gameResult = new GameResult(participants);

    // then
    assertThat(gameResult.getBettingResult(player)).isEqualTo(expect);
}

 

아래는 라면이 공유해주신 글ㅎㅎ

문서로서의 역할이 가능하다.
-> 테스트 코드는 개발자가 작성한 메소드가 어떻게 동작했으면, 어떤 결과를 반환했으면, 하는 것을 작성한 것이기 때문에 처음 코드를 보는 개발자들이 테스트 코드를 통해 코드의 동작을 조금더 수월하게 이해할 수 있다.

참고: https://galid1.tistory.com/783

 

4-2. 역할과 책임에 대해 생각해보기

블랙잭에서 ACE의 값은 1이 될 때도 있고 11이 될 때도 있다.

이 규칙에 대한 책임은 GameResult에게 있는가 Cards에게 있는가?에 대한 생각이 계속 들었다.

 

처음에는 해당 규칙은 블랙잭이라는 게임에 대한 규칙이니까 GameResult에게 책임을 지웠는데,

해당 프로젝트는 오직 블랙잭만 구현하고 카드를 사용하는 곳은 오직 블랙잭에서만 사용한다.

(=원카드 같은 다른 게임의 규칙을 미리 고려할 필요가 없다)

 

미리 다른 게임에서도 사용할걸 예상하고 코딩하는건 오버 프로그래밍이라고 생각했다.

Cards에게 책임을 지우도록 했다.

 

요 부분에 대해서 이야기하다가 로운의... 너무나도 맞는 말을 가져와봤다😂

로운: 제가 다는 코멘트는 한가지의 의견일 뿐입니다! 정답이 아니기 때문에 보고 맞다고 생각하는 부분은 반영하고 아닌 것 같다라고 느껴지는 부분은 지금처럼 자신의 생각을 얘기하고 의견을 물어보는 방식이 연로그만의 기준을 잡는 것에 도움이 될거라고 생각해요ㅎㅎ

 

4-3. instanceof() 사용하지 않기

👉 https://tecoble.techcourse.co.kr/post/2021-04-26-instanceof/

 

instanceof의 사용을 지양하자

instanceof란? 우리는 종종 그림과 같이 부모를 상속해서 만들어진 자식 객체가 여러 타입인 경우에 특정 클래스가 맞는지 확인하기 위해 아래 코드와 같이 라는 메서드를 사용하곤 한다. 코드는 pi

tecoble.techcourse.co.kr

 

4-4. View와 엮이는 Domain

ConsoleGame에서는 Participants에서 List<Player>를 꺼내 Player 한명한명 카드를 분배해주는 메서드가 있다.

원래는 List를 꺼낼 필요 없이 Participants 안에서 로직을 처리해주는 것이 좋지만 조건이 걸렸다.

 

카드 분배 기준

  • player의 카드가 21 미만
  • 사용자에게 입력 받은 값이 y

 

두번째 조건 때문에 View와 엮일 수 밖에 없었다.

도메인과 뷰를 철저하게 분리하기 위해 List를 꺼낸 후 하나하나를 순회하는 방식을 택했는데 로운이 아래와 같은 의견을 주셨다.

 

🙍‍♂️: participants에 물어볼 수는 없을까요??  inputView와 엮여 있기때문에 힘든 부분이 있는데요.
한가지 방법으로 Consumer를 사용하는 것이 있습니다. 참고만 하시면 될 거 같아요~

 

🙋‍♀️: Consumer에 대해 처음 들어봐서 간단하게 살펴봤는데 `함수를 파라미터로 넘겨서 해당 함수를 실행시키는 방식`이라고 이해되네요! 이런 식으로 함수를 파라미터로 넘기는 경우 아래와 같이 느껴지는데 로운은 어떻게 생각하시나요?

  • participants를 호출하는 클래스에 너무 의존적인 코드가 되지는 않은가?
  • 함수 내부에 View의 로직이 존재하니까 도메인이 View와 완전히 분리되지 않는다

 

🙍‍♂️: view와 완전히 분리되지 않는 느낌때문에 이 부분에 대해서도 개발자들마다 의견이 다른데요ㅎㅎ 저는 view와의 의존성은 ConsoleGame에서 가지고 있고, 행동을 ConsoleGame에 위임했다고 생각을 해서 한가지 방법으로 말씀드렸던 거였습니다. enum에 Function이나 Predicate등을 정의해서 행동을 위임하는 것처럼 생각을 했던거 같아요.

 

도메인과 view가 분리되지 않다고 느껴지고 메서드를 호출하는 쪽에 너무 의존적이다라는 느낌이 크다면 기존 로직대로 남겨놓는 것도 좋다고 생각합니다~ 정답은 없으니까요 😄

 

 


5. 셀프 회고

 여태까지 생성자는 한번 생성 후에 잘 변경되지 않을거라고 생각을 했다. 근데 요구사항에 따라 통째로 바뀌기도 하고... 아무튼 요번 미션을 진행하며 생성자 변경이 많았다. 처음에는 기존 테스트 코드 돌아가게 한답시고 기존 생성자를 냅뒀다가 테스트 코드에서만 사용하는 생성자가 생겨나서 곤란을 겪었다. 다음 프로젝트부터는 안쓰는 생성자/메소드 절대 바로바로 삭제해ㅠ0ㅠ

 

 예전에 일 했을 때 안쓰는거여도 만약을 위해 삭제 안해버릇 하다보니 리팩터링하는게 좀 두려운가보다. 이전 코드에 영향을 줄까봐... 영향 가는지 안가는지 체크를 위해 테스트 코드가 있는거다!!!1 앞으로는 좀 과감히 삭제해보자. 적어도 우테코에서 공부할 동안은 리팩터링을 두려워하지 말자. (몰랐는데 IntelliJ가 show History도 제공해주더라 겁먹지말긔)

 

 지난 금요일에 상태 패턴에 대해서 강의를 들어서 적용해보고 싶었으나.. 마음처럼 되지는 않았다. 왜냐... 이해를 잘 못했다. 사칙연산 하고 있었는데 정신 차리니까 미적분 벡터하고 있고... 마감 기한이 촉박해서 상태 패턴을 이해하고 적용하고 리팩터링까지 하는건 힘들 것 같아서 일단 현재 코드를 최대한 살렸다. 1레벨 끝나고 다시 강의 보면서 상태 패턴에 공부해보고 적용까지 해봐야겠다😅 일단 머지 됐으니까 체스부터 생각하기.

 

+

리팩토링 -> 리팩터링 반영 완료 ^ ^

반응형