CS

디자인 패턴 - 전략 패턴

ri5 2023. 1. 11. 00:19

전략 패턴

결제 플랫폼을 만드는데 카드마다 다른 동작을 사용한다고 정의하고 클래스 다이어그램을 그려보면 아래와 같다.

카드를 상속 받은 구조

환불, 포인트 적립은 동일하게 동작하지만 결제만은 다르게 동작한다. 라고 가정한다면 위와같이 슈퍼 클래스를 만들고 결제만 재정의하여 상속하는 구조가 나오게 됩니다. 이렇게 구현한 상태에서 요구사항이 추가되어 할부결제가 추가되었다. 

위와같이 할부결제를 카드에 추가한다면 할부가 되지 않는 체크카드와 포인트카드도 할부로 결제가 됩니다. 이러한 문제를 해결하려면 아래처럼 재정의하여 아무것도 하지않는 메서드로 정의하여야 한다.

public class CheckCard {


    @Override
    public void intsallmentPayment() {
    
    }
}

이러한 구조로 유지보수하다가 처음 보는 개발자가 기능을 확장해서 개발한다면 어떠한 문제점을 겪게될까?

  • 서브클래스에서 중복된 코드로 인해 지저분해진다.
  • 모든 카드의 기능이 어떤 기능을 제공하는지 알기 어렵다.
  • 만약 무이자라는 정책이 새로운 추가 된다면 원치 않은 카드들에게 영향을 끼칠 수 있다.
  • 다른 인스턴스로 변경하기 어렵다. 

결제라는 행동은 법이나 금융사 지침 변경 등 바뀔 가능성이 매우 큰 요구사항이다. 그렇기 때문에 결제라는 행동을 격리시켜 다른 코드에게 지장이 가지 않도록 설계해야 한다. 

SOLID 원칙 중 하나는 고수준 모듈은 저수준 모듈에 의존하면 안된다. 추상화에 의존해야된다고 한다. 그렇다면 위처럼 추상화 시킨 모듈에 의존시킨 다음 구현한다면 어떻게 될까? 지금 상태에서는 괜찮지만 나중에 신용카드 종류가 늘어나게 된다면 어땋게 될까?

 

  • 신용카드와 같은 기능을 하는 서브클래스가 있을 경우 똑같은 코드를 계속해서 구현해야한다.
  • 할부결제라는 코드를 구현하기 위해 기존에 상속하고 있던 서브클래스의 코드를 바꿔야 한다.

소프트웨어를 개선하기 위해서 기존의 코드에 대한 영향을 최소화하면서 효율적으로 개선할 방법을 찾아야한다. 디자인 패턴에서 가장 중요한 법칙은 바뀌는 부분을 찾아내고 바뀌지 않는 부분과 분리하는 것이다. 바뀌는 부분을 캡슐화를 한다면 나중에 바뀌지 않는 부분에서는 영향을 미치지 않고 고치거나 확장할 수 있다.

 

위처럼 바뀌는 기능을 분리했다. 그리고 실행하고 있을 때에도 다른 카드로 변경가능하도록 구현하려면 디자인 법칙 중 하나인 구현에 의존하지 않고 추상화에 의존한다. 법칙을 생각하고 디자인 해야한다.

디자인 법칙에 맞춰서 바뀔 수 있는 부분에 대하여 분리시켜 추상화에 의존하도록 구현을 변경하였다. 

  • 신용카드와 같은 기능을 하는 서브클래스가 있을 경우 똑같은 코드를 계속해서 구현해야한다.
  • 할부결제라는 코드를 구현하기 위해 기존에 상속하고 있던 서브클래스의 코드를 바꿔야 한다.

바뀔 수 있는 기능을 인터페이스로 추상화를 시킨 후 분리하였기 때문에 더이상의 중복을 제거하였다. 이제 기존에 있던 할부결제라는 코드를 모두 바꿔야하는 문제를 최소화 시키려면 어떻게 해야될까?

public class Card {
    private Payable payable;
    private InstallmentAvailiable intallmentAvaliable;
	
    public Card() {
    	this.payable = new CashPayment();
        this.intallmentAvaliable = new InstallmentImPossible();
    }
    
    public void payment() {
        this.payable.payment();
    }
    
    public void installment() {
        this.intallmentAvaliable.installment();
    }
}

위처럼 메서드를 직접 구현하지 않고 다른 클래스에게 위임함으로써 바뀔 수 있는 부분을 최소화 하여 구현이 되었다. 만약 신용카드 처럼 새로운 카드를 정의해야한다면 아래처럼 인스턴스 변수를 설정해주면 된다.

public class CreditCard extendes {
    private Payable payable;
    private InstallmentAvailiable intallmentAvaliable;
	
    public CreditCard() {
    	this.payable = new DeferredPayment();
        this.intallmentAvaliable = new InstallmentPossible();
    }
}

다형성을 통해 아래의 문제를 모두 해결하였다 하지만 그외에도 다른 문제를 가지고 있는데 그것은 실행중에 동적으로 변경하지 못한다는 것이다.

  • 신용카드와 같은 기능을 하는 서브클래스가 있을 경우 똑같은 코드를 계속해서 구현해야한다.
  • 할부결제라는 코드를 구현하기 위해 기존에 상속하고 있던 서브클래스의 코드를 바꿔야 한다.
  • 실행중에 다른 알고리즘(행동)을 선택하지 못한다.

위에서 새로운 문제점을 해결하기 위해 어떤 행동을 취해야 할까? setter를 활용하는 것이다. 

public class CreditCard extendes {
    private Payable payable;
    private InstallmentAvailiable intallmentAvaliable;
	
    public CreditCard() {
    	this.payable = new DeferredPayment();
        this.intallmentAvaliable = new InstallmentPossible();
    }
    
    public void setPayable(Payable payable) {
       this.payable = payable
    }
}

하지만 많은 개발자들이 setter의 사용을 지양한다. 그이유는 무엇일까?

  • setter는 무엇 때문에 변경되는지 변경하는 이유가 무엇인지 파악하기 어렵다.
  • 객체의 일관성을 유지하기 어렵다. public한 메서드이기 때문에 어디서든지 접근하여 수정할 수 있다. 그렇기 때문에 객체 자체의 일관적인 구조를 유지하기가 어려워지는 것이다.

 

개선 방향

아래와 같이 생성자를 통해 다시 정의하게 된다면 객체가 불변하기 때문에 다른 곳에서 변경될 여지도 없고 생성할 때 부터 어떤 인스턴스 변수가 설정되는지 알 수 있기 때문에 객체지향적을 설계하는데 있어서 더 도움이 된다. 그외에도 빌더 패턴과 정적 팩토리 메서드를 활용하여 생성할 수 있다. 

public class CreditCard extendes {
    private Payable payable;
    private InstallmentAvailiable intallmentAvaliable;
	
    public CreditCard(Payable payable, InstallmentAvailiable intallmentAvaliable) {
    	this.payable = payable;
        this.intallmentAvaliable = intallmentAvaliable;
    }
    
    public CreditCard(Payable payable) {
    	this.payable = payable;
        this.intallmentAvaliable = new InstallmentPossible();
    }
}