5, 6주차 기간동안 진행된 블랙잭 미션에 대한 회고를 작성한다.
https://github.com/woowacourse/java-blackjack/pull/147
https://github.com/woowacourse/java-blackjack/pull/200

블랙잭은 우아한테크코스 레벨1 기간동안 가장 많은 충격을 받았던 미션이었다. 제이슨의 유한상태패턴 강의를 통해 추상 클래스를 왜 쓰는지, 상속이란 무엇인지 에 대해 어렴풋이 알게 되었고, 디자인 패턴에 대한 개념을 깨우쳤다. 디자인 패턴을 왜 흔히 필살기라고 부르는지, 왜 포비가 디자인 패턴부터 학습하는걸 지양하시는지 조금은 알 것 같았다. 학부시절에 배웠던 FSM(Finite State Machine)이 소프트웨어 개발에 적용되는 모습을 보면서도 한참 즐겁곤 했다. 또 객체지향의 사실과 오해 책을 심도 깊게 읽으면서 객체지향이 현실세계를 모방하는게 아니라 재창조한다는 것에 대해 이해하는 시간이었다.


🃏 카드 덱을 딜러가 가지고 있어야 하나?

사실 객체지향의 사실과 오해를 다 읽었다면 필요없는 질문이었다. 당시 객체지향의 사실과 오해 초반부를 한참 읽던 중이라 질문을 드렸다.

Q. 카드덱 셔플을 딜러가 해야할까요? 현실세계에서 카드 게임이 진행될 땐 매 게임이 진행될 때마다 딜러가 직접 카드덱을 셔플하는데, 저희는 미리 셔플된 카드를 딜러가 가지도록 구현했어요. 객체지향이 현실세계를 완전하게 모방할 필요는 없다고 생각해서 이런 결정을 했는데, 혹시 딜러가 셔플하도록 하는게 더 나았는지, 카드덱이 미리 섞일 필요는 없었는지 고민이 돼요!


A. 객체지향에서는 사물도 의인화가 가능하므로 덱이 셔플과 카드를 나눠주는 행위를 모두 할 수 있다고 생각해요. 그런 의미에서 딜러가 덱을 가지고 있어야 할지도 의문이에요 🤔

자문자답으로 정리하자면, 딜러 는 어디까지나 카드를 나눠주는 메서드를 가진 객체 덩어리일뿐, 현실세계의 딜러가 아니다. 딜러처럼 팔을 뻗어서 카드를 나눠주지도 않고, 음성으로 게임 진행을 도와주지도 않는다. 그저 카드를 나눠준다는 행동 하나 때문에 딜러라는 이름을 갖게 된 것이다.

즉 객체지향에서 네이밍은 해당 객체가 “이런 행동을 수행하겠지!” 와 같이, 쉽게 객체의 행동을 추측 할 수 있도록 현실세계 존재의 이름을 빌려 쓰는 것 뿐이다. ‘객체지향은 현실세계의 모방이다’ 라는 말이 이 과정에서 와전되어 우리를 괴롭히는게 아닐까.


🃏 캐싱된 객체를 제공하는건 누가 좋을까?

Q. 카드를 캐싱하고 덱에 채워넣는 제네레이터 클래스를 따로 구현했어요. 그런데 오늘 인비가 공유해줬던 이야기 중에 “캐싱은 캐싱되는 객체에 구현해야한다.” 가 떠올라서 고민이 생겼어요. 사실 새로운 카드를 만들 때도 Card 로 먼저 접근할 생각을 하기 때문에 Card 쪽에 static 블럭을 이용해 구현하는게 나았나? 하면서 고민중이에요. 미립이 생각하시기엔 어떤지 궁금해요!


A. 지금은 덱이 제너레이터를 통해서 항상 생성되므로 누가 가지고 있어도 상관 없다고 생각합니다. 그러나 Card 객체를 통해서 값을 생성하지 못하도록 막거나, Card를 통하여 직접 생성하는 경우를 고려한다면 Card가 캐시된 값을 가지고 있는것이 좋아 보이네요.

