본문 바로가기
우아한 테크코스

[LEVEL 1] 블랙잭 미션 회고

by 고선제 2025. 3. 26.

 

우아한테크코스의 세 번째 미션인 블랙잭 미션을 마쳤다.

이번 페어프로그래밍은 미미와 함께 진행하였는데, 정말 잘 맞아서 재미있게 했던 것 같다.

 

코스 때 구현해 봤던 로또미션, 최종 코딩테스트 때 구현해 봤던 출석미션과 달리 처음 해보는 구현미션이었다.

그래서인지 미션이 정말 재밌었다!

🗣️ 구현하면서 고민했던 점들

이전 로또, 출석 미션에도 회고를 어떤 식으로 작성할까 많은 고민을 했었다.

그래서 그냥 단순히 소프트 스킬, 하드 스킬 구분할 것 없이 고민했던 점들을 나열해볼 것이다!

뭐 다시 다른 좋은 방법이 생각나면 바꿔야겠다

 

✔️ 도메인 용어 vs 직관적인 용어

나는 블랙잭이라는 게임을 잘 알지 못한 상황이었다. 이전처럼 단순히 요구사항에 적힌 내용들만 보고 구현할 수 있을 것이라 생각했다. 하지만 클래스, 메서드, 변수 네이밍할 때 어려움을 많이 겪었다.

아래의 예시를 살펴보자

// before
public boolean isOverStandard(int number) {
     if (number > 21) {
        return true;
     }
     
     return false;
}

// after
public boolean isBust(int number) {
     if (number > 21) {
        return true;
     }
     
     return false;
}

도메인 용어를 모른채 메서드 이름을 짓는 것과 알고 짓는 것의 차이이다.

처음에는 도메인을 모르는 사람이 봤을 때도 이해하기 쉽게 직관적인 용어로 사용해야 하는 것이 아닐까? 생각하였다. 하지만 개발자도 단순히 개발만 하는 것이 아니라 비즈니스에 대한 이해가 있어야 한다고 생각하기 때문에 후자를 선택했다.

개발하기 전, 도메인 먼저 공부해라!!

 

✔️ 데이터 중심 설계

블랙잭 미션 중 딜러와 플레이어를 객체로 분리해야했다.

나는 당연히 아래와 같이 단순히 Type으로 구분하면 될 것이라 생각하였다.

enum ParticipantType {
  DEALER, PLAYER
}

class Participant {

    private final ParticipantType participantType;
    ...
}

실제로 위와 같은 코드로 구현을 마쳤다.

하지만 무언가 잘못됨을 느꼈다. 딜러만을 위한 로직이 Participant 클래스 내부에 구현되고, 플레이어만을 위한 로직이 Participant 클래스 내부에 구현되고 있는 것이었다.

또한, 딜러와 플레이어를 구분하기 위해 if문의 반복되는 것을 볼 수 있었다.

 

이것은 객체의 역할과 행위를 데이터(타입)로 구분하려고 한 것으로 데이터 중심 설계를 한 것이었다.

그래서 아래와 같이 상속을 활용하도록 바꾸었다.

abstract class Participant {

    public abstract void method();
}

class Dealer extends Participant {

    @Override
    public void method() {
        // 구현로직
    }
}

class Player extends Participant {

    @Override
    public void method() {
        // 구현로직
    }
}

하지만, 상속을 활용하는 것은 문제가 없을까??

 

✔️ 상속 vs 조합

이번 미션에서는 상속과 조합에 대해 정말 많은 고민을 하였다.

어떤 경우에 상속을 사용하고, 조합을 사용해야 하는지 고민되었다. 흔히 말하는 has-a, is-a 관계는 나한테 와닿지 않았다.

 

앞서 예시로 든 딜러, 플레이어, 참여자는 어떤 관계일까?

내가 생성한 추상클래스 Participant의 코드는 아래와 같다.

public abstract class Participant {

    protected final String name;
    protected final Hand hand;

    protected Participant(String name, Hand hand) {
        this.name = name;
        this.hand = hand;
    }

