블랙잭 미션 회고록

kdkdhoho·2023년 4월 6일
0

블랙잭 미션부터 삶이 너무 촉박했다. 확 올라간 난이도의 미션, 계속해서 추가적으로 생기는 미니 미션들, 스터디, 이론 공부 등..
다음 미션인 체스 미션도 난이도가 확 올라갔고, 체스 미션이 끝나고 바로 레벨 인터뷰가 있다보니 이제야 쓸 여유가 생긴다.
블랙잭 미션이 끝난 지 거의 3주가 지나가는 이 시점에서, 더 늦기 전에 블랙잭 미션 회고록을 작성하려고 한다.

디미터의 법칙

private Map<String, List<String>> getPlayersCards(final Dealer dealer) {
    List<Player> players = dealer.getPlayers().getPlayers();
    Map<String, List<String>> playersCards = new HashMap<>();
    for (Player player : players) {
        playersCards.put(player.getName(), player.getCards().getCards().stream()
                .map(Card::toString)
                .collect(Collectors.toList()));
    return playersCards;
}

Dealer는 Players 타입의 객체를 가지고 있고, Players 객체는 List<Player> 를 가진다.
따라서 Controller에서 OutputView로 값 타입을 언박싱해주기 위해 위와 같이 코드를 작성했다.

처음 페어와 코드를 작성할 때, 위 방식을 수정하고 싶었다. 하지만 수정하는 근거에 대해 페어와 나 모두 명확하게 모르고 있었고, 그저 이렇게 하면 안좋다는 느낌만 받았다.

아니나 다를까 리뷰어 웨지에게 디미터의 법칙을 학습해보라는 피드백이 달렸다.

내가 아는 디미터의 법칙은, 한 객체가 알고 있는 다른 객체의 내부 사정을 몰라야 한다는 법칙이다.
이는 객체를 보다 객체스럽게, 그리고 객체의 캡슐화를 지켜 결합도는 낮추고 응집도는 높일 수 있는 법칙으로 알고 있다.

때문에 위 코드를 개선하기 위해서는

List<Player> playerValues = dealer.getPlayerValues();

혹은

Map<String, List<String>> playerCards = dealer.getPlayerCards();

처럼, Controller가 유일하게 알고 있는 dealer에게 메시지를 던져 물어보는 식으로 구현해야 될 것이다.

String.join(CharSequence, Iterable<? extends CharSequence>)

String names = players.stream()
        .collect(Collectors.joining(", "));

를 다음과 같이 개선할 수 있다.

String names = String.join(", ", players);

toString()

Card를 출력할 때 내가 간편하려고 toString에 출력형식을 넣어두었다.

이와 관련해 다음과 같은 리뷰가 달렸다.

비즈니스적으로 유의미한 곳에 toString()을 통해 객체의 값을 역직렬화 하지 말아주세요~!
toString()은 거의 모든 객체가 override하는 객체고, 누군가 card을 서브타이핑해 toString을 오버라이드 한 경우 해당 라인에서 바로 버그가 발생합니다.
toString()은 개발자가 확인하는 로깅 용도로만 활용해주셔요!

리뷰어께서 말씀하신 최소 놀람의 법칙도 위배할 뿐더러, toString()을 View를 위해 사용하고 있었다.
만약 View가 변경되면 결국 toString()은 수정 및 삭제가 이뤄져야 한다.
과연 toString()이 출력을 위해 사용되어야 할까?

싱글톤

게임이 시작하면 Delaer가 가지는 52장의 카드 뭉치를 Deck이라는 객체로 만들었다.
처음에는 일반 객체로 구현했지만, Deck 객체가 게임이 한번 실행되고 다시는 생성되지 않기에 메모리적 성능 향상을 기대하고 싱글톤으로 구현했다.

하지만 웨지가 다음과 같은 테스트 코드를 작성하셨고, 테스트가 팡팡 터진다고 하셨다! ..

@Test
@DisplayName("스태틱 덱의 위험성")
void deckStaticDanger() {
    List<Dealer> dealers = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        dealers.add(new Dealer(new Players(List.of("A","B"))));
    }

    for (Dealer dealer : dealers) {
        dealer.settingPlayersCards();
        dealer.settingSelfCards();
    }
}

사실 싱글톤에 대해 깊게 알지 못했고, 얕은 지식으로 좋을 것 같다는 느낌으로만 사용했다.
하지만 역시나 잘못 사용하고 있었다.

싱글톤 패턴이 유의미하려면, 상태가 변경 가능해선 안된다.
하지만 Deck 객체는 상태가 변하기 정말 쉬운, 아니 변할 수 밖에 없는 객체이다. 그런데 이를 싱글톤으로 사용한다는 것은 말이 되지 않는다.

