이번 포스트는 개발을 하면서 하나의 서비스가 여러 개의 레포지토리를 의존하는 방식을 해결하기 위해서 퍼사드 패턴을 적용한 과정에 대해서 써보려 합니다.
목차
- 하나의 서비스가 여러 개의 레포지토리를 의존하는 코드의 문제점
- 퍼사든 패턴 적용
- 결론
하나의 서비스가 여러 개의 레포지토리를 의존하는 코드의 문제점
밑은 퍼사든 패턴을 적용하기 전 주문/결제 서비스 코드와 아키텍처 입니다.
지금 생각하면 정말 부끄러울 정도로 생각 없이 코드를 작성한 거 같다고 느껴지네요...ㅋㅋ
@Service
@RequiredArgsConstructor
public class OrdersServiceImpl implements OrdersService{
private final OrdersRepository ordersRepository;
private final OrdersDetailRepository ordersDetailRepository;
private final PaymentsRepository paymentsRepository;
private final PaymentsCancelRepository paymentsCancelRepository;
private final PortOneClient portOneClient;
private final ProductService productService;
// 주문/결제에 관련된 코드
}
제가 생각한 위 코드의 문제점은 아래와 같습니다.
- OrdersService가 가지는 책임이 너무 많아 SRP를 위반하고 있음.
- unit test시 mocking 해야 하는 레포지토리가 너무 많음.
- transaction이 걸린 코드에 외부 API(PortOneClient)를 호출하는 코드가 들어감.(제일 마음에 안 들고 치명적인 문제라고 생각)
특히 3번 같은 문제는 사용자가 많은 서비스인 경우 transaction이 걸린 코드에서 외부 API를 호출하면 최악의 경우 read time out이 발생할 때까지 DB Connection을 반납하지 못하는 일이 발생할 수 있다고 판단하여 치명적인 문제점이라고 생각하였습니다.
이렇게 하나의 서비스가 여러 개의 레포지토리를 의존하는 방식을 어떻게 해결하면 좋을까 생각하다가
디자인 패턴을 공부할 당시 퍼사드 패턴에 대해서도 공부하였는데 이 패턴을 적용하면 해결할 수 있다는 생각이 들어서 바로 리팩토링에 돌입하게 되었습니다.
퍼사드 패턴 적용
먼저 퍼사드 패턴에 대해서 간략하게 알아보고 가도록 하겠습니다.
퍼사드 패턴이란 복잡한 비즈니스 로직을 상위 레벨의 인터페이스로 캡슐화해서 하위 시스템에 더 쉽게 접근할 수 있도록 도와주는 디자인 패턴입니다.
기존 방식처럼 컨트롤러에서 복잡한 비즈니스 로직이 있는 서비스로 바로 접근하는 방식이 아니라
컨트롤러에서 복잡한 비즈니스 로직을 캡슐화 시킨 퍼사드로 접근하고 퍼사드에서 각 서비스로 접근하는 방식으로 바뀌게 됩니다.
밑은 퍼사드 패턴을 적용한 후 주문/결제에 대한 코드와 아키텍처 입니다.
OrdersController
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/orders")
public class OrdersController {
private final OrdersFacade ordersFacade;
// 주문/결제와 관련된 컨트롤러 코드....
/**
* 상세 주문 취소(환불) API
*
* @param ordersId 주문 일련번호
* @param merchantUid 고유 주문 번호
* @param cancelPaymentDto 주문 취소 요청 객체
* @return 주문 취소 응답 객체 목록
*/
@PostMapping("/{ordersId}/{merchantUid}")
public ResponseEntity<List<CancelPayments.Response>> cancelPayments(
@PathVariable Long ordersId, @PathVariable String merchantUid,
@RequestBody(required = false) CancelPayments.Request cancelPaymentDto) {
return ResponseEntity.ok(
ordersFacade.cancelPayments(ordersId, merchantUid, cancelPaymentDto)
);
}
}
OrdersFacade
@Slf4j
@Component
@RequiredArgsConstructor
public class OrdersFacade {
private final PortOneClient portOneClient;
private final OrdersService ordersService;
private final OrdersDetailService ordersDetailService;
private final DeliveryService deliveryService;
private final PaymentsService paymentsService;
private final PaymentsCancelService paymentsCancelService;
private final ProductService productService;
// 주문/결제와 관련된 비즈니스 코드....
/**
* 상세 주문 취소(환불) API
*
* @param ordersId 주문 일련번호
* @param merchantUid 고유 주문 번호
* @param cancelPaymentDto 주문 취소 요청 객체
* @return 주문 취소 객체 목록
*/
public List<CancelPayments.Response> cancelPayments(
Long ordersId, String merchantUid, CancelPayments.Request cancelPaymentDto) {
// 코드 공개 불가로 어떤 서비스만 호출하는지만 적음
ordersService
ordersDetailService
portOneClient
paymentsCancelService
paymentsService
productService
}
}
퍼사드 패턴 적용 후 책임에 맡게 각각 분리된 서비스 코드들
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrdersServiceImpl implements OrdersService{
private final OrdersRepository ordersRepository;
private final OrdersQueryRepository ordersQueryRepository;
// code ...
}
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrdersDetailServiceImpl implements OrdersDetailService{
private final OrdersDetailRepository ordersDetailRepository;
private final OrdersDetailQueryRepository ordersDetailQueryRepository;
// code ...
}
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PaymentsServiceImpl implements PaymentsService {
private final PaymentsRepository paymentsRepository;
// code ...
}
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PaymentsCancelServiceImpl implements PaymentsCancelService {
private final PaymentsCancelRepository paymentsCancelRepository;
private final PaymentCancelQueryRepository paymentCancelQueryRepository;
// code ...
}
제가 생각한 퍼사든 패턴 적용 후 장점은 아래와 같습니다.
- 각 레이어의 책임이 명확해짐.
- 컨틀롤러는 요청을 받아 퍼사드로 요청을 넘겨주는 역할을 하게 되고 퍼사드는 넘어온 요청을 받아 책임을 맡고 있는 서비스에게 필요한 데이터에 대한 조회 및 처리를 요청을 하는 클라이언트 역할을 하게 되고 서비스는 각자 책임을 맡고 있는 부분에 대해서만 데이터만 처리하는 역할을 하기 때문에 각 레이어의 책임이 명확해집니다.
- 서비스 코드의 unit test시 mocking 해야 하는 레포지토리의 수가 줄어듦.
- 위 코드에서 보시다싶이 하나의 서비스가 많아야 최대 2개까지의 의존성을 가지고 있기 때문에 퍼사든 패턴을 적용하기 전과 비교했을 때 확실히 레포지토리의 mocking 수가 줄어들게 됩니다.
- transaction이 적용된 코드에 외부 API를 호출하는 코드가 분리됨.
- transaction은 대부분 서비스 레이어에 적용하게 되는데 비즈니스 로직을 상위 레벨인 퍼사드로 캡슐화했기 때문에 외부 API 호출도 transaction이 적용된 코드에서 분리가 가능하게 됩니다.
결론
퍼사드 패턴을 적용 후 기존 하나의 서비스가 여러 개의 레포지토리를 의존하고 그 안에서 모든 비즈니스 로직이 처리했을 때보다 비즈니스 로직을 퍼사드로 캡슐화하고 각 서비스에 대한 책임을 명확하게 해줌으로써 유지 보수 관점에서 굉장히 좋아졌다고 생각합니다.
그리고 제가 가장 치명적이라고 생각했던 transaction 코드 안에서 외부 API를 호출하는 코드에 대한 분리가 가능하다는 점에서 굉장히 큰 이점도 얻었다고 생각합니다. (이 부분은 오픈채팅에 있는 개발자분께서 Saga Pattern을 이용하여 해결하는 방법이 더 적합하다고 피드백을 주셨습니다.)
퍼사드 패턴을 적용하는 게 꼭 정답은 아니겠지만 이렇게 사소한 부분부터 리팩토링을 하다 보면 정답에 가까운 코드를 작성할 수 있다는 생각을 가지고 있기 때문에 앞으로도 더 좋은 코드를 작성하기 위해 더 많은 공부와 고민을 하고 리팩토링을 시도해 봐야겠습니다.
'코비의 개발일지' 카테고리의 다른 글
스프링 부트로 메일 발송 기능 구현하기 (1) | 2023.06.24 |
---|---|
필터에서 응답 데이터 가공 (1) | 2023.04.15 |
컨트롤러 단위 테스트시 모든 요청에 대한 공통 속성 적용하기 (1) | 2023.04.10 |
Spring REST Doc 적용기 (0) | 2023.04.02 |