글의 목적
프로젝트 코드 내용 중 다중 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 |