관리 메뉴

SW

[개발서적] 헤드퍼스트 디자인 패턴 Ch1. 전략(Strategy) 패턴 본문

개발서적/헤드퍼스트 디자인패턴

[개발서적] 헤드퍼스트 디자인 패턴 Ch1. 전략(Strategy) 패턴

SWKo 2024. 7. 13. 00:38

1. 오리 시뮬레이션 게임, SimUduck

모든 오리는 꽥(quack) 소리를 낼 수 있고, 헤엄(swim)을 칠 수 있으므로 quack, swim 메소드는 슈퍼클래스로 작성합니다.

모든 오리의 모양이 다르므로 display 메소드는 추상 메서드로 작성하고, 오버라이드 합니다.

 

 

2. 상속을 생각하기

일부 오리가 하늘을 날아야 하는 기능을 추가해달라는 요청이 들어왔습니다.

슈퍼클래스인 Duck에 fly를 추가해봅니다. 그럼 모든 오리는 날게 됩니다.

날지 못하는 고무오리(RubberDuck)도 날게 되는 오류가 발생합니다.

 

위 그림처럼 RubberDuck이 날지 못하도록 fly에서 아무 동작 없이 오버라이딩을 해봅니다. 

일단 해결은 되겠죠.

그러나, 만약 날지못하는 여러 오리가 추가된다면 일일이 fly에서 아무 동작 없도록 오버라이딩을 해줘야 합니다.

특정 형식의 오리만 날거나 꽥꽥 거릴 수 있도록 더 좋은 방법을 찾아야 합니다.

 

상속은 올바른 해결책이 아니다.

 

 

3. 인터페이스 설계하기

이번엔 인터페이스를 설계해봅니다.

Flyable과 Quackable 인터페이스를 만들어서 해당 기능을 원하는 오리에게만 넣어봅니다.

인터페이스(Interface) : 객체 지향 프로그래밍에서 클래스가 구현해야 하는 메서드의 집합을 정의하는 일종의 참조 타입,
Java8 이전에는 추상 메서드만 존재했지만, Java8 부터는 Default&Static Methods, Java9 부터는 Private Methods 존재

 

 

하지만, fly, quack 메서드에서 코드 중복이 일어나고 조금의 기능 수정이 필요하다면 모든 코드를 고쳐야 하는 문제점이 있습니다.

 

인터페이스는 올바른 해결책이 아니다.

 

 

4. 문제를 명확하게 파악하기

상속은 해결책이 될 수 없습니다.

서브클래스마다 오리의 행동이 바뀔 수 있는데 모든 서브클래스에서 한가지 행동만 사용하도록 하면 올바르지 못합니다.

 

인터페이스도 해결책이 될 수 없습니다.

Java 인터페이스에는 구현된 코드가 없어서 코드를 재사용할 수 없고, 일부 기능을 수정할 때 각 서브클래스를 찾아서 코드를 하나하나 수정해야합니다.

 

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

 

 

5. 바뀌는 부분과 그렇지 않는 부분 구분하기

fly()와 quack()은 Duck 클래스의 오리 종류에 따라 달라집니다.

이 2개를 끄집어내서 각 행동을 나타낼 클래스 집합을 새로 만들어야 합니다.

 

 

6. 오리의 행동을 디자인하는 방법

오리의 행동(Quack, Fly)을 디자인해봅시다.

 

각 행동은 인터페이스(QuackBehavior, FlyBehavior)로 표현하고, 이런 인터페이스를 사용해서 행동을 구현하겠습니다.

나는 행동(Fly)과 꽥꽥거리는 행동(Quack)은 Duck 클래스에서 구현하지 않습니다.

행동 인터페이스는 Duck 클래스가 아니라 행동 클래스에서 구현합니다.

 

이 방법은 지금까지 썼던 행동을 Duck 클래스에서 구체적으로 구현하거나 서브클래스 자체에서 별도로 구현하는 방법과는 상반된 방법입니다. 

전에 썼던 방법은 항상 특정 구현에 의존했기에 행동을 변경할 여지가 없었습니다.

새로운 디자인을 사용하면 Duck 서브클래스는 인터페이스로 표현되는 행동을 사용합니다. (8에서 자세한 설명이 나옵니다.)

실제 행동 구현(QuackBehavior, FlyBehavior)은 Duck 서브클래스에 국한되지 않습니다.

 

 

