✅ 내가 생각하던 Service 클래스
내가 이전까지 생각했던 서비스 클래스는 여러 도메인이 협력하거나 도메인이 외부 계층(데이터베이스, 랜덤 로직)을 조합해 비즈니스 기능을 수행하도록 조율하는 역할을 한다고 생각했다.
따라서 이번 블랙잭 미션에서도 다음과 같이 서비스 클래스를 구현했었다.
public class BlackjackService {
private final CardShuffler cardShuffler;
public BlackjackService(CardShuffler cardShuffler) {
this.cardShuffler = cardShuffler;
}
public List<Card> drawCard(CardDeck cardDeck, int drawCount) {
// 카드 덱에서 지정된 갯수만큼 카드를 뽑는다.
}
public List<FinalResult> getFinalResults(Participant dealer, List<Participant> players) {
// 딜러와 플레이어들의 카드를 비교해 승/무/패를 계산한다.
}
}
- `drawCard()`: 카드 덱에서 지정된 갯수만큼 카드를 뽑는다.
- `getDealerFinalResult()`: 딜러와 플레이어들의 카드를 비교해 승/무/패를 계산한다.
`drawCard()`의 경우, 나는 랜덤로직같은 통제가 힘든 로직이 도메인에 흘러들어가면 안된다고 생각했고 이를 외부 계층이라고 생각하여 서비스 클래스에 두었고
`getDealerFinalResult()`의 경우 여러 Participant가 협력하기 때문에 서비스에 들어가야 한다고 생각했었다.
✅ Service에 대한 내 생각의 변화
하지만, 이 코드를 보고 리뷰어 분은 `'해당 기능들이 서비스가 꼭 필요할까?'`라는 피드백을 주셨고, 나는 이에 대해 내 생각을 전달드렸다.
🧑🎓: 말씀감사합니다! 우선 이렇게 구현한 이유는 `drawCard()`는 랜덤로직을 필요로 하기때문에 도메인에 통제 불가능한 랜덤로직으로 오염되면 안좋다고 생각해 해당 랜덤 로직을 인프라 계층으로 분리하였습니다. 그러다보니 도메인과 랜덤로직을 묶어줄 클래스가 필요하다고 생각했고, 그래서 Service클래스를 만들었습니다.
여기서 제가 궁금한 부분은 크게 두가지입니다
1. 제가 생각하는 서비스는 여러 도메인이 협력해야하는 기능이나 데이터베이스 같은 외부 계층이 필요한 기능인 경우 사용하는데, 초코칩이 생각하는 서비스클래스의 역할이나 사용할 때의 주의할 점이 있는지 궁금합니다!
2. Participant 내부로 카드뽑는(랜덤) 로직을 넣으라고 하셨는데 도메인안에 랜덤 로직이 들어가도 괜찮을까요??
그리고 이에 대한 리뷰어분의 답변은 다음과 같았다.
🧑🏫: 남겨주신 질문에 대해 몇 가지 생각거리를 드려볼게요.
서비스의 역할
여러 도메인이 협력해야 하는 기능에 Service를 사용한다고 말씀해주셨는데, 혹시 어떤 경우를 떠올리신 걸까요?구체적인 예시를 하나 들어주시면 더 이해하기 쉬울 것 같아요 👀
데이터베이스와 같은 외부 계층과의 연결 역할을 Service가 맡는다는 의견에는 저도 공감합니다.
다만 “도메인들이 협력해야 해서 Service가 필요하다” 라는 상황이라면, 저는 한 번 더 이런 부분을 고민해볼 것 같아요.
각 객체가 자신의 책임을 충분히 가지고 있는가?혹시 객체에게 줄 수 있는 책임을 Service가 대신 가지고 있지는 않은가?지금 모델이 실제 도메인을 잘 표현하고 있는가?
예를 들어 A와 B가 협력하는 로직이 Service에 있다면 정말로 A와 B 사이의 메시지로 표현할 수 없는지 한 번 더 고민해보면 좋을 것 같습니다.
도메인의 랜덤 침투
카드를 랜덤으로 뽑는 것도 결국 블랙잭 게임의 규칙 중 하나라고 볼 수 있지 않을까요? 그렇다면 저는 이것 역시 도메인 로직에 가깝다고 생각합니다.
또 한 가지 생각해볼 부분이 있는데요. 만약 Participant가 카드를 뽑는 책임을 가진다면 Participant는 단순히 "카드를 한 장 받는다"는 메시지를 수행하는 것일 수도 있어요.
이때 실제로 카드가 어떻게 섞였는지, 랜덤이 어떻게 생성되는지, 같은 세부 구현을 Participant가 직접 알고 있을까요?
Participant가 랜덤이나 카드 섞기 방식까지 알고 있다면, 도메인이 불필요한 정보까지 알게 되는 구조라고도 볼 수 있겠네요.
1️⃣ 도메인이 협력해야해서 서비스가 필요하다면, 책임 분리를 다시 생각해봐야 한다.
다만 “도메인들이 협력해야 해서 Service가 필요하다” 라는 상황이라면, 저는 한 번 더 이런 부분을 고민해볼 것 같아요.
각 객체가 자신의 책임을 충분히 가지고 있는가?혹시 객체에게 줄 수 있는 책임을 Service가 대신 가지고 있지는 않은가?지금 모델이 실제 도메인을 잘 표현하고 있는가?
사실 나는 이전까지 어떤 로직을 구현할 때 어느 도메인에 넣어야하지? 라는 고민이 될때마다 각 도메인의 책임 분리에 대해 의심해볼 생각은 못하고 서비스 클래스에 기댔던 것 같다.
해당 답변을 보고 나서 다시 내 코드를 천천히 읽어보았다.
public List<FinalResult> getFinalResults(Participant dealer, List<Participant> players) {
List<FinalResult> finalResults = new ArrayList<>();
int dealerScore = dealer.getScore();
int dealerWinCount = 0;
int dealerDrawCount = 0;
int dealerLoseCount = 0;
for (Participant player : players) {
int playerScore = player.getScore();
// 딜러 승
if (player.isBust()
|| (!dealer.isBust() && dealerScore > playerScore)
|| (dealer.isBlackjack() && !player.isBlackjack())) {
dealerWinCount++;
finalResults.add(new FinalResult(player.getName(), 0, 0, 1, false));
continue;
}
// 무승부
if (!dealer.isBust()
&& (dealerScore == playerScore)
&& ((player.isBlackjack() && dealer.isBlackjack())
|| (!player.isBlackjack() && !dealer.isBlackjack()))) {
dealerDrawCount++;
finalResults.add(new FinalResult(player.getName(), 0, 1, 0, false));
continue;
}
// 딜러 패배
dealerLoseCount++;
finalResults.add(new FinalResult(player.getName(), 1, 0, 0, false));
}
finalResults.add(
new FinalResult(dealer.getName(), dealerWinCount, dealerDrawCount, dealerLoseCount, true));
return finalResults;
}
최종 결과를 계산하는 책임을 어느 도메인에 넣어야 자연스러울까? 생각했을 때 가장먼저 게임의 모든 참가자(딜러, 플레이어)를 갖고 있는 `Participants`가 떠올랐다. 거기서 부터 책임을 하나하나 분배해 나갔다
1. `Participants.calculatePlayersMatchResult()`
public PlayersMatchResult calculatePlayersMatchResult() {
return players.calculateMatchResult(dealer);
}
`Participants`는 플레이어 리스트에 대한 일급 컬렉션인 `Players`를 갖고 있다. 플레이어들이 딜러와 비교하여 결과를 계산한다는게 자연스럽다고 생각하여 해당 기능을 `Players`에 위임하였다.
2. `Players.calculateMatchResult()`
public PlayersMatchResult calculateMatchResult(Dealer dealer) {
Map<Player, MatchCase> playerMatchResult = new LinkedHashMap<>();
for (Player player : players) {
if (player.isLose(dealer)) { // 딜러승, 플레이어 패배
playerMatchResult.put(player, MatchCase.LOSE);
continue;
}
if (player.isDraw(dealer)) { // 무승부
playerMatchResult.put(player, MatchCase.DRAW);
continue;
}
playerMatchResult.put(player, WIN);
}
return new PlayersMatchResult(playerMatchResult);
}
내부에서는 각 플레이어에 대해 딜러와 비교해 결과를 계산하고 있다. 단, 특정 플레이어가 딜러와 상대해서 지거나 비겼는지에 대한 로직(`isLose()`, `isDraw()`)은 개별적인 플레이어를 의미하는 `Player` 객체 내부에서 이루어지는게 자연스럽다고 생각했다.
3. `Player.isLose()`, `Player.isDraw()`
public boolean isLose(Dealer dealer) {
return isBust()
|| (!dealer.isBust() && dealer.getScore() > getScore())
|| (dealer.isBlackjack() && !isBlackjack());
}
public boolean isDraw(Dealer dealer) {
return !dealer.isBust()
&& (dealer.getScore() == getScore())
&& ((isBlackjack() && dealer.isBlackjack())
|| (!isBlackjack() && !dealer.isBlackjack()));
}
시퀀스 다이어그램으로 보면 다음과 같다.