    public Hand getHand() {
        return hand;
    }

    public String getName() {
        return name;
    }

    public boolean isEqualName(String name) {
        return Objects.equals(this.name, name);
    }

    public void addDefaultCards(List<Card> cards) {
        hand.addAll(cards);
    }

    public void addCard(Card receivedCard) {
        hand.add(receivedCard);
    }
}

이 코드는 정말 좋은 추상클래스일까?

아니라고 생각한다. 그냥 단순히 중복 코드를 제거하기 위해 사용된 추상 클래스이다.

중복 코드를 제거하기 위해 상속을 사용한다고 하는데, 그러면 잘 사용된 것 아닌가요?라고 말할 수 있다.

하지만 추상클래스는 단순히 중복코드가 있다고 사용해서는 안된다. 추후 확장될 여지가 있다면 코드를 수정하기 힘들기 때문이다. 그래서 먼저 추상클래스와 자식클래스가 어떻게 구성되고 사용될지 설계를 신중히 한 뒤 사용해야 한다고 한다.

 

나는 이 설계를 해야 한다는 것이 이해가 되지 않았다. 어떤 설계를 하는 거지?

그래서 나만의 기준을 따로 세웠다.

상속은 클래스의 행동을 확장(extend)하는 것이 아니라 정제(refine)할 때만 사용할 것이다. 확장이란 새로운 행동을 덧붙여 기존의 행동을 부분적으로 보완하는 것을 의미하고 정제란 부분적으로 불완전한 행동을 완전하게 만드는 것을 의미한다.

 

즉, 추상클래스에서 정의된 행위 외에 자식클래스가 추가로 행위를 정의할 일이 생긴다면 사용하지 않고, 조합을 사용할 것 같다.

이것은 권장하는 바는 있지만 정답은 없다. 그래서 현재까지 세운 나만의 기준이다.

 

✔️ 일급 컬렉션 사용 기준

일급 컬렉션은 언제 사용될까? 에 대해 고민했던 것 같다.

그저 컬렉션을 사용한다면 일급컬렉션으로 무조건 만들어야 할까? 아니라고 생각한다!

또 한 번 나만의 기준을 세워봤다. 이 기준은 컬렉션이 도메인 개념으로 만들어질 수 있는지를 먼저 판단해야 한다.

 

도메인 로직이 포함되어야 할 경우 일급컬렉션을 만든다.

위 말이 조금은 추상적일 수 있지만, 아직 내가 이해하는 부분이 구체적이지 않기 때문일 수 있다.

도메인 로직이 포함되어야 하는 경우는 컬렉션을 생성할 때 검증이 필요한 경우와 컬렉션을 사용하여 도메인 로직을 구현할 경우가 있다고 생각한다.

// 1. 검증이 필요한 경우

class Hand {

    public Hand(List<Card> cards) {
        validate(cards);
        ....
    }
}

// 2. 컬렉션을 이용하여 도메인 로직을 작성한 경우

class Hand {

    private final List<Card> cards;
    
    //생성자 생략
    
    public List<Card> findAllAce() {
        return cards.stream()
                  .filter(card -> card.isAce())
                  .toList();
    }
}

이외에 컬렉션을 사용한 로직이 중복될 때 사용할 수도 있을 것 같다.

 

✔️ 불변 객체 사용

어느 범위까지 불변객체를 사용해야 할까?

불변 객체는 사이드 이펙트, 시간적 결합, 스레드로부터 위험을 모두 보장해 준다는 장점이 있다. 하지만, 계속해서 생성해야 하기 때문에 성능상 단점도 존재한다.

 

그렇다면 어느 객체까지 불변으로 만들어야 하는가? 에 대해 고민이 들었다.

 

→ 사실 아직 잘 모르겠다.. 해결하지 못한 문제다

 

✔️ TDA 원칙 vs 결합도

리뷰어인 엘리에게 객체지향 프로그래밍을 하기 위한 원칙 중 하나인 TDA원칙을 지켜보자는 피드백을 받았다.

