Strategy(전략) 패턴

2024. 2. 10. 01:08Design Pattern

[목차]

1. 전략패턴에 대해 알아보자
2. 전략패턴은 필요할까?
3. 코드로 구현해보자
    3.1  파트너 수수료
    3.2  파트너 수수료 코드 설명
    3.3  새로운 요구사항 "딜러라는 파트너 추가해주세요!"
    3.4  새로운 요구사항에 맞추어서 코드 설명
4. 전략패턴으로 구현한 파트너 수수료금액 구하기
    4.1 전략패턴 다이어그램
    4.2 전략패턴 Source code
    4.3 전략패턴 Source code_Dealer 추가
5. 결론

1. 전략패턴에 대해 알아보자

전략패턴(StrategyPattern)은 알고리즘군을 정의하고 캐슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다. 전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.

- 헤드퍼스트 디자인 패턴 60p - 

헤드퍼스트 책에는 저렇게 정의가 되어있었다. 하지만 저 글로는 전략패턴이 무엇인지 와닿지가 않았다. 한번 위키에서 전략패턴으로 검색을 해보았다. 

전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다. 전략 패턴

(1) 특정한 계열의 알고리즘들을 정의하고
(2) 각 알고리즘을 캡슐화하며
(3) 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

전략은 알고리즘을 사용하는 클라이언트와는 독립적으로 다양하게 만든다.전략은 유연하고 재사용 가능한 객체 지향 소프트웨어를 어떻게 설계하는지 기술하기 위해 디자인 패턴의 개념을 보급시킨 디자인 패턴(Gamma 등)이라는 영향력 있는 책에 포함된 패턴들 가운데 하나이다.


2. 전략패턴은 왜 필요할까?

전략패턴은 내 생각에 SOLID 법칙 중에 O(Open close principle)에 해당되는것 같다. 즉, 확장에는 열려있고, 변경에는 닫혀있어야한다는 법칙을 적용하기에 제일 좋은 패턴 같다. 왜냐하면 전략패턴으로 코드를 구현하면 새롭게 추가되더라도 기존에 소스에는 변함이 없고, 새로운 것을 추가할 수 있기 때문이다. 따라서 이 패턴을 익혀두면 요구사항이 오더라도 확정에 유리하게 설계를 할 수 있을거 같다. 일단 여기까지 무슨 이야기인지 모르는 사람도 있을것이다. 코드를 보면 더 이해할수 있다.

3. 코드로 구현해보자

먼저 코드로 구현해보기 전에 전략 패턴은 어떤 구조인지 그림을 통해서 알아보자

전략패턴 (출처: wiki)

 저 그림을 보면 Context에 전략(Strategy)을 주입한다. Strategy class에서는 각자의 알고리즘을 작성하고, 어떠한 것을 주입하느냐에 따라 다른 알고리즘이 실행된다. 말로만 하면 어려울 수 있으니 코드로 살펴보자!  

3.1  파트너 수수료

파트너 수수료를 주는 계산기가 있다고 가정하자. 현재 운영하고 있는 시스템에서 수수료를 주는 로직을 짰는데 전략패턴을 썼으면 더 좋은 코드를 짤수 있었을텐데 라는 아쉬움이 가득했다. 

파트너 수수료 요구사항을 보자 

파트너 수수료 요구사항 
1. 파트너 종류에 따라서 파트너 수수료율이 달라진다. (파트너 종류 = AP) 
2. 파트너 수수료금액은 "대출금 X 파트너수수료율 = 수수료금액" 이다. 
3. 세금부분은 요구사항에서 제외하도록 하자(간단한 예제를 위해서) 
대출금 : 3천만원
파트너 종류 : AP, CM 
파트너 수수료율 : AP(0.3%), CM(0.5%)

3.2  파트너 수수료 코드 설명 

먼저 파트너 종류에 대해서 정의를 하자! enum을 이용해서 파트너의 종류를 정의하였다. 코드는 아래와 같다. 

enum PartnerKind 

package org.example.normal;

import lombok.Getter;

@Getter
public enum PartnerKind {
    AP("01", "Ap"),
    CM("02", "Cm");

    private String code;
    private String value;