3대450 (쳤던) 인비가 준 힌트에서 큰 영감을 얻었다. 클라이언트가 최초로 Card 객체에 접근할 땐 캐싱여부를 알 수 없다. Card 객체 생성자 호출을 먼저 시도하게 된다. 한참 후에야 CardGenerator 객체를 발견하고 캐싱된 Card 객체를 뽑아낸다. 분명 설계자를 욕할 것이다. 욕은 먹지 말아야지.

public class Card {

    private static final List<Card> CACHE_DECK = new ArrayList<>();

    static {
        for (Pattern pattern : Pattern.values()) {
            for (Number number : Number.values()) {
                CACHE_DECK.add(new Card(pattern, number));
            }
        }
    }

    private final Pattern pattern;
    private final Number number;

    private Card(final Pattern pattern, final Number number) {
        this.pattern = pattern;
        this.number = number;
    }

    public static Card valueOf(final Pattern pattern, final Number number) {
        Card card = CACHE_DECK.stream()
            .filter(cardPatterns -> cardPatterns.hasPattern(pattern))
            .filter(cardNumbers -> cardNumbers.hasNumber(number))
            .findAny()
            .get();
        if (Objects.isNull(card)) {
            throw new IllegalArgumentException("해당 카드가 존재하지 않습니다.");
        }
        return card;
    }
}


🃏 도메인이 출력 양식을 가지는 예외

이전 로또 미션 회고 에 기록했던 사항으로, ‘Enum도 도메인이라 출력 양식을 가지면 안된다.’ 가 있었다. 그러나 블랙잭 미션에서 예외가 발생했다.

한 카드 덱의 카드는 (조커 제외) 총 52장이다. 이를 표현하기 위해서 52개의 Output 양식을 만드는게 영 석연찮았다. 그러나 마땅한 해결방법도 떠오르지 않아 피드백을 받은 후 변경하자는 생각으로 숫자, 심볼 Enum 클래스들에 출력 양식을 저장해두고 질문을 남겼다. 그런데 예상 외의 답변을 받았다.

“카드번호와 심볼 같은경우에는 보편적인 진리(카드의 번호가 추가 된다거나 심볼이 추가되는 경우는 없음)에 가깝기 때문에 Enum 에서 출력양식을 가지고 있어도 괜찮다고 생각합니다.”

52장의 카드들은 수 세기가 넘는 역사 동안 절대로 수정된 적이 없는 보편적인 진리 라는 특성을 갖고 있다. 절대로 추가나 삭제, 수정 되는 일이 없기 때문에 Enum이 가지고 있어도 괜찮다는 예외가 생겨났다. MVC의 룰을 깨는 것이다.

52개의 출력 양식을 만들기 vs MVC 룰 살짝 어기기 간의 트레이드 오프에서 보편적인 진리 하나 때문에 후자가 승리한 셈이다. MVC도 결국 하나의 디자인 패턴일 뿐, 소프트웨어 본질인 시간, 공간 이슈 앞에서는 장사가 없다는 걸 깨닫는다.


🃏 컨트롤러는 내부 도메인을 몰라야 한다

Input/OutputView - Controller - BlackjackGame - Players - Player

내가 구현했던 블랙잭 게임의 객체간 관계다. 매 턴마다 사용자별 손패의 상태와 이름을 출력해야 했으므로, 자연스럽게 Player 객체가 컨트롤러까지 노출되었다. 그러나 리뷰어 미립은 이에 굉장히 반대하셨고, 어떻게든 BlackjackGame 선에서 Player 객체의 노출을 막아보라하셨다.

‘컨트롤러까지 도메인이 노출되면 안되는 이유가 뭐지?’

