본문 바로가기

[Project] 프로젝트 삽질기48 (feat 전략 패턴)

어가며

NestJS와 TypeORM을 활용하여 프로덕트를 만들고 있습니다. 비즈니스 로직을 구성하는데, if else if 문이 계속해서 추가되다 보니 코드의 가독성이 좋지 못한 문제가 발생했습니다. if - else 문을 개선하기 위해 노력하면서 전략 패턴에 대해 알 수 있었습니다. 이번 글은 전략 패턴에 대해 공부하고, 전략 패턴을 적용하기 위해 노력하면서 작성된 글입니다. 

 

 

 

 


 

 

 

If문의 증가

API를 개발하면서, 기획의 요청이 들어오면 API의 동작 방식을 계속해서 변경해야 했습니다. if문을 사용하여 API 동작을 제어했는데, 추가적인 조건들이 계속해서 들어올 때마다 if-else if 문이 점점 쌓여가는 문제가 발생했습니다.

예를 들어 조종석 Class에서 조종석의 종류에 따라 방어력이 바뀐다고 가정해 보겠습니다. 

 

class Cockpit {
  private kindCockpit: string; // 조종석의 종류
  private armorPower: number;
  private attackPower: number;

  constructor(kindCockpit: string) {
    this.kindCockpit = kindCockpit;
  }

  public armorFeature(): number {
    if (this.kindCockpit === '기본 조종석') {
      this.armorPower = 50;
    } else if (this.kindCockpit === '초강력 장갑 조종석') {
      this.armorPower = 300;
    } else if (this.kindCockpit === '최첨단 조종석') {
      this.armorPower = 100;
    }
    return this.armorPower;
  }
}

const basicCockpit = new Cockpit('기본 조종석');
const powerfulCockpit = new Cockpit('초강력 장갑 조종석');
const advancedCockpit = new Cockpit('최첨단 조종석');

console.log(basicCockpit.armorFeature()); // 50
console.log(powerfulCockpit.armorFeature()); // 300
console.log(advancedCockpit.armorFeature()); // 100

 

 

조종석의 종류에 따라 다른 로직을 가져야 해서, if - else if 문 등으로 분기 처리를 했습니다. 여기서 문제는 조종석의 종류가 추가, 수정, 삭제될 때마다 조종석 클래스의 if문을 일일이 다 고쳐야 한다는 점이었습니다. 그리고 조종석의 종류가 늘어날 때마다 if문이 계속 증가하고, 수정되어야 해서 코드의 가독성이 떨어지는 문제가 발생했습니다. 이 문제를 전략 패턴을 사용해서 해결할 수 있었습니다. 

 

 

 

 

 

 

 


 

전략 패턴

전략 패턴을 알아보기 전에, 디자인 패턴에 대해 한 번 정리해보고자 합니다.

 

디자인 패턴은 소프트웨어 공학에서 좋은 코드를 설계하기 위한 설계 방법론입니다. 좋은 코드는 설계적인 측면에서 확장과 유지보수가 용이하며, 추가적인 수정에 비용이 적게 들어가는 코드를 말합니다. 디자인 패턴에는 생성 패턴, 구조 패턴, 행위 패턴이 있으며, 각 패턴은 다른 목적을 가지고 있습니다.

 

생성 패턴은 생성 패턴은 객체 생성에 관련된 패턴으로, 객체 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공합니다.

 

구조 패턴은 클래스나 객체를 조합해 더 큰 구조를 만드는 패턴으로, 서로 다른 인터페이스를 지닌 객체를 묶어 단일 인터페이스를 제공하거나 서로 다른 객체들을 묶어 새로운 기능을 제공하는 패턴입니다.

 

행위 패턴은 객체나 클래스 사이의 알고리즘이나 책임 분배에 관련된 패턴으로, 한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점을 두는 방식입니다. 

 

전략 패턴은 행위 패턴 중 하나입니다. 어떤 일을 수행하는 방식이 여러 가지일 때, 각각의 알고리즘을 클래스로 캡슐화하고 공통된 인터페이스로 추상화해서 로직을 수행하는 곳에서는 추상된 인터페이스만 사용함으로써 코드 변경 없이 알고리즘을 바꿀 수 있도록 돕는 패턴입니다. 이를 통해 프로그램이 유지보수와 확장에 용이하며, 객체 간 결합도가 낮아지는 장점이 있습니다.

 

API를 개발하다 보면, 변경 또는 확장이 될수록 코드가 복잡해집니다. 리팩토링 시 리팩토링 부분을 찾는데 점점 오래 걸리게 됩니다. 실수로 추가하지 않고 누락하는 부분이 생길 가능성이 있습니다. 하지만 전략 패턴을 사용하면 OCP를 지키면서, 다중 조건문을 없앨 수 있습니다. 

 

 

 

 

1. 적용 방법 및 특징

전략 패턴은 상속과 컴포지션 두 가지 방법으로 적용할 수 있습니다.

상속 방법은 슈퍼클래스로부터 상속을 받아 서브클래스에서 메서드를 오버라이딩하여 구현하는 방식입니다. 하지만 이 방법은 서브클래스에서 코드 중복이 발생하며, 슈퍼클래스에서 변경이 있을 경우에는 다른 서브클래스에도 원하지 않는 영향을 미칠 수 있습니다.