2️⃣ 도메인이 어떻게(how) 행동를 하는지 생각하지말고 무슨(what) 행동을 하는지 생각해라
도메인의 랜덤 침투
카드를 랜덤으로 뽑는 것도 결국 블랙잭 게임의 규칙 중 하나라고 볼 수 있지 않을까요? 그렇다면 저는 이것 역시 도메인 로직에 가깝다고 생각합니다.
또 한 가지 생각해볼 부분이 있는데요. 만약 Participant가 카드를 뽑는 책임을 가진다면 Participant는 단순히 "카드를 한 장 받는다"는 메시지를 수행하는 것일 수도 있어요.
이때 실제로 카드가 어떻게 섞였는지, 랜덤이 어떻게 생성되는지, 같은 세부 구현을 Participant가 직접 알고 있을까요?
Participant가 랜덤이나 카드 섞기 방식까지 알고 있다면, 도메인이 불필요한 정보까지 알게 되는 구조라고도 볼 수 있겠네요.
나는 이 답변을 보고 큰 깨달음을 얻었다.
내가 카드를 뽑는 기능(`drawCard()`)을 서비스에 뒀던 이유는 랜덤 로직이 도메인에 침투하면 안된다고 생각했기 때문이다.
하지만 곰곰이 생각해보면, 카드를 뽑는 기능은 이미 인터페이스로 설계해두었었다.
public interface CardShuffler {
int getRandomCardIndex(int deckSize);
}
따라서 도메인 내부에서 해당 인터페이스를 사용하더라도 어떻게 뽑는지는 외부에서 결정되기 때문에 랜덤 로직이 침투하는 것은 아니라는 사실을 놓치고 있었다.
다시 말해, `Participant`가 '카드를 한장 뽑는다'는 책임을 가지더라도 어떻게 행동하는지는 다른 곳에서 결정된다. 랜덤으로 뽑는지, 그냥 고정된 상태로 뽑는지 등의 방법은 `Participant`가 모른다.
나는 머리 속에 이 사실을 상기한 상태로 리팩토링 해나갔다.
1. `Pariticipant.drawCard()`
protected void drawCard(Deck deck) {
hand.addCard(deck.draw());
}
여기선 카드를 뽑는다는 행동만 한다. 실제 카드를 어떻게 뽑는지는 `Deck`에서 이루어진다.
2. `Deck.draw()`
public class Deck {
private final List<Card> deck;
private final CardShuffler cardShuffler;
public Card draw() {
return deck.remove(cardShuffler.getRandomCardIndex(deck.size()));
}
}
`Deck`은 카드를 뽑는 `CardShuffler`를 가지고 있다. 언뜻보면 도메인에 랜덤로직이 들어가는 것처럼 보일 수 있지만 이는 인터페이스이기 때문에 도메인에 랜덤로직이 들어가는 것이 아니다.
🧐 그럼 Service는 언제 쓸까?
데이터베이스와 같은 외부 계층과의 연결 역할을 Service가 맡는다는 의견에는 저도 공감합니다.
앞으로 Service는 외부 계층과의 연결이 필요할 때만 사용하는 것으로 결론 내렸다. 도메인이 외부계층에 대해 알면 안되기 때문이다.
(예시: Controller -> `Service` -> Repository)
또한 단순히 도메인간의 협력이 필요하다고 해서 Service를 쓰고 싶어진다는 것은 도메인 책임 분리가 부족하다는 신호로 인식해야겠다.
'우테코 8기 > 본과정 탐구 일지' 카테고리의 다른 글
| [레벨1 선택미션] 《객체지향의 사실과 오해》 읽고 장기 도메인 모델 설계하기 (0) | 2026.03.22 |
|---|---|
| [레벨1 블랙잭] 딜러와 플레이어의 관계 (i.e. 상속을 잘 쓰려면?) (0) | 2026.03.14 |