체스 미션이 끝난 지금은 왜 반대하셨는지 이해가 되지만, 당시엔 ‘View와 대화하려면 알 수 밖에 없는거 아니야?’ 라는 생각에 사로잡혀있었다. 결론부터 이야기 하자면, 노출되어도 된다. 다만 그 외에도 컨트롤러가 해야할 일이 굉장히 많다. 그래서 몰라야 한다. 컨트롤러는 웹 요청을 받아들이고, 이를 DTO로 변환하고, DAO에 던져주고… 수도 없이 많은 일을 한다. 그 중간에서 내부 도메인 행동까지 처리해준다는건 절차지향 프로그래밍과 다를바가 없다.

결국 Players 내부에 순서(인덱스)를 관리하는 필드를 두어 컨트롤러 쪽에 Player 객체의 이름만 넘어가는 형태로 구현했다. 이름만 넘기는 걸 넘어, BlackjackGame 쪽에도 Player 객체가 노출되지 않아, Players 를 정말 일급컬렉션 답게 사용할 수 있었다. 킹갓제네럴마이티 미립…


🃏 객체간 메세지의 방향

객체지향에 맞춰 프로그래밍을 하다보면 현실세계와 괴리감이 느껴진다. 예를 들자면 알아서 ‘딜러’와 ‘플레이어’에게 카드를 나눠주는 ‘카드덱’, ‘손패’를 가지고 있는 ‘게임진행 상태’ 등이 있다. 설계 단계에선 괴리감을 떨쳐내지만, 구현 단계에서 다시 괴리감에 굴복해 객체간 관계에 의문을 가지곤 한다.

미립이 객체지향이 만드는 괴리감을 이겨낼 수 있는 기준점을 제시해주셨는데, 바로 객체간 메세지의 방향이다. 베팅금액을 뜻하는 BetAmount 객체를 PlayerState 중 어디에 두어야할지 고민하는 예시로 메세지 방향의 중요도를 확인해보자.

우선 StateBetAmount가 존재하는 경우다.

Players - Player - State - BetAmount 가져오기 - Player 에서 승패비교 후 BetAmount 증감

Player의 승패비교는 Dealer 객체와 이루어지는데, 이 때 BetAmount를 증감시키기 위해 State로부터 값을 끌어오는 (getter와 같은) 상황이 벌어진다. 하나의 명령을 수행하기 위해 메세지가 역방향으로 다시 흐르게 되는 것이다.

이번엔 PlayerBetAmount가 존재하는 경우를 생각해보자.

Players - Player - Player에서 승패비교 후 BetAmount 증감

굳이 State 객체까지 접근할 필요도 없고, 이 덕분에 getter 같은 상황도 벌어지지 않는다. 명령 메세지가 한방향으로 흐르고 종료된다.

물론 객체간 메세지의 방향도 만능키가 될 순 없다. 그러나 객체를 구상하고 구현하는 과정에서 헷갈리고, 괴리감이 느껴지고, 애매하다고 생각될 때 밀어 붙여나갈 수 있는 기준점이 되어줄 수 있다. 이렇게 하나씩 나만의 코딩 스타일을 갖게 되는거 아닐까?


🃏 도메인간 데이터 교환은 무조건 Wrapping!

image

규칙을 정했으면 지켜야지 왜 원시값으로?

multiply 객체가 워낙 단순한 일을 하고 있기 때문에 double 원시값을 인자로, 반환값으로 사용해도 문제가 없을 거라고 생각했다. 그렇지만 multiply는 단순한 곱셈 메서드가 아니다. 사용자의 베팅 금액인 BetAmount를 증감시키는 메서드다. 신뢰를 위해서 끝까지 Wrapping하자.


🃏 null도 한 시점에서는 ‘값’이 된다.

image

BetAmount 피드백

BetAmount 필드는 변화하는 값이기 때문에 어쩔 수 없이 setter 성격을 가진 메서드를 사용하게 됐다. 이 때문에 BetAmount를 굳이 초기화 시킬 필요가 없다고 생각했고, null 로 초기화를 해두었다. 그러나 곧바로 new Player(name, betAmount); 로 해보는게 어떻냐는 피드백을 받았다.

