전략 패턴은 정책 패턴이라고도 부른다.
여러 알고리즘을 각각 별도의 클래스로 캡슐화하여, 실행 시점에 자유롭게 교체할 수 있도록 하는 디자인 패턴이다.
더 멋지게 말하면
객체의 행위를 바꾸고 싶은 경우 '직접' 수정하지 않고 전략이라 부르는 '캡슐화한 알고리즘'을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다.
즉, 특정 작업을 수행하는 여러 가지 방법(전략)을 정의하고, 클라이언트 코드에서는 이들 전략 중 하나를 선택해 사용함으로써, 알고리즘을 클라이언트와 분리할 수 있다.
예시 코드를 보자.
// 전략 인터페이스: 두 숫자를 입력받아 연산 결과를 반환하는 메서드 정의
interface OperationStrategy {
int doOperation(int a, int b);
}
// 구체적인 전략 1: 덧셈 연산
class AdditionStrategy implements OperationStrategy {
@Override
public int doOperation(int a, int b) {
return a + b;
}
}
// 구체적인 전략 2: 뺄셈 연산
class SubtractionStrategy implements OperationStrategy {
@Override
public int doOperation(int a, int b) {
return a - b;
}
}
// 구체적인 전략 3: 곱셈 연산
class MultiplicationStrategy implements OperationStrategy {
@Override
public int doOperation(int a, int b) {
return a * b;
}
}
// 컨텍스트 클래스: OperationStrategy를 이용하여 계산을 수행
class Calculator {
private OperationStrategy strategy;
// 생성자를 통해 초기 전략을 설정
public Calculator(OperationStrategy strategy) {
this.strategy = strategy;
}
// 필요에 따라 전략을 변경할 수 있음
public void setStrategy(OperationStrategy strategy) {
this.strategy = strategy;
}
// 현재 설정된 전략을 이용하여 연산 수행
public int calculate(int a, int b) {
return strategy.doOperation(a, b);
}
}
// 클라이언트 코드: 다양한 연산 전략을 적용하여 계산 수행
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator(new AdditionStrategy());
System.out.println("10 + 5 = " + calculator.calculate(10, 5));
calculator.setStrategy(new SubtractionStrategy());
System.out.println("10 - 5 = " + calculator.calculate(10, 5));
calculator.setStrategy(new MultiplicationStrategy());
System.out.println("10 * 5 = " + calculator.calculate(10, 5));
}
}
코드 설명
전략 인터페이스 (OperationStrategy):
두 개의 정수를 받아 결과를 반환하는 doOperation 메서드를 정의한다.
→ 이 인터페이스는 다양한 연산 전략(더하기, 빼기, 곱하기 등)을 통일된 방식으로 구현하도록 한다.
구체적인 전략 클래스:
• AdditionStrategy는 두 숫자를 더하는 연산
• SubtractionStrategy는 두 숫자를 빼는 연산
• MultiplicationStrategy는 두 숫자를 곱하는 연산
컨텍스트 클래스 (Calculator):
내부에 OperationStrategy를 멤버 변수로 보유하고, 생성자나 setStrategy 메서드를 통해 전략을 설정한다.
calculate 메서드는 현재 설정된 전략의 doOperation을 호출하여 연산을 수행하게 된다.
클라이언트 코드 (Main 클래스):
Calculator 객체를 생성할 때 초기 전략으로 덧셈을 사용하고, 이후 필요에 따라 전략을 변경하면서 빼기, 곱셈 연산을 수행한다.
여기서 OperationStrategy는 설정한 이름일 뿐이며, 전략 패턴을 구현할 때는 여러 알고리즘을 캡슐화하는 인터페이스(또는 추상 클래스)를 정의하는 것이 핵심이다. 이때 그 이름은 개발자가 상황에 맞게 자유롭게 정하면 된다.
하나 더 긴 예시를 보여주겠다.
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
interface PaymentStrategy {
public void pay(int amount);
}
class KAKAOCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate){
this.name=nm;
this.cardNumber=ccNum;
this.cvv=cvv;
this.dateOfExpiry=expiryDate;
}
@Override
public void pay(int amount) {
System.out.println(amount +" paid using KAKAOCard.");
}
}
class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String email, String pwd){
this.emailId=email;
this.password=pwd;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using LUNACard.");
}
}
class Item {
private String name;
private int price;
public Item(String name, int cost){
this.name=name;
this.price=cost;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
class ShoppingCart {
List<Item> items;
public ShoppingCart(){
this.items=new ArrayList<Item>();
}
public void addItem(Item item){
this.items.add(item);
}
public void removeItem(Item item){
this.items.remove(item);
}
public int calculateTotal(){
int sum = 0;
for(Item item : items){
sum += item.getPrice();
}
return sum;
}
public void pay(PaymentStrategy paymentMethod){
int amount = calculateTotal();
paymentMethod.pay(amount);
}
}
public class HelloWorld{
public static void main(String []args){
ShoppingCart cart = new ShoppingCart();
Item A = new Item("kundolA",100);
Item B = new Item("kundolB",300);
cart.addItem(A);
cart.addItem(B);
// pay by LUNACard
cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo"));
// pay by KAKAOBank
cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01"));
}
}
/*
400 paid using LUNACard.
400 paid using KAKAOCard.
*/
위 코드의 흐름도 간단하게 정리하면 아래와 같다.
이와 같이, 전략 패턴을 통해 결제 방법을 별도의 전략 클래스로 캡슐화하고, 실행 시점에 원하는 결제 방식을 선택할 수 있게 된다.
전략 패턴을 활용한 라이브러리가 존재한다.
passport이다.
passport는 Node.js에서 인증 모듈을 구현할 때 쓰는 미들웨어 라이브러리로, 여러 가지 '전략'을 기반으로 인증할 수 있게 한다.
서비스 내의 회원가입된 아이디와 비밀번호를 기반으로 인증하는 LocalStrategy 전략과 페이스북, 네이버 등의 다른 서비스를 기반으로 인증하는 OAuth 전략 등을 지원한다.
개발하면서는 자체 회원가입 및 로그인, 소셜 로그인으로 말하는 것들이다.
js에서 다음 코드와 같이 사용할 수 있다.
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
passport.use(new LocalStrategy(... 처럼 passport.use() 라는 메서드에 '전략'을 매개변수로 넣어서 로직을 수행하는 것을 볼 수 있다.
** 일뷰 코드는 https://github.com/wnghdcjfe/csnote/tree/main/ch1 에서 참고하였습니다.