프로젝트를 진행하던 중, 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);
}
}
보다싶이, 코드의 결합도가 높아 분리하기도 어렵고, 어떤 도메인의 역할이 어디서 어떻게 사용되는지도 인지하기가 어렵다.
그렇다면 어떻게 분리하는 게 좋을까?
Service
라는 이름으로 서로 다른 역할을 처리 하는 게 주요원인이였다.
따라서 Service Layer 에서 두 가지 역할을 각각 분리하기로 하였다.
복합 유스케이스 처리는 각 도메인의 단일 유스케이스를 의존할 수 밖에 없다.
각 도메인의 역할에 따른 단일 유스케이스를 의존, 로직에 대한 전체적인 흐름제어 처리를 하기 때문이다.
즉, Outer Class/Method 일 확률이 크다.
따라서 복합 유스케이스 처리 레이어를 상단 레이어로, 각 도메인의 단일 유스케이스 레이어를 하단 레이어로 나누기로 하였다.
도메인에 대한 역할이 미지정되어있어, 복합 유스케이스에 대한 분리가 어려운 게 주요원인이었다.
이에 따라 "도메인에 대한 책임과 역할을 부여하자" 라는 기준을 명세하였다.
예를 들면 아래와 같이 책임과 역할을 부여할 수 있게할 수 있다.
이러한 각 도메인의 역할에 따라, 파사드 패턴을 사용하여 결합도를 낮추면서 분리할 수 있다고 판단했다.
도메인 분리 기준에 대한 명세화가 되어있지 않은 게 주요 원인이었다.
도메인 분리가 제대로 되어있지 않아 유스케이스 처리에 대한 곤경에 빠졌다.
해당 로직에 대해서 DI를 통해 행동을 위임시킬지, 아니면 내부적으로 처리해야할지 말이다.
이에 따라 아래 분기점으로 이야기가 갈렸다.
도메인이 분리되어야 하는 도메인인가, 아니면 서브셋의 도메인인가?_
가령 쿠폰과 같은 사용자와 밀접한 도메인이 있다._
이런 도메인은 분리를 하는 게 맞을까, 아니면 서브셋으로 넣는 게 맞을까?_
일전에 다른 도메인을 의존하는 복합 유스케이스 처리` 를 상단 레이어로, 각 도메인의 단일 유스케이스 처리는 하단 레이어로 분리하기로 하였다.
이에 따라 회의를 통해, 복합 유스케이스
를 Provider Layer
, 단일 유스케이스
를 Service Layer
로 컨벤션을 규정하였다.
Provider
Service
Facade 라고 이름을 규정지을 수 있었으나, 생각보다 헷갈리린다는 의견이 많았다.
복합 유스케이스 처리가 아닌, 파사드 패턴을 통해 여러 곳에서 공통적으로 사용할 수 있는 객체 라는 착각을 불러일으켰기 때문이다.
이에 따라 NestJS 에서 Provider Layer 를 따와 Provider 라는 이름을 사용하였다.
앞서 복합 유스케이스 흐름제어 처리를 Provider Layer에서, 각 도메인의 단일 유스케이스 처리를 Service Layer 로 나누었다.
복합 유스케이스는 서로 다른 도메인을 의존받아 처리해야만 하므로, 파사드 패턴을 활용하기로 하였다.
파사드 패턴을 사용하여 아래와 같은 이점을 챙길 수 있다고 판단했다.
외부에서는 어떤 도메인 서비스가 사용되었는지 알 필요 없게 하며, 내부 코드만 수정할 수 있도록 하여 유지보수성을 높일 수 있다.
요구사항변경에 따라 필요한 도메인 서비스가 변경되면 다른 서비스를 주입하게끔 할 수 있다.
이에 따라 자연스레, Provider Layer 는 파사드 패턴을 적용하는 상위 Layer 가 되었다.
도메인 분리와 Service Layer 분리를 하다보니, 자연스레 Controller Layer 또한 분리하자는 의견이 나왔다.
이에 따라 Controller Layer 분리 기준안을 회의를 통해 다음과 같이 규정하였다.
도메인에 따른 비슷한 역할의 처리
를 하나의 Controller
로 묶어주기로 하였다.Query
와 Command
를 기준으로 나누어주기로 하였다.GET
→ Query
POST,PATCH,PUT,DELETE
→ Command