내가 생각해본 장점은 캡쳐화면과 같다. 특히 3 번이 중요한 장점이라고 생각하는데, 자바 프로그래밍에서 null은 너무 많은 위험을 포용해준다. 어느 한 시점에는 null도 ‘값’으로 보일 수 있다. 객체가 탄생부터 소멸까지 항상 유의미한 값만 가질 수 있도록 구상하자.


🃏 DTO는 도메인을 몰라야 한다

“서비스에 따라 DTO 의 형태는 여러가지로 변할 수있지만 도메인은 그렇지 않거든요. 예를 들어 게시글의 조회수를 관리자만 볼 수 있게 하고싶다면 관리자 DTO 엔 조회수가 포함되고 일반 유저 DTO 엔 조회수를 포함시키지 않을 수 있겠죠. 그럴 때마다 도메인에 DTO 를 만드는 로직을 추가하는것은 유지보수하기 어려운 코드를 만든다고 생각해요.”

말이 필요한가? DTO는 View다. 도메인과 DTO를 철저히 분리하자.


페어 회고

블랙잭 미션을 함께 진행한 찰리 는 static 인척 하는 static 아닌 크루다. 말수도 적고 정적(static)인것처럼 보이지만, 친해지면 예상치 못한 타이밍에 예상치 못한 드립을 던지는 정적이지 않은…🙄

찰리에게 가장 흡수하고 싶었던 장점은 신뢰다. 찰리를 만난지 얼마되지 않았을 때, 라이언이 “찰리는 무언가를 물어보면 항상 알고있다. 굉장히 편하다.” 라는 이야기를 했었다. 실제로 페어를 진행하는 동안 찰리는 내가 꺼낸 이야기를 모두 이해했고, 거기에 살을 덧붙이는 경우도 많았다. 덕분에 찰리에 대한 신뢰가 쌓여갔다.

찰리에게서 베어나오는 신뢰의 근원지는 역시 넓고 깊은 프로그래밍 지식인 것 같다. 전공자가 아님에도 나보다 넓고, 깊게 많은 프로그래밍 관련 지식을 알고 있었고, 하나를 물어보면 둘을 대답하곤 했다. 지식이 무기가 된다는 걸 단적으로 보여준 사람! 찰리를 보면 어떻게든 시간을 확보해서 더 많은 책을 읽어야겠다는 자극을 받는다. 더 열심히 시간을 확보해야지.

찰리와 페어를 하면서 잘했다고 생각이 드는 점은, 포츈과 페어를 하면서 느꼈던 문제점을 보완하기 위해 이면지에 계속해서 그림을 그리면서 내 머리 속 생각을 설명한 것이다. 덕분에 찰리가 내 생각을 빠르게 이해했고, 찰리도 같이 그림을 그리면서 설명해준 덕분에 쉽게 이해하고 기억할 수 있었다. 반대로 아쉬운 점은 찰리가 키보드를 오래 잡고 있을 때 시간이 지났음을 곧장 알리지 못한 점… 눈에 불을 켜고 집중하고 있는 모습에 건드리기가 미안하다고 생각했다. 그게 찰리와 나에게 아무런 도움이 되지 않는다는 걸 늦게 깨달았다. 다음 페어 땐 더 자신감있게 키보드를 뻇자.

찰리가 한 번 더 페어를 하자고 하면, 우선 메모장부터 켜둘거 같다. 찰리가 가진 수많은 프로그래밍 지식을 옮겨 적어두고 공부해야하니까 😆. 요즘 찰리한테 DM이 오면, 반은 고양이 스티커 이야기다. 찰리는 나를 고양이 스티커 기증자로 기억하는걸까? 좋은거겠지..? 우선 메모해둔 지식들부터 흡수해야겠다.

끗!

댓글남기기