    PartnerKind(String code, String value) {
        this.code = code;
        this.value = value;
    }

}

 

PartnerFeeCalculator class에서는 실제 파트너 종류에 따라서 수수료금액을 구하는 클래스이다. 생성자를 통해서 대출금 정보를 받는다. 왜냐하면 하나의 대출계약에서 대출금 정보는 변하지 않기 때문이다. 그리고 pay method에서는 파트너 종류에 따라서 각각의 수수료율을 곱해서 수수료금액을 구하는 method이다. 

PartnerFeeCalculator.java

package org.example.normal;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

@Slf4j
public class PartnerFeeCalculator {

    private final BigDecimal loanAmount;

    public PartnerFeeCalculator(BigDecimal loanAmount) {
        this.loanAmount = loanAmount;
    }

    public BigDecimal pay(PartnerKind partnerKind) {
        BigDecimal result;

        if (partnerKind == PartnerKind.AP) {
            BigDecimal apFeeRate = new BigDecimal("0.3");
            result = calculatePartnerFee(apFeeRate);
            return result.setScale(0, RoundingMode.FLOOR);
        }

        if (partnerKind == PartnerKind.CM) {
            BigDecimal cmFeeRate = new BigDecimal("0.5");
            result = calculatePartnerFee(cmFeeRate);
            return result;
        }
        throw new IllegalArgumentException("파라미터에 잘못된 파트너 유형입니다. 입력된 파트너 유형 : " + partnerKind);
    }

    private BigDecimal calculatePartnerFee(BigDecimal partnerFeeRate) {
        return partnerFeeRate.divide(new BigDecimal(100), MathContext.DECIMAL128).multiply(loanAmount);
    }
}

Main class에서는 파트너수수료 계산기를 통해서 Ap와 Cm의 파트너 수수료금액을 계산한다. 

Main.java

package org.example;

import lombok.extern.slf4j.Slf4j;
import org.example.normal.PartnerFeeCalculator;
import org.example.normal.PartnerKind;

import java.math.BigDecimal;
import java.math.RoundingMode;

@Slf4j
public class Main {

    public static void main(String[] args) {
        BigDecimal loanAmount = new BigDecimal("30000000");
        PartnerFeeCalculator partnerFeeCalculator = new PartnerFeeCalculator(loanAmount);
        BigDecimal apResult = partnerFeeCalculator.pay(PartnerKind.AP);
        log.info(PartnerKind.AP.getValue() + "Fee = {}", apResult);

        BigDecimal cmResult = partnerFeeCalculator.pay(PartnerKind.CM);
        log.info(PartnerKind.CM.getValue() + "Fee = {}", cmResult.setScale(0, RoundingMode.FLOOR));
    }
}

수수료금액 result

대출금 : 3천만원
AP 수수료 금액 : 30,000,000 * 0.3% = 90,000원
CM 수수료 금액 :30,000,000 * 0.5% = 150,000원 

위에 보면 AP수수료는 90,000원, CM수수료는 150,000원이 나온다. 

3.3  새로운 요구사항 "딜러라는 파트너 추가해주세요!"

그런데 이렇게만 운영을 한다면 얼마나 좋을 것인가..이런식으로 코드를 유지하다가 어느날 현업에서 새로운 요구사항이 들어왔다. 

파트너 종류를 하나 더 추가해서 계약이 더 잘 이루어지도록 하였으면 좋겠어요.
파트너 종류를 하나 더 추가할수 있게 개발해주세요.
"딜러" 라는 파트너 종류를 추가해주시고 파트너 수수료율은 0.8%로 주고 싶어요.

그렇다. 나는 처음에는 파트너 종류는 무조건 2개밖에 없고 더 생길일 없다고 말한 사람을 너무 믿었던 것이다...여태까지 2개로만 생각하고 코드를 짰기 때문에 실무에서는 엄청 많은 부분을 변경하여야했다...ㅠ 
예제 코드는 간단해서 이 요구사항이 왔을때 상대적으로 쉽게 요구사항에 맞추어서 개발할수가 있다. 좌절하지 말고 한번 바꾸어보자!
여기서 먼저 바꾸어야할 부분을 먼저 대략적으로 생각을 해보았다. 

