
들어가며
오늘은 카드 효과별 턴 진행 로직을 어떻게 하면 더 가독성 좋고 깔끔하게 작성할 수 있을지 고민하며 하루 종일 코드와 씨름했다.
카드 타입에 따라 진행 순서를 다르게 처리해야 했는데,
매번 if 분기문을 늘어놓지 않고도 복잡하지 않게 효과를 적용할 수 있는 구조가 잘 떠오르지 않았다.
그래서 우선 “절차적으로 먼저 이해해보자”는 마음으로, 하나의 메서드 안에 전체 흐름을 주르륵 적어보며 로직을 시각적으로 이해하려고 했다.
그렇게 작성된 코드를 다시 들여다보니 중복되는 패턴들이 눈에 들어왔고,
이를 바탕으로 전략 패턴을 적용해 카드 효과를 동적으로 처리하는, 나름 만족스러운 구조를 만들어낼 수 있었다.
오늘 한 일
- 카드 효과별 턴 진행 전략 패턴 적용
- 카드 제출 방식 리팩토링 시도
📌 복잡한 카드 효과별 턴 로직, 어떻게 더 단순하게 만들까?
▪️ 작은 것부터 차근차근
로보77에는 세 가지 카드 타입이 존재한다.
- reverse: 게임 진행 순서를 반대로 뒤집는다.
- x2: 다음 플레이어가 연속으로 두 번 카드를 제출해야 한다.
- sum: 제출한 카드의 숫자를 합계에 더한다.
여기서 문제가 시작됐다.
규칙만 보면 단순해 보이지만, 이 효과들을 한 번에 모두 고려하며 게임 순서 진행 기능을 구현하려니
로직이 머릿속에서 계속 꼬여버렸다.
턴 순서가 바뀌고, 두 번 연속 진행되고, 합계 계산도 해야 하고…
이걸 객체 간 협력을 지키면서 한 번에 작성하려니 너무 막막했다.
그래서 접근 방식을 다음과 같이 바꿨다. 우선 뼈대를 구축하고 살을 덧붙이는게 진행하기 쉬울 것 같았다.
- 카드 효과를 전부 고려하지 말고, 가장 단순한 sum 효과부터 먼저 구현해보자.
- 객체마다 책임을 분리하기보다, 하나의 클래스에 절차적으로 작성하며 전체 흐름을 이해해보자.
그 결과, 아주 웅장한 만능 객체 RoboGame이 탄생했다.
public class RoboGame {
private final InputView inputView;
private final OutputView outputView;
public RoboGame(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}
public void run() {
String playerName = inputView.readPlayerName();
Deck deck = new Deck();
List<Hand> hands = deck.shareCards();
Player player = new Player(playerName, hands.getFirst());
Player bot = new Player("bot", hands.getLast());
int sum = 0;
play(sum, player, deck, bot);
}
private void play(int sum, Player player, Deck deck, Player bot) {
while (true) {
outputView.showSumAndHandMessage(sum, player.getHand());
String cardToSubmit = inputView.readCardToSubmit();
Card submittedCard = Card.from(cardToSubmit);
boolean hasCard = player.hasSubmittedCard(submittedCard);
if (!hasCard) {
throw new IllegalArgumentException(ExceptionMessage.INVALID_CARD.getMessage());
}
sum = getSum(deck, player, submittedCard, sum);
if (hasEndCondition(sum)) {
outputView.showWinner(sum, bot.getName());
break;
}
sum = getSum(deck, bot, sum);
if (hasEndCondition(sum)) {
outputView.showWinner(sum, player.getName());
break;
}
}
}
private int getSum(Deck deck, Player player, Card submittedCard, int sum) {
Card newCard = deck.shareCard();
player.submitCard(submittedCard, newCard);
if (submittedCard.getCardType() == CardType.SUM) {
sum += submittedCard.getValue();
}
outputView.showSubmittedCard(player.getName(), submittedCard);
return sum;
}
private int getSum(Deck deck, Player bot, int sum) {
Card newCard2 = deck.shareCard();
Card submittedCard2 = bot.submitCardByBot(newCard2);
if (submittedCard2.getCardType() == CardType.SUM) {
sum += submittedCard2.getValue();
}
outputView.showSubmittedCard(bot.getName(), submittedCard2);
return sum;
}
private boolean hasEndCondition(int sum) {
return sum > 77 || sum % 11 == 0;
}
}
▪️ TurnManager 객체 필요성 인식
절차적으로 코드를 작성한 결과, TurnManager라는 객체의 필요성을 강하게 느끼게 되었다.
현재는 유저 → 봇 순서로 메서드를 단순히 나열하며 진행 흐름을 강제하고 있기 때문이다.
reverse나 x2 같은 카드 효과가 등장하면 순서가 동적으로 변할 수밖에 없기 때문에,
순서를 동적으로 관리하는 객체의 필요성을 느꼈다.
따라서 TurnManager가 플레이어들의 순서를 어떻게 관리할지 고민하다가
List<Player>와 currentIndex로 순서를 넘기는 방식을 떠올렸다.
하지만 구현을 해보니 금방 한계가 드러났다.
1. List 기반 턴 관리의 한계
인덱스를 증가시키며 턴을 넘기도록 로직을 작성했는데, 이 방식의 문제는 명확했다.
int currentIndex = 0;
Player current = players.get(currentIndex);
currentIndex = (currentIndex + 1) % players.size();
인덱스 계산으로 인해 복잡도가 증가했으며,
카드 효과가 늘어나거나 플레이어가 추가·제거될수록 현재 플레이어를 결정하는 로직 자체가 점점 복잡해질 것이 눈에 보였다.
2. Queue vs Deque
이후 턴이라는 개념을 다시 생각해보니,
앞에서 하나 뽑아서 뒤에 넣는 행동에 가깝다고 느껴져서 Queue가 떠올랐다.
하지만 Queue는 단방향 흐름(FIFO)만 가능하기 때문에 reverse와 같은 역방향 처리가 불편했다.
반면 Deque는 양쪽 끝에서 삽입/삭제가 가능해서 역방향 처리와 같은 턴의 흐름을 유연하게 다룰 수 있다.
그래서 Deque를 기반으로한 TurnManager를 만들었다.
▪️ TurnPolicy 전략 패턴 도입
Deque를 통해 턴의 흐름을 유연하게 다룰 수 있었지만,
다음 플레이어를 구하는 로직을 작성하려고 하니 문제가 다시 드러났다.
reverse, x2, sum처럼 카드마다 턴 이동 규칙이 다르다 보니
TurnManager 내부에서 또다시 if문 분기가 늘어나기 시작한 것이다.
만약 카드 타입이 늘어나게 된다면 메서드 길이는 점점 늘어나고,
가독성 또한 안 좋을 것 같은데 어떡하지? 하는 고민이 컸던 것 같다.
public class TurnManager {
private static final int SPECIAL_NUMBER_OF_PLAYERS = 2;
private final Deque<Player> players;
// 생성자 생략
public Player findNextTurnPlayer(Card submittedCard) {
CardType type = submittedCard.getCardType();
// DOUBLE TURN (예: SUM 카드)
if (type == CardType.SUM) {
Player current = players.pollFirst();
Player nextPlayer = players.peekFirst();
players.addFirst(nextPlayer);
players.addLast(current);
return nextPlayer;
}
// NORMAL TURN
if (type == CardType.NORMAL) {
Player current = players.pollFirst();
players.addLast(current);
return players.peekFirst();
}
// REVERSE TURN
if (type == CardType.REVERSE) {
if (players.size() == SPECIAL_NUMBER_OF_PLAYERS) {
return players.peekFirst();
}
reverseOrder();
return players.peekFirst();
}
throw new IllegalArgumentException(ExceptionMessage.INVALID_CARD.getMessage());
}
}
이때 문득 프리코스 1-2주차 다른 분들의 코드를 보며 배웠던 전략 패턴이 생각났고,
카드 효과에 따라 턴이 어떻게 이동해야 하는지를 별도 정책 객체로 분리할 수 있지 않을까? 라는 생각이 들었다.
이후 정확히 내 상황에 맞는 패턴인지 확인하고 싶어
『디자인 패턴의 아름다움』 책에서 주문 타입에 따라 할인 정책이 바뀌는 예제를 다시 읽어보면서
나 또한 비슷한 상황인지, 전략 패턴을 통해 순서를 유연하게 관리할 수 있을지 스스로 점검해 보았다.
"턴을 구하는 행위 자체는 동일하지만,
카드마다 ‘어떻게’ 구하는지가 다르다.
그렇다면 이 ‘다름’을 전략으로 분리할 수 있지 않을까?”
TurnManager는 상태(순서·플레이어)만 관리하고,
턴 이동에 필요한 구체적인 규칙은 카드 타입별 TurnPolicy에게 맡기는 것이다.
따라서, TurnPolicy 인터페이스를 만들고,
reverse, x2, sum에 맞는 정책 구현체들을 각각 작성했다.
그리고 이들을 카드 타입에 따라 선택할 수 있도록
TurnPolicyFactory를 도입했다.
이 팩토리에서는 Map 캐싱을 활용해 카드 타입 → 전략 객체를 매핑함으로써
TurnManager나 게임 로직 내부에서 불필요한 if/else 분기를 완전히 제거할 수 있었다.
그 결과
- TurnManager는 오로지 상태만 관리하고,
- 턴을 어떻게 옮길지는 전략 객체가 결정하며,
- 카드 타입이 늘어나도 TurnManager는 수정할 필요가 없는 구조가 되었다.
⬇️ 결과 커밋
이번 턴 관리 구조를 만들면서 가장 크게 느낀 점은,
좋은 코드는 한 번에 만들어지는 것이 아니라 여러 번의 작은 개선을 통해 진화한다는 것이었다.
처음에는 머릿속이 도화지처럼 아무것도 떠오르지 않았지만,
일단 가장 단순한 부분부터 절차적으로 구현해보니 이전에는 보이지 않던 구조적 문제가 하나씩 드러났다.
그 문제를 해결하려다 보니 TurnManager가 필요하다는 걸 알게 됐고,
Deque를 적용하며 흐름을 정리하다가
카드 효과별 규칙을 전략 패턴으로 분리하는 방향까지 자연스럽게 이어졌다.
큰 그림을 한 번에 떠올리려 하기보다 작게 시도해보고 개선하는 방식이 얼마나 중요한지 다시 느꼈다!
📌 전략 패턴, 카드 제출 방식에도 통할 줄 알았다,,, (negative)
턴 관리 구조를 잘 세운 후,
나는 같은 패턴을 ‘카드 제출 방식’에도 적용해보려고 했다.
처음에는 사람과 봇 모두 “카드를 제출한다”는 면에서 동일한 행위를 하고 있고,
사람은 직접 고르고, 봇은 자동으로 고르는 차이만 있어 보였다.
제출 방식만 다르기 때문에,
제출 방식을 하나의 전략으로 추상화하면 더 깔끔해지지 않을까? 라는 생각으로
CardSubmitStrategy 인터페이스와 AutoSubmitStrategy, ManualSubmitStrategy를 도입해 보았다.
public class AutoSubmitStrategy implements CardSubmitStrategy {
@Override
public Card submit(Hand hand) {
return hand.removeCard();
}
}
public class ManualSubmitStrategy implements CardSubmitStrategy {
private final Card submittedCard;
@Override
public Card submit(Hand hand) {
return submittedCard;
}
}
하지만 구현을 진행할수록 위화감이 점점 커졌다.
🤔 나는 왜 해당 코드가 어색하다고 느꼈는가?
1. 전략이 스스로 일을 하지 않는다

