코딩하는 털보

디자인 패턴 1. 디자인 패턴 소개 / 스트래티지 패턴 본문

Book/헤드 퍼스트 디자인 패턴

디자인 패턴 1. 디자인 패턴 소개 / 스트래티지 패턴

이정인 2021. 8. 24. 21:41

디자인 패턴 1. 디자인 패턴 소개 / 스트래티지 패턴

  • 객체지향 디자인 원칙
    1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
    2. 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다.
    3. 상속보다는 구성(컴포지션)을 활용한다.

OOP의 핵심인 '상속', 하지만 이 책은 1부에서부터 상속의 단점을 먼저 보여주었다. 그것에 대한 책의 예시는 아래와 같다.

public abstract class Duck {
    public void quack() {
        System.out.println("꽥!");
    }
    public void swim() {
        System.out.println("수영을 합니다.");
    }
    public abstract void display();
}
public class MallardDuck extends Duck {
    @Override
    public void display() {
        System.out.println("청둥오리");
    }
}

객체지향적으로 Duck 추상클래스를 상속받은 모든 오리 클래스는 소리 낼 수 있고 수영을 할 수 있다. 또한 display 메서드를 구현하여 각자의 모양을 화면에 보여준다.
만약 날아다니는 새로운 기능이 필요하다면?, Duck 클래스에 추가하면 그만이다. 다른 하위 클래스는 변경 없이 날아다닐 수 있다.

public abstract class Duck {
    ...
    public void fly() {
        System.out.println("날아갑니다.");
    }
    ...
}

모든 오리 클래스는 상위클래스로부터 상속받은 메서드를 사용하므로 객체지향에서 말하는 재사용성은 지켜지는 것 같다. 하지만 고무 장난감 오리같은 날지 못하는 오리는 어쩌나?? fly 메서드를 Duck 클래스에 추가하여 모든 하위 오리 클래스가 날게 되면서 날지 못하는 오리들도 날아다니게 된다. 날 수 있는게 좋은게 아니다 장난감 오리가 날아다니는 것은 버그이다.

public class RubberDuck extends Duck {
    @Override
    public void quack() {
        System.out.println("삑!");
    }
    @Override
    public void display() {
        System.out.println("고무오리");
    }
    @Override
    public void fly() {
    }
}

고무오리는 실제 오리들과 다른 소리를 내고 날지도 못한다. 때문에 상속받은 메서드를 오버라이딩하여 직접 동작을 변경하여 구현해야 한다.
fly 나 quack 뿐 만이 아니다. 새로운 기능을 상위 클래스에 추가하면 다른 하위클래스에 어떤 원치 않는 영향을 끼칠 위험을 항상 가지고 있다. 애플리케이션에서 변화는 필연적인 것인데, 위의 예시에서는 상속이 이런 변화에 잘 대응하지 못하고 있다.

그대신 인터페이스를 사용하는 것은?
날 수 없거나 다른 소리를 내는 오리들이 문제라면 전체가 아닌 일부 형식의 오리만 날거나 꽥꽥거리도록 인터페이스를 상속받는 것은 어떠한가.

public interface Flyable {
    public void fly();
}
public interface Quackable {
    public void quack();
}

인터페이스를 사용하면 이전의 문제가 사라질 수 있다. 인터페이스를 구현하지 않는 클래스는 영향을 받지 않기 때문이다.
그러나 다른 문제가 생긴다. 모든 날고 소리내는 클래스는 이 행위에 대해서 각자 구현해야 한다. 즉, 재사용성을 전혀 기대할 수 없다.

디자인원칙 1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리시킨다.
바뀌는 부분을 따로 뽑아서 캡슐화시킨다. 그렇게 하면 나중에 바뀌지 않는 부분에는 영향을 미치지 않은 채로 그 부분만 고치거나 확장할 수 있다.

Duck 클래스에서 변하는 부분은 fly와 quack이다. 날고 소리내는 두 행위를 따로 캡슐화 해보자. (사실 display도 변하는 부분이긴 하지만 display는 추상화만 필요할 뿐 재사용이 필요하지 않는다. 모든 오리는 그 모습이 다르기 때문에 직접 구현해야 하기 때문이다.)
캡슐화를 위해 두 행위에 대한 클래스 집합을 만들어야 한다. 집합에는 각각의 행동을 구현한 것을 전부 집어넣을 것이다.

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

public interface QuackBehavior {
    public void quack();
}
public interface FlyBehavior {
    public void fly();
}

이제 달라지지 않는 부분인 기존 클래스에서 행위를 구현하지 않고 행위 인터페이스를 구현하는 새로운 클래스를 만들어서 클래스 집합에 모을 것이다.

public class FlyWithWings implements FlyBehavior{
    @Override
    public void fly() {
        System.out.println("날아다닙니다.");
    }
}
public class FlyNoWay implements FlyBehavior{
    @Override
    public void fly() {
    }
}

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

원래의 Duck 클래스는 특정 구현에 의존했기 때문에 행동을 변경할 여지가 없었다. 하지만 인터페이스로 표현되는 행동을 사용하면 특정 행동 구현으로 제한되지 않을 수 있다.

import lombok.Setter;

public abstract class Duck {
    @Setter
    QuackBehavior quackBehavior;
    @Setter
    FlyBehavior flyBehavior;

    public void performQuack() {
        quackBehavior.quack();
    }

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

    public void swim() {
        System.out.println("수영을 합니다.");
    }

    public abstract void display();
}

원래 Duck 클래스에서 구현하던 것을 다른 클래스로 넘겼기 때문에 구현을 '위임'했다고도 한다. Duck 클래스는 더이상 날거나 소리내는 행동의 특정 종류에 대해서 의존하지 않는다. 그리고 그것에 대해 구현할 필요도 없고 구현이 어떻게 되어 있는지 알 수도 알 필요도 없어진다.
더이상 Duck 클래스가 행동에 대해 구현하고 있지 않기 때문에 다른 클래스가 이 행동을 가지게 할 수 도 있다.(예를들면 오리 소리를 내는 오리 호출기는 Duck을 상속받지 않으면서 오리 소리를 내도록 만들 수 있다.)

public class MallardDuck extends Duck {

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

    @Override
    public void display() {
        System.out.println("청둥오리");
    }
}

생성자를 통해 기본적으로 행동을 설정하고 있지만 setter 메서드를 사용하면 동적으로 행동을 변경할 수 있다.

디자인원칙 3. 상속보다는 구성(컴포지션)을 활용한다.
예시처럼 상속보다 컴포지션을 이용하여 시스템을 만들면 유연성을 크게 향상시킬 수 있다. 알고리즘군(예시에서는 각 행동)에 대해 캡슐화할 수 있고 동적으로 바꿀 수도 있다.

개발이 끝나기 전보다 개발이 끝난 후에 더 코드에 많은 시간을 쓰게 된다. 그런 면에서 재사용성 보다도 관리의 용이성이나 확장성등 유지보수성에 대해서 더 생각해야 한다. 상속에는 상속만의 문제점이 있고, 재사용을 성취하는 데에는 다른 방법도 있다.

디자인 패턴

대부분의 패턴과 원칙은 소프트웨어의 변경 문제와 관련이 있다. 패턴의 밑바탕에는 객체지향이 있으며 재사용성, 확장성, 관리의 용이성을 갖춘 객체지향 시스템을 만드는 게 패턴의 궁극적인 목적이다.

스트래티지 패턴

스트래티지 패턴에서는 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 스트래티지 패턴을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

Comments