Loopers

설계는 정답이 없다고 했지만, 그래도 너무 어렵잖아

그zi운아이 2025. 8. 1. 16:53

🧭 시작은 단순했습니다

Loopers 과제 Round 3에서는
상품, 브랜드, 좋아요, 주문, 결제 도메인을 구현해야 했습니다.

기능만 보면 단순한 CRUD처럼 보였지만,
실제로는 도메인 간 협력, 구조 확장, 책임 분리 등
설계 측면에서 많은 고민이 필요했습니다.


📦 1. 상품 조회 – 정렬 기준이 늘어날수록 구조는 어떻게 바뀔까?

처음엔 이렇게 생각했습니다:

“상품 리스트를 가져오고, 브랜드/좋아요 정보를 붙이면 되지!”

이 방식의 장점은 도메인 경계가 명확하다는 점이었습니다.

  • Product는 ProductService에서
  • Brand와 Like는 각각의 서비스에서 분리되어 처리

하지만 정렬 기준이 다양해지면서 문제가 생겼습니다.
예를 들어:

  • 좋아요 순 정렬
  • 브랜드 인기순 정렬
  • 주문/리뷰 기반 정렬 가능성

기존 구조에서는 정렬을 Product 기준으로만 처리할 수 있었기 때문에,
다른 기준으로 정렬하려면 조합 흐름을 분기해야 했고
→ 코드 중복과 복잡도가 증가했습니다.


🔁 그래서 선택한 해결책은 "조인 + DTO"

정렬 조건마다 흐름을 나누는 대신,
필요한 정보를 조인하여 한 번의 쿼리로 조회하고
Projection된 DTO로 결과를 반환하는 방식으로 바꿨습니다.


⚠️ 그런데 이건 도메인 침해 아닐까?

기술적으로는

“조회니까 조인하면 되지”
라고 할 수 있지만, 설계 관점에선 고민이 생겼습니다:

  • RepositoryImpl에서 도메인 간 조인이 이뤄지면 경계가 모호
  • 정렬 조건이 늘어날수록 특정 도메인에 의존한 쿼리가 증가
  • 유지보수 시 다른 도메인 변경에 따라 조회 쿼리까지 영향 받을 가능성

그래서 다음과 같은 절충안을 정했습니다:

  • 도메인 로직이 아닌 단순 데이터 조회만 수행
  • Projection 결과는 DTO로만 사용
  • 해당 DTO에서 다른 도메인 기능을 호출하지 않음

완벽한 정답은 아닐 수 있지만,
현재로선 현실적인 선택이었습니다.


🧩 2. 결제 흐름 – 전략은 썼다, 그런데 책임은 누구인가?

결제 흐름은 다음과 같았습니다:

  1. 상품 재고 차감
  2. 주문 생성
  3. 결제
  4. 주문 상태 전이

처음엔 포인트 결제만 있었지만,
**카드 또는 복합 결제(포인트 + 카드)**로 확장될 가능성이 있었습니다.

그래서 결제 방식은
👉 전략 패턴(Strategy Pattern) 으로 구현했습니다.


💡 전략 패턴의 장점

  • 새로운 결제 방식이 생겨도 기존 코드 수정 없이 전략만 추가
  • OCP(Open-Closed Principle) 만족, 확장성 우수

🤔 그런데 전략 선택은 누가 책임져야 할까?

전략을 선택하는 책임이 PaymentService에 있는 게 맞는가?

이건 결제 방식이라는 ‘흐름’에 가까운 컨텍스트이므로
UseCase나 Facade에서 선택하는 게 맞지 않을까 생각했습니다.


✅ 그래서 책임을 Facade로 이동

  • 결제 전략은 OrderFacade에서 선택
  • PaymentService는 실행만 담당

또는
👉 전략 선택과 실행을 묶는 PaymentProcessor 컴포넌트 도입도 고려할 수 있습니다.


💡 Composite 전략까지 확장 고려

“포인트 부족하면 나머지 카드로 결제해줘요”

같은 요구사항을 대비해
CompositePaymentStrategy 구조도 함께 설계했습니다.

public class CompositePaymentStrategy implements PaymentStrategy {

    private final List<PaymentStrategy> delegates;

    @Override
    public void pay(PaymentCommand command) {
        for (PaymentStrategy strategy : delegates) {
            strategy.pay(command);
        }
    }

    @Override
    public boolean supports(PaymentMethod method) {
        return delegates.stream().anyMatch(s -> s.supports(method));
    }
}

🤯 그리고 현실은... JPA가 기억에 남지 않음

설계는 좋았지만, 실제 구현에선 많은 오류를 마주쳤습니다.

  • Q클래스 미생성
  • queryDsl 작성 오류로 인한 에러들
  • 잘못된 연관관계 설정 

좋아요 정렬 쿼리 하나에 반나절이 날아갔습니다 😇


✅ 마무리

이번 과제를 통해 느낀 건

"코드는 지워지지만, 설계 고민은 남는다"

좋아요 정렬 하나로 도메인 경계까지 고민하게 됐고,
포인트 결제 하나로 전략 패턴과 책임 분리까지 생각하게 됐습니다.

정답은 아닐 수 있지만,
고민들이 코드에 녹아 있다는 것만으로도 의미가 있다고 생각합니다.

 


✍️ 다음 글 예고:

“Q클래스가 생성이 안 됩니다”… 왜 매번 그놈의 Gradle은 나를 배신하는가
실전에서 겪은 QueryDSL 삽질 일지와 해결법 총정리 🔍