전략 패턴은 전략 객체가 알고리즘을 수행한다는 전제가 있다.
하지만 ManualSubmitStrategy는 스스로 카드 선택을 하지 않는다.
- AutoSubmitStrategy → 메서드를 호출하는 로직 존재
- ManualSubmitStrategy → 외부가 미리 고른 값을 넣어줘야 함
즉, 전략이 행동의 주체가 아니라, 값을 보관하는 객체가 되어버렸다.
2. Auto, Manual 두 객체는 서로 추상화 수준이 다르다
전략 패턴이 유지되려면 두 전략이 같은 추상화 수준에 있어야 한다.
하지만 내가 구현한 전략 두 개는 추상화 수준이 맞지 않았다.
- Auto → Hand로부터 어떤 카드를 낼지 계산함
- Manual → 인자로 받은 hand는 무시하고 이미 선택된 것을 단순 반환
이로 인해, "submit(Hand)를 호출하면 이 전략이 Hand를 보고 어떤 카드를 낼지 계산해준다"
라는 인터페이스 의미가 흐려져, 더이상 명확한 책임을 지고있지 못하게 된다.
3. 전략 패턴을 통해 얻는 이점을 생각해보았을 때 떠오르지 않았다
Manual 전략을 통해 얻은 이점은 무엇일까?
사실상 없다.
전략이 하는 일이 단순히 값을 반환하는 것이기 때문에
전략으로 부터 얻는 이득보다는, 전략을 생성하기 위해 더 복잡한 구조가 필요해졌다.
정리하자면
둘 다 카드를 제출한다는 결과는 같지만,
제출이라는 문제를 해결하는 방식과 책임이 완전히 다르기 때문에 전략 패턴으로 묶을 수 없다.
이러한 이유로 카드 제출 로직에서는 전략 패턴이 도움이 안 되는 것 같아 쓰기 않기로 결정하였다.
카드 제출하는 메서드를 전략 패턴으로 통일하고 싶었지만 결국 실패하였다.
전략 패턴은 "어? 행위가 다르니 -> 전략으로 빼야지" 와 같이 if-else를 없애는 용도가 아니라,
동일한 문제를 다른 알고리즘으로 해결할 때 의미가 있다는 사실을 이번 실패로 경험했다.
전략 패턴의 본질은 단순히 분기를 제거하는 것이 아니라, 아래 세 가지를 분리하여 코드의 복잡도를 낮추기 위함인 것이다.
- 전략의 정의(무엇을 하는가)
- 전략의 생성(어떤 전략을 쓸지 결정하는가)
- 전략의 사용(어떻게 실행하는가)
마치며
오늘은 전략패턴에 대해 열심히 파헤친 것 같다.
겉으로 보기에 행위가 다르다는 이유만으로 전략으로 분리하면
오히려 원하는 구조를 얻지 못한다는 사실을 몸소 느꼈다.
전략 패턴은 동일한 문제를 서로 다른 방식으로 해결하는 알고리즘을 분리할 때 의미가 있는 도구였다.
턴 이동 로직에서는 이 조건이 우연히 들어맞아 깔끔한 구조가 나왔지만,
카드 제출 로직에서는 두 전략의 책임이 달라 전략 패턴이 어울리지 않았다.
앞으로는 아래 3가지에 대해 신중히 고민해보고 전략패턴을 도입해봐야겠다.
- 해결해야 하는 문제가 같은가?
- 단지 방식(알고리즘)만 다른가?
- 확장이 자주 일어나는 영역인가?
절차적으로 코드를 적어보는 단순한 작업에서 시작해
턴 구조를 개선하고, 전략 패턴을 도입하고,
나아가 전략 패턴 적용에 실패해 위화감의 원인을 분석하기까지 머리를 정말 열심히 굴린 것 같다.
머리가 지끈지끈 하니,, 이만 퇴고!
'Project > Robo77' 카테고리의 다른 글
| 로보 77 구현기 8일 차 - 동작 우선 코드, 객체지향으로 CPR 하기 (0) | 2025.11.14 |
|---|---|
| 로보 77 구현기 7일 차 - 빈 깡통이었던 디스코드 봇에 생명 불어넣기 (0) | 2025.11.13 |
| 로보 77 구현기 6일 차 - Phase 2를 향한 재정비와 첫 디스코드 봇 생성! (0) | 2025.11.12 |
| 로보 77 구현기 1일 차 - 예상보다 험난했던 프로젝트 세팅과 카드 제출 구현 (0) | 2025.11.07 |
| 로보 77 구현기 0일 차 - 왜 이 도전을 하는가? (0) | 2025.11.06 |