전략 패턴

돔푸·2023년 10월 31일

<들어가며>

디자인패턴 공부를 시작했다. 먼저 전략 패턴이다.

전략 패턴은 객체지향 5원칙 중 OCP(Open-Close-Principle)과 밀접한 관련이 있다. 즉, 애플리케이션의 달리지는 부분과 달라지지 않는 부분을 분리하여 확장하기 쉽고 수정할 일이 적은 코드를 짜는 것이다.

<전략 패턴>

만약 Duck 클래스가 있고 그 Duck의 서브클래스로 HealtyDuck, OldDuck, ToyDuck등이 있다고 생각해보자. 우리는 Duck에게 새로운 기능-Fly()를 주고 싶다. 어떻게 코드를 작성하는게 바람직할까?

1. 상속하기

슈퍼클래스 Duck에게 Fly()메서드를 주고, 이를 서브클래스들에서 상속하게 하면 어떨까? 코드도 재사용할 수 있고
이 방법에는 몇가지 문제점이 있다.

  • 모든 서브클래스들이 Fly()메서드를 구현해야 한다. 만약 Fly()메서드를 구현하고 싶지 않은 서브클래스가 생기면 어떻게 할 것인가?
  • 새로운 서브클래스를 만들때마다 Fly()메서드를 어떻게 구현해야 할지 살펴보고 일일히 다 구현해야 한다. 상당히 귀찮은 일이 아닐 수 없다.

따라서 우리는 다음 방법을 생각해본다.

2. 인터페이스로 설계하기

Flyable 인터페이스를 만들어 서브클래스들이 이를 구현하게 한다. 이렇게 하면 Flyable을 구현하고 싶은 서브클래스만 구현하게 만들 수 있고, 전 방법의 1번 문제를 해결할 수 있다. 그렇다면 과연 이게 최선일까?

  • 여전히 서브클래스(이 클래스들은 Flyable 인터페이스를 구현하고 싶어한다.)를 만들때 구현하는 작업이 상당히 귀찮다. 다른 클래스들은 어떻게 구현했는지 찾아봐야 한다.
  • 이미 서브 클래스가 많으면 힘든 작업이 예상된다. 모든 서브클래스에 implements를 달아주고 구현해야 한다. 그 수많은 서브클래스들에 대해 이 작업을 '반복'할 것인가?

상속하는 방법과 비슷하거나 더 심각하다. 코드 재사용도 못하고 손은 손대로 아프다. 다른 방법이 없을까?

3. 구상클래스로 설계하기!!!

위 방법과 거의 비슷하지만 이번에는 서브클래스들이 인터페이스를 직접 구현하는 것이 아니라, 인터페이스를 구현한 구상클래스를 따로 두고, 서브클래스들은 구상클래스를 사용하기만 하도록 한다. 즉, FlyBehavior 인터페이스를 구현한 FlyWithWing, FlyNoWay등의 구상클래스들을 서브클래스들이 사용하게 한다.
이 방법의 장점은 1,2번 방법과 다르게 드디어 달라지는 부분과 달라지지 않는 부분이 분리되었다는 점이다.

  • 구상클래스를 통해 달라지는 부분을 우리의 객체에서 떼어내고, 우리의 객체(서브클래스)들은 구상클래스가 열심히 구현해놓은 코드를 가져다 쓰기만 하면 된다.
  • 서브클래스들은 구상클래스의 자세한 구현방법을 몰라도 된다.
  • 심지어 동적으로 구상클래스를 교체할 수도 있다.

위 방법을 코드로 한번 보겠다.(전체 코드는 아래 깃헙 주소로 공유했다.)

Duck 슈퍼클래스

@Slf4j
public abstract class Duck {

    public FlyBehavior flyBehavior;
    public QuackBehavior quackBehavior;
    
    public abstract void introduce();
    
    public void fly() {
        flyBehavior.fly();
    }

    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }
}

HealthyDuck 서브클래스

@Slf4j
public class HealthyDuck extends Duck {

    @Override
    public void introduce() {
        log.info("저는 건강한 오리입니다.");
    }

    public HealthyDuck() {
        flyBehavior = new FlyWithWing();
    }
}

FlyBehavior 인터페이스

public interface FlyBehavior {
    void fly();
}

FlyWithWing 구상클래스

@Slf4j
public class FlyWithWing implements FlyBehavior {

    @Override
    public void fly() {
        log.info("날개로 날고 있습니다!");
    }
}

Main 클래스

@Slf4j
public class StudyApplication {

	public static void main(String[] args) {
		Duck healtyDuck = new HealthyDuck();

		healtyDuck.introduce();
		healtyDuck.fly(); //날개로 날고 있습니다!

		log.info("건강한 오리가 총에 맞았습니다! 건강한 오리가 행동합니다.");
		healtyDuck.setFlyBehavior(new FlyNoWay());
		healtyDuck.fly(); //날 수 없습니다!
	}
}

이런식으로 Fly를 구현하면 HealthyDuck 서브클래스는 생성자를 통해 FlyWithWing 구상클래스가 열심히 구현해놓은 코드를 가져다 쓸 수 있다. 또한 Duck.setFlyBehavior()를 통해 동적으로 구상클래스를 교체할 수도 있게 되었다.

출력결과

저는 건강한 오리입니다.
날개로 날고 있습니다!
큰 소리로 꿕!
저는 늙은 오리입니다.
날개로 날고 있습니다!
작은 소리로 삑.
저는 장난감 오리입니다.
날지 못합니다.
소리를 내지 못합니다.
건강한 오리가 총에 맞았습니다! 건강한 오리가 행동합니다.
날지 못합니다.
작은 소리로 삑.

<마무리>

지금까지 전략 패턴을 알아보았다. 상속이 객체지향의 큰 무기는 맞지만, 무작정 상속을 사용하다보면 유지보수의 측면에서 문제점을 느끼는 경우가 있다. 전략 패턴을 통해 달라지는 부분과 달라지지 않는 부분을 분리해낸다면 유지보수가 쉬운 좋은 코드를 짤 수 있을 것이다.

위 글은 다음 책을 바탕으로 작성하였습니다.
헤드퍼스트 디자인패턴 : https://www.yes24.com/Product/Goods/108192370
소스 코드 : https://github.com/Dompoo/DesignPatternStudy/tree/master/src/main/java/dompoo/study/strategyPattern

profile
나중에 또 모를 것들 모음

0개의 댓글