- 저장소: https://github.com/yoondgu/java-blackjack/tree/step1
- 코드리뷰 진행 PR: https://github.com/woowacourse/java-blackjack/pull/414
기능 요구사항
블랙잭 게임을 변형한 프로그램을 구현한다. 블랙잭 게임은 딜러와 플레이어 중 카드의 합이 21 또는 21에 가장 가까운 숫자를 가지는 쪽이 이기는 게임이다.
- 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다.
- 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다.
- 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다.
- 게임을 완료한 후 각 플레이어별로 승패를 출력한다.
구현하며 고민한 내용
이전 미션보다 난이도가 높다고 느껴졌다.
그럼에도 초반에 여러 고려해야 할 세부 요구사항을 생각하지 못해서인지 금방 할 수 있을 거라는 오만(?)으로 인해
시간 관리를 잘 하지 못했다.
그래서 제출 마감까지 급하게 구현을 하다보니 후반부에는 TDD를 많이 포기했던 점이 아쉬웠다.
대신 "일단 구현"한 뒤 이를 리팩터링해보는 경험에 시간을 많이 투자해본 것은 좋았다.
다만, 다음 미션부터는 도메인 이해, 설계를 위한 시간을 더 많이 가진 뒤 구현을 시작해야겠다고 생각했다.
이 미션에서는 "상속 vs 조합"이 5기 크루 사이의 뜨거운 감자였다.
그에 비해 미션 기간 동안 이 부분을 깊이 고민해보지 못했던 것이 아쉬워 지금 기록을 정리하며 추가로 학습해보았다.
중복 코드를 줄이기
- Dealer, Player 클래스가 Participant를 상속하도록 했다.
- 하지만 뒤늦게 상속의 단점으로 캡슐화가 깨진다는 것 을 알게 되었다.
- 대충 귓등으로만(?) 들을 때는 그래도 중복 코드를 제거하는 것도 장점이 아닌가? 하고 넘겼는데, 이에 대한 대안으로 제안되는 "조합"이 무엇인지 아래 참고링크를 읽고서야 이해가 되었다.
Dealer, Player 모두에게 필요한 메서드를 제공하는 Participant 객체를 각 클래스가 인스턴스 변수로 가지는 것이다.
부모 클래스에 접근하지 않고 메서드 호출을 통한 값을 얻기 때문에 Participant의 세부 구현사항이 바뀌어도 문제가 없다! - 참고링크 : 테코블 상속보다는 조합(Composition)을 사용하자.
- 대충 귓등으로만(?) 들을 때는 그래도 중복 코드를 제거하는 것도 장점이 아닌가? 하고 넘겼는데, 이에 대한 대안으로 제안되는 "조합"이 무엇인지 아래 참고링크를 읽고서야 이해가 되었다.
인스턴스 변수를 2개 이하로 유지하기
- Dealer와 Players를 인스턴스 변수로 관리하는 Participants 클래스를 따로 만들었다.
코드리뷰에서 배운 것들
테스트를 위한 생성자와 단위 테스트
자동차 경주 미션 때도 리뷰어님께 여쭤봤던 "테스트를 위한 생성자"에 대한 의견을 이번 리뷰어님께도 여쭤보았다.
리뷰어님과 나눈 대화를 토대로 정리해보자면
객체가 상태를 가질 때의 테스트 코드를 작성할 때,
(1) "테스트를 위한 생성자"를 사용해도 된다 vs (2) 안된다 두 가지 의견이 있다.
(1)의 경우: 테스트를 위한 생성자를 사용해 특정 상태를 가진 객체를 생성해 테스트한다.
- 단점: 외부에서 해당 생성자를 이용해 의도하지 않은 방식으로 객체를 생성하고 사용할 위험이 있다.
(2)의 경우: 테스트를 위한 생성자는 정의하지 않고 다른 방법으로 객체의 상태를 변경해 테스트한다.
- 단점: 상태 값을 세팅해주는 메서드가 필요하거나, 상태 설정을 위해 하나의 단위 테스트가 다른 메서드의 실행 결과에 의존해야 할 수 있다.
나의 결론은
- 만약 설계에 문제가 있어 단위 테스트가 독립적이지 못한 상황이라면, 클래스 설계를 수정한다.
- 그렇지 않다면, 테스트를 위한 생성자를 사용해 단위 테스트의 독립성을 확보한다.
위와 같은 순서로 진행하는 것이 좋다고 생각한다.
테스트를 위한 생성자를 지양하려다가, 오히려 테스트 코드가 프로덕션 코드의 영향을 더 많이 받아야 하는 상황이 될 수 있기 때문이다.
책임의 분리에는 항상 트레이드 오프가 있기 마련!
- 위 고민한 점 중 인스턴스 변수를 2개 이하로 유지하기 위해 또 하나의 클래스를 분리했다고 했다.
하지만 Players 객체에서 바로 처리할 수 있는 로직들이 대부분이었고 BlackJackGame에서는 그저 전달, 중개만 하는 메서드가 많아졌다. - 이에 대해 리뷰어님이 남겨주신 아래 코멘트가 인상적이었다.
프로그래밍 요구사항에만 집착하기 보다는 그것이 왜 필요한지, 또 궁극적으로는 더 좋은 코드를 짜는 것이 목적임을 잊지 말아야겠다.
코드리뷰 피드백:
책임의 분리에는 항상 트레이드 오프가 있기 마련입니다. 요구사항 중 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.를 지키기 위해 많이 노력을 하신 것 같아요. 하지만 이는 한 클래스가 과하게 책임을 가져가는 걸 막기 위함이지, 더 간단하고 깨끗한 코드를 짤 수 있다면 과감하게 규칙을 부숴보고 의견을 구하는 것도 좋다고 생각합니다.
바로 이 Participants라는 클래스가 해당되는 사항입니다. Players으로 충분히 처리할 수 있는 로직들이 대부분입니다. BlackJackGame에서 Dealer와 Players를 필드 변수를 두고 있어도 무리 없이 게임이 돌아갈 것 같은데, 어떻게 생각하시나요?
도메인 속성에 대한 불필요한 뷰-모델 분리
- 도메인 패키지에는 카드의 슈트, 끗수를 나타내는 Enum 클래스 Suite, Denomination이 있다.
- "스페이드", "하트"와 같이 뷰에서 출력할 때 사용하는 문자열은 도메인에서 알 필요가 없으므로 뷰 패키지에는 도메인과 문자열을 매칭하는 Enum 클래스 SuiteWord, DenominationWord를 두었다.
하지만 생각하지 못한 부분을 리뷰어님께서 말씀해주셨다. - 다국어를 지원하는 프로그램이었다면 분리를 하는 것이 좋았겠지만, 지금은 그런 요구사항이 없으므로 불필요한 분리였다고 생각한다. 그리고 "스페이드", "하트"와 같은 문자열도 해당 속성의 "이름"이라고 생각하면 충분히 도메인의 속성이 아닐까?
코드리뷰 피드백:
enum 클래스에 든 상수들은 기본적으로 static final 합니다.
때문에 이미 모든 클래스가 알고 있어 뷰와 도메인을 구분해줄 필요가 없습니다 😎
미션 피드백 강의에서 배운 것들
- 상속 주의하기
- 상속 대신에 특정 기능을 구현하도록, 인터페이스를 쓰는 게 더 나은지 생각해볼 수도 있다. (조합 뿐만이 대안은 아니다)
- 상속하는 하위 클래스가 몇 개 되지 않는다면, 상속으로 중복을 제거하는 게 장점이 아닐 수 있다.
- 추상 클래스를 사용할 경우, 생성자는 protected로 하고 재정의를 방지해야 할 메서드는 final을 붙이는 등 캡슐화를 위한 처리를 하자.
- 캐싱
- 슈트, 끗수가 같은 카드는 다 같은 카드인데 매번 새로운 객체를 만들 필요가 있을까? 캐싱을 하자.
- 매번 동일한 객체인 카드를 반환해주기 위해, 일종의 바구니인 정적 변수를 만들어 캐싱 처리를 해준다.
- 불변 객체
- 모든 클래스를 상태를 변경할 수 없는 불변 클래스로 만들면 유지 보수성이 크게 향상된다.
- 상태를 변경해야 할 때마다 setter를 하는 것이 아니라, 다른 상태를 가진 새 객체를 반환해주는 방식으로도 클래스를 불변하게 다룰 수 있다.
- 테스트 픽스쳐의 활용
- 좋은 객체의 7가지 덕목
'우아한테크코스 > 미션 기록' 카테고리의 다른 글
[레벨1] 체스 1, 2단계 - 체스판 초기화, 말 이동 (0) | 2023.04.14 |
---|---|
[레벨1] 블랙잭 2단계 - 베팅 (0) | 2023.04.08 |
[레벨1] 사다리 타기 2단계 - 사다리 게임 실행 (0) | 2023.04.07 |
[레벨1] 사다리 타기 1단계 - 사다리 생성 (0) | 2023.04.07 |
[레벨1] 자동차 경주 2단계 - 리팩터링 (0) | 2023.04.07 |