1. 파트너 종류가 추가되었으니 Enum에다가 Dealer를 추가한다. 
2. pay Method가 수수료금액을 구하는 핵심 method이니깐 그 부분에 딜러수수료금액을 구하는 if문을 추가한다.

3.4  새로운 요구사항에 맞추어서 코드 설명

1. 파트너 종류가 추가되었으니 Enum에다가 Dealer를 추가한다. 

package org.example.normal;

import lombok.Getter;

@Getter
public enum PartnerKind {
    AP("01", "Ap"),
    CM("02", "Cm"),
    DEALER("03", "Dealer");

    private String code;
    private String value;

    PartnerKind(String code, String value) {
        this.code = code;
        this.value = value;
    }

}

2. pay Method가 수수료금액을 구하는 핵심 method이니깐 그 부분에 딜러수수료금액을 구하는 if문을 추가한다.

package org.example.normal;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

@Slf4j
public class PartnerFeeCalculator {

    private final BigDecimal loanAmount;

    public PartnerFeeCalculator(BigDecimal loanAmount) {
        this.loanAmount = loanAmount;
    }

    public BigDecimal pay(PartnerKind partnerKind) {
        BigDecimal result;

        if (partnerKind == PartnerKind.AP) {
            BigDecimal apFeeRate = new BigDecimal("0.3");
            result = calculatePartnerFee(apFeeRate);
            return result.setScale(0, RoundingMode.FLOOR);
        }

        if (partnerKind == PartnerKind.CM) {
            BigDecimal cmFeeRate = new BigDecimal("0.5");
            result = calculatePartnerFee(cmFeeRate);
            return result;
        }

        if (partnerKind == PartnerKind.DEALER) {
            BigDecimal dealerFeeRate = new BigDecimal("0.8");
            result = calculatePartnerFee(dealerFeeRate);
            return result;
        }
        throw new IllegalArgumentException("파라미터에 잘못된 파트너 유형입니다. 입력된 파트너 유형 : " + partnerKind);
    }

    private BigDecimal calculatePartnerFee(BigDecimal partnerFeeRate) {
        return partnerFeeRate.divide(new BigDecimal(100), MathContext.DECIMAL128).multiply(loanAmount);
    }
}

 

 

 

위에 보면 if문에 Dealer 수수료금액 구하는 부분이 추가 된 것을 확인할 수 있다. 이렇게 하나의 종류가 추가될 때마다 코드가 변경이 되고 있다. 지금 보면 SOLID에서 Open-Closed 법칙을 위배하게 된다. 즉, 확장에는 열려있고, 변경에는 닫혀있어야하는데 코드의 변경이 필요하게 되어서 기존 소스에서 버그나 운영장애가 일어날 가능성이 높아진다. 이럴때 기존 소스를 건드리지 않고 어떻게 해야하나? 라는 생각을 할 수 있는데 앞에 설명한 전략패턴을 사용하면 기존 소스 변경을 아주 최소화 할 수 있다. 

4. 전략패턴으로 구현한 파트너 수수료금액 구하기

4.1 전략패턴 다이어그램

먼저 전략패턴에 맞추어서 다이어그램을 그려보았다. 

이렇게 그려보았는데 PartnerFeePayment라는 클래스는 FeePayable을 의존한다. 여기서 Ap수수료를 받을것인지 Cm수수료를 받을 것인지 결정한다. 글로만 설명하면 잘 이해 못할수 있다. 소스로 한번 보자! 

4.2 전략패턴 Source code

먼저 ApFee를 클래스를 보면 대출금액 X 수수료율 = 수수료금액을 구하는 로직이 들어있다. 

ApFee.java

package org.example.pattern.strategy;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

@Slf4j
public class ApFee implements FeePayable {

    private static final BigDecimal apFeeRate = new BigDecimal("0.3");

    @Override
    public void pay(BigDecimal loanAmount) {
        BigDecimal result = apFeeRate.divide(new BigDecimal(100), MathContext.DECIMAL128).multiply(loanAmount);
        log.info("ApFee = {}", result.setScale(0, RoundingMode.FLOOR));
    }
}

 

CmFee.java

package org.example.pattern.strategy;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

