Skip to content

[🚀 사이클1 - 미션 (블랙잭 게임 실행)] 이안 미션 제출합니다#1014

Merged
Chocochip101 merged 48 commits intowoowacourse:eian1106from
Eian1106:step1
Mar 11, 2026
Merged

[🚀 사이클1 - 미션 (블랙잭 게임 실행)] 이안 미션 제출합니다#1014
Chocochip101 merged 48 commits intowoowacourse:eian1106from
Eian1106:step1

Conversation

@Eian1106
Copy link

@Eian1106 Eian1106 commented Mar 8, 2026

안녕하세요 초코칩
페어랑 얘기를 나눌 부분이 많기도 했고, 많이 헤매기도 해서 생각보다 첫 PR이 늦어지게 되었습니다.
많이 배우겠습니다. 잘 부탁드립니다!

체크 리스트

  • 미션의 필수 요구사항을 모두 구현했나요?
  • Gradle test를 실행했을 때, 모든 테스트가 정상적으로 통과했나요?
  • 애플리케이션이 정상적으로 실행되나요?

어떤 부분에 집중하여 리뷰해야 할까요?

페어와 개발 경험이 많이 부족한 저는 개발 철학, 객체지향의 개념이 많이 모호했고, 구현에 바쁘기도 했습니다. AI를 사용하지 않기도 집중했습니다. 너무 늦을까 급해지다보니 코드가 많이 난잡하기도 합니다😢.
페어와 함께 많은 고민을 하다보니 질문이 많이 나왔습니다😅 잘 부탁드립니다..!

1. 데이터 단순 조회의 getter와 잘못된 설계에 사용된 getter의 차이

  • 객체 지향에서 getter와 setter를 지양해야한다고 알고 있었습니다. 그런데 데이터 조회를 할 때는 필연적으로 getter가 필요한데 어떻게 그게 가능한가 고민을 좀 하다가, 단순 데이터를 조회하는 건 괜찮지만 데이터를 꺼내 활용하는 로직이 메소드 바깥에 떨어져 있으면 안 된다는 것을 의미함을 알게 되었습니다.
    그래서 도메인 데이터 처리 관련 로직은 최대한 가장 원초적인 도메인 안으로 밀어 넣었습니다. 이렇게 하니 호출하는 상위 객체에는 관련 로직이 없긴 하지만, 해당 메소드를 호출하기 위해 상위객체-서비스-컨트롤러단까지 동명(혹은 유사한 이름의) 메소드로 해당 메소드를 체인처럼 호출해야했는데(처리 로직은 도메인에만 존재), 이 방식이 맞는 걸까요?

2. 일급컬렉션의 사용

  • 이번 미션에서 일급 컬렉션을 처음 사용해보았습니다. 플레이어 그룹과 플레이어가 손에 쥔 카드 목록을 각각의 일급컬렉션으로 만들었습니다. 일급컬렉션의 사용 목적 중 하나에는 불변성이 있던데, 카드 목록은 계속해서 값이 추가되는데도 일급컬렉션으로 사용해도 괜찮은 걸까요? 현재는 값이 변동이 있지만, 카드 목록에 이름을 붙여 컬렉션으로 만드는 것이 가독성에 좋다고 생각하였고, 요구사항에 되도록 모든 원시값을 감싸고 일급 컬렉션을 활용하라고 했기에 일급 컬렉션으로 작성했습니다.

3. DTO

  • 현재는 돌아가는 구현에 집중하느라 DTO를 작성하지 못했습니다. 지금 코드에서도 ResultView에 대해서는 전반적으로 DTO를 사용해 리팩토링해야한다고 생각합니다. 그런데 name(String) 처럼 정말 단순한 데이터도 DTO로 감싸야 하는지, 과한 것은 아닌지, 그 선이 어디인지가 고민됩니다.

  • 처음에는 DTO 사용에 대해 무지했습니다. 이번 미션을 진행하면서 DTO에 대해서 배웠고 필요성을 직접 체화 했던 것 같습니다. 직접 깨달은 것은 아래와 같은데, 제대로 이해한 것이 맞는지 궁금합니다

    • ResultView에 객체를 직접 전달해주면 안되나?
      • 그러면 메소드를 통해 값을 변경할 수 있다. 그래서 객체를 직접 전달해주면 안된다.
    • 그러면 DTO를 꼭 써야하는건가? LIst과 같은 컬렉션을 전달해주면 안되나?
      • String의 값이 어떤 걸 의미하는 지 정확히 알기 어렵다.
      • DTO를 사용한다면 어떤 값이 필수적인지, 어떤 것을 의미하는지 알 수 있다.
    • DTO를 사용하도록 리팩토링을 진행해야 할 것 같습니다.

