전략패턴

객체의 행위를 바꾸고 싶은 경우 직접 수정하지 않고, 캡슐화한 알고리즘 즉, 전략을 컨텍스트 안에서 바꿔주면서 교체가 가능하도록 만드는 것이다.

기존방식이 하나의 함수 안에 A일 때는 a실행, B일 때는 b 실행이라고 명시적으로 모든 방법을 다 적어두는 것이라고 한다면, 전략 패턴은 A와 B를 각각 독립된 객체로 만들고, 상황에 따라 필요한 객체를 넘겨주는 방식이다.

전략 패턴을 왜 쓸까?

if-else문이 길게 늘어지고 복잡해지는 걸 방지하기 위해서이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const authentication = (strategy) => {
    if(!strategy) {
        return `error`
    }

    if(strategy === 'faceID' ) {
        return `user choose faceID. process...`
    } else if (strategy === 'fingerPrint' ) {
        return `user choose fingerPrint. process...`
    } else if( strategy === 'PIN' ) {
        return `user choose PIN. process...`
    }
}

console.log(authentication('faceID'))
console.log(authentication('fingerPrint'))
console.log(authentication('PIN'))

만약 여기서 인증방식이 추가된다면 else if로 기존 코드를 계속 수정해야 한다. 이는 ‘기능이 변하거나 확장되는 것은 가능하지만 그 과정에서 기존의 코드가 수정되지 않아야 한다’는OCP 원칙을 위반한다. 따라서 위의 코드를 전략 패턴이 적용된 코드로 수정하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const faceID = (user) => `${user} choose faceID. process...`
const fingerPrint = (user) => `${user} choose fingerPrint. process...`
const PIN = (user) => `${user} choose PIN. process...`

const authentication = (user, strategy) => {
    if(!strategy) {
        return 'Error: Invalid authentication strategy'
    }
    if(!user) {
        return 'Error: Invalid user'
    }
    return strategy(user);
}

console.log(authentication('Joan', faceID)) //Joan choose faceID. process...
console.log(authentication('Joan', fingerPrint))//Joan choose fingerPrint. process...
console.log(authentication('Joan', PIN)) //Joan choose PIN. process...

만약 금융인증서, 모바일 신분증 등 인증수단이 추가되어도 authentication()은 수정할 필요 없다.

실제 전략패턴을 사용하는 라이브러리 : Passport

Passport라는 인증 모듈을 구현할 때 사용하는 미들웨어 라이브러리에서 전략패턴이 사용된다는 걸 알 수 있다. 약 500가지 정도 되는 인증 strategies documentation을 보면

1
2
passport.use(new BearerStrategy(
...
1
2
passport.use(new TwitterStrategy({
...
1
2
passport.use(new GoogleStrategy({
...

모두 passport.use()라는 함수에 각 인증 전략 GoogleStrategy, TwitterStrategy, BearerStrategy 를 변수로 넣어서 로직을 수행하는 걸 볼 수 있다.

실제 passport.use()함수를 들여다보면,

1
2
3
4
5
6
7
8
9
10
Authenticator.prototype.use = function(name, strategy) {
  if (!strategy) {
    strategy = name;
    name = strategy.name;
  }
  if (!name) { throw new Error('Authentication strategies must have a name'); }
  
  this._strategies[name] = strategy;
  return this;
};

이렇게 작성되어 있다.

마치며

결국 전략 패턴은 변경에는 닫혀 있고 확장에는 열려 있어야 한다는 OCP(개방-폐쇄 원칙)를 가장 직관적으로 실현하는 방법이다. 단순한 if-else문의 나열에서 벗어나 각각의 알고리즘을 독립된 객체로 분리함으로써, 코드의 가독성을 높이고 유지보수를 용이하게 만든다. Passport 라이브러리의 사례처럼 복잡한 인증 로직조차 단일한 인터페이스를 통해 유연하게 관리할 수 있다는 점은 전략 패턴이 실무에서 얼마나 강력한 도구가 되는지를 잘 보여준다.

카테고리:

업데이트: