이번 미션은 블랙잭 게임을 구현하고 게임을 실행하는 프로그램을 만드는 것이다.
자동차 미션과 사다리 미션을 겪으며 아, 이정도 난이도군 🤔 하며 나름 내 속도를 찾고 적응하고 있었는데, 블랙잭 미션의 등장을 기점으로 해서 많이 휘청휘청 거렸던 것 같다.
현재까지 우테코 미션 중 가장 어려웠던 것 같은데, 내가 무엇을 배워갔는 지 정리해보려고 한다.
💎 블랙잭 미션에서 달성하고 싶은 목표
지난 사다리 미션과 비교해서 부족한 부분을 토대로, 이번 블랙잭 미션에서 이루고 싶은 것을 정리하였었다.
- 메타인지를 사용해서 내가 관념적으로 작성하던 코드에 의문을 제기하고 고민해보기
- 리뷰어가 제시하는 의견을 무조건 따르지 말고, 내가 이해가 안가는 부분이 있다면 리뷰어에게 반박 의견을 제시해보기
- 리뷰어의 의견을 눈으로만 읽지 말고, 직접 코드를 작성해보면서 장단점을 비교해보고 내 결론을 내리기
- 내가 할 수 있는 최대한 객체지향적인 코드를 작성하기
- 초기 설계 없이 TDD를 사용해서 단계적으로 코드를 완성하기
📝 미션 구현 과정
사용자 이름을 입력받고, 딜러와 플레이어들에게 2장 씩 나누어주는 게임의 초기 세팅 기능
게임에 참여할 사람의 이름을 입력하세요.(쉼표 기준으로 분리)
pobi,jason
플레이어가 한 장의 카드를 더 받을 지 말 지 결정하면서 카드를 받는 기능
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
딜러의 점수가 16을 초과할 때까지 카드를 받는 기능
딜러는 16이하라 한장의 카드를 더 받았습니다.
딜러와 플레이어가 손에 쥔 카드와 점수의 총합을 출력하는 기능
딜러카드: 3다이아몬드, 9클로버, 8다이아몬드 - 결과: 20
pobi카드: 2하트, 8스페이드, A클로버 - 결과: 21
jason카드: 7클로버, K스페이드 - 결과: 17
최종 승패를 결정하고 출력하는 기능
최종 승패
딜러: 1승 1패
pobi: 승
jason: 패
🤔 미션을 구현하며 학습한 내용
1️⃣추상 클래스 상속을 이용한 다형성 구현으로 반복 로직 줄이기
[문제 상황]
이번 미션에서는 딜러와 플레이어가 대부분 같은 역할을 담당한다.
그래서 Participant라는 추상 클래스를 상속하여 Dealer, Player 객체를 만들어주었는데, 게임 상태 초기화와 모든 참가자들에게 돌아가면서 카드를 주는 부분, 그리고 승패를 판단 하는 부분의 세부적인 로직이 달라서 다형성을 구현하기 어려웠다.
[문제 해결을 위해 노력한 과정]
Participant 추상 클래스를 상속하였다는 이점을 더욱 활용하기 위해, 딜러와 플레이어 각각이 아닌 상위 클래스 Participant
의 리스트를 BlackJackGame
에 두었다. 그리고 for
문을 도는 등 모든 참가자들에게 유사한 작업을 할 때 다형성을 활용하여 객체마다 서로 다른 동작을 하도록 하였다.
예를 들어, 플레이어와 딜러는 카드를 더 받을 수 있는 조건이 다르다.
- 플레이어는
y
를 입력받고, 21을 초과하지 않은 경우 카드를 더 받을 수 있다. - 딜러는 16 이하이면 카드를 더 받을 수 있다.
그래서 abstract
메서드에서 이를 오버라이딩할 수 있도록 선언하여 아래와 같이 딜러와 플레이어가 서로 다른 검증을 하도록 수정하였다.
public class Dealer extends Participant {
....
@Override
public boolean canPickCard(final DecisionToContinue decision) {
return calculateScore() <= CARD_PICK_THRESHOLD;
}
}
public class Player extends Participant {
....
@Override
public boolean canPickCard(final DecisionToContinue decision) {
return isNotBusted() && CardCommand.HIT.equals(decision.get());
}
}
이렇게 다형성을 활용하니, 위에서 제시한 돌아가면서 카드를 받는 메서드
에서 while문의 조건을 분기할 필요 없이 한 번에 모든 참가자에게 카드를 제공하는 문제를 해결할 수 있었다.
public void giveCard(
final Participant participant,
final ActionAfterPick action,
final DecisionToContinue decision
) {
while (participant.canPickCard(decision)) {
participant.pickCard(deck, CARD_PICK_SIZE);
ParticipantHandStatus currentStatus = participant.createHandStatus();
action.accept(currentStatus);
}
}
2️⃣ 함수형 인터페이스를 이용한 도메인 로직과 컨트롤러의 분리
[문제 상황]
컨트롤러는 도메인 로직이 수행되었을 때, 도메인에서 정보를 받고 뷰로 출력을 할 것을 요청하는 역할을 한다.
따라서 컨트롤러는 비즈니스 요구사항을 직접 구현하기보다, 이를 구현한 도메인에게 메시지를 보내는 것이 역할이라고 할 수 있다.
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
y
pobi카드: 2하트, 8스페이드, A클로버
pobi는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason는 한장의 카드를 더 받겠습니까?(예는 y, 아니오는 n)
n
jason카드: 7클로버, K스페이드
딜러는 16이하라 한장의 카드를 더 받았습니다.
하지만 위의 예시와 같이 블랙잭 게임에서 모든 참가자에게 참가자가 더 이상 받을 수 없을 때까지 카드를 제공하는 것을 구현하다 보니, 이 중 루프가 필연적으로 생길 수 밖에 없었다. 그 사이에 뷰에게 출력을 요청하려다 보니 이중 루프를 컨트롤러에서 작동하도록 할 수 밖에 없었다.
[해결하고자 하는 문제]
나는 컨트롤러가 최소한으로 비즈니스 로직을 담당하고, 최대한 도메인이 로직을 처리하도록 구현하고 싶었다. 대신, 뷰를 호출하는 역할도 컨트롤러가 해야만 했다.
[문제 해결을 위해 노력한 과정]
나는 컨트롤러가 아닌 도메인에서 딜러와 플레이어에게 입력을 받고, 카드를 주고, 출력을 하고 사이클을 반복할 수 있도록 하려고 한다. 그러기 위해서는 참가자가 더 이상 카드를 받을 수 없을 때까지 로직을 반복하는 while문을 도메인에서 구현해야한다.
해당 기능을 동작하기 위해서는 딜러든, 플레이어든 딱 세가지의 기능이 필요하다.
- 카드를 받을 수 있는 지 확인하는 기능
- 카드를 받는 기능
- 카드를 받았음을 뷰에 출력하는 기능
이 세가지 기능을 도메인에서 수행해야하기 때문에, 뷰와 도메인이 코드 상으로 결합될 수 밖에 없다.
하지만 첫 번째 기능과 마지막 기능을 하나의 매개변수로 컨트롤러에서 주입받게 된다면, 이를 도메인 단에서 뷰와 느슨한 결합으로 구현할 수 있지 않을까?
이러한 생각을 기반으로 컨트롤러에서 행동과 조건을 함수형 인터페이스로 정의하여 파라미터로 넘기도록 구현하였습니다.
public void giveCard(
final Participant participant,
final ActionAfterPick action,
final DecisionToContinue decision
) {
while (participant.canPickCard(decision)) {
participant.pickCard(deck, CARD_PICK_SIZE);
ParticipantHandStatus currentStatus = participant.createHandStatus();
action.accept(currentStatus);
}
}
- 카드를 받을 수 있는 지 검사하고
- 카드를 받고
- 로직을 출력한다.
- 컨트롤러에서는
giveCard
를 호출할 때 카드를 뽑고 나서 어떤 출력을 낼 지, 어떤 조건을 검사할 지만 정의해주면 된다. 앞서 다형성을 이용해 구현하기로 하였으니, 이 조건과 출력 함수는Participant
가 딜러이냐, 참가자이냐에 따라 다르게 정의하였다.
그러나 아쉬운 점은, 다형성을 제대로 이용할 수 없다는 것이다.
// GameManager.java
private void rotate(final BlackJackGame blackJackGame) {
List<Participant> participants = blackJackGame.getParticipants();
for (Participant participant : participants) {
blackJackGame.giveCard( // 함수 호출
participant,
getAction(participant),
getSupplier(participant)
);
}
}
private ActionAfterPick getActionAfterPick(final Participant participant) {
if (participant instanceof Player) {
return outputView::printHandStatus;
}
return handStatus -> outputView.printDealerCardSavedMessage();
}
private DecisionToContinue getDecisionToContinue(final Participant participant) {
if (participant instanceof Player) {
return () -> CardCommand.from(
inputView.requestCommandWhetherGetMoreCard(participant.name())
);
}
return () -> CardCommand.HIT;
}
GameManager은 게임의 전반적인 진행을 담당하는 컨트롤러 클래스이다.
컨트롤러에서 뷰의 입출력 함수를 호출하는 것을 담당하고 있는데, 이를 다형성을 사용해버리면 도메인에서 뷰의 메서드를 호출하게 되고, 그야말로 강한 결합이 생겨버리는 것이다. 하지만 1단계 미션이 마무리 된 지금도 이 부분은 수정하지 못하고 instanceof를 사용한 코드로 남아있다.
그 점이 아쉽긴 하지만, 컨트롤러가 수행할 수 있는 최소한의 비즈니스 로직을 작성하고, 그 외에는 도메인이 모두 담당하도록 개선할 수 있었다.
💫 회고
초기 목표를 달성하기 위해 1단계 미션에서는 어떻게 노력하였는가?
초기 설계 없이 TDD를 사용해서 단계적으로 코드를 완성하기
페어 프로그래밍에서 페어인 리건과 초기 설계 없이 TDD 부터 돌입해보았다. 간단했던 지난 미션과는 다르게, 복잡도가 올라간 미션에서 TDD를 수행하니 내가 구현해야 할 프로그램의 경계가 확실히 잡혔고, 아 이 기능을 이렇게 구현하면 되곘구나 혹은 이런 상황에서는 이런 예외를 던지면 되겠구나 하는 안정감을 느낄 수 있었다. 😀
그러나 당장 초록불을 뜨게 하기 위해 구현하고 리팩토링 하는 과정에서 복잡한 요구사항 구현에 골머리를 앓는 순간이 많았고, 설계 없이 개발을 하다 보니 잘못된 설계 때문에 코드가 엉키는 문제가 발생하여 개발 시간이 지체되는 일이 있었다. 이전 미션과 다르게 따져봐야할 요구사항이 많았기 때문에, 그때 그때 로직을 구현하고 객체의 역할을 고민하는 것이 상당히 어렵다고 느꼈다.
그래서 이번 1단계 미션에서 느낀 점은 프로그램의 복잡도가 높을 수록 미리 설계를 해봐야겠구나,를 알게 되었다. 객체의 역할을 명확히 하는 것이 프로그램의 흐름을 깔끔하게 하고 시간적으로 여유가 생기는 장점이 있었다.
더 중요한 것은, 페어와 나는 서로 다른 생각을 가지고 개발을 해왔기 때문에 같은 문제를 겪어도 해결을 위해 생각하는 방향성이 다르다. 그렇기에 어떤 결과물을 원하는 지 미리 싱크를 맞춰보고, 합의한 대로 페어 프로그래밍을 진행하는 것이 더 효과적이라는 것을 알게 되었다.
메타인지를 사용해서 내가 관념적으로 작성하던 코드에 의문을 제기하고 고민해보기
내가 알고 있는 것과 리뷰어가 알고 있는 것은 다르다.
리뷰어가 알고 있는 것이 통상적으로 옳을 확률이 높지만 😅 이번 미션에서는 내가 맞다고 생각하는 것을 주장해보고 싶었다. 그리고 그 과정은 나에게 더 큰 도움이 되었다.
예를 들어, 이번 미션 리뷰어 미르가 테스트 코드를 위한 생성자를 만들어 주는 것에 대해서 의문을 제기하였다. 이 부분은 게임을 실행하는 Round라는 객체를 테스트를 위해 직접 만든 Deck을 주입하도록 설계한 생성자에 대해 달린 리뷰이다.
나는 코치 제이슨에게 들은 내용을 기반으로 내 생각을 정리해서, 왜 테스트를 위한 생성자를 만들어주는 것이 괜찮은 지 나의 생각을 공유하였다.
이에 대해 미르는 자신이 왜 의문을 제기하였는 지, 어쩔 수 없는 상황에서 생성자를 만들어주는 것은 괜찮지만 정말 어쩔 수 없는 상황이 맞는 건지에 대해 한번 더 질문을 해주었다.
이 리뷰를 통해, 생성자를 여는 게 무조건 나쁘다는 게 아니라, 그렇게 열 수 밖에 없는 설계가 문제였구나 라는 것을 깨닫게 되었다. 한번 더 질문을 함으로써 미르가 리뷰를 남긴 진짜 의도를 파악할 수 있었고, 아래와 같이 인터페이스를 생성하여 덱을 생성하는 역할을 내부적으로 분리하도록 하였다.
이를 통해 생성자를 private로 유지하더라도 직접 커스터마이징 한 Deck을 Round가 사용할 수 있도록 설정할 수 있었다. 👍
내가 할 수 있는 최대한 객체지향적인 코드를 작성하기
다른 사람이 봤을 때는 아, 이거 좀 더 객체지향 적일 수 있을 것 같은데 .. 라고 생각할 수 있다. 객체지향 적으로 완벽한 코드일 것 같지는 않지만, 내가 시간 내에 내가 적용할 수 있는 최대한 깔끔한 코드를 작성하였다고 생각해서 해당 목표를 달성하였다고 볼 수 있다 😊
2단계 미션에서는 블랙잭 게임을 위해 베팅을 구현하고, 수익률을 계산하는 기능이 추가된다.
1단계보다 조금 더 객체의 역할을 많이 고민해보고 책임 할당에 대한 나만의 기준을 더 만들어보고 싶다.
'우아한테크코스 > 레벨1' 카테고리의 다른 글
[회고] 우아한테크코스 6기 백엔드 레벨1 6주차 회고 (3) | 2024.03.24 |
---|---|
[회고] 우아한테크코스 6기 백엔드 레벨1 5주차 회고 (2) | 2024.03.24 |
[TDD] 2단계 `사다리 게임 실행`구현 & 코드 리뷰 회고 (4) | 2024.03.21 |
[회고] 우아한테크코스 6기 백엔드 레벨1 4주차 회고 (4) | 2024.03.11 |
[회고] 우아한테크코스 6기 백엔드 레벨1 3주차 회고 (0) | 2024.03.03 |