강의

멘토링

로드맵

인프런 커뮤니티 질문&답변

위고잉업님의 프로필 이미지
위고잉업

작성한 질문수

제미니의 개발실무 - 커머스 백엔드 기본편

개념 정리

Controller에서 비즈니스 로직 흐름이 나타나는 것에 대하여..

작성

·

84

·

수정됨

7

안녕하세요.

결제 부분 강의를 보니 payments API를 보면 컨트롤러에서 주문을 조회하고, 사용 할 쿠폰을 조회하고, 포인트를 조회하고, 조회 된 데이터를 PaymentService로 전달하는 스타일이더라구요.

 

제가 진행중인 사이드 프로젝트도 커머스가 주제입니다. 제 프로젝트도 처음에는 강의 코드 스타일대로 컨트롤러에서 필요한 데이터를 조합하고, 결제를 처리하는 Service 쪽으로 넘기는 형식이었는데,
이게 점점 결제 기능이 고도화되면서 뭔가 컨트롤러에서 비즈니스 로직의 흐름이 보이는게 맞나? 라는 생각이 들게 되었고 어느 순간부터 웬만한 Controller에서는 1개의 xxxService.method()만 호출하고 이 method가 요청에 대한 비즈니스 로직을 전부 담당하게 되었습니다.

@Service
class QuestionPaymentService(
    private val questionOrderGenerator: QuestionOrderGenerator,
    private val promotionApplier: PromotionApplier,
    private val orderCouponApplier: OrderCouponApplier,
    private val paymentCouponApplier: PaymentCouponApplier,
    private val questionPaymentRecorder: QuestionPaymentRecorder,
    private val pointCommandAPI: PointCommandAPI,
    private val eventPublisher: EventPublisher,
) {
    @Transactional
    fun payment(command: QuestionPaymentCommand): QuestionPayment {
        val order = questionOrderGenerator.generateQuestionOrder(command.userId, command.questionIds)
        val questionPayment = QuestionPayment.create(command.userId, order)
        
        promotionApplier.apply(order)
        orderCouponApplier.apply(questionPayment, command)
        
        paymentCouponApplier.apply(questionPayment, command)
        pointCommandAPI.usePoint(questionPayment.userId, questionPayment.realAmount)
        questionPaymentRecorder.record(questionPayment)
        
        eventPublisher.publish(toEvent(questionPayment))
        return questionPayment
    }
}

위 코드는 제 프로젝트의 결제 부분인데요. 강의에서 말씀하신 것처럼 Service가 너무 많은 걸 알게되더라구요.
(주문도 생성하고, 쿠폰도 적용하고, 프로모션도 적용하고...)
지금 이 글을 작성하다보니, 갑자기 제 코드가 못생겨보이네요..

강의 코드와 비슷한 방식으로 위 코드를 바꿔본다면, 컨트롤러에서는 orderService를 이용해서 주문을 생성하고, couponService, promotionService 등을 이용해서 전처리를 한 뒤 PaymentService을 이용해
실 결제 금액만큼 금액을 지불하도록 하는 로직과 결제 내역을 저장하는 로직만 있을 것 같아요.

 

반대로 제 프로젝트 방식대로 강의 코드의 payments API를 만들어본다면, Payment를 만들기 위해서
PaymentCreateService와 같은 곳에서, orderReader, ownedCouponReader, pointReader 등을 조합해서 Payment를 생성하는 방식이 될 것 같아요.


결국 Service가 적은 책임만 가지게 된다면, Controller 입장에서는 복잡한 요청을 처리하기 위해선 다양한 Service를 조합하게 되고 Controller가 비즈니스 로직의 흐름을 보여주는 형태가 될 수 있다고 생각이 드는데요.
(사실 Controller가 비즈니스 로직의 흐름을 보여주면 안된다는 걸 어디서도 듣지 않았지만 뭔가 어색한 것 같아요.)
물론 계속 말씀하시는것 처럼 정답은 없다는 것은 알지만, 그냥 단순히 재민님은 주로 많은 책임을 가지는 Service보다는 Controller에서 작은 단위의 Service로 조합해서 처리하는 것을 선호하시는지 궁금합니다.


재민님을 지속 성장 가능한 소프트웨어 포스팅으로 알게되었고, 유튜브에서도 많은 도움이 되었어요.

그렇게 얻은 다양한 인사이트들을 개인 프로젝트에도 적용해보면서 다양한 시도를 하고 있는데 마침 제 관심사인 커머스 주제로 강의가 나와서 정말 행복합니다.


답변 1

4

제미니님의 프로필 이미지
제미니
지식공유자

우선 첫 질문 감사합니다! 너무 기쁩니다!
후후.. 제 의도와 일치하게 고민이 생기기 시작하셨다니 아주아주 기쁜 마음입니다 +_+!

저는 결국 여기서 우리가 팀이라고 가정한다면 방향과 기준을 선택해야한다고 봅니다!
(사실 컨트롤러에서 조합하는게 이질적이고 어색하게 느껴지신다면 일반적으로 대부분 사람들이 그렇게 느낄 것이라서 그렇게 느끼시는게 맞습니다 +_+ 후후후... (계획대로...))

적어주신 것과 같이 QuestionPaymentService 가 너무 많은 것을 알고 있는 형태라고 보여집니다
그렇다면 우리가 여기서 선택 할 수있는 전략은 아래와 같은 것들이 있습니다 (당장 생각나는 것들만 적었습니다 ㅎㅎ)

