리팩토링 -도메인 분리의 실제 사례: Card 도메인의 전략 패턴 적용기

junsung kim·2025년 5월 6일

[project]- thirdTool

목록 보기
9/29

🍀 도메인 분리의 실제 사례: Card 도메인의 전략 패턴 적용기

문제 상황 1: Card 도메인의 과도한 책임

초기 Card 도메인 설계에서는 카드의 상태뿐 아니라 초기화, 복습 결과 처리, 주기 조정 등 여러 3day context와 permanent에 대해서 한번에 처리해야하는 문제가 발생 → 오브젝트 사상 하에 문제가 된다고 판단

카드를 생성하고 초기화하는 과정에서 Card 도메인 내에 두 가지 이상의 행동이 존재하고 있다는 사실을 인식했고, 이는 SRP(단일 책임 원칙) 위배라는 의심을 받기에 충분했습니다.

문제 해결 방향: 행동과 상태의 분리 고민

🔍 해결 방법에 대한 브레인스토밍

  1. 기존 상태 유지 + Service 레벨 분기→ 초기 아이디어 service 레이어에서 context 분리를 고민
    • if(card.isArchived) 조건문으로 분기
    • ❌ 로직 중복과 확장성 부족
  2. Card에 모든 행동 구현 (failReview, resetGroup 등)
    • ❌ 책임이 Card 내부에 너무 집중됨
    • ❌ 테스트 어려움, 재사용성 낮음
  3. 추상 클래스 기반 전략 분리 (AbstractStudyPolicy)
    • apply(Card) 안에서 공통 로직 → doApply() 메서드 분리
    • 공통 상태(User, Deck 등) 공유 가능
    • ✅ 상태 공유 용이
    • ❌ 동적 교체 어려움, 전략 확장에 불리
  4. 인터페이스 기반 전략 패턴 적용 (Strategy Pattern) → 이것도 2가지 방식에 대해서 고민
    • CardBehavior 인터페이스로 행동을 추상화
    • ThreeDayCardBehavior, PermanentCardBehavior로 분리
    • ✅ 책임 분리 명확
    • ✅ 테스트 용이, 상태 재사용 가능
    • ✅ OCP(Open-Closed Principle) 만족

선택된 설계: 전략 패턴 기반 구조

핵심 설계 방향

  • Card는 상태만 들고 있는 객체 (Context)
  • CardBehavior는 행동만 정의 (Strategy)
  • 각 전략은 Card의 행동 메서드만 호출하며, 상태를 직접 변경하지 않음 (캡슐화 유지)

구조 요약

Card
├── 상태: dueDate, easeFactor, reps 등
└── 전략 주입: CardBehavior

CardBehavior (인터페이스)
├── ThreeDayCardBehavior
└── PermanentCardBehavior

CardBehaviorFactory → 상태(archived)로 전략 선택

실제 코드 구조

1. 인터페이스 정의

public interface CardBehavior {
    void processStudyResult(Card card, StudyResult result);
    void resetDueGroup(Card card, String groupName);
}

2. 전략 구현체: ThreeDayCardBehavior

public class ThreeDayCardBehavior implements CardBehavior {
    public void processStudyResult(Card card, StudyResult result) {
        switch (result) {
            case AGAIN -> card.failReview(0.8f, 1);
            case NORMAL -> card.restoreToGeneralStudy();
            case GOOD -> card.successReview();
        }
    }
    public void resetDueGroup(Card card, String groupName) { }
}

3. 전략 구현체: PermanentCardBehavior

public class PermanentCardBehavior implements CardBehavior {
    public void processStudyResult(Card card, StudyResult result) { }
    public void resetDueGroup(Card card, String groupName) {
        if (groupName == null || groupName.isBlank()) {
            card.keepCurrentDue();
            return;
        }
        switch (groupName) {
            case "3day", "1week", "2week", "1month" -> card.resetDueGroup(groupName);
            default -> throw new IllegalArgumentException("잘못된 그룹 선택: " + groupName);
        }
    }
}

4. 전략 선택 팩토리

public class CardBehaviorFactory {
    private static final CardBehavior THREE_DAY = new ThreeDayCardBehavior();
    private static final CardBehavior PERMANENT = new PermanentCardBehavior();

    public static CardBehavior from(Card card) {
        return card.isArchived() ? PERMANENT : THREE_DAY;
    }
}

5. CardService 리팩토링

@Transactional
public void processStudyResult(Long cardId, StudyResult result) {
    Card card = cardRepository.findById(cardId).orElseThrow();
    CardBehavior behavior = CardBehaviorFactory.from(card);
    behavior.processStudyResult(card, result);
}

정리: 왜 전략 패턴이 적합한가?

항목설명
SRP 유지Card는 상태, 행동은 전략에서 분리
캡슐화전략은 Card의 메서드만 호출 (내부 필드 접근 없음)
테스트 용이전략 단위로 독립 테스트 가능
유지보수성새로운 행동 추가 시 OCP 만족, Card 수정 없음

결론

Card는 상태만 가진 순수한 객체이고, 다양한 행동은 전략 객체에 위임됨으로써 객체지향의 원칙을 충실히 지켰습니다. 이 구조는 학습 도구의 복잡한 상태 흐름을 유연하고 확장 가능하게 만들어주는 핵심 설계 전략이 됩니다.

전략 패턴으로 바뀌고 좋았던 점들

전략 패턴으로의 전환: 행동의 분리와 캡슐화

이 문제를 해결하기 위해 Strategy 패턴을 도입했습니다.

즉, "카드가 어떻게 복습되어야 하는가"에 대한 행동을 도메인 객체가 결정하지 않고, 전략 객체에 위임한 것입니다.

public interface StudyPolicy {
    void applyTo(Card card);
}

이제 카드 객체는 자신이 복습되어야 하는 방식에 따라 적절한 StudyPolicy 전략을 주입받고, 전략이 행동을 수행하게 됩니다.

장점 요약:

  • 조건문 제거 → 코드 가독성 증가
  • 새로운 정책 도입 시 기존 코드 수정 불필요 (OCP 원칙 준수)
  • 로직 재사용 가능성 증가

유지보수성과 확장성 향상

Strategy 패턴은 복잡도가 커질수록 빛을 발합니다.

기존 구조전략 패턴 적용 후 구조
여러 if/else로 정책 분기각 정책은 독립된 클래스로 구현됨
변경 시 기존 도메인도 수정 필요변경 시 전략 클래스만 수정하면 됨
확장이 어려움전략 클래스를 추가만 하면 됨

즉, 새로운 학습 방식이나 카드의 특수한 학습 규칙이 생기더라도, 전략 클래스 하나만 추가하면 되기 때문에 유지보수가 대폭 쉬워집니다.

profile
edit하는 개발자! story 있는 삶

0개의 댓글