Easy Understanding

웹 백엔드 관점으로 본 디자인 패턴 정리(2) - 전략 패턴(Strategy Pattern) 본문

Study

웹 백엔드 관점으로 본 디자인 패턴 정리(2) - 전략 패턴(Strategy Pattern)

appleg1226 2022. 5. 5. 23:38

 

앞으로 패턴들에 대해서 어떻게 백엔드의 관점에서 사용할 것이며,

얼마나 유용할지 평가를 해보려고 한다. 

 

애초에 패턴들을 실용적인 관점에서 검토하지 않으면 의미가 없다는 생각이다.

이 공부가 나의 실무 코드에 도움이 되어야하니 그런 점들을 위해서 정리해두려고 한다.

 

그러나 딱 내 학습 수준에 맞는 평가가 나올 것 같아서 그 점은 참고해야 한다.

 

추가로 디자인 패턴의 개념에 대한 기초 설명은 패스하려고 한다.

 


전략 패턴

- 필요 개념: 컴포지션
- 활용도: 부담없이 사용해볼만 함
- 난이도: 간단
- 패턴이 필요한 상황: 상속 상황에서 내부 코드의 빈번한 중복 

 

보통 이 패턴에 가장 많이 나오는 예제는 Duck에 대한 예제이다.

quack, fly 메서드가 나오고 여러 오리종류가 나오는 그 예제다. 

헤드퍼스트에서 예로 들어서 유명하지만,

솔직히 이런 예시는 기초를 이해할 때나 도움이 되지 실제 프로그램과는 너무 동떨어진다.

 

백엔드 어플리케이션을 개발하면서 상속이 많아지는 순간이 종종 존재하곤 한다.

스프링을 기준으로 보통 두 레이어에서 상속이 많이 발생한다.

 

1. 서비스: @Service 어노테이션을 달고 있는 스프링 빈

2. 도메인 객체: 다양하기는 하지만 JPA 기준 @Entity가 달린 객체가 대표적이다.

 

서비스에서는 공통의 상위 클래스가 있고 다양한 요구사항에 따라서 하위 서비스가 생성되는 식이다.

결제를 예로 들면 이런 식으로 구성이 될 수가 있을 것 같다.

public interface PaymentService { ... }
public class CardPaymentService implements PaymentService { ... }
public class MobilePaymentService implements PaymentService { ... }
public class KakaoPaymentService implements PaymentService { ... }

과연 이게 끝일까?

비즈니스 요구사항은 변화무쌍해서 여기서 끝나지 않을 가능성이 무조건 백퍼센트다.

 

ios, google이 추가될 수도 있고, 다른 결제수단이 추가될 수도 있고, 

같은 결제수단인데 살짝 다른 서비스가 필요할 수도 있다.

 

일단 클래스가 늘어나는 걸 막을 수 없는 건 확정이다.

일단 처음에 클래스가 몇 종류 되지 않을 때는 복붙으로 개발하는 게 맞다.

 

그러다가 슬슬 중복이 보인다 싶은 때가 오는데 그때가 전략패턴이 슬슬 도입될 때이다.

@Service
public class CardPaymentServiceA implements PaymentService{
  public void startPayment(){
    ... 수십줄의 로직 A ...
  }
  public void endPayment(){
    ... 수십줄의 로직 B ...
  }
  public void cancelPayment(){
    ... 수십줄의 로직 C ...
  }
}

@Service
public class CardPaymentServiceB implements PaymentService{
  public void startPayment(){
    ... 수십줄의 로직 A ...
  }
  public void endPayment(){
    ... 수십줄의 로직 B ...
  }
  public void cancelPayment(){
    ... 수십줄의 로직 D ...
  }
}

 

정말 싫은 상황이지만 어쩌다보니 거의 다 비슷한데 약간 비슷한 로직의 카드결제가 하나가 더 추가되었는데,

(이런 일은 정말 일어날 수 있다) 딱 cancelPayment만 코드가 다르다.

그럼 당연히 CardPaymentService A와 B의 중복을 빼내야 할 것이고 그 때 전략패턴을 사용하면 된다.

@Service
public class CardPaymentServiceA implements PaymentService{
  private StartPaymentBehavior spb;
  private EndPaymentBehavior epb;
  private CancelPaymentBehavior cpb;
  
  public CardPaymentServiceA(){
    this.spb = new startMethodA();
    this.epb = new endMethodB();
    this.cpb = new cancelMethodC();
  }
  
  public void startPayment(){
    spb.start();
  }
  public void endPayment(){
    epb.end();
  }
  public void cancelPayment(){
    cpb.cancel();
  }
}

 

아마도 CardPaymentServiceB에서는 

this.cpb = new cancelMethodD(); 

가 사용될 것으로 예상할 수 있다.

 

또한 전략 패턴을 통해서 유닛 테스트를 하기도 좀 더 편해진 것 같다.

원래대로였으면 각 서비스별로 테스트 코드를 다 만들어야 했지만,

이렇게 중복 코드를 모아두면 각 전략 클래스만 테스트할 수 있어서 편리하다.

 

추가로 상속 때문에 복잡해진 코드를 컴포지션을 이용해서 간단하게 만들었다는 점을 기억하면 좋다.

 

이 패턴에 대해서 정리하자면

'상속이 많아져서 중복이 겉잡을 수 없어져 복붙이 많이 필요한 상황이라면 도입을 고려해볼만 하다.'
애초에 협업하는 사람들이 따라가기 어려운 부분도 적어서 부담없이 도입해볼만 한 것 같다.
다만 한두개의 상속 케이스로 이 패턴을 도입하는 건 좀 아닌 것 같다.