컴포지션 방법은 바뀌는 부분을 따로 뽑아서 캡슐화하고, 변경될 부분과 변하지 않을 부분을 구분하여 변경될 부분을 인터페이스로 추출하는 방법입니다. 모듈이 만나는 지점에 인터페이스를 정의하고, 인터페이스에 의존하도록 코드를 작성합니다.

 

 

 

 

2. 전략 패턴 장단점

전략 패턴을 사용하면 동일 계열의 관련 알고리즘이 생깁니다. Strategy 클래스 계층을 통해 동일 계열의 알고리즘군을 정의하고, 정의한 알고리즘을 재사용할 수도 있습니다. 또한 Strategy를 사용하는 Composition 클래스에 서브클래싱을 하지 않아도 됩니다. 그리고 서로 다른 Strategy 클래스의 행동을 캡슐화를 통해 조건문을 없앨 수 있습니다. 하지만 전략 패턴을 활용하면 복잡도가 증가하고, 클라이언트 코드가 구체적인 전략을 알아야 한다는 단점이 있습니다. 

 

 

 

 

 

 

 


 

 

 

전략 패턴 적용

그럼 이런 전략 패턴을 활용해서 위에서 나온 if 중첩 문제를 해결해 보겠습니다.

 

 

interface CockpitStrategy {
  armorFeature(): number;
}

class BasicCockpitStrategy implements CockpitStrategy {
  armorFeature(): number {
    return 50;
  }
}

class PowerfulCockpitStrategy implements CockpitStrategy {
  armorFeature(): number {
    return 300;
  }
}

class AdvancedCockpitStrategy implements CockpitStrategy {
  armorFeature(): number {
    return 100;
  }
}

class Cockpit {
  private cockpitStrategy: CockpitStrategy;

  constructor(cockpitStrategy: CockpitStrategy) {
    this.cockpitStrategy = cockpitStrategy;
  }

  public armorFeature(): number {
    return this.cockpitStrategy.armorFeature();
  }
}

const basicCockpit = new Cockpit(new BasicCockpitStrategy());
const powerfulCockpit = new Cockpit(new PowerfulCockpitStrategy());
const advancedCockpit = new Cockpit(new AdvancedCockpitStrategy());

console.log(basicCockpit.armorFeature()); // 50
console.log(powerfulCockpit.armorFeature()); // 300
console.log(advancedCockpit.armorFeature()); // 100

 

 

 

전략 패턴에서 크게 3가지 개념인 context, strategy, concrateStrategy에 대해 알아야 합니다.

 

Context란 Strategy 패턴을 이용하는 역할을 수행합니다. 필요에 따라 DI를 활용해서 동적으로 구체적인 전략을 바꿀 수 있도록 합니다. 위의 예시에서는 Cockpit 클래스가 context에 해당합니다.

 

Strategy는 인터페이스나 추상 클래스로 외부에서 동일한 방식으로 알고리즘을 호출하는 방법을 명시합니다. 위의 예시에서 CockpitStrategy가 Strategy의 예시에 해당합니다.

 

마지막으로 ConcreteStrategy는 전략 패턴에서 명시한 알고리즘을 실제로 구현한 클래스를 말합니다. BasicCockpitStrategy, PowerfulCockpitStrategy, AdvancedCockpitStrategy가 ConcreteStrategy의 예시에 해당합니다. 

 

 

전략 패턴을 활용해서 if문 내에 있던 로직을, 해당 Strategy의 메서드를 불러와서 실행시키는 형태로 바꿨습니다. 이를 통해 상황별로 다른 로직을 처리해야 했던 경우에 if문으로 분기 처리를 했던 방식에서, Strategy 객체를 주입해 주는 방식으로 상황별로 로직을 다르게 처리할 수 있도록 만들 수 있었습니다. 



저는 전략 패턴을 활용하여 유저의 닉네임을 검색하는 로직을 구성했습니다. 다음 글에서 전략 패턴과 pg_bigm을 활용하여 full text search 닉네임 검색 시스템을 만드는 방식에 대해 살펴보겠습니다. 

 

 

 

 

 


 

 

 

 

 

 

 

 

마치며

공부를 하며 그동안 아주 기초적인 것들을 제대로 공부하지 않았다는 것을 깨닫습니다. 좋은 기술을 배우는 것도 좋지만, 기술의 기반이 되는 기초적인 지식을 먼저 쌓는 것이 중요하다는 것을 다시금 깨달았습니다. 기초가 튼튼한 개발자가 되고 싶습니다.

 

 

 

 

 

 

 


 

 

 

출처

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

전략패턴과 커맨드패턴

인터페이스를 구현하여 사용하는 두 가지 패턴인 전략 패턴과 커맨드 패턴. 자주 사용하지만, 이 둘의 차이점은 무엇일까? 알쏭달쏭한 전략 패턴과 커맨드 패턴의 차이점을 알아보는 전략 패턴

tecoble.techcourse.co.kr

 

[디자인패턴] 전략 패턴 ( Strategy Pattern )

전략 패턴 ( Strategy Pattern )객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여,객체의 행위를 동적으로 바꾸고 싶은 경우 직접

victorydntmd.tistory.com

 

전략 패턴 (Strategy Pattern)

서론 우아한테크코스 레벨1 첫번째 미션인 자동차 경주를 구현하며, '랜덤값을 사용하는 메서드에 대한 테스트는 어떻게 하면 좋을까?' 에 대한 고민을 했다. 결론적으로 RandomGeneratable 인터페이

hudi.blog

 

더블유클럽 기술블로그

더블유클럽 기술블로그 (W Club Tech Blog)

techblog.wclub.co.kr