Java

전략패턴(Strategy Pattern)를 이용해 코드 리팩토링

Daniel0617 2020. 2. 13. 10:22

글의 목적
프로젝트 코드 내용 중 다중 if문으로 처리되어 있는 것을 객체 지향 설계 원칙(단일 책임 원칙)에 맞는 구조로 변경하기 위해 리팩토링 했다. 리팩토링 중 전략패턴을 사용하게 되었고, 리팩토링 과정과 코드의 장단점을 이해하기 위해 작성했다.

리팩토링(전) 코드

아래는 리팩토링 하기 전 각각의 '결제 타입'에 따라 '결제' 기능을 구현하는 코드를 작성했다.

public class OrderController{

    @Autowired OrderService orderService;

    @PostMapping("/payments")
    public void payment(@RequestBody PaymentInfoDTO paymentInfoDTO) {
        orderService.payment(paymentInfoDTO);
    }
}
public class OrderService{

    @Autowired CardDao cardDao;
    @Autowired AccountDao accountDao;
    @Autowired CardAPI cardApi;
    @Autowired AccountAPI accountApi;

    ...

    public void payment(PaymentInfoDTO paymentInfoDto){

        if(paymentInfoDto.type == "CARD") {
            cardDao.insertCardInfo(paymentInfoDto); // 카드 정보를 저장한다.
            cardApi.pay(paymentInfoDto); // 카드 정보를 기반으로 결제를 진행한다.
        } 
        else if(paymentInfoDto.type == "ACCOUNT") {
            accountDao.insertAccountInfo(paymentInfoDto); // 계좌정보를 저장한다.
            accountApi.pay(paymentInfoDto);  // 계좌 정보를 기반으로 결제를 진행한다.
        } 
        else {
            // 존재하지 않는 결제 타입인 경우 에러 발생
        }
  }    

}

위의 코드 내용은 몇가지 치명적인 단점이 존재한다.

  • 현재는 CARD(카드)와 ACCOUNT(계좌이체) 결제 타입만 존재하지만, 인터넷 뱅킹, 신용카드, 체크카드, 휴대폰 결제 등 계속해서 타입이 늘어날 경우 분기처리 해야하는 코드가 생성된다. 또한 각 타입 내에서 분기처리되는 로직이 발생하는 경우 if문 안에 if문이 존재해 코드 가독성을 현격히 떨어뜨린다.

  • 만약 위의 결제 코드가 다른 클래스에서도 사용되는 경우 위의 코드 내용을 그대로 작성해야 함으로 코드의 재사용성이 떨어지고, 변경사항 발생 시 일괄 적용이 어려워 버그가 발생 할 확률이 높다.

이외에도 메소드 내에서 단일 책임 원칙을 지키지 못해 코드 응집도가 낮다는 점과 여러 모듈을 사용함으로써 높은 결합도를 나타내는 등 구조적으로 여러 문제점을 갖고 있다.

리팩토링(후) 코드

코드를 리팩토링 하기 전 추상화를 시켜보자. 위의 코드 내용은 무엇을 위한 코드인가. 공통된 기능이 무엇인가. 바로 '결제' 기능을 구현했다는 점이 모두의 공통점이다. 단지, '결제 타입'에 따라 로직이 변경될 뿐 '결제'라는 하나의 기능을 구현했다는 것은 동일하다. 그렇다면 어떻게 설계하면 좋을까?

 

아래 그림과 같이 코드를 설계했다.

설계 내용은 '결제(PaymentService)' 라는 추상화된 개념을 각각의 '결제 타입(Card, Account)'에서 implements 받아 정의 할 수 있도록 변경했다. 각각의 '결제 타입'에서는 '결제' 기능을 담당하는 pay() 메소드에 원하는 로직을 실행시킬 수 있게 정의 했고, 추가로 컨트롤러에서 각각의 타입을 나눌 수 있게 작성했다.

// 결제 기능 추상화
public interface PaymentService {
    public void pay(PaymentInfoDTO paymentInfoDTO);
}
public class CardService implements PaymentService {