@Slf4j
public class CmFee implements FeePayable{
    private static final BigDecimal cmFeeRate = new BigDecimal("0.5");

    @Override
    public void pay(BigDecimal loanAmount) {
        BigDecimal result = cmFeeRate.divide(new BigDecimal(100), MathContext.DECIMAL128).multiply(loanAmount);
        log.info("CmFee = {}", result.setScale(0, RoundingMode.FLOOR));
    }
}

FeePayable.java

package org.example.pattern.strategy;

import java.math.BigDecimal;

public interface FeePayable {
    public void pay(BigDecimal loanAmount);
}

PartnerFeePayment.java

package org.example.pattern.strategy;

import java.math.BigDecimal;

public class PartnerFeePayment implements FeePayable {

    private final FeePayable feePayable;

    PartnerFeePayment(FeePayable feePayable) {
        this.feePayable = feePayable;
    }

    @Override
    public void pay(BigDecimal loanAmount) {
        feePayable.pay(loanAmount);
    }
}

 

PartnerFeeCalculator.java

package org.example.pattern.strategy;


import java.math.BigDecimal;

public class PartnerFeeCalculator {
    public static void main(String[] args) {
        BigDecimal loanAmount = new BigDecimal("30000000");
        FeePayable apFee = new PartnerFeePayment(new ApFee());
        apFee.pay(loanAmount);

        FeePayable cmFee = new PartnerFeePayment(new CmFee());
        cmFee.pay(loanAmount);

    }
}



AP, CM 수수료금액

 

4.3 전략패턴 Source code_Dealer 추가

이제 새로운 요구사항이 Dealer를 추가하도록 하자! 

딜러는 DealerFee라는 클래스를 통해서 pay method를 통해서 핵심로직을 짜도록 한다. 

DealerFee.Java

package org.example.pattern.strategy;

import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

@Slf4j
public class DealerFee implements FeePayable {
    private static final BigDecimal dealerFeeRate = new BigDecimal("0.8");
    @Override
    public void pay(BigDecimal loanAmount) {
        BigDecimal result = dealerFeeRate.divide(new BigDecimal(100), MathContext.DECIMAL128).multiply(loanAmount);
        log.info("DealerFeeRate = {}", result.setScale(0, RoundingMode.FLOOR));
    }
}

위의 클래스에서 Dealer 수수료가 0.8%로 정의하였고, 나머지는 대출금 X 수수료율 = 수수료금액을 구할 수 있다. 

이제는 PatnerFeeCalculator.java를 수정해보자! 여기서는 딜러를 구하는 소스를 구현해야한다. 

package org.example.pattern.strategy;


import java.math.BigDecimal;

public class PartnerFeeCalculator {
    public static void main(String[] args) {
        BigDecimal loanAmount = new BigDecimal("30000000");
        FeePayable apFee = new PartnerFeePayment(new ApFee());
        apFee.pay(loanAmount);

        FeePayable cmFee = new PartnerFeePayment(new CmFee());
        cmFee.pay(loanAmount);

        FeePayable dealerFee = new PartnerFeePayment(new DealerFee());
        dealerFee.pay(loanAmount);
    }
}



 

이렇게 DealerFee를 구한다. 그러면 실행결과는 아래와 같다. 

딜러수수료 금액 포함 결과

위처럼 딜러를 추가하였지만 클래스(DealerFee.java)를 추가하고 PartnerFeeCalculator에서 딜러수수료금액을 구하면 해결이 된다.
전략패턴을 쓰지 않았던 처음보다 기존코드를 수정하지 않아도 되니 안정성 면에서 훨씬 좋아졌다! 기존 소스에 버그가 날 확률이 많이 적어지기 때문이다. 

5. 결론

업무를 하면서 전략패턴을 미리 알았으면 진짜 더 좋게 코드를 짤수 있었을텐데라는 아쉬움이 많았다. 다음 번에 개발을 하게 된다면 전략패턴으로 리팩토링을 해서 적용을 해봐야겠다. 

'Design Pattern' 카테고리의 다른 글

Template Callback Pattern(템플릿 콜백 패턴) - 견본 /회신 패턴  (0) 2024.01.11
Builder Pattern  (1) 2023.10.09