이번주차의 과제는 지난주에 주어진 시나리오를 분석한 것을 토대로 클린 아키텍처, 헥사고날 아키텍처 등의 구조를 기반으로 테스터블하고 객체지향적인 코드를 설계하는 것이었다.
이론적으로만 알고 있던 DDD, 계층 간 분리, DIP 등의 개념을 실제 코드에 녹여내는 과정이었고, 발제와 피드백을 통해 결국 이 구조들이 추구하는 본질은 "도메인을 외부로부터 격리하고, 의존성을 역전시킴으로써 변경에 강한 구조를 만드는 것"이라는 걸 체감할 수 있었다.
초기에는 레이어드 아키텍처처럼 Controller → Service → Repository 구조를 익숙하게 생각했지만, 이번 과제를 통해 Interface → Application (Facade / Service) → Domain ← Infrastructure 형태로 책임을 재정의하면서 구조에 대한 시야가 훨씬 넓어졌다.
헥사고날 아키텍처를 처음엔 어렵게 느꼈지만, '도메인이 중심에 있고 in/out 포트를 통해 어댑터들과 통신한다’는 흐름이 결국은 도메인을 보호하기 위한 장치라는 걸 알게 되었고, 이 흐름은 결국 클린 아키텍처의 계층 분리와도 크게 다르지 않다는 걸 깨달았다.
다만 헥사고날의 경우 in-port(UseCase)와 out-port(Repository 등)를 반드시 인터페이스로 정의해야 한다는 규칙이 있어서 더 엄격하게 느껴지긴 했는데, 결국 중요한 건 "조직에서 어떤 컨벤션으로 이 구조를 유연하게 해석하느냐" 라는 점이었다. 어떤 아키텍처든 본질은 변경에 유연하고, 도메인을 중심에 두는 설계라는 점에서 같다고 생각한다.
초기 설계 단계에서는 클린 아키텍처 원칙에 따라 도메인 모델과 JPA 엔터티를 분리하는 방식을 고려했다. JPA 엔터티는 영속성과 밀접하게 연결된 객체이기 때문에, 이를 비즈니스 도메인으로 직접 사용하는 것은 객체지향적인 설계 원칙과는 다소 맞지 않다는 판단이었다.
또한, 순수한 도메인 모델을 별도로 두면 테스트 용이성과 구조적 명확성이 높아진다는 장점도 있다.하지만 실제로 적용해보니, 도메인 ↔ 엔터티 매핑 단계가 추가되며 복잡도가 올라가고, JPA가 제공하는 지연 로딩, 영속성 컨텍스트, 변경 감지 등 핵심 기능들을 활용하기 어려워지는 단점도 있었다.
결국 이번 과제에서는 도메인과 엔터티를 분리하지 않고, 동일한 객체 내에서 비즈니스 로직과 영속성을 함께 다루는 구조로 설계하기로 결정했다.상황에 따라 트레이드오프가 필요한 영역이라는 걸 경험적으로 체감할 수 있었고,이번 과제에서는 복잡도보다 실용성과 학습 목적에 맞는 구조 선택이 더 중요하다고 판단했다.
이번 과제에서는 도메인 복잡도가 높은 쿠폰 발급 기능에 전략 패턴을 도입할 수 있을지 고민했다.
현재 구조에서는 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);
}
}

이번 주는 정말 구현도 구현이지만, 설계를 어떻게 풀어낼지를 깊이 고민한 한 주였다. 추상적으로만 알고 있던 아키텍처 개념들이 실제 코드에서 왜 필요한지, 어떤 효과를 주는지 몸으로 체감했고, 도메인 중심 설계가 단순한 구조 설계를 넘어서 팀 전체 개발 방향성과 유지보수성에 직결된다는 걸 느낄 수 있었다.
다음 주에는 인프라 계층 구현, 통합 테스트, 이벤트 발행 등까지 진행할 예정인데, 이번에 다져놓은 구조를 기반으로 어떻게 확장해갈 수 있을지 기대된다. 특히 Coupon, WaitingQueue 같은 복잡한 상태 기반 도메인을 어떻게 유연하게 풀어낼지 스스로도 궁금하다.