전략 패턴(Strategy Pattern)

전략 패턴이란?

전략에 따라 쉽게 바꿀 수 있게 하는 디자인 패턴이다. 같은 문제를 해결하는 여러 알고리즘이 클래스 별로 캡슐화되어 있고, 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결할 수 있는 패턴이다.

상황 가정

위의 코드는 조종석이 1개 였을 때 코드를 가지고 있다가, 여러 조종석의 아이템(기본 조종석, 초강력 장갑 조정석, 최첨단 조종석, 생존 극대화 조종석, 무적 조종석 등)을 만들어야 한다는 요청이 들어왔다고 가정하자.

1. 조정석이 1개 였을 때 구현한 코드

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bf29258e-b688-48d8-ac44-56609e07e29d/Untitled.png

조종석(DefaultCockpitIf) 클래스

public class Cockpit {
    final private int armorPower = 50; // 조정석의 방어력
    final private int attackPower = 10; // 조정석의 공격력
    

    // 방어력 가져오기 (getter 함수)
    public int armorFeature() {
            return armorPower;
    }

    // 공격력 가져오기
    public int attackFeature() {
            return attackPower;
    }
}	

조종석(Cockpit)을 가지는 비행기(Airplane) 클래스

public class Airplane {
    protected int armorPower = 100; // 비행기 기본 방어력
    protected int attackPower = 50; // 비행기 기본 공격력
    protected Cockpit cockpit; // 조종석 객체
    
    // 생성자
    public Airplane() {
            // 조종석 객체를 생성해서 조종석 객체의 방어력을 얻는다.
            cockpit = new Cockpit(); // 조종석 객체 생성
            armorPower += cockpit.armorPower(); // '비행기 방어력'에 '조종석 방어력'을 추가
            armorAttack += cockpit.attackPower(); // '비행기 공격력'에 '조종석 공격력'을 추가
    }
}

비행기(Airplane)을 가지는 파일럿(Pilot) 클래스

public class Pilot {
    private Airplane airplane;

    // 파일럿이 조종석에 올라타는 메서드 (조종석을 가진 비행기 인스턴스를 return)
    public Airplane intoCockpit() {
            return new Airplane();
    }
}

2. 조종석의 종류를 3가지로 늘려달라고 해서, 그에 맞게 코드를 추가

조종석(DefaultCockpitIf) 클래스

public class cockpit {
    String kindCockpit = ""; // 조종석의 종류
    
    // 여러 조종석들 중에서 어떤 조종석을 선택할 지 생성자의 파라미터로 받는 코드
    public Cockpit(String kindCockpit) {
            this.kindCockpit = kindCockpit;
    }
    
    public int armorFeature() {
        if (kindCockpit.equals("기본 조종석")) {
                armorPower = 50;
        } else if (kindCockpit.equals("초강력 장갑 조종석")) {
                armorPower = 300;
        } else if (kindCockpit.equals("최첨단 조종석")) {
                armorPower = 100;
        }
        return armorPower;
    }

    public int attatckFeature() {
        if (kindCockpit.equals("기본 조종석")) {
                attackPower = 10;
        } else if (kindCockpit.equals("초강력 장갑 조종석")) {
                attackPower = 30;
        } else if (kindCockpit.equals("최첨단 조종석")) {
                attackPower = 20;
        }
        return attackPower;
    }
}

조종석의 종류에 따라서 다른 로직을 가져야 해서, 일단은 조종석 클래스의 생성자에서 조종석의 종류를 의미하는 파라미터를 받는다. 그리고 그 파라미터를 기준으로 if문을 만들어 다른 로직에 따른 다른 값을 return하도록 만든다.

조종석(Cockpit)을 가지는 비행기(Airplane) 클래스

public class Airplane {
    protected int armorPower = 100; // 비행기 기본 방어력
    protected int attackPower = 50; // 비행기 기본 공격력
    protected Cockpit cockpit; // 조종석 객체

    // Airplane을 생성할 때 조종석의 종류를 파라미터로 받음으로써, 조종석 종류를 선택할 수 있음**
    public Airplane(String kindCockpit) {
        cockpit = new CockpitIf(kindCockpit);
        armorPower += cockpit.armorFeature();
        attackPower += cockpit.attackFeature();
    }
}

Cockpit 생성자의 파라미터에 ‘조종석의 종류’를 결정짓는 값을 입력해서, 최첨단 조종석 인스턴스를 생성했다.

비행기(Airplane)을 가지는 파일럿(Pilot) 클래스

public class Pilot {
    private Airplane airplane;

    // 파일럿이 조종석에 올라타는 메서드 (조종석을 가진 비행기 인스턴스를 return)
    public Airplane intoCockpit() {
        return new Airplane("최첨단 조종석");
    }
}

3. if문이 거의 비슷한 형태로 반복되는 로직을 보고 무언가 잘못됨을 인지

문제점

  • 조종석의 종류가 추가, 수정, 삭제될 때마다 조종석(CockpitIf) 클래스의 if문을 일일이 다 고쳐야 한다. → 기존 코드를 수정하면 사이드 이펙트가 생길 가능성도 높다.
  • 조종석의 종류가 늘어날 때마다 if문이 계속 늘어나고 수정되어야 해서 코드가 지저분해지고 가독성이 떨어진다. (위 코드는 간단한 로직을 예로 들어서 생각보다 복잡해 보이지 않을 수도 있지만, 실전에서는 위 로직보다 훨씬 복잡하고 많은 코드일 가능성이 높다.)

4. if문의 반복을 없애기 위해 리팩토링

1) 서로 다른 종류의 조종석을 Cockpit 클래스 하나에서 전부 다루던 것을, 조종석 각각을 클래스로 나눈다.

