본문 바로가기

디자인 패턴

전략 패턴 (Strategy Pattern)

전략 패턴 (Strategy Pattern)

오리의 행동 들을 쉽게 확장하거나 변경할 수 있는 클래스들의 집합으로 캡슐화하는 방법.

이를 통해 실행 중에도 확장과 변경이 가능해진다.

 

키워드

- 변하는 코드와 변하지 않는 코드의 분리

- 상위 형식 사용 (클래스 상속/인터페이스 구현)

 

용어

  • 부모 클래스 = super class
  • 자식 클래스 = sub class
  • 베이스 클래스
    : 다른 클래스로부터 상속받지 않는 클래스
  • 오버라이드 (Override)
    : 상속받은 메서드 덮어쓰기
    부모 클래스와 자식 클래스 사이에서만 성립할 수 있다.

    • 오버로딩 (Overload)
    • : 함수명은 같게 하되, 파라미터 또는 리턴값을 다르게 함으로써, 같은 기능을 동일한 메서드 명으로 정의하는 방법


  1. 상속을 받은 자식 클래스는 오버라이드하지 않은 수퍼 클래스의 메서드일 지라도 사용할 수 있다.
public class Duck {
    public void quack() {
        System.out.println("꽥꽥");
    }

    public void swim() {
        System.out.println("헤엄");
    }

    protected void display() {
        System.out.println("나는 오리야");
    }

    public void fly() {     // 새로 추가된 기능
        System.out.println("하늘 날기");
    }
}
public class MallardDuck extends Duck{
    @Override
    public void display() {
        System.out.println("나는 청둥오리야");
    }

}
public class Main {
    public static void main(String[] args) {

        MallardDuck mallardDuck = new MallardDuck();
        mallardDuck.display();
        mallardDuck.swim();
        mallardDuck.quack();

        mallardDuck.fly();      // 사용 가능. override하지 않은 수퍼 클래스의 메서드
    }
}

나는 청둥오리야
헤엄
꽥꽥
하늘 날기

 

그렇기 때문에 상속은 바람직하지 않다.

 

fly()기능이 필요하지 않은 자식 클래스가 있을 수도 있는데, 수퍼 클래스에 fly() 기능을 추가해버렸기 때문에 모든 자식 클래스에는 fly() 기능이 생긴다.

 

즉 코드를 변경했을 때 다른 객체/클래스에 원치 않은 영향을 끼칠 수 있다.

+ 모든 Duck의 행동을 파악하기 힘들다.


디자인 원칙 1

애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다

💡 바뀌는 부분은 따로 뽑아서 캡슐화한다.
그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.

 

즉, 새로운 요구 사항이 있을 때마다 코드에 바뀌는 부분이 있다면 분리해야 한다.

 

매우 기초가 되는 원칙이다. 모든 디자인 패턴은 시스템의 일부분을 다른 부분과 분리하여, 독립적으로 변경시킬 수 있는 방법을 제공하기 때문이다.

 

디자인 원칙 2

구현보다는 인터페이스/상위 형식에 맞춰서 프로그래밍한다.

💡 바뀌는 부분(객체의 ‘행동’과 같은 구체적인 것들)은 인터페이스로 표현하고, 그 인터페이스를 이용해서 구현한다. 인터페이스를 구현한 별도의 클래스 안에서 구체적인 내용을 구현한다.

 

그렇게 하면 부모 클래스든 자식 클래스든 클래스 내에서 구체적인 내용을 직접 구현하지 않아도 된다.

즉 특정 구현에 의존하지 않을 수 있다.

 

→ 다른 형식의 객체에서도 바뀌는 부분을 가져다가 사용할 수 있게 된다. ( =바뀌는 부분을 다른 객체에서 재사용할 수 있게 된다.)

 

또한 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있게 된다.

public class Main {
    public static void main(String[] args) {

        Duck mallardDuck = new MallardDuck();  // 상위 형식에 맞춰서 객체 생성
        mallardDuck.display();
        mallardDuck.swim();
        mallardDuck.quack();

        mallardDuck.fly();      // 사용 가능. override하지 않은 수퍼 클래스의 메서드
    }
}

나는 청둥오리야
헤엄
꽥꽥
하늘 날기

예시: 오리 코드