디자인 원칙
구현보다는 인터페이스에 맞춰서 프로그래밍한다.

 

 

"인터페이스에 맞춰서 프로그래밍한다"는 말은 사실 "상위 형식에 맞춰서 프로그래밍한다"라는 말입니다.

핵심은 실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용해야 한다는 점에 있습니다.

 

"변수(animal)를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식(Animal)으로 선언해야 한다.

객체(new Dog() or new Cat())를 변수(animal)에 대입할 때 상위 형식(Animal)을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다. 

그러면 변수를 선언하는 클래스에서 실제 객체의 형식(Dog, Cat)을 몰라도 된다."

 

 

Animal animal = getAnimal();
animal.makeSound();

 

7. 오리의 행동을 구현하는 방법

FlyBehavior와 QuackBehavior라는 2개의 인터페이스를 사용합니다. 그리고 구체적인 행동을 구현하는 클래스들이 있습니다.

 

나는 것과 관련된 클래스는 무조건 FlyBehavior 인터페이스를 구현합니다.

나는 것과 관련된 클래스를 새로 만들 때는 무조건 fly 메서드를 구현해야 합니다.

꽥꽥 행동도 마찬가지입니다.

 

이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 꽥꽥거리는 행동을 재사용할 수 있습니다.

그리고 기존의 행동 클래스나 날아다니는 행동을 사용하는 Duck 클래스를 수정하지 않고도 새로운 행동을 추가할 수 있습니다.

 

 

8. 오리 행동 통합하기

가장 중요한 점은 나는 행동(fly())과 꽥꽥거리는 행동(quack())을 Duck 클래스에서 정의한 메서드를 써서 구현하지 않고 다른 클래스(FlyWithWings, Quack, ...)에 위임한다는 것입니다.

// 행동 인터페이스들
interface FlyBehavior {
    void fly();
}

interface QuackBehavior {
    void quack();
}

// 구체적인 행동 클래스들 (전략)
class FlyWithWings implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("저는 날개로 날아요!");
    }
}

class Quack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("꽥꽥!");
    }
}

// Duck 추상 클래스 (컨텍스트)
abstract class Duck {
	// 모든 Duck에는 FlyBehavior, QuackBehavior 인터페이스를 구현하는 것의 레퍼런스(변수)가 있다.
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public void performFly() {
        flyBehavior.fly();
    }

    public void performQuack() {
    	// 꽥꽥거리는 행동을 quackBehavior로 참조되는 객체에 위임한다.(행동 클래스에 위임한다.)
        // 어떤 오리인지는 신경 쓸 필요 없이 quack()을 실행할 줄 알면된다.
        quackBehavior.quack();
    }

    // 동적으로 행동 지정하기
    public void setFlyBehavior(FlyBehavior fb) {
        flyBehavior = fb;
    }

    public void setQuackBehavior(QuackBehavior qb) {
        quackBehavior = qb;
    }

    public abstract void display();
}

// MallardDuck 구체적인 클래스
public class MallardDuck extends Duck {

    public MallardDuck() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("저는 물오리 입니다.");
    }

    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.performFly(); // 출력: 저는 날개로 날아요!
        mallard.performQuack(); // 출력: 꽥꽥!

        // 행동을 동적으로 변경
        mallard.setFlyBehavior(() -> System.out.println("저는 날 수 없어요."));
        mallard.performFly(); // 출력: 저는 날 수 없어요.
    }
}

 

 

9. 캡슐화된 행동 살펴보기

클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현합니다.

각 오리에는 FlyBehavior, QuackBehavior가 있고, 두 행동을 위임받습니다.

이런 식으로 두 클래스를 합치는 것을 '구성(composition)'을 이용한다고 합니다.

Duck 클래스에서는 행동을 상속받는 대신, 행동 객체로 '구성'되어 행동을 부여받습니다.

디자인 원칙
상속보다는 구성을 활용한다.


10. 정리

전략 패턴(Strategy Pattern)은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.

전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.

 

 


 

FE 전략 패턴 : https://velog.io/@lky5697/the-power-of-strategy-design-pattern-in-javascript

 

(번역) 자바스크립트에서 전략 디자인 패턴의 힘

원문 : https://betterprogramming.pub/the-power-of-strategy-design-pattern-in-javascript-df1a17bc2c72자바스크립트는 유연함으로 매우 잘 알려진 언어입니다. 아마도 유연함이 자바스크립트의

velog.io

 

Comments