    @Autowired CardDao cardDao;
    @Autowired CardAPI cardApi;

    // 카드 결제 로직    
    @Override
    public void pay(PaymentInfoDTO paymentInfoDTO){
        cardDao.insertCardInfo(paymentInfoDto); // 카드 정보를 저장한다.
        cardApi.pay(paymentInfoDto); // 카드 정보를 기반으로 결제를 진행한다.
  }
}
public class AccountService implements PaymentService {

    @Autowired AccountDao accountDao;
    @Autowired AccountAPI accountApi;

    // 계좌이체 결제 로직    
    @Override
    public void pay(PaymentInfoDTO paymentInfoDTO){
        accountDao.insertAccountInfo(paymentInfoDto); // 계좌정보를 저장한다.
        accountApi.pay(paymentInfoDto);  // 계좌 정보를 기반으로 결제를 진행한다.
  }
}
public class OrderService{

    @Autowired CardService cardService;
    @Autowired AccountService accountService;

    public void payment(PaymentService paymentService, PaymentInfoDTO paymentInfoDto){
        paymentService.pay(paymentInfoDto);
  }
}
public class OrderController{

    @Autowired AccountService accountService;
    @Autowired CardService cardService;
    @Autowired OrderService orderService;

    @PostMapping("/payments/card")
    public void payment(@RequestBody PaymentInfoDTO paymentInfoDTO) {
        orderService.payment(cardService, paymentInfoDTO);
    }


    @PostMapping("/payments/account")
    public void payment(@RequestBody PaymentInfoDTO paymentInfoDTO) {
        orderService.payment(accountService, paymentInfoDTO);
    }
}

언뜻 보기에 이전과 달리 코드가 복잡해 보일 수 있으나 이전보다 몇가지 장점을 갖고 있다.

  • 각 타입별 비즈니스 로직을 하나의 클래스(OrderService)에서 모두 정의하는 것이 아니라 타입별 클래스(CardService, AccountService)에서 정의함으로써 보다 명확한 코드를 작성 할 수 있게 되었다. 따라서 각 타입별 로직이 변경되는 경우 이전에 OrderService 클래스에서 모두 수정하는 것이 아니라 각 타입별 클래스의 로직을 수정해주면 된다.

  • 추상화된 PaymentService를 통해 코드의 재사용성을 높일 수 있다. OrderService 클래스를 보면 각 타입을 알 필요 없이 원하는 로직을 실행시킬 수 있다. 따라서 OrderService 클래스 외에 결제 기능이 필요한 경우 이전에 if문으로 분기처리한 코드를 그대로 복사 붙여넣기 하는 것이 아니라 추상화된 PaymentService를 이용해 코드 양을 줄이고, 재사용성을 높일 수 있게 된다

자바 디자인 패턴에서 위와 같은 설계를 전략 패턴(Strategy Pattern)이라 부른다. 왜 전략 패턴이라 부를까? 일관된 동작이 각 전략에 따라 변경되기 때문이다. 그럼 위의 코드에서 일관된 동작은 무엇이고, 전략은 무엇일까? 이미 반복해서 말했지만 '결제'라는 행위가 일관된 동작이고 '결제 타입'이 전략이라 볼 수 있다. PaymentService 인터페이스 자체가 CardService, AccountService 타입에 따라 행동하는 방법이 다르기 때문이다.

 

디자인 패턴? 중요한가? 생각했던 내용들을 예제만 보고 이해하는 것이 아니라 프로젝트에 고민하고 적용함으로써 왜 필요한지 이전보다 깊게 이해 할 수 있게 되었다.(역시.. 개발은 어려워.. 하지만 깨닫거나 해결하는 순간 늘 짜릿해 ~.~)

'Java' 카테고리의 다른 글

Template Pattern(템플릿패턴) 적용기  (0) 2022.03.04
Transaction & AOP  (0) 2020.02.26
Spring Transaction 관리  (0) 2020.02.11
SpringBoot + Maven => JAR파일 생성  (0) 2019.06.16
Node.js + mongoDB 설치 및 mongoose 연동  (0) 2018.12.24