싱글턴 패턴과 관련해서는 Tecoble - 싱글톤 패턴이란? 글을 참고해보자.

Deque vs Stack

위의 Deck 객체의 기존 필드는 List<Card> 타입이다.
하지만 Deck과 좀 더 유사한 자료구조가 없을까 고민하다가 Stack<Card>으로 수정했다.

그런데 스택은 사용하지 말아달라는 피드백과 함께 이 레퍼런스도 함께 남겨주셨다.

레퍼런스의 핵심 내용은 다음과 같다.

1. Stack은 class, Deque는 interface이다.
Java는 다중 상속이 불가능하다. 따라서 이미 다른 class의 서브 타입인 경우, stack을 extends 하지 못한다.
하지만 Interface는 다중 구현이 가능하다. 따라서 보다 유연한 설계 및 확장이 가능하다.
2. thread-safe가 필요하지 않다면, 더 나은 성능을 제공한다
Stack과 Vector는 메서드에 기본적으로 synchronized 키워드가 붙어있다. 이는 성능에 영향을 끼친다. 따라서 thread-safe가 필요한 상황이 아니라면 Deque이 더 나은 성능을 가진다.
3. LIFO 구조에 맞지 않는 Iterator 순서

@Test
void givenAStack_whenIterate_thenFromBottomToTop() {
    Stack<String> myStack = new Stack<>();
    myStack.push("I am at the bottom.");
    myStack.push("I am in the middle.");
    myStack.push("I am at the top.");

    Iterator<String> it = myStack.iterator();

    assertThat(it).toIterable().containsExactly(
      "I am at the bottom.",
      "I am in the middle.",
      "I am at the top.");
}
@Test
void givenADeque_whenIterate_thenFromTopToBottom() {
    Deque<String> myStack = new ArrayDeque<>();
    myStack.push("I am at the bottom.");
    myStack.push("I am in the middle.");
    myStack.push("I am at the top.");

    Iterator<String> it = myStack.iterator();

    assertThat(it).toIterable().containsExactly(
      "I am at the top.",
      "I am in the middle.",
      "I am at the bottom.");
}

등이 있지만 여기까지 알아보겠다.
.. 앞으로 Stack을 쓰려거든 Deque 타입의 인터페이스를 사용하자!

Domain과 View

블랙잭 미션에는 카드에 대한 정보를 문자열로 출력하는 요구사항이 있다.
이 요구사항을 충족하기 위해 도메인에 해당하는 카드 객체를 Enum 클래스로 Suit와 Denomination로 나누어 두 객체를 필드로 가지도록 구현했다.
이때 Suit와 Denomination에 다음과 같이 출력에 필요한 문자열을 넣어두는 것이 과연 옳은 것인가? 에 대한 의구심이 들었다.

블랙잭 미션을 하면서 동시에 테코톡으로 MVC 패턴을 발표했었지만, 위 내용에 대한 나만의 기준은 세우지 못했던 것 같다.
리뷰어인 웨지에게도 이 부분에 대해 질문을 남겼고 이런 답변을 남겨주셨다.

MVC 패턴이 해결하고자 하는 과제는 도메인 모듈화입니다. 차후 어떤 뷰가 들어오더라도 동일한 비즈니스 로직을 활용하려는 것이 목적이므로 MVC를 적용하기로 결심한 순간부터 특정 뷰에 의존적인 코드가 생기는 것은 안티패턴으로 볼 수 있습니다.

나는 유지보수를 쉽게 하기 위함이 MVC 패턴의 주 목적으로 알고 있었다.
하지만 사실 직접 도메인과 뷰를 같은 클래스 내에서 구현함으로써 생기는 불편함을 느끼지 못해 "그렇다더라~" 정도만 알고 있었지만, 웨지의 답변이 정말 살갗으로 와닿는 MVC 패턴의 장점이라고 생각한다.

그렇다. 뷰는 정말 다양한 방법으로 표현될 수 있다.
하지만 도메인은 사용자가 프로그램을 사용하는 목적과도 같다.
프로그램의 근본이 되는 이 도메인이, 변화무쌍한 뷰에 의해 쉽게 흔들린다면 프로그램 자체가 쉽게 흔들릴 것이다.
그렇기에 Domain과 View를 분리시키고자 탄생한 것이 MVC 패턴이다.

MVC 패턴 발표에 이 내용을 넣었으면 어땠을까하는 아쉬움이 있다.

profile
newBlog == https://kdkdhoho.github.io

0개의 댓글