파사드 패턴을 통한 역할/책임 분리

tony·2024년 2월 15일
1

판단여행기

목록 보기
3/7

문제 (Business Context) ❗


프로젝트를 진행하던 중, Service Layer / Controller Layer 에 문제점이 있었다.

  • Service라는 이름으로 서로 다른 역할을 처리하고 있음

    • 해당 도메인 역할에 따른 단일 유스케이스 처리
      ( e.g. 입찰도메인 :: 입찰판매가 조회 )

    • 다른 도메인 서비스을 사용한 복합 유스케이스 처리
      ( e.g. 주문도메인 :: 입찰도메인,쿠폰도메인 등등에 따른 주문처리 )

      여기에는 확실히 결함이 존재한다.
      서비스 레이어에 동일한 도메인에 대해 서로 다른 역할을 수행하는 클래스들이 존재한다.
      예컨대 다른 도메인을 사용한 유스케이스 처리는 조금 더 상위 레이어로 분리할 수 있을 것이다.
      그렇다면 우리는 어떻게 레이어를 나눌 수 있을까?

  • 주문서비스 내에서 여러 도메인 서비스에 대한 로직을 직접 작성하고 있다.

    • 여러 도메인에 대한 결합도가 높아 주문 로직 수정 및 추가가 어렵다.

    • 유스케이스에 대한 역할/책임 분리가 되지 않아 테스트 코드 작성이 어렵다. 기능 동작에 대한 확인이 어려워진다.

    • 도메인 로직 담당 메서드가 비대해지고 가독성이 떨어져 작성자 외 다른 팀원의 참여가 불가능하다.

    • 아래는 리팩토링 이전 주문 서비스 로직 예시이다. 해당 PR에서 확인가능하다.

      @Service
      @RequiredArgsConstructor
      @Transactional
      public class SellService {
      
        private final SellRepository sellRepository;
        private final ProductRepository productRepository;
        private final GifticonRepository gifticonRepository;
        private final BuyRepository buyRepository;
        private final OrderRepository orderRepository;
      
        private final BuyService buyService;
      
        public SellBidResponseDto sellBidProduct(User user, SellBidRequestDto requestDto, Long productId) {
            Long price = requestDto.price();    // 판매하려는 가격
            Integer period = getPeriod(requestDto.period()); // requestDto에서 기간을 설정하는 값이 존재하는지 여부 체크하는 메소드
            LocalDate date = LocalDate.now();   // 현재 동작하고 있는 시스템의 날짜 표현
            LocalDateTime deadlineAt = date.atTime(LocalTime.MAX).plusDays(period); // 마감 기한을 위한 로직
            // date.atTime(LocalTime.Max)를 사용하면
            // 해당 date의 날짜의 가장 마지막 시간으로 세팅됩니다. 23시 59분 59초
            // 해당 date 기준으로 period만큼 날이 지난 날이 마감날로 세팅됩니다.
            Product product = getProductById(productId);
            Gifticon gifticon = saveGifticon(requestDto.gifticonUrl(), null); // Order는 우선 null로 입력 추후 즉시 구매 시 Order 넣어줄 예정
      
            Sell sell = Sell.builder()
                .price(price)
                .deadlineAt(deadlineAt)
                .user(user)
                .product(product)
                .gifticon(gifticon)
                .build();
      
            Sell savedSell = sellRepository.save(sell);
      
            return SellMapper.INSTANCE.toSellBidResponseDto(savedSell);
        }
      }

      보다싶이, 코드의 결합도가 높아 분리하기도 어렵고, 어떤 도메인의 역할이 어디서 어떻게 사용되는지도 인지하기가 어렵다.
      그렇다면 어떻게 분리하는 게 좋을까?

  • 도메인 분리에 대한 기준이 명세화되어있지 않아 도메인 분리가 잘 안 되어 있다.
    • 특정 객체가 도메인의 서브셋인지, 다른 도메인으로 분리해야하는지 애매모호했다.
    • 내부적인 로직으로 작성해야하는지 분간이 어려워 기능 추가 및 유지보수가 어렵다고 판단하였다.

원인파악과 결정과정 💬


1. Service라는 이름으로 다양한 역할과 책임

Service라는 이름으로 서로 다른 역할을 처리 하는 게 주요원인이였다.

따라서 Service Layer 에서 두 가지 역할을 각각 분리하기로 하였다.

  1. 해당 도메인 역할에 따른 단일 유스케이스 처리
  1. 다른 도메인 서비스들을 사용한 복합 유스케이스 처리

복합 유스케이스 처리는 각 도메인의 단일 유스케이스를 의존할 수 밖에 없다.

각 도메인의 역할에 따른 단일 유스케이스를 의존, 로직에 대한 전체적인 흐름제어 처리를 하기 때문이다.

즉, Outer Class/Method 일 확률이 크다.

따라서 복합 유스케이스 처리 레이어를 상단 레이어로, 각 도메인의 단일 유스케이스 레이어를 하단 레이어로 나누기로 하였다.

2. 도메인에 대한 역할 미지정

도메인에 대한 역할이 미지정되어있어, 복합 유스케이스에 대한 분리가 어려운 게 주요원인이었다.