1안. Controller 에서 Service를 조합한다
2안. QuestionPaymentService 안에 의존되어있는 컴포넌트들을 더 '개념화'하여 컴포넌트를 한 단계 더 '응집' 시킨다
3안. Controller 와 Service를 연결해주는 단계(ex] 레이어)을 구성한다

저는 보통 2안하향식 (Service 밑으로 응집을 내려서 하위 컴포넌트를 구축), 3안상향식 (Service 위로 상위 영역을 구성해서 구축) 이라고 부르긴합니다만

--

위와 같은 안에서 위고잉업님은 어떤 전략을 선택하실 것 같으신가요?

몇가지 생각할 거리를 더 던져드리면....

  • 1안 고민

    • 서비스가 한두개에선 나쁘지 않아보임, 근데 많아지면..? 흐름이 여기 보이는게 맞나..?

  • 2안 고민

     

    • 어느 단위로 응집 시켜야하지?, 모든 비즈니스 로직에서 컴포넌트 응집이 생기지는 않을텐데 (조회성나 단순한 기능에서는 여러 개념이 묶이는 경우가 적으니까..?), 그렇다면 '응집 된 컴포넌트'를 만드는 기준은 어떻게 해야하지..?

    • 무엇을 기준으로 응집 시켜야하지?

  • 3안 고민

    • 그 구분을 '레이어'라고 지칭한다면 전체 프로젝트의 통일을 해야하는 느낌인데... 모든 비즈니스가 이 '레이어'가 필요하지 않은데 그럼 선택적으로 구성해야할까........?

    • 레이어(컴포넌트 간의 경계 영역)란 무엇일까.....!?

사실 저 중에 제가 제일 선호하는 안은.......... (더보기)

 

 

 

 

 

 

 

 

 

 


선호 안은 위의 내용을 생각해보신 후의 위고잉업님의 '결론 or 질문'을 다시 달아주시면 답변 드리겠습니다 :p)

 

제미니님의 프로필 이미지
제미니
지식공유자

오잉 다른 분들이 추가 답글을 달아주셨다는 메일이 왔는데 왜 안보일까요 ㅠㅠ

위고잉업님의 프로필 이미지
위고잉업
질문자

제 개인적인 생각으로는...

시간이 촉박하고, 1회성 기능이라면 1안으로 샤샤샥 만들 것 같습니다. (트랜잭션이 필요없는 단순한 로직일경우) -> 어차피 사라질거기에...

이제 계속해서 고도화 되는 기능이라면 2안 혹은 3안으로 처리해야 유지보수도 편하고 기능 확장도 좋아질 것 같다고 생각해요.

먼저 저는 2안 방식을 사용할 것 같아요. (그리고 종종 유튜브를 챙겨봤던 구독자로서 재민님도 2안을 선호하실 것 같다는 예상을 감히 해보겠습니다.)

현재 QuestionPaymentService는 너무 많은 것을 알고 있기에 복잡하게 보이는데, 말씀해주신대로 이걸 한번 더 개념화 시킨다면,

결제 전처리, 할인 적용, 지불, 결제 후처리 이렇게 되지 않을까 생각합니다. (목적이 무엇이냐로 개념화를 했어요.)

그래서 제가 예시로 올린 코드 기준에서는

결제 전처리(PaymentPreProcessor) - 주문 생성

할인 적용(DiscountProcessor) - 프로모션 적용, 쿠폰 적용

재화 지불(PayProcessor) - 포인트(서비스 재화) 차감

결제 후처리(PaymentPostProcessor) - History 저장

위와 같이 분리 될 것 같아요. 이렇게 하면 나중에 할인을 처리하는 비즈니스가 추가 되어도 DiscountProcessor 수정되면 되니 QuestionPaymentService가 복잡해지진 않을 것 같아요.

또 후처리 비즈니스 로직이 추가된다 하더라도 PaymentPostProcessor만 수정되면 되겠네요.

그리고 트랜잭션 범위도 개별적으로 줄 수 있구요. 그렇게 QuestionPaymentService는 4개의 비즈니스 로직으로 줄어들 것 같아요.

2안 고민중에 무엇이 기준이 되어야하냐 라고 해주셨는데 사실 제가 실무 경험이 없고

그나마 제일 복잡한 로직이 결제 부분이기에 확 하고 생각나는것은 없어서 위에서 잠깐 말한대로 지금 당장 떠오른 것은 "목적"이 무엇이냐가 될 것 같아요. 하하..


3안의 경우는 흔히 Facade, UseCase 계층을 하나 만들어서 처리하는 것과 비슷하다고 이해했어요. 제가 이 패턴은 사용해보지 않아서 잘은 모르겠지만,

이건 약간 Service 레이어에서 Repository를 직접 사용하지 않고 무조건 Reader, Finder로 처리 해야할까?와 유사한 고민인 것 같아요.

일관성을 가져가느냐, 유연하게 가져가느냐인 것 같은데 복잡한 로직이라면 계층 추가! 이렇게 정한다고 해도, 사람에 따라 복잡하다고 느끼는 것은 다르기 때문에

어디는 추가적인 계층을 사용하고 있고, 어디는 그냥 컨트롤러에서 처리하고 있고, 이렇게 되서 사실 일관성을 가져가는게 더 좋지는 않을까 싶어요.

물론 일관성을 가져가게되면 무조건 UseCase, Facade 계층을 생성해야한다는 트레이드 오프가 생길 것 같습니다.

답변이 거의 바로 해주셔서 황홀하네요. 감사합니다!

위고잉업님의 프로필 이미지
위고잉업

작성한 질문수

질문하기