항해 백엔드 3주차 회고(WIL)

JUNYOUNG·2025년 4월 11일
post-thumbnail

이번주차의 과제는 지난주에 주어진 시나리오를 분석한 것을 토대로 클린 아키텍처, 헥사고날 아키텍처 등의 구조를 기반으로 테스터블하고 객체지향적인 코드를 설계하는 것이었다.

이론적으로만 알고 있던 DDD, 계층 간 분리, DIP 등의 개념을 실제 코드에 녹여내는 과정이었고, 발제와 피드백을 통해 결국 이 구조들이 추구하는 본질은 "도메인을 외부로부터 격리하고, 의존성을 역전시킴으로써 변경에 강한 구조를 만드는 것"이라는 걸 체감할 수 있었다.

초기에는 레이어드 아키텍처처럼 Controller → Service → Repository 구조를 익숙하게 생각했지만, 이번 과제를 통해 Interface → Application (Facade / Service) → Domain ← Infrastructure 형태로 책임을 재정의하면서 구조에 대한 시야가 훨씬 넓어졌다.

헥사고날 아키텍처를 처음엔 어렵게 느꼈지만, '도메인이 중심에 있고 in/out 포트를 통해 어댑터들과 통신한다’는 흐름이 결국은 도메인을 보호하기 위한 장치라는 걸 알게 되었고, 이 흐름은 결국 클린 아키텍처의 계층 분리와도 크게 다르지 않다는 걸 깨달았다.

다만 헥사고날의 경우 in-port(UseCase)와 out-port(Repository 등)를 반드시 인터페이스로 정의해야 한다는 규칙이 있어서 더 엄격하게 느껴지긴 했는데, 결국 중요한 건 "조직에서 어떤 컨벤션으로 이 구조를 유연하게 해석하느냐" 라는 점이었다. 어떤 아키텍처든 본질은 변경에 유연하고, 도메인을 중심에 두는 설계라는 점에서 같다고 생각한다.


1. 도메인과 엔터티 분리에 대한 고민

초기 설계 단계에서는 클린 아키텍처 원칙에 따라 도메인 모델과 JPA 엔터티를 분리하는 방식을 고려했다. JPA 엔터티는 영속성과 밀접하게 연결된 객체이기 때문에, 이를 비즈니스 도메인으로 직접 사용하는 것은 객체지향적인 설계 원칙과는 다소 맞지 않다는 판단이었다.

또한, 순수한 도메인 모델을 별도로 두면 테스트 용이성과 구조적 명확성이 높아진다는 장점도 있다.하지만 실제로 적용해보니, 도메인 ↔ 엔터티 매핑 단계가 추가되며 복잡도가 올라가고, JPA가 제공하는 지연 로딩, 영속성 컨텍스트, 변경 감지 등 핵심 기능들을 활용하기 어려워지는 단점도 있었다.

결국 이번 과제에서는 도메인과 엔터티를 분리하지 않고, 동일한 객체 내에서 비즈니스 로직과 영속성을 함께 다루는 구조로 설계하기로 결정했다.상황에 따라 트레이드오프가 필요한 영역이라는 걸 경험적으로 체감할 수 있었고,이번 과제에서는 복잡도보다 실용성과 학습 목적에 맞는 구조 선택이 더 중요하다고 판단했다.

2. 전략 패턴 적용 고민 - 쿠폰 발급

이번 과제에서는 도메인 복잡도가 높은 쿠폰 발급 기능에 전략 패턴을 도입할 수 있을지 고민했다.

현재 구조에서는 Coupon 엔티티 내부에서 모든 유효성 검증과 상태 변경을 수행하고 있었지만,

쿠폰 타입별 정책이 달라질 수 있는 상황을 고려해 전략 패턴 기반 설계로 분리하면 더 유연할 것 같다는 판단이 들었다.

예를 들어, CouponType 별로 정책을 다형적으로 위임하는 방식은 아래와 같다:

public interface CouponIssuePolicy {
    void validateIssuable(Coupon coupon, Long userId);
}

public class LimitedCouponPolicy implements CouponIssuePolicy {
    public void validateIssuable(Coupon coupon, Long userId) {
        if (coupon.isExpired()) throw new CouponException.ExpiredException();
        if (coupon.isExhausted()) throw new CouponException.AlreadyExhaustedException();
    }
}

또한 발급 로직 자체도 전략으로 위임할 수 있다:

public interface CouponIssueStrategy {
    CouponResult execute(IssueCouponCommand command);
}

@RequiredArgsConstructor
public class LimitedCouponIssueStrategy implements CouponIssueStrategy {
    private final CouponReader couponReader;
    private final CouponIssueWriter couponIssueWriter;

    @Override
    public CouponResult execute(IssueCouponCommand command) {
        Coupon coupon = couponReader.findByCode(command.couponCode());
        coupon.validateUsable();  // 정책 위임 또는 내부 처리

        if (couponIssueWriter.hasIssued(command.userId(), coupon.getId())) {
            throw new CouponException.AlreadyIssuedException(command.userId(), command.couponCode());
        }

        CouponIssue issue = couponIssueWriter.save(command.userId(), coupon);
        return CouponResult.from(issue);
    }
}

쿠폰 발급 시나리오 시퀀스 다이어그램

3. 구현 관점 정리

  • 각 도메인은 Facade → Service → Domain 계층으로 책임을 명확히 분리
  • 외부 입력은 Request/Command, 출력은 Response/Result로 구분하여 의존 역전 실현
  • 복잡한 유즈케이스(ex. 결제 성공 시 주문 확정 + 이벤트 저장)는 Facade 계층에서 orchestration
  • 결제 방식(Balance, External 등)은 PaymentProcessor 전략으로 분리하여 확장성 확보
  • 도메인 이벤트 기반 아웃박스 구조 설계 (OrderEvent, EventRelayScheduler 등)
  • 전 계층 단위 테스트 완비 (Repository 없이 mock 기반 테스트)

이번 주는 정말 구현도 구현이지만, 설계를 어떻게 풀어낼지를 깊이 고민한 한 주였다. 추상적으로만 알고 있던 아키텍처 개념들이 실제 코드에서 왜 필요한지, 어떤 효과를 주는지 몸으로 체감했고, 도메인 중심 설계가 단순한 구조 설계를 넘어서 팀 전체 개발 방향성과 유지보수성에 직결된다는 걸 느낄 수 있었다.

다음 주에는 인프라 계층 구현, 통합 테스트, 이벤트 발행 등까지 진행할 예정인데, 이번에 다져놓은 구조를 기반으로 어떻게 확장해갈 수 있을지 기대된다. 특히 Coupon, WaitingQueue 같은 복잡한 상태 기반 도메인을 어떻게 유연하게 풀어낼지 스스로도 궁금하다.

profile
Onward, Always Upward - 기록은 성장의 증거

0개의 댓글