4. Player와 Dealer

  • Dealer 객체를 따로 생성하려다, 설계의 진도가 나가지 않아 일단 구현 우선으로 딜러를 플레이어의 일환으로 생각해 구현했습니다..
  • 겹치는 부분을 부모클래스로 만들지, 아니면 추상 클래스로 만들어 각각 구현할 지, 아니면 새로운 접근법으로 겹치는 로직을 새로운 객체로 빼서 해야할지 고민이었는데 이 부분을 추후 리팩토링에 어떻게 적용해야할 지 고민입니다.
    • 둘은 행위가 비슷할 뿐 본질적인 역할(플레이어는 게임 참여, 딜러는 게임 중앙 관리자에 가까운 느낌이라고 생각했습니다)이 다르다고 생각합니다. 그렇기에 부모클래스를 만들어 상속하기보다는 인터페이스나 기능 객체 생성을 생각하고 있는데, 리뷰어님께서는 어떻게 생각하시는 지 궁금합니다.

5. TDD

  • 초반 이후에는 구현에 쫓겨 거의 TDD를 활용하지 못했습니다. 저는 원래 TDD가 그래도 기본 도메인 구조와 메소드 이름, 메소드 인자 및 리턴값까지는 생각을 해서 스켈레톤 코드로 만들어 놓고 테스트 RED를 만든 다음 이후 로직을 짜 GREEN으로 만들고 리팩토링을 하는 거라고 생각했습니다. 그런데 다른 크루들과 이야기를 나누다보니 컴파일 에러도 RED의 일종이며, 실제로 엄격한 TDD에서는 정말 테스트 코드 작성 전에는 한 줄의 기능 코드도 안 쓰는 경우가 있다는 걸 알게 되었습니다. 그러나 저는 미리 생각해서 커다란 기능(요구사항에서 입출력값이 명확히 존재하는 가장 윗 단계의 기능)정도는 스켈레톤 코드로 작성해놓는 걸 선호하는데, 이러면 TDD에 본질에 어긋나는 건지도 궁금합니다.

  • 테스트 코드를 작성 → 기능 구현 → 통과 확인을 과정을 반복하는데, 책임을 다른 객체에게 주어야 한다는 것을 알았을 땐 테스트 코드의 변경이 필요했습니다.

    • TDD에서 기능 구현도중 테스트 코드의 변경은 괜찮은 행위일까요? 설계 부족일까요?

6. git commit의 역할

  • git commit에는 1) 버전 저장과 2) 내가 아닌 타인이 작업흐름/코드 흐름을 일목요연하게 이해할 수 있는지 도움을 주는 것의 두 가지 역할이 있다고 생각합니다. 코드를 어느정도 안정적으로 짤 수 있을 때는 버전 저장의 용도로 쓰더라도 후자가 자연스럽게 병행된다고 생각하는데, 아직 저는 그런 수준이 아니라고 단순 버전 저장 용도로만 커밋을 하면 노력을 해도 중간부터는 커밋 로그가 깔끔하지 않게 생성되는 것 같습니다. 그래서 지금 수준의 단계에서는 완전히 다 구현하고 커밋을 정리하는 게 나은지, 아니면 그래도 실제로 코드 짤 때 편하도록 바로바로 커밋하는 게 나은지 궁금합니다. 찾아보니 일단 커밋을 하고 나중에 커밋 단위를 정리하는 방법도 있다고 하던데, 실무에서는 어떤 방식으로 많이 하는지도 궁금합니다.

7. 리팩토링

  • 대규모 리팩토링이 발생할 것 같은데, 대규모 리팩토링에 슬기롭게 대처하는 리뷰어님만의 팁이 있으실까요?

페어: @choubung

choubung added 23 commits March 7, 2026 23:47
이름 리스트 공백 검증은 이름을 담을 vo에 존재해야하는 로직이라고 판단하여 InputParserTest에서 해당 테스트를 삭제함
플레이어 히트 로직 구현
버스트 판별 구현
카드 점수 계산 로직 구현
에이스 점수 결정 구현
플레이어의 승패 계산 구현
딜러의 결과 계산 구현
최종 결과 출력 구현
Copy link