TDA는 Tell Don’t Ask. 묻지 말고 시키라는 것이다.

나는 이것을 getter를 사용하지 말고 객체 내부에서 해결하라는 것으로 해석했다.

 

해당 객체와 관련된 책임을 모두 객체 내부에서 완성되니 캡슐화를 지킬 수 있고, 완성도가 높은 객체가 되었다. 하지만, 다른 객체와 결합도가 높아지는 문제점이 발생했다. 모든 것을 만족시킬 수 없는 Trade-Off의 영역이라고 하였다.

 

하지만 객체를 더 잘 분리하거나 설계를 더 잘하는 것으로 해결가능하다고 생각하긴 한다.

현재로서는 TDA원칙과 결합도 사이 적당히 만족하여 장점을 최대화하는 객체를 만들 수 있는 기준이 잡혀있지 않았고, 그 기준을 세울 이유를 찾지 못해 아직은 최대한 책임을 적절히 분배하여 객체 설계를 잘해야겠다는 생각을 가지고 있다

 

✔️ 정적 팩토리 메서드 사용 기준

평소에 아무 생각이 없이 사용했던 정적 팩토리 메서드는 언제 사용해야 적절할까? 의 기준을 세워봤다.

  1. 객체를 생성함에 있어 복잡한 로직이 필요할 때 사용한다.
  2. 생성자가 많아져 무엇을 생성하는지 이해하기 어려울 때 명시하기 위해 사용한다.

위 두 가지 기준을 세웠다!

 

😢 아쉬운 점

선택과 집중

이번 미션에서는 도메인 로직을 객체지향적으로 작성하는 공부를 하고 있었다. 네오의 피드백에서 상태패턴을 활용해서 블랙잭의 상태를 객체로 분리하는 것을 봤다.

엄청 신선했다. 이런 것까지 객체로?라는 생각을 하였다. 정말 객체 분리에 새로운 시각을 받았던 것 같다.

이와 동시에 컨트롤러라는 이름을 사용한 클래스에 흐름이 존재한다며 잘못된 컨트롤러 이름을 사용하고 있다는 피드백을 받았다.

인정하는 부분이었고, 그러면 흐름을 한번 제거해 볼까? 고민하였다.

시간이 부족하였기에 위 상태 패턴을 적용해 보는 것과 흐름을 제거해 보는 연습 중 선택을 해야 했다.

나는 후자를 선택했는데, 생각해 보니 이것은 도메인 로직과 전혀 관계없는 기술적인 코드였다.

물론 도움은 되었지만, 객체 분리하는 연습을 더 했으면 하는 아쉬움이 남았다

 

👨 수업 중에..

네오의 말 중 정말 기억에 남는 말이 있어 기록해보려 한다

우테코에서 무엇을 기르길 원하는지 말하신 것 같다.

100명의 트래픽을 스스로 고민하여 1000명의 틀래픽틀 받을 수 있도록 해결하면, 10000명, 10만 명도 스스로 고민하여 해결할 수 있을 것이다.

하지만 스스로 고민하지 않고 레퍼런스나 남의 의견으로 해결한다면 그 이상의 발전을 할 수 없을 것이다.

우리가 지금까지 공부하는 것은 남이 이미 경험해 본 것이기에 레퍼런스, 해결 방법을 쉽게 찾을 수 있지만, 회사에서는 남이 해결하지 못하는 문제들을 해결해야 하는 문제가 많다

그래서 스스로 생각하고 해결해야하는 방법을 길러야 한다!

 

이거 듣고 와.. 했다


벌써 우테코에서 공부한 지 1달이 넘어가고 다음 미션이 끝나면 Level1이 종료가 된다.

시간이 참 빠르다.. 더 빨라진다고 한다.

 

아직까지 내가 무엇을 얻고 싶은지 찾지 못하고 그냥 공부하는 중이다.

나는 무엇을 얻고 싶어 하는 걸까..? 충분히 학습하고 있지만 무언가 채워지지 않는 기분이다.