→ DefaultCockpit(기본 조종석), PowerArmorCockpit(초강력 장갑 조종석), HighTechCockpit(최첨단 조종석), ComfortableCockpit(생존 극대화 조종석)

2) 서로 다른 종류의 조종석 중에 Default 값으로 설정할만한 조종석이 있다면, 그 조종석을 부모 클래스로 만들고 나머지 조종석을 상속해서 자식 클래스로 만들면 된다. (만약에 Default를 설정할만한 조종석이 없다면, 인스턴스화가 불가능한 부모 클래스나 인터페이스를 만들어서 서로 다른 종류의 조종석들을 포괄할 수 있는 추상적인 클래스 하나를 만든다.)

→ DefaultCockpit(기본 조종석)을 부모 클래스로 만들고, 나머지 조종석들은 상속을 받아 자식클래스로 만들었다.

→ 이 과정이 ‘위임‘을 활용한 것이다. 위임은 메소드의 실제 구현을 다른 클래스에 위임하는 것이다. 비행기 기체(Airplane) 클래스의 내부를 보면 조종석 클래스 그룹(DefaultCockpit)을 활용해 같은 이름을 가진 메소드에 일을 위임하는 것을 볼 수 있다. 이렇게 하면 비행기 기체(Airplane) 클래스는 별도의 코드 수정 없이, 조종석 클래스 그룹(DefaultCockpit)의 하위 클래스를 자유롭게 교체할 수 있고, 또는 새로운 조종석 클래스를 추가해도 기존 코드의 수정은 발생하지 않는다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/5fcef6b1-f077-4d10-ad7a-7d6ec4d29aa1/Untitled.png

조종석(Cockpit) 관련 클래스들

public class DefaultCockpit {
    int armorPower = 100;
    int attackPower = 50;

    public int getArmorPower() {
        return armorPower;
    }

    public int getAttackPower() {
        return chairFunction;
    }
}
public class PowerArmorCockpit extends DefaultCockpit {
    @Override
    public int getArmorPower() {
        armorPower = 300;
        return armorPower;
    }

    @Override
    public int getAttackPower() {
        attackPower = 200;
        return attackPower;
    }
}
public class HighTechCockpit extends DefaultCockpit {
    @Override
    public int getArmorPower() {
        panelFunction = 300;
        return panelFunction;
    }

    @Override
    public int getAttackPower() {
        attackPower = 100;
        return attackPower;
    }
}
public class ComfortableCockpit extends DefaultCockpit {
    @Override
    public int getArmorPower() {
        panelFunction = 200;
        return panelFunction;
    }

    @Override
    public int getAttackPower() {
        attackPower = 500;
        return attackPower;
    }
}

3) 조종석(Cockpit)을 가지는 비행기(Airplane) 클래스의 로직 수정하기

public class Airplane {
    protected int armorPower = 100; // 비행기 기본 방어력
    protected int attackPower = 50; // 비행기 기본 공격력
    // Cockpit을 대표하는 부모 클래스로 type으로 정하기
    // 이 때, 파라미터의 type을 Cockpit을 대표하는 부모 클래스를 type으로 함으로써, 
    //        Cockpit의 부모 클래스 뿐만 아니라 자식 클래스도 값으로 할당할 수 있다.
    protected **DefaultCockpit** cockpit; 

    // 여러 조종석들 중에서 어떤 조종석을 선택할 지 생성자의 파라미터로 받는 코드
    // 이 때, 파라미터의 type을 Cockpit을 대표하는 부모 클래스를 type으로 함으로써, 
    //        Cockpit의 부모 클래스 뿐만 아니라 자식 클래스도 파라미터로 전달할 수 있다.
    public Airplane(DefaultCockpit cockpit) {
        this.cockpit = cockpit;
        armorPower += cockpit.armorFeature();
        attackPower += cockpit.attackFeature();
    }
}

5. 리팩토링이 끝났으니 비행기(Airplane)을 가지는 파일럿(Pilot) 클래스에서, 비행기(Airplane) 클래스 사용해보기

비행기(Airplane)을 가지는 파일럿(Pilot) 클래스

public class Pilot {
    private Airplane airplane;

    // 파일럿이 조종석에 올라타는 메서드 (조종석을 가진 비행기 인스턴스를 return)
    public Airplane intoCockpit() {
            return new Airplane(new HightechCockpit());
    }
}

위 코드를 보면 파일럿(Pilot) 클래스에서 DefaultCockpit의 하위 클래스인 최첨단 조종석 객체(HightechCockpit)를 Airplane 클래스에 인자로 넘겨서 사용할 수 있다. 비행기 기체(Airplane) 클래스는 하위 클래스(PowerArmorCockpit, HighTechCockpit, ComfortableCockpit)에 의존하지 않고 오로지 부모 크래스(DefaultCockpit)에만 의존하니 유지보수도 아주 편리해진다.

Strategy 패턴이 가진 장점

Strategy 패턴은 프로그램의 변경되는 부분을 찾아내서, 바뀌지 않는 부분으로부터 분리시킨다. 즉, 바뀌는 부분은 따로 뽑아서 캡슐화시킨다는 뜻이다. 예제를 보면 바뀌지 않은 부분(Airplane 클래스)은 불필요하게 간섭받지 않는다. 기능을 캡슐화한 바뀌는 그룹(DefaultCockpit)만 고치거나 확장할 수 있다. 여기서 쓰인 캡슐화의 개념은 속성 보호의 개념이 아니다. 여기서 캡슐화는 객체가 해야 할 기능 중에 일부를 별도의 그룹으로 분리하여 다른 객체 그룹으로 감싸는 의미이다.

References