@Chocochip101 Chocochip101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 이안:) 만나서 반갑습니다, 리뷰어 초코칩입니다 🍪 :)
블랙잭 미션을 잘 구현해주셨네요!👍
피드백이 많아져서 나눠서 리뷰해드리도록 하겠습니다.
코멘트로 남겼으니 확인해보시고 재요청주세요!


private void validatePlayerNum(List<Player> players){
if (players.size() > 5) {
throw new IllegalArgumentException();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 에러 메시지에서는 참여자 수가 어떤 범위를 벗어났는지 알기 어려운 것 같아요 😔

사용자가 입력한 참여자 수나 허용 범위를 함께 보여주면 사용자가 상황을 파악하기 더 쉽지 않을까요?

static Scanner sc = new Scanner(System.in); // TODO: close 어떻게...?

// 플레이어 이름 입력받기
public static List<String> askName() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static으로 선언한 이유가 무엇인가요? 🧐 우테코에서는 '3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다'라는 제약을 두었는데, 그 이유가 무엇일까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static으로 선언한 이유가 무엇인가요?

  • 객체마다 서로 다른 상태(데이터)를 가질 필요가 없는 유틸리티성 클래스라고 판단했습니다.
    따라서 인스턴스 생성 비용을 줄이고 어디서든 편리하게 접근할 수 있도록 static 메서드와 필드로 구성했습니다.

  • 그런데 어디서든 편리하게 접근할 수 있다면, Controller 외에서도 사용할 수 있기에 controller에서 view를 생성하여 사용하거나, Application에서 주입해주는 방식으로 수정해야 할 것 같습니다.

우테코에서는 '3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다'라는 제약을 두었는데, 그 이유가 무엇일까요?

  • 인스턴스 변수가 3개 이상이 되면 하나의 클래스가 많은 책임을 지닐 수 있게 될 것 같습니다.
    따라서 해당 제약을 통해 클래스를 분리하고, 책임을 분산할 수 있도록 유도하는 것이 아닐까 싶습니다. 🤔

this.gameService = gameService;
}

public void run() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드가 10줄 넘습니다. 어떻게 줄일 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드를 조금 분리해야 할 것 같습니다..


import java.util.List;

public class Player {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트가 필요해보입니다.

@Eian1106
Copy link
Author

Eian1106 commented Mar 10, 2026

안녕하세요 초코칩

자세한 리뷰 정말 감사합니다!😄
피드백 주신 부분에 대해 리팩토링을 진행해보았으나 아직 많이 부족하네요 😢

PlayerGroups 내부에서 index를 사용함으로써 발생되는 여러 문제들이 있었습니다.
사실 index 사용을 지양하고 싶었는데, 페어와 코드를 처음 작성하는 시점에는 우선 해보자가 되었습니다.
알면서도 진행했고, 그 결과 리팩토링에 조금 어려움을 겪었던 것 같습니다. 그래서 시간도 많이 소요된 것 같습니다.
이게 기술적 부채 인가요 ..? 😓

Domain과 Service의 차이와 기준
이 내용에 대해서는 사이클을 함께하고 있는 크루원들과 아직 까지도 열띤 토론을 하고 있습니다
결론은 아직 나지 않았습니다. 😅 정답이 없다는 의미 같습니다. 조금 더 고민을 해보도록 하겠습니다

리팩토링
(피드백 호흡이 길어지는 것 같아 눈에 보임에도 아직 수정하지 못하고 리뷰요청 드렸습니다 😢)

  • 아직 원시 타입을 사용하는 곳이 많은 것 같습니다. 이 점은 추후 수정이 필요해 보입니다
  • DealerResult, PlayerResult는 중복 같아 보입니다
    • 이처럼 객체를 활용하고 싶은데, 무언가 감이 잡히질 않습니다.
    • 네이밍, 전달하고자 하는 것, 이미 만들어진 것에서 필드가 하나 추가된 정보가 필요하다면? 🤔
      • 경험의 부족인지 지식의 부족인지 모르겠습니다

Java 기본기에 대해서도 아직 많이 부족함을 느낍니다.
(프리코스 1주차에 static 남발했던 저를 비교하면 성장했음을 느끼지만요 😅)

휘몰아치는 새로운 내용들에 체력적으로 정신적으로 힘이 들기도 하지만 열심히 나아가겠습니다!

다만 PR의 크기가 커지면 리뷰어가 변경 사항을 파악하기 어려워지기 때문에,
그때부터는 커밋 단위를 조금 더 신경 쓰려고 했던 것 같습니다.

죄송합니다.. 제 기준 대규모(?) 리팩토링을 진행하다 보니 유의미한 단위로 커밋하지 못한 것 같습니다. 다음번엔 신경쓰겠습니다!

Copy link

@Chocochip101 Chocochip101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빠르게 수정해주셨네요!
리팩터링 덕분에 코드의 가독성이 좋아졌습니다 👍🏻
조금 더 개선해보면 좋을것 같아 리뷰를 커맨트로 남겨두었으니
확인 후 다시 요청해주세요~

cards.add(new Card(suit, number));
}
}
Collections.shuffle(cards);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collection.shuffle이 로직에 존재해서 테스트하기가 어렵군요 😢 어떻게 구조를 바꿔볼 수 있을까요?

