이번 미션에서 가장 많이 고민했던 부분 중 하나는 '딜러와 플레이어를 어떤 구조로 나눌까?' 였다.
✅ 딜러와 플레이어를 하나의 클래스로
가장 먼저 생각했던 방법은 딜러와 플레이어를 하나의 클래스로 두고 isDealer 필드로 구분하는 것이었다.
public class Participant {
private final String name;
private final Hand hand;
private final boolean isDealer; // 해당 필드로 구분
}
플레이어와 딜러는 많은 공통점을 가지고 있다:
- 카드 묶음을 가진다.
- 카드를 뽑는다.
- 다른 카드와 비교하여 승/무/패 결과를 계산한다.
이러한 공통점이 있기 때문에 하나의 클래스에서 처리할 수 있을 것이라고 생각했고 설령 차이점이 있다 하더라도, isDealer 필드를 통해 충분히 처리가 가능하다고 생각했다.
또한 상속은 최대한 사용하지 않는게 좋다는 이야기를 들은적이 있어서 더더욱 상속 없이 풀어내려고 했다.
⚠️ 문제점
1. 객체가 필요없는 상태와 행동을 내부에 가져야 한다.
아무리 플레이어와 딜러가 공통점이 많다고 해도 엄연히 차이점이 존재한다.
히트/스탠드는 플레이어만 수행하고, 16이하여부에 따라 카드를 추가로 히트 하는건 딜러만 수행한다.
즉, 하지도 않는 행동을 객체 내부에 둬야한다.
public void addAdditionalCard() {
if(!isDealer) {
return;
}
// 추가로 뽑는 로직...
}
또한 딜러 입장에서는 베팅 금액을 몰라도 되는데 플레이어가 사용한다는 이유만으로 필요없는 상태를 가져야 한다.
public class Participant {
private final String name;
private final Hand hand;
private final boolean isDealer;
private final BettingAmount bettingAmount; //
}
❌ 이는 엄연한 SRP 원칙 위반이다. 객체에 대한 변경 이유는 오직 한가지여야 하는데 자신이 하지도 않는 행동때문에 자신이 변경돼야 한다.
2. 분기문의 반복
현재 딜러와 플레이어를 구분하는 방법은 isDealer 하나이다.
즉, 이 말은 외부에서 해당 객체를 사용하려면 항상 딜러인지 확인하는 분기문을 작성해야 한다는 것을 의미한다.
public Participant getDealer() {
for (Participant participant : participants) {
if (participant.isDealer()) {
return participant;
}
}
throw new IllegalStateException(DEALER_NOT_FOUND_ERROR.getMessage());
}
3. 안정성 문제
객체를 생성할 때 isDealer 필드가 잘못 설정 된다 한들 이 오류를 처리할 수 있는 방법이 없다. 객체 생성 시점에는 이 객체가 딜러로 사용될지, 플레이어로 사용될지 알 수 없기 때문이다.
이렇게 잘못 만들어진 객체가 밖으로 퍼지면, 딜러가 플레이어 행세를 하고, 플레이어가 딜러 행세를 하는 불상사가 생기게 된다.
✅ 상속을 사용하자.
❌ 잘못된 상속: 딜러가 플레이어를 상속
처음에 생각했던 방법은 딜러가 플레이어를 상속받도록 하는 것이었다.

하지만 이렇게 상속하면, 딜러는 딜러만의 기능을 추가할 수 있지만, 플레이어는 플레이어만의 기능(ex: 베팅)을 가질 수가 없다.
결국 앞서 isDealer로 구분했던 것처럼 이번에는 isInstanceOf로 구분해줘야 하는 문제가 발생한다.
⭕️ 좋은 상속: 딜러와 플레이어가 참가자 상속
딜러와 플레이어가 Participant라는 추상 클래스를 동시에 상속받는다.

Participant에 공통 로직(초기 카드 뽑기, 블랙잭 여부 판단하기 등등)을 넣고 각각의 관심사는 하위 클래스에 추가한다.
이렇게 되면 앞서 있었던 문제들을 모두해결할 수 있다.
1. 객체가 필요없는 상태와 행동을 내부에 가져야 한다.
➡️ 각자의 책임은 Dealer, Player 내부에 각각 위치함
2. 분기문의 반복
➡️
public Dealer getDealer() {
return dealer;
}
타입으로 구분이 가능하기 때문에 이제 분기문을 작성하지 않아도 됨
3. 안정성 문제
➡️ 확실하게 클래스로 구분되기 때문에, 객체를 잘못 만들어도 컴파일 오류로 사전에 파악 가능
✅ 상속을 잘쓸려면?
is-a 관계가 성립하는지 확인하자

처음에는 딜러가 플레이어를 상속받도록 설계했다. 하지만 ‘딜러는 플레이어다’라는 is-a 관계가 성립하는지 고려해 보았을 때 적절하지 않았다. is-a 관계가 성립하려면 플레이어가 수행할 수 있는 모든 행동을 딜러도 수행할 수 있어야 한다. 그러나 베팅과 같이 플레이어에게만 존재하는 행동이 있기 때문에, 딜러가 플레이어를 상속받는 구조는 올바르지 않다고 판단했다.

반면 참가자라는 공통 추상 클래스를 두고, 딜러와 플레이어가 이를 상속받는 구조에서는 is-a 관계가 성립한다. 즉, 딜러는 참가자이며 플레이어 또한 참가자이다.
부모 클래스가 독립적인 의미를 갖는 것이 아니라면 추상클래스로 만들어라

Participant는 그 자체만으로는 의미를 가지지 않는다. 블랙잭 게임에서는 딜러이거나 플레이어여야만 그 의미를 가진다.
이런 클래스를 추상클래스로 두면 이러한 의미없는 객체 생성을 막을 수 있고, '이 클래스는 상속용'이라는 설계의도를 코드 읽는 사람에게 전달 할 수 있다.
'우테코 8기 > 본과정 탐구 일지' 카테고리의 다른 글
| [레벨1 선택미션] 《객체지향의 사실과 오해》 읽고 장기 도메인 모델 설계하기 (0) | 2026.03.22 |
|---|---|
| [레벨1 블랙잭] Service 클래스에 대한 고찰 (0) | 2026.03.14 |