public abstract class Duck {
    FlyBehavior flyBehavior;    // 변경이 발생할 수 있는 부분을 의존성 주입
    QuackBehavior quackBehavior;    // 변경이 발생할 수 있는 부분을 의존성 주입

    /**
     * 변하는 부분
     */
    public void performFly() {
        flyBehavior.fly();      // 인터페이스/상위 타입
    }

    public void performQuack() {
        quackBehavior.quack();    // 인터페이스/상위 타입
    }

    /**
     * 변하지 않는 부분
     */
    public void swim() {    // 모든 오리는 swim이 동일하기 때문에 직접 구현.
        System.out.println("헤엄");
    }

    public abstract void display();     // 자식 클래스에 따라 구현이 달라지기 때문에 추상 메소드.
}
public class MallardDuck extends Duck{

    public MallardDuck(QuackBehavior quackBehavior, FlyBehavior flyBehavior) {
        super.quackBehavior = quackBehavior;
        super.flyBehavior = flyBehavior;
    }

    @Override
    public void display() {
        System.out.println("나는 청둥오리야");
    }
}
public interface FlyBehavior {
    public void fly();
}
public class FlyWithWings implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("날개로 날기");
    }
}
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");

        QuackBehavior quack = new Quack();      // 상위 형식, 인터페이스에 맞춰서 프로그래밍 -> 변경 사항 대응에 용이함.
        FlyBehavior fly = new FlyNoWay();       // [1]
//        FlyBehavior fly = new FlyWithWings();     // [2]  이런식으로 갈아끼우기만 하면 됨.

        Duck mallardDuck = new MallardDuck(quack, fly);       // 상위 형식, 인터페이스에 맞춰서 프로그래밍

        mallardDuck.display();
        mallardDuck.swim();
        mallardDuck.performQuack();
        mallardDuck.performFly();
    }
}

[1]: ‘날 수 없음’ 동작

[2]: ‘날 수 있음’ 동작

 

 

위의 경우 malladDuck의 날기 동작을 ‘날 수 없음’ 에서 ‘날 수 있음’으로 변경하고자 한다면 위처럼 fly 변수에 할당된 객체만 바꿔주면 된다.

인터페이스/상위 형식을 이용하면 변경 사항이 발생했을 때 갈아끼우기가 매우 용이하다.

 

동적으로 변경 사항을 반영할 때에는 setter를 통해 동적으로 반영한다.

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world!");

        QuackBehavior quack = new Quack();      // 상위 형식, 인터페이스에 맞춰서 프로그래밍 -> 변경 사항 대응에 용이함.
        FlyBehavior fly = new FlyNoWay();       // [1]
//        FlyBehavior fly = new FlyWithWings();     // [2]  이런식으로 갈아끼우기만 하면 됨.

        Duck mallardDuck = new MallardDuck(quack, fly);       // 상위 형식, 인터페이스에 맞춰서 프로그래밍

        mallardDuck.display();
        mallardDuck.swim();
        mallardDuck.performQuack();
        mallardDuck.performFly();

        mallardDuck.setFlyBehavior(new FlyWithWings());     // 동적으로 변화 반영하기
        mallardDuck.performFly();
    }
}

디자인 원칙 3

💡 상속보다는 구성(composition)을 활용한다.

상위 형식을 통해 상속받는 것보다는 의존성 주입을 활용하라는 것이다.

이렇게 하면 시스템 유연성을 향상시킬 수 있으며, 구성 요소로 사용되는 객체에서 인터페이스를 구현하기만 하면 동적으로 변경 사항을 반영할 수 있기 때문이다.

 

요약

  1. 변경이 발생하는 부분과 발생하지 않는 부분으로 구분하고 분리함으로써, 코드가 달라지더라도 나머지 코드에는 영향을 주지 않도록 ‘캡슐화’한다.
    ⇒ 코드를 변경하는 과정에서 의도치 않게 발생하는 변경(위험)을 줄이면서 시스템의 유연성을 향상시킬 수 있다.
  2. 구현보다는 인터페이스/상위 형식을 활용해서 구현함으로써, 변경 사항이 발생해도 기존 코드를 변경시켜야 하는 상황이 발생하지 않도록(=변화에 유연하게) 만들 수 있다.
    즉, 다형성을 활용해야 한다는 것이다.
  3. 기능을 추가해야 할 때에는 직접 구현 또는 상속을 하는 것보다는 구성(의존성 주입)을 이용한다.