이에 따라 "도메인에 대한 책임과 역할을 부여하자" 라는 기준을 명세하였다.

예를 들면 아래와 같이 책임과 역할을 부여할 수 있게할 수 있다.

  • 판매입찰 : 판매입찰 상태와 가격에 따른 판매입찰 조회
  • 기프티콘 : 기프티콘 상태에 따른 기프티콘 조회
  • 마감기한 계산 : 주어진 마감기한에 따라 마감기한일자 계산

이러한 각 도메인의 역할에 따라, 파사드 패턴을 사용하여 결합도를 낮추면서 분리할 수 있다고 판단했다.

3. 도메인 분리 기준 미지정

도메인 분리 기준에 대한 명세화가 되어있지 않은 게 주요 원인이었다.

도메인 분리가 제대로 되어있지 않아 유스케이스 처리에 대한 곤경에 빠졌다.

해당 로직에 대해서 DI를 통해 행동을 위임시킬지, 아니면 내부적으로 처리해야할지 말이다.

이에 따라 아래 분기점으로 이야기가 갈렸다.

도메인이 분리되어야 하는 도메인인가, 아니면 서브셋의 도메인인가?_

가령 쿠폰과 같은 사용자와 밀접한 도메인이 있다._

이런 도메인은 분리를 하는 게 맞을까, 아니면 서브셋으로 넣는 게 맞을까?_

해결방안 💡


Service 분리 ⇒ Provider 와 Service로 !

일전에 다른 도메인을 의존하는 복합 유스케이스 처리` 를 상단 레이어로, 각 도메인의 단일 유스케이스 처리는 하단 레이어로 분리하기로 하였다.

이에 따라 회의를 통해, 복합 유스케이스Provider Layer, 단일 유스케이스Service Layer 로 컨벤션을 규정하였다.

  • Provider
    • 주입 : 다른 도메인 서비스를 주입받기 ✅
    • 역할 : 다른 도메인 서비스를 활용하여 복합 유스케이스 처리
    • 기능 : 전체 로직 흐름제어**
  • Service
    • 주입 : 다른 도메인 주입 ❌
    • 역할 : 본인 도에민 역할에만 집중 :: 본인 도메인의 유스케이스 처리
    • 기능 : Repository에 접근하여 데이터 조회 / 데이터 가공

Facade 라고 이름을 규정지을 수 있었으나, 생각보다 헷갈리린다는 의견이 많았다.
복합 유스케이스 처리가 아닌, 파사드 패턴을 통해 여러 곳에서 공통적으로 사용할 수 있는 객체 라는 착각을 불러일으켰기 때문이다.
이에 따라 NestJS 에서 Provider Layer 를 따와 Provider 라는 이름을 사용하였다.

각 도메인의 역할에 따른 파사드 패턴 적용

앞서 복합 유스케이스 흐름제어 처리를 Provider Layer에서, 각 도메인의 단일 유스케이스 처리를 Service Layer 로 나누었다.

복합 유스케이스는 서로 다른 도메인을 의존받아 처리해야만 하므로, 파사드 패턴을 활용하기로 하였다.

파사드 패턴을 사용하여 아래와 같은 이점을 챙길 수 있다고 판단했다.

  • 외부에서는 어떤 도메인 서비스가 사용되었는지 알 필요 없게 하며, 내부 코드만 수정할 수 있도록 하여 유지보수성을 높일 수 있다.

  • 요구사항변경에 따라 필요한 도메인 서비스가 변경되면 다른 서비스를 주입하게끔 할 수 있다.

이에 따라 자연스레, Provider Layer 는 파사드 패턴을 적용하는 상위 Layer 가 되었다.

도메인 분리 => 속성값으로 가지는 값이라면 서브셋 , 아니라면 분리 !

  • 다음 세 가지 도메인이 긴밀하게 연관되어있다.
    • User
    • Coupon
    • Buy(구매입찰)
    • Point
    • Point 는 User 내의 필드값으로 있어야한다고 판단하였다.
    • 왜냐하면 User HAS-A Point 관계이기 때문이다.
    • 하지만, Coupon,Buy,User는 각각의 도메인으로 분리해주었다.
    • 왜냐하면 각각의 비즈니스 역할에 따른 행위를 지정할 수 있기 때문이다.
  • 이에 따라 도메인을 아래와 같이 분리해주었다.

Controller 분리는 어떤 기준으로 할까?

도메인 분리와 Service Layer 분리를 하다보니, 자연스레 Controller Layer 또한 분리하자는 의견이 나왔다.

이에 따라 Controller Layer 분리 기준안을 회의를 통해 다음과 같이 규정하였다.

  • API 당 Controller가 1:1이면 Overhead가 너무 크다고 생각한다.
    • 하나의 컨트롤러가 다중의 역할/책임을 수행하게 되고, 이렇게 비대해지면 어떤 역할을 수행 중인지 분간이 어려워져 리팩토링이 힘들어지기 때문이다.
    • 이에 따라 도메인에 따른 비슷한 역할의 처리를 하나의 Controller로 묶어주기로 하였다.
    • Controller를 나누는 기준
      • QueryCommand를 기준으로 나누어주기로 하였다.
        • GETQuery
        • POST,PATCH,PUT,DELETECommand
profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

0개의 댓글