ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Design Pattern] 전략 패턴(Strategy pattern) in Javascript
    Backend/Design Pattern 2024. 7. 16. 14:46
    728x90

    작은 게임을 하나 제작하려고 하는데 처음 기획이 아래와 같다고 가정합니다.

    • 챔피언은 이름, 체력, 공격타입(AD, AP)을 가집니다.
    • 챔피언의 역할군에는 전사와 마법사가 있습니다.

    따라서 아래 코드처럼 챔피언 추상 클래스를 정의하고 상속받아 전사와 마법사 챔피언 클래스를 생성합니다.

    // 챔피언 이름
    enum Championname {
      BRIAR = 'Briar',
      VAX = 'Vax',
    }
    
    // 공격 종류
    enum AttackType {
      AD = 'AD',
      AP = 'AP',
    }
    
    abstract class Champion {
      constructor(
        public readonly championInfo: {
          name: Championname;
          health: number;
          attackType: AttackType;
          basePower: number;
        }
      ) {}
    
      abstract championRole(): void;
    }
    
    // 챔피언 클래스를 상속하는 전사, 마법사 클래스
    /** 전사 챔피언 */
    class FighterChampion extends Champion {
      championRole(): void {
        console.log('전사 역할 챔피언');
      }
    }
    
    /** 마법사 챔피언 */
    class MageChampion extends Champion {
      championRole(): void {
        console.log('마법사 역할 챔피언');
      }
    }

     

    기능을 추가하게 되서 아래와 같이 추가로 구현을 해야하는 상황이 되었습니다.

    • 스킬셋은 Q, W, E, R로 구성되어 있습니다. 
    • 챔피언의 역할군에 따라 스킬의 효과가 다릅니다.

     

    챔피언 추상 클래스에 추상 스킬 메서드를 추가하고, 상속을 사용하여 전사 챔피언과 마법사 챔피언에 추상 스킬 메서드를 구현한다면 아래와 같은 문제점이 발생합니다.

    1. 중복 코드:

    각 챔피언 클래스 내부에 스킬 로직을 추가해야 합니다. 이는 각 역할군마다 반복되는 코드가 많아지고, 코드의 중복이 발생합니다.

    2. 유연성 부족:

    스킬 로직이 각 챔피언 클래스 내부에 고정되므로, 새로운 스킬을 추가하거나 기존 스킬을 변경할 때 챔피언 클래스를 수정해야 합니다. 이는 코드의 유연성을 저하시킵니다.

    3. 확장성 제한:

    새로운 스킬셋을 추가하거나 역할군에 따라 스킬 효과를 변경하려면 많은 수정을 해야 합니다. 이는 코드의 확장성을 제한합니다.

    4. 유지보수 어려움:

    스킬 로직이 각 챔피언 클래스에 흩어져 있으면, 특정 스킬 로직을 수정해야 할 때 여러 클래스를 수정해야 할 수 있습니다. 이는 유지보수를 어렵게 만듭니다.

     

    전략 패턴을 사용하면 이 문제점을 해결할 수 있습니다.

    전략패턴 : 객체 지향 디자인 패턴 중 하나로 동일한 문제를 해결하기 위한 여러 알고리즘(전략)을 정의하고, 각각을 캡슐화하여 상호 교환 가능하도록 만드는 패턴

    캡슐화 : 객체의 상태와 행위를 하나로 묶어 외부에서 접근을 제어하는 것
    • 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리해서 캡슐화합니다.
      • 이 기획에서 변하는 부분은 스킬셋입니다.
      • 따라서 스킬셋을 캡슐화합니다.
    • 구현보다는 인터페이스에 맞춰서 프로그래밍합니다.
    • 상속보다는 구성(Composition)을 활용합니다.

     

    구성도를 그려보면 아래와 같습니다.

     

    변하는 부분인 스킬셋의 인터페이스를 만듭니다.

    // 챔피언 이름
    enum Championname {
      BRIAR = 'Briar',
      VAX = 'Vax',
    }
    
    // 공격 종류
    enum AttackType {
      AD = 'AD',
      AP = 'AP',
    }
    
    // (추가)Skill 인터페이스
    interface Skill {
      useSkill(champion: Champion): void;
    }

    => 인터페이스를 사용해 Skill 인터페이스를 구현하는 클래스는 useSkill 메서드를 무조건 구현하도록 강제합니다. 

     

    Dependency Injection을 이용해 Champion 클래스에 SkillSet을 주입합니다.

    // Champion 클래스
    abstract class Champion {
      constructor(
        public readonly championInfo: {
          name: Championname;
          health: number;
          attackType: AttackType;
          basePower: number;
        },
        private readonly skillSet: { // composition 사용
          qSkill: Skill;
          wSkill: Skill;
          eSkill: Skill;
          rSkill: Skill;
        }
      ) {}
    
      // 스킬 메서드 추가
      useQ(): void { 
        this.skillSet.qSkill.useSkill(this);
      }
    
      useW(): void {
        this.skillSet.wSkill.useSkill(this);
      }
    
      useE(): void {
        this.skillSet.eSkill.useSkill(this);
      }
    
      useR(): void {
        this.skillSet.rSkill.useSkill(this);
      }
    
      abstract championRole(): void;
    }
    • private readonly skillSet : Champion 클래스의 skillSet 속성은 private로 선언되어 있어 외부에서 직접 접근할 수 없습니다.
      • => 즉, 캡슐화의 핵심 요소입니다.

     

    • 또한 useQ, useW, useE, useR 메서드를 통해서만 skillSet에 접근할 수 있습니다.
      • => 내부 스킬 로직을 보호하고, 외부에서 직접 스킬을 수정하거나 접근하지 못하도록 합니다.

     

    • Champion 클래스는 추상 클래스입니다. 추상 클래스는 인스턴스화할 수 없고, 다른 클래스가 상속받아 구현해야 하는 메서드를 정의할 수 있습니다.
    추상화 : 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정

    불필요한 세부 사항을 숨기고 중요한 부분만 노출하여 시스템을 단순화하고 사용하기 쉽게 만들 수 있습니다.

     

    위에서 구현한 skill 인터페이스를 구현하는 전사 Q, W, E, R 스킬과, 마법사 Q, W, E, R 스킬 클래스를 생성하고 전사 스킬셋 클래스와 마법사 클래스 스킬셋 클래스를 생성합니다.

    // Skill 인터페이스를 구현하는 클래스
    // Fighter Skills
    class FighterQSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.2;
        console.log(
          `\n 전사 Q 스킬 사용 \n${champion.championInfo.name} uses Fighter Q skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class FighterWSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.8;
        console.log(
          `\n 전사 W 스킬 사용 \n${champion.championInfo.name} uses Fighter W skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class FighterESkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.3;
        console.log(
          `\n 전사 E 스킬 사용 \n${champion.championInfo.name} uses Fighter E skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class FighterRSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 1.0;
        console.log(
          `\n 전사 R 스킬 사용 \n${champion.championInfo.name} uses Fighter R skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    // 전자 skillset
    class FighterSkillSet {
      qSkill: Skill = new FighterQSkill();
      wSkill: Skill = new FighterWSkill();
      eSkill: Skill = new FighterESkill();
      rSkill: Skill = new FighterRSkill();
    }
    // Mage Skills
    class MageQSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.4;
        console.log(
          `\n 마법사 Q 스킬 사용 \n${champion.championInfo.name} uses Mage Q skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class MageWSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.5;
        console.log(
          `\n 마법사 W 스킬 사용 \n${champion.championInfo.name} uses Mage W skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class MageESkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 0.6;
        console.log(
          `\n 마법사 E 스킬 사용 \n${champion.championInfo.name} uses Mage E skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    class MageRSkill implements Skill {
      useSkill(champion: Champion): void {
        const damage = champion.championInfo.basePower * 1.3;
        console.log(
          `\n 마법사 R 스킬 사용 \n${champion.championInfo.name} uses Mage R skill dealing ${damage} ${champion.championInfo.attackType} damage!`
        );
      }
    }
    
    // 마법사 skillset
    class MageSkillSet {
      qSkill: Skill = new MageQSkill();
      wSkill: Skill = new MageWSkill();
      eSkill: Skill = new MageESkill();
      rSkill: Skill = new MageRSkill();
    }

     

    챔피언 클래스를 상속하는 전사와 마법사 클래스를 생성합니다.

    상속 : 부모의 메서드, 프로퍼티를 자식 클래스가 받아서 재사용할 수 있는 것
    /** 전사 챔피언 */
    class FighterChampion extends Champion {
      championRole(): void {
        console.log('전사 역할 챔피언');
      }
    }
    
    /** 마법사 챔피언 */
    class MageChampion extends Champion {
      championRole(): void {
        console.log('마법사 역할 챔피언');
      }
    }

     

    전사와 마법사 스킬셋의 인스턴스를 생성하고 브라이어와 벡스 챔피언의 기본 능력치를 정의합니다.

    // 전사, 마법사 스킬들
    const fighterSkills = new FighterSkillSet();
    const mageSkills = new MageSkillSet();
    
    /** 브라이어 챔피언 정보 */
    const briarInfo: ChampionInstanceType = {
      name: Championname.BRIAR,
      health: 590,
      attackType: AttackType.AD,
      basePower: 60,
    };
    /** 벡스 챔피언 정보 */
    const vaxInfo: ChampionInstanceType = {
      name: Championname.VAX,
      health: 550,
      attackType: AttackType.AP,
      basePower: 70,
    };

     

    브라이어는 전사 챔피언, 벡스는 마법사 챔피언입니다. 따라서 브라이어 인스턴스는 전사 챔피언 클래스가 생성하고, 브라이어 정보와 전사 스킬셋을 사용합니다. 벡스 인스턴스는 마법사 챔피언 클래스가 생성하고, 벡스 정보와 마법사 스킬셋을 사용합니다.

    /** 브라이어 */
    const Briar = new FighterChampion(briarInfo, fighterSkills);
    /** 벡스 */
    const Vex = new MageChampion(vaxInfo, mageSkills);

     

    생성된 두 객체의 역할군과 스킬을 출력하는 코드를 작성합니다.

    [Briar, Vex].forEach((champion: FighterChampion | MageChampion) => {
      champion.championRole();
      champion.useQ();
      champion.useW();
      champion.useE();
      champion.useR();
      console.log('---------------------------------------------------');
    });

    => 다형성을 적용했습니다.

    다형성 : 동일한 인터페이스나 부모 클래스를 공유하는 객체들이 서로 다른 방식으로 동작하도록 하는 특성

    1. 공통 인터페이스 제공:

    • FighterChampion과 MageChampion은 모두 Champion 클래스를 상속받습니다. Champion 클래스는 championRole, useQ, useW, useE, useR 메서드를 정의하고 있습니다.
    • 따라서 FighterChampion과 MageChampion 인스턴스는 Champion 클래스의 인터페이스를 따릅니다.

    2. 인터페이스를 통한 메서드 호출:

    • forEach 루프에서 각 champion 객체는 Champion 클래스를 상속받은 객체이므로, Champion 클래스의 메서드를 사용할 수 있습니다.
    • 각 챔피언 객체가 실제로 어떤 타입(FighterChampion인지 MageChampion인지)에 상관없이, 공통된 메서드(championRole, useQ, useW, useE, useR)를 호출할 수 있습니다.
    • 이는 런타임에 실제 객체 타입에 따라 메서드가 다르게 동작합니다. 예를 들어, FighterChampion의 useQ 메서드와 MageChampion의 useQ 메서드는 다르게 구현되어 있습니다.

     

    출력하면 다음과 같은 결과를 얻습니다.

     


    결과

    스킬셋의 알고리즘을 정의하고 캡슐화했기에 객체가 독립성을 갖게되어 재사용성이 코드의 유연성이 높아졌습니다.

     


    REF

    https://velog.io/@junnyletsgo/designPattern-%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4Strategy-Pattern-with-typescript

     

    designPattern) 전략패턴(Strategy Pattern) with typescript

    디자인 패턴 중 기본! 전략패턴에 대해서 알아보자

    velog.io

     

    728x90

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

    [Design Pattern] Singleton Pattern, Nest.js  (0) 2024.07.18
Designed by Tistory.