본문 바로가기
코비의 개발일지

Facade Pattern 적용기

by 규난 2023. 8. 27.
728x90

이번 포스트는 개발을 하면서 하나의 서비스가 여러 개의 레포지토리를 의존하는 방식을 해결하기 위해서 퍼사드 패턴을 적용한 과정에 대해서 써보려 합니다.

 

목차

  1. 하나의 서비스가 여러 개의 레포지토리를 의존하는 코드의 문제점
  2. 퍼사든 패턴 적용
  3. 결론

하나의 서비스가 여러 개의 레포지토리를 의존하는 코드의 문제점

밑은 퍼사든 패턴을 적용하기 전 주문/결제 서비스 코드와 아키텍처 입니다.

지금 생각하면 정말 부끄러울 정도로 생각 없이 코드를 작성한 거 같다고 느껴지네요...ㅋㅋ

@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;
    
    // 주문/결제에 관련된 코드
}

Facade Pattern 적용 전 아키텍처와 서비스 코드

제가 생각한 위 코드의 문제점은 아래와 같습니다.

  1. OrdersService가 가지는 책임이 너무 많아 SRP를 위반하고 있음.
  2. unit test시 mocking 해야 하는 레포지토리가 너무 많음.
  3. 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 ...
}

Facade Pattern 적용 후 아키텍처와 코드

제가 생각한 퍼사든 패턴 적용 후 장점은 아래와 같습니다.

  1. 각 레이어의 책임이 명확해짐.
    • 컨틀롤러는 요청을 받아 퍼사드로 요청을 넘겨주는 역할을 하게 되고 퍼사드는 넘어온 요청을 받아 책임을 맡고 있는 서비스에게 필요한 데이터에 대한 조회 및 처리를 요청을 하는 클라이언트 역할을 하게 되고 서비스는 각자 책임을 맡고 있는 부분에 대해서만 데이터만 처리하는 역할을 하기 때문에 각 레이어의 책임이 명확해집니다.
  2. 서비스 코드의 unit test시  mocking 해야 하는 레포지토리의 수가 줄어듦.
    • 위 코드에서 보시다싶이 하나의 서비스가 많아야 최대 2개까지의 의존성을 가지고 있기 때문에 퍼사든 패턴을 적용하기 전과 비교했을 때 확실히 레포지토리의 mocking 수가 줄어들게 됩니다.
  3. transaction이 적용된 코드에 외부 API를 호출하는 코드가 분리됨.
    • transaction은 대부분 서비스 레이어에 적용하게 되는데 비즈니스 로직을 상위 레벨인 퍼사드로 캡슐화했기 때문에 외부 API 호출도 transaction이 적용된 코드에서 분리가 가능하게 됩니다.

 

결론

퍼사드 패턴을 적용 후 기존 하나의 서비스가 여러 개의 레포지토리를 의존하고 그 안에서 모든 비즈니스 로직이 처리했을 때보다 비즈니스 로직을 퍼사드로 캡슐화하고 각 서비스에 대한 책임을 명확하게 해줌으로써 유지 보수 관점에서 굉장히 좋아졌다고 생각합니다.

 

그리고 제가 가장 치명적이라고 생각했던 transaction 코드 안에서 외부 API를 호출하는 코드에 대한 분리가 가능하다는 점에서 굉장히 큰 이점도 얻었다고 생각합니다. (이 부분은 오픈채팅에 있는 개발자분께서 Saga Pattern을 이용하여 해결하는 방법이 더 적합하다고 피드백을 주셨습니다.)

 

퍼사드 패턴을 적용하는 게 꼭 정답은 아니겠지만 이렇게 사소한 부분부터 리팩토링을 하다 보면 정답에 가까운 코드를 작성할 수 있다는 생각을 가지고 있기 때문에 앞으로도 더 좋은 코드를 작성하기 위해 더 많은 공부와 고민을 하고 리팩토링을 시도해 봐야겠습니다.

728x90