Comment on lines 35 to 36
List<PlayerResult> playersEndStatus = gameService.getPlayersStatus();
Map<String, Integer> playersTotalScore = gameService.getPlayersTotalScore();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DealerResult, PlayerResult는 중복 같아 보입니다. 이처럼 객체를 활용하고 싶은데, 무언가 감이 잡히질 않습니다.

PlayerResult와 DealerResult의 차이는 점수 필드 차이인데, 여기서는 totalScore 따로 주고 있네요. PlayerResult를 삭제하고 DealerResult를 GameResult(?)로 바꿔서 중복을 줄일 수 있지 않을까요?

    private void printFinalStatus() {
        GameResult dealerResult = gameService.getDealerResult();
        ResultView.printCardSumResult(dealerResult);

        List<GameResult> playersResults = gameService.getPlayersStatus();
        ResultView.printCardSumResult(playersResults);
    }

Copy link

@Chocochip101 Chocochip101 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요, 이안 😄

이번 사이클에서 구현해야 하는 것은 충족한 듯하여 다음 사이클을 위해 Approve & Merge합니다 👍🏻
고민해보면 좋을 코멘트 남겨두었으니, 확인해주세요.

수고하셨습니다~

}
}

Collections.shuffle(cards);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 되면 Deck의 응집도가 낮아질 것 같아요.(덱이라는 것은 생성되는 순간 셔플된 카드의 상태를 뜻하는 것일테니까요). 또한 결국에 서비스에서 initCardDeck의 테스트가 제어 불가능한 Collections.shuffle(cards); 테스트가 불가능해보여요.

그럼 요구사항이 'Deck에서 셔플하되, Deck은 테스트 가능해야한다.' 가 되겠네요. 이때 활용할 수 있는게 전략 패턴이에요.
셔플 전략을 외부로 분리해서 테스트 가능하게 하는거죠.

public interface ShuffleStrategy {
    void shuffle(List<Card> cards);
}

public class RandomShuffleStrategy implements ShuffleStrategy {
    @Override
    public void shuffle(List<Card> cards) {
        Collections.shuffle(cards);
    }
}

public class CardDeck {

    private final Deque<Card> cardDeck = new ArrayDeque<>();

    public CardDeck(List<Card> cards, ShuffleStrategy shuffleStrategy) {
        shuffleStrategy.shuffle(cards);
        addAllCards(cards);
    }
    ....
}

// 테스트에서는 테스트 케이스를 통제 가능하기 위해 섞지 않는다.
class NoShuffleStrategy implements ShuffleStrategy {

    @Override
    public void shuffle(List<Card> cards) {
        // do nothing
    }
}

playerGroups = new PlayerGroups(playerNames);
}

public void initAllPlayerCard() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카드를 나눠주는게 서비스여야하나? 라는 생각이 드네요.

return playerGroups.getDealerResult();
}

private CardDeck initCardDeck() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

덱의 init인데 생성자에 있어도 되지 않나요? 🤔


public class HandCards {
private static final int MAX_SCORE = 21;
private static final int ACE_BONUS_SCORE = 10;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 에이스를 1 또는 11로직이 드러나있지 않아요.(TrumpNumber를 보면 에이스에는 1만 존재하고 11이 없죠.)
만약 게임 로직이 에이스 점수를 1 또는 13으로 바꾼다고 가정해볼게요. 개발자가 이를 변경하려면 Hand의 로직을 변경해야하고 계산 로직에 익숙하지 않다면, 시간을 소모하게 되어요.

1 또는 11의 의미가 그 자체로 드러날 수 있도록, TrumpNumber의 score를 int value;가 아닌 List<Integer> 고려해봐도 좋을 것 같아요.

    ACE("A", listOf(1, 11)),
    TWO("2", listOf(2)),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants