전략 패턴 (Strategy Patter) 이란
전략 패턴은 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다.
여기서 '전략'이란 일종의 알고리즘이 될 수 도 있으며, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.
즉, 어떤 일을 수행하는 알고리즘이 여러가지 일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이다.
전략 패턴 구성 요소
전략 패턴을 이루는 구성 요소는 다음과 같다.
- 전략 알고리즘 객체들(concrete strategy) : 알고리즘, 행위, 동작을 객체로 정의한 구현체
- 전략 인터페이스 : 모든 전략 구현제에 대한 공용 인터페이스. 구체화된 여러 알고리즘들의 추상화로써, 변하는 부분을 담당
- 컨텍스트(Context) : 알고리즘을 실행해야 할 때마다 해당 알고리즘과 연결된 전략 객체의 메소드를 호출. 전략을 사용하는 프로그램의 흐름으로, 변하지 않는 것
- 클라이언트 : 특정 전략 객체를 컨텍스트에 전달 함으로써 전략을 등록하거나 변경하여 전략 알고리즘을 실행한 결과를 누린다.
보다시피 상황에 따라 다양한 알고리즘을 필요로 하는 경우 전략 패턴을 사용할 수 있다.
그러면 이런 발상을 코드로 옮기기 위해서는 어떻게 strategy와 concrete strategy를 작성하는 게 좋을까?
바로 인터페이스와 해당 인터페이스를 구현한 클래스를 사용하는 것이다.
인터페이스를 통해 전략을 추상화 시켜놓은 후, 적재 적소에 필요한 전략을 구현한 Class를 삽입하는 것이다.
※ 참고
프로그래밍에서의 컨텍스트(Context) 란 콘텐츠(Contetns)를 담는 그 무엇인가를 뜻한며, 어떤 객체를 핸들링 하기 위한 접근 수단이다.
즉, 물컵에 물이 담겨있으면 물은 콘텐츠가 되고, 물컵은 컨텍스트가 되며, 물을 핸들링 하기 위한 접근 수단이 된다.
전략 패턴은 OOP의 집합체
GoF의 디자인 패턴 책에서는 전략 패턴을 다음과 같이 정의한다.
- 동일 계열의 알고리즘군을 정의하고
- 각각의 알고리즘을 캡슐화하여
- 이들을 상호 교환이 가능하도록 만든다.
- 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로
- 알고리즘을 다양하게 변경할 수 있게 한다.
무슨 논문글 같이 설명되어 있어 되게 심오하고 난도가 높은 패턴인줄은 알겠지만, 사실 전략 패턴은 우리가 지금까지 자바 언어를 공부하면서 배운 여러 객체 지향 문법 기법들인, SOLID 원칙의 OCP 원칙, DIP 원칙과 합성(compositoin), 다형성(polymorphism), 캡슐화(encapsulation) 등 OOP 기술들의 총 집합 버전이라고 보면 된다.
따라서 위의 전략 패턴의 정의를 다음과 같이 설명하면 이해하기 쉬울 것이다.
- 동일 계열의 알고리즘군을 정의하고 → 전략 구현체로 정의
- 각각의 알고리즘을 캡슐화하여 → 인터페이스로 추상화
- 이들을 상호 교환이 가능하도록 만든다. → 합성(composition)으로 구성
- 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로 → 컨텍스트 객체 수정 없이
- 알고리즘을 다양하게 변경할 수 있게 한다. → 메소드를 통해 전략 객체를 실시간으로 변경함으로써 전략을 변경
전략 패턴 흐름
클래스 구성
// 전략(추상화된 알고리즘)
interface IStrategy {
void doSomething();
}
// 전략 알고리즘 A
class ConcreteStrateyA implements IStrategy {
public void doSomething() {}
}
// 전략 알고리즘 B
class ConcreteStrateyB implements IStrategy {
public void doSomething() {}
}
// 컨텍스트(전략 등록/실행)
class Context {
IStrategy Strategy; // 전략 인터페이스를 합성(composition)
// 전략 교체 메소드
void setStrategy(IStrategy Strategy) {
this.Strategy = Strategy;
}
// 전략 실행 메소드
void doSomething() {
this.Strategy.doSomething();
}
}
클래스 흐름
// 클라이언트(전략 교체/전략 실행한 결과를 얻음)
class Client {
public static void main(String[] args) {
// 1. 컨텍스트 생성
Context c = new Context();
// 2. 전략 설정
c.setStrategy(new ConcreteStrateyA());
// 3. 전략 실행
c.doSomething();
// 4. 다른 전략 설정
c.setStrategy(new ConcreteStrateyB());
// 5. 다른 전략 시행
c.doSomething();
}
}
전략 패턴 특징
전략 패턴 사용 시기
- 전략 패턴은 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 그리고 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때 사용할 수 있다.
- 또 전략 패턴은 객체의 행동들을 즉, 특정 하위 행동들을 다양한 방식으로 수행할 수 있는 다른 하위 객체들과 연관시켜 객체의 행동들을 런타임에 간접적으로 변경할 수 있게 해준다.
- 전략 패턴은 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우에 사용하자.
- 전략 패턴은 다양한 행동들을 별도의 클래스 계층구조로 추출하고 원래 클래스들을 하나로 결합하여 중복 코드를 줄일 수 있게 해준다.
- 전략 패턴을 사용하여 클래스의 비즈니스 로직을 해당 로직의 콘텍스트에서 그리 중요하지 않을지도 모르는 알고리즘들의 구현 세부 사항들로부터 고립시킨다. (캡슐화)
- 전략 패턴은 코드의 나머지 부분에서 해당 코드, 내부 데이터, 그리고 다양한 알고리즘들의 의존 관계들을 고립시킬 수 있다. 다양한 클라이언트들이 알고리즘들을 실행하고 런타임에 전환하기 위한 간단한 인터페이스를 사용한다.
- 이 패턴은 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 있을 때 사용한다.
- 전략 패턴을 사용하면 모든 알고리즘을 같은 인터페이스를 구현하는 별도의 클래스들로 추출하여 이러한 조건문을 제거할 수 있다. 원래 객체는 알고리즘의 모든 변형들을 구현하는 대신 이러한 객체들 중 하나에 실행을 위임한다.
전략 패턴의 장단점
앞서 전략 패턴은 Behavioral 패턴 중에 하나로, 객체 간의 커뮤니케이션에 유연성을 부여한다고 했다.
[장점]
- 전략 사용자(context)의 코드 변경 없이 새로운 전략을 추가 할 수 있다.
- 이를 통해 if - else 분기를 제거할 수 있다.
- if - else 분기를 제거하면, 단일 책임 원칙을 준수하기 더 수월해진다.
- 확장에 유리한 코드를 작성할 수 있다.
- 새롭게 필요한 전략 콘크리트 클래스를 쉽게 만들 수 있다.
- 개방 폐쇄 원칙을 준수한 코드 작성이 가능하다.
- 런타임에 전략을 변경시킬 수 있다.
- 상속을 합성으로 대체할 수 있다.
[단점]
- 알고리즘이 많아질수록 관리해야할 객체의 수가 늘어난다는 단점이 있다.
- 만일 어플리케이션 특성이 알고리즘이 많지 않고 자주 변경되지 않는다면, 새로운 클래스와 인터페이스를 만들어 프로그램을 복잡하게 만들 이유가 없다.
- 개발자는 적절한 전략을 선택하기 위해 전략 간의 차이점을 파악하고 있어야 한다. (복잡도 ↑)
- 어플리케이션에 들어가는 모든 전략을 알고 있어야 한다.
- 클래스로 분리한 각 전략들이 어느 상황에 사용되어야 할 지 알고 있어야 한다.
- 이 같은 특성이 어쩌면 유지보수를 더 힘들게 할 수도 있다.
- 전략을 추상화한 인터페이스가 효율적이지 못할 수 있다.
- 어떤 전략 콘크리트 객체에서는 사용하지 않는 메서드들 역시 전략 인터페이스에 정의해 주어야 한다.
- 현대의 많은 프로그래밍 언어에는 익명 함수들의 집합 내에서 알고리즘의 다양한 버전들을 구현할 수 있는 함수형 지원이 있으며, 클래스들과 인터페이스들을 추가하여 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 이러한 함수들을 사용할 수 있다.
Strategy 패턴 예제
RPG 게임에서 캐릭터의 무기 전략
실제로 게임에서 이런식으로 되어있지는 않지만, 그래도 재미를 위해 RPG 게임을 예시로 들어보았다.
이 패턴 구현 예제의 컨셉은 적의 특성에 따라 주인공이 무기 전략을 바꿔가며 대응하는 것이다.
전략 패턴을 적용하지 않은 문제의 코드 ❌
아래 코드를 살펴보면 state 매개변수의 값에 따라서 간접적으로 attack() 함수의 동작을 제어하도록 되어 있다. 적이 오면 상수를 메소드에 넘겨 조건문으로 일일히 필터링하여 적절한 전략을 실행하였다.
하지만 상태 변수를 통해 행위를 분기문으로 나누는 행위는 좋지 않은 코드이다. 자칫 잘못하면 if else 지옥에 빠질 수 있기 때문이다.
class TakeWeapon {
public static final int SWORD = 0;
public static final int SHIELD = 1;
public static final int CROSSBOW = 2;
private int state;
void setWeapon(int state) {
this.state = state;
}
void attack() {
if (state == SWORD) {
System.out.println("칼을 휘두르다");
} else if (state == SHIELD) {
System.out.println("방패로 밀친다");
} else if (state == CROSSBOW) {
System.out.println("석궁을 발사하다");
}
}
}
class User {
public static void main(String[] args) {
// 플레이어 손에 무기 착용 전략을 설정
TakeWeapon hand = new TakeWeapon();
// 플레이어가 검을 들도록 전략 설정
hand.setWeapon(TakeWeapon.SWORD);
hand.attack(); // "칼을 휘두르다"
// 플레이어가 방패를 들도록 전략 설정
hand.setWeapon(TakeWeapon.SHIELD);
hand.attack(); // "방패로 밀친다"
}
}
전략 패턴을 적용한 코드 ✔️
위의 클린하지 않은 코드를 해결하는 가장 좋은 방법은 변경시키고자 하는 행위(전략)를 직접 넘겨주는 것이다.
우선 여러 무기들을 객체 구현체로 정의하고 이들을 Weapon이라는 인터페이스로 묶어 주었다. 그리고 인터페이스를 컨텍스트 클래스에 합성(composition) 시키고, setWeapon() 메소드를 통해 전략 인터페이스 객체의 상태를 바로바로 변경할 수 있도록 구성 하였다.
// 전략 - 추상화된 알고리즘
interface Weapon {
void offensive();
}
class Sword implements Weapon {
@Override
public void offensive() {
System.out.println("칼을 휘두르다");
}
}
class Shield implements Weapon {
@Override
public void offensive() {
System.out.println("방패로 밀친다");
}
}
class CrossBow implements Weapon {
@Override
public void offensive() {
System.out.println("석궁을 발사하다");
}
}
// 컨텍스트 - 전략을 등록하고 실행
class TakeWeaponStrategy {
Weapon wp;
void setWeapon(Weapon wp) {
this.wp = wp;
}
void attack() {
wp.offensive();
}
}
// 클라이언트 - 전략 제공/설정
class User {
public static void main(String[] args) {
// 플레이어 손에 무기 착용 전략을 설정
TakeWeaponStrategy hand = new TakeWeaponStrategy();
// 플레이어가 검을 들도록 전략 설정
hand.setWeapon(new Sword());
hand.attack(); // "칼을 휘두르다"
// 플레이어가 방패를 들도록 전략 변경
hand.setWeapon(new Shield());
hand.attack(); // "방패로 밀친다"
// 플레이어가 석궁을 들도록 전략 변경
hand.setWeapon(new Crossbow());
hand.attack(); // "석궁을 발사하다"
}
}
전략 패턴을 적용하지 않은 코드에서는 메서드에 상수값을 넘겨주었지만, 전략 패턴에선 인스턴스를 넣어 알고리즘을 수행하도록 했다.
이런식으로 구성하면 좋은 점은 나중에 칼이나 방패외에 도끼나 창과 같은 전략 무기들을 추가로 등록할때, 코드의 수정없이 빠르게 기능을 확장할 수 있다는 장점이 있다. (클래스를 추가하고 implements 해주면 끝)
결국 객체 지향 프로그래밍의 핵심인 유지보수를 용이하게 하기위해, 약간 복잡하더라도 이러한 패턴을 적용하여 프로그램을 구성해 나가는 것이다.
여러 기능 전략을 가진 로봇
전략 패턴을 적용하지 않은 문제의 코드 ❌
다음과 같이 Robot이라는 추상 클래스가 있고 이를 상속하는 걷는 로봇(WalingRobot), 뛰는 로봇(RunningRobot) 으로 구성된 객체가 있다.
public abstract class Robot {
public abstract void display();
public abstract void move();
}
class WalkingRobot extends Robot {
public void display() {
System.out.println("걷기 로봇");
}
public void move() {
System.out.println("걸어서 배달합니다 삐-빅");
}
}
class RunningRobot extends Robot {
public void display() {
System.out.println("달리는 로봇");
}
public void move() {
System.out.println("뛰어서 배달합니다 삐-빅");
}
}
class Main {
public static void main(String[] args) {
Robot robot1 = new WalkingRobot();
robot1.display();
robot1.move();
Robot robot2 = new RunningRobot();
robot2.display();
robot2.move();
}
}
보기에는 객체 지향적인 문제 없는 코드이지만, 만일 고객으로부터 로봇의 기능 추가를 요청받았을때 코드의 유지 보수 면에서 문제가 발생한다.
예를들어 로봇에 번역 기능도 추가한다고 가정한다면 translate() 메서드를 각각의 걷는 로봇(WalingRobot), 뛰는 로봇(RunningRobot)에 추가해줘야 한다. 그래서 추상화 구조에 맞게 Robot 추상 클래스에 메서드를 추가하고 각기 기능을 알맞게 하는 로봇 클래스를 분리하여 구현하니 자식 로봇 클래스가 두배로 늘어나 버렸다.
여기서 또 기능을 추가하면 이번엔 클래스가 8개로 불어날 것이다. 그뿐만 아니라 어느 하나의 기능 move() 메서드 스펙을 변경해야 된다면 전체 자식 클래스에 등록 되어있는 move() 메서드를 일일히 뒤져 수정해야 할 것이다.
public abstract class Robot {
public abstract void display();
public abstract void move();
public abstract void translate(); // 번역 메소드 하나를 추가했을 뿐인데
}
// --- 클래스 갯수가 두배로 늘어나 버렸다 !!
class KoreanWalkingRobot extends Robot {
public void display() {
System.out.println("걷기 로봇");
}
public void move() {
System.out.println("걸어서 배달합니다 삐-빅");
}
public void translate() {
System.out.println("한국어로 번역합니다 삐-비-빅");
}
}
class KoreanRunningRobot extends Robot {
public void display() {
System.out.println("달리는 로봇");
}
public void move() {
System.out.println("뛰어서 배달합니다 삐-빅");
}
public void translate() {
System.out.println("한국어로 번역합니다 삐-비-빅");
}
}
class JapaneseWalkingRobot extends Robot {
public void display() {
System.out.println("걷기 로봇");
}
public void move() {
System.out.println("걸어서 배달합니다 삐-빅");
}
public void translate() {
System.out.println("일본어로 번역합니다 삐-비-빅");
}
}
class JapaneseRunningRobot extends Robot {
public void display() {
System.out.println("달리는 로봇");
}
public void move() {
System.out.println("뛰어서 배달합니다 삐-빅");
}
public void translate() {
System.out.println("일본어로 번역합니다 삐-비-빅");
}
}
전략 패턴을 적용한 코드 ✔️
위와 같은 '클래스 폭발' 문제가 일어난 이유는 객체를 사물 / 생물 정도로 밖에 인식하지 못해서 이다.
객체는 하나의 기능이나 행위, 동작으로도 표현할 수 있다. 전략 패턴은 이러한 접근으로 복잡한 문제를 해결해 나가는 식이다.
예를 들어 로봇 클래스가 있고 서빙하는 동작 메소드가 있다면, 이 서빙 메소드를 로봇 사물 객체에 국한되게 하는게 아니라, 따로 행위 구현체로 빼서 정의하고 관리하는 것이다. 그리고 이 행위 객체들을 모아 인터페이스로 묶어 하나의 전략 묶움을 구성하고, 이것을 컨텍스트에 합성시켜 다형성을 통해 유기적으로 여러 전략 행위들을 사용할 수 잇도록 하는 것이다.
그래서 만들고자 하는 사물인 로봇 클래스는 하나이고, 로봇에 서로 다른 다양한 전략 객체들을 적용 시킴으로써 각기 다른 전략을 수행하는 로봇들을 여러개 만들거나 혹은 하나의 로봇을 가지고 여러가지 전략을 스위칭하여 실행할수 있게 하는 것이다.
// Run / Walk 전략(추상화된 알고리즘)
interface MoveStrategy {
void move();
}
class Walk implements MoveStrategy {
public void move() {
System.out.println("걸어서 배달합니다 삐-빅");
}
}
class Run implements MoveStrategy {
public void move() {
System.out.println("뛰러서 배달합니다 삐-빅");
}
}
// 한국어 / 일본어 번역 전략(추상화된 알고리즘)
interface TranslateStrategy {
void translate();
}
class Korean implements TranslateStrategy {
public void translate() {
System.out.println("한국어로 번역합니다 삐-비-빅");
}
}
class Japanese implements TranslateStrategy {
public void translate() {
System.out.println("일본어로 번역합니다 삐-비-빅");
}
}
// 컨텍스트(전략 등록/실행)
public class Robot {
MoveStrategy moveStrategy;
TranslateStrategy translateStrategy;
Robot(MoveStrategy moveStrategy, TranslateStrategy translateStrategy) {
this.moveStrategy = moveStrategy;
this.translateStrategy = translateStrategy;
}
void move() {
moveStrategy.move();
}
void translate() {
translateStrategy.translate();
}
void setMove(MoveStrategy moveStrategy) {
this.moveStrategy = moveStrategy;
}
void setTranslate(TranslateStrategy translateStrategy) {
this.translateStrategy = translateStrategy;
}
}
// 클라이언트(전략 교체/전략 실행한 결과를 얻음)
class User {
public static void main(String[] args) {
Robot robot = new Robot(new Walk(), new Korean());
robot.move(); // 걸어서 배달합니다 삐-빅
robot.translate(); // 한국어로 번역합니다 삐-비-빅
// 로봇의 전략(기능)을 run과 Japanese 번역으로 변경
robot.setMove(new Run());
robot.setTranslate(new Japanese());
robot.move(); // 뛰러서 배달합니다 삐-빅
robot.translate(); // 일본어로 번역합니다 삐-비-빅
}
}
카드 결제 전략 시스템
이번에는 좀더 실무에서 사용하는 전략 패턴 예제를 가져와 보았다.
다음 예제는 쇼핑 카트에 아이템을 담아 LUNA 신용카드 또는 KAKAO 신용카드라는 두 개의 전략을 이용해 상황에 따라 결제를 진행한다는 컨셉이다.
이번 예제와 위의 예제 간의 다른점은 전략 인터페이스인 PaymentStrategy를 클래스 필드로 합성(composition) 하지 않고 컨텍스트의 pay() 메소드의 매개변수로 합성(composition) 한다는 차이가 있다.
// 전략 - 추상화된 알고리즘
interface PaymentStrategy {
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 ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<Item>();
}
public void addItem(Item item) {
this.items.add(item);
}
// 전략을 매개변수로 받아서 바로바로 전략을 실행
public void pay(PaymentStrategy paymentMethod) {
int amount = 0;
for (Item item : items) {
amount += item.price;
}
paymentMethod.pay(amount);
}
}
class Item {
public String name;
public int price;
public Item(String name, int cost) {
this.name = name;
this.price = cost;
}
}
// 클라이언트 - 전략 제공/설정
class User {
public static void main(String[] args) {
// 쇼핑카트 전략 컨텍스트 등록
ShoppingCart cart = new ShoppingCart();
// 쇼핑 물품
Item A = new Item("맥북 프로", 10000);
Item B = new Item("플레이스테이션", 30000);
cart.addItem(A);
cart.addItem(B);
// LUNACard로 결제 전략 실행
cart.pay(new LUNACardStrategy("kundol@example.com", "pukubababo")); // 4000원 paid using LUNACard.
// KAKAOBank로 결제 전략 실행
cart.pay(new KAKAOCardStrategy("Ju hongchul", "123456789", "123", "12/01")); // 4000원 paid using KAKAOCard.
}
}
메소드의 입력값으로 객체를 할당하는 방식이 좋은 점은, 각 전략에 따라 초기화하는 생성자 매개변수 갯수가 다를 수 있기 때문이다.
예를들어 LUNA카드는 메일과 비밀번호만 필요하지만, KAKAO카드는 회사 정책에 따라 여러가지 정보들이 더 필요할 수 있기 때문이다.
실무에서 찾아보는 Strategy 패턴
Java
- Collections의 sort() 메서드에 의해 구현되는 compare() 메서드에 이용
- javax.servlet.http.HttpServlet에서 service() 메서드와 모든 doXXX() 메서드에 이용
- javax.servlet.Filter의 doFilter() 메서드에 이용
Comparator
사실 자바에서 익명 클래스로 구현체를 그때그때 정의하고 안의 동작 메소드의 로직을 그때그때 만들어 할당하는 방식도 일종의 전략을 실시간으로 변경하여 지정하는 패턴과 많이 유사하다고 볼 수 있다.
class StrategyInJava {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>();
numbers.add(2);
numbers.add(1);
numbers.add(3);
numbers.add(5);
numbers.add(4);
// sort 메서드의 매개변수로 익명 클래스로 Comparator 객체를 인스턴스화하여
// 그 안의 compare 메서드 동작 로직(ConcreteStrategy)를 직접 구현하여 할당하는 것을 볼 수 있다.
Collections.sort(numbers, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(numbers);
}
}
Spring Framework
public class StrategyInSpring {
public static void main(String[] args) {
// 각각의 설정에 따라 나눠진 ApplicationContext 들
ApplicationContext applicationContext = new ClassPathXmlApplicationContext();
ApplicationContext applicationContext1 = new FileSystemXmlApplicationContext();
ApplicationContext applicationContext2 = new AnnotationConfigApplicationContext();
BeanDefinitionParser parser;
PlatformTransactionManager platformTransactionManager; // 다양한 트랜잭션 메니저를 제공한다
CacheManager cacheManager; // 다양한 캐시 전략을 제공
}
}
Node.js
이부분은 노드를 다루는 자바스크립트 개발자들을 위한 예시이다. 디자인 패턴은 꼭 자바(java) 프로그래밍 언어에만 적용되는 사례가 아님을 보여주기 위해 넣어보았다.
Passport 라이브러리
passport.js라이브러리는 네이버, 카카오, 페이스북 로그인과 같은 OAuth 로그인을 구현할때 사용되는 아주 유명한 자바스크립트 라이브러리 이다. 그리고 이 passport.js가 바로 전략 패턴으로 구성 되어 있다.
아래 코드를 보면 알수 있듯이, passport.use(new SNSStrategy(), ...) 처럼 passport.use()라는 메서드에 입력값으로 전략 객체를 만들어 로직을 수행하는 것을 볼 수 있다.
아직 전략 패턴을 배우기전에 왜 변수명을 Strategy 라고 명명하는지 몰랐었는데, 이제 패턴을 배우고 나니 상당히 구조적으로 짜여있다는 것을 느낄 수 있을 것이다.
const passport = require('passport'); // passport.js의 객체 가져오기
const KakaoStrategy = require('passport-kakao').Strategy;
const NaverStrategy = require('passport-naver-v2').Strategy;
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// 카카오 로그인 전략 등록
passport.use(new KakaoStrategy({ clientID, callbackURL }, async (accessToken, refreshToken, profile, done) => {
// ...
});
// 네이버 로그인 전략 등록
passport.use(new NaverStrategy({ clientID, clientSecret, callbackURL }, async (accessToken, refreshToken, profile, done) => {
// ...
});
// 구글 로그인 전략 등록
passport.use(new GoogleStrategy({ clientID, clientSecret, callbackURL }, async (accessToken, refreshToken, profile, done) => {
// ...
});
비슷한 디자인 패턴 비교
Strategy vs Temaplate Method
패턴 유사점
- 전략 패턴과 템플릿 메서드 패턴은 알고리즘을 때에 따라 적용한다는 컨셉으로써, 둘이 공통점을 가지고 있다.
- 전략 및 템플릿 메서드 패턴은 개방형 폐쇄 원칙을 충족하고 코드를 변경하지 않고 소프트웨어 모듈을 쉽게 확장할 수 있도록 하는 데 사용할 수 있다.
패턴 차이점
- 전략 패턴은 합성(composition)을 통해 해결책을 강구하며, 템플릿 메서드 패턴은 상속(inheritance)을 통해 해결책을 제시한다.
- 그래서 전략 패턴은 클라이언트와 객체 간의 결합이 느슨한 반면, 템플릿 메서드 패턴에서는 두 모듈이 더 밀접하게 결합된다. (결합도가 높으면 안좋음)
- 전략 패턴에서는 대부분 인터페이스를 사용하지만, 템플릿 메서드 패턴서는 주로 추상 클래스나 구체적인 클래스를 사용한다.
- 전략 패턴에서는 전체 전략 알고리즘을 변경할 수 있지만, 템플릿 메서드 패턴에서는 알고리즘의 일부만 변경되고 나머지는 변경되지 않은 상태로 유지된다. (템플릿에 종속)
- 따라서 단일 상속만이 가능한 자바에서 상속 제한이 있는 템플릿 메서드 패턴보다는, 다양하게 많은 전략을 implements 할 수 있는 전략 패턴이 협업에서 많이 사용되는 편이다.
다른 패턴과의 관계
- 브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있다. 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 한다.
하지만 이 패턴들은 모두 다른 문제들을 해결한다. 패턴은 특정 방식으로 코드의 구조를 짜는 레시피에 불과하지 않는다. 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문이다. - 커맨드와 전략 패턴은 비슷해 보일 수 있다. 왜냐하면 둘 다 어떤 작업으로 객체를 매개변수화하는 데 사용할 수 있기 때문이다. 그러나 이 둘의 의도는 매우 다르다.
- 커맨드를 사용하여 모든 작업을 객체로 변환할 수 있다. 작업의 매개변수들은 해당 객체의 필드들이 된다. 이 변환은 작업의 실행을 연기하고, 해당 작업을 대기열에 넣고, 커맨드들의 기록을 저장한 후 해당 커맨드들을 원격 서비스에 보내는 등의 작업을 가능하게 한다.
- 반면에 전략 패턴은 일반적으로 같은 작업을 수행하는 다양한 방법을 설명하므로 단일 콘텍스트 클래스 내에서 이러한 알고리즘들을 교환할 수 있도록 해야한다.
- 데코레이터는 객체의 피부를 변경할 수 있고 전략 패턴은 객체의 내장을 변경할 수 있다고 비유할 수 있다.
- 템플릿 메서드는 상속을 기반으로 한다. 이 메서드는 자식 클래스들에서 알고리즘의 부분들을 확장하여 변경할 수 있도록 한다.
전략 패턴은 합성을 기반으로 한다. 객체 행동의 일부분들을 이러한 행동에 해당하는 다양한 전략들을 제공하여 변경할 수 있다.
템플릿 메서드는 클래스 수준에서 작동하므로 정적이다.
전략 패턴은 객체 수준에서 작동하므로 런타임에 행동들을 전환할 수 있도록 한다. - 상태는 전략의 확장으로 간주할 수 있다. 두 패턴 모두 합성을 기반으로 한다. 그들은 어떤 작업을 도우미 객체들에 전달하여 콘텍스트의 행동을 바꾼다. 전략 패턴은 이러한 객체들을 완전히 독립적으로 만들어 서로를 인식하지 못하도록 만든다. 그러나 상태는 구상 상태들 사이의 의존 관계들을 제한하지 않으므로 그들이 콘텍스트의 상태를 마음대로 변경할 수 있도록 한다.
참고