Featured image of post 1. 디자인 패턴 소개와 전략 패턴

1. 디자인 패턴 소개와 전략 패턴

헤드 퍼스트 디자인 패턴

디자인 패턴 만나기

누군가가 이미 우리의 문제를 해결해 놓았다.

디자인 패턴은 다른 개발자가 똑같은 문제를 경험하고 해결하면서 익혔던 지혜와 교훈이 담겨있다.

디자인 패턴은 코드가 아닌 경험을 재사용 하는 것과 같다.

디자인 원칙

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

  • 달라지는 부분을 찾아 나머지 코드에 영향을 주지 않도록 캡슐화 한다.
  • 이로 인해 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이며 시스템의 유연성을 향상시킬 수 있다.

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

이 디자인 원칙은 다음과 같이 해석할수도 있다.

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

이 개념은 매우 간단하지만 다른 모든 디자인 패턴의 기반을 이루는 원칙이다.

모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공한다.

패턴과 전문 용어

패턴으로 소통하면 일상어로 구구절절 말할 때보다 훨씬 효율적인 의사소통을 할 수 있다.

디자인 패턴은 개발자 사이에서 서로 모두 이해할 수 있는 용어를 제공한다.

용어를 이애하고 나면 다른 개발자와 더 쉽게 대화할 수 있고, 패턴을 아직 모르는 사람들에게는 패턴을 배우고 싶은 생각이 들도록 자극을 수 있다.

또한 자질구레한 객체 수준에서의 생각이 아닌, 패턴 수준에서 생각할 수 있기에 아키텍처를 생각하는 수준도 끌어올릴 수 있다.

  • 패턴으로 의사소통하면 패턴 이름과 그 패턴에 담겨있는 모든 내용, 특성, 제약조건 등을 함께 이야기 할 수 있다.
    • 전략 패턴을 사용했다는 말은, 대상의 동작을 쉽게 확장하거나 변경할 수 있는 클래스들의 집합으로 캡슐화 했다는 내용을 간략하게 설명할 수 있다.
  • 패턴을 사용하면 간단한 단어로 많은 얘기를 할 수 있다.
    • 뭔가를 설명할 때 패턴을 사용하면 생각하고 있는 디자인을 다른 개발자가 빠르고 정확하게 파악할 수 있다.
  • 패턴 수준에서 이야기하면 디자인에 더 오랫동안 집중할 수 있다.
    • 패턴을 사용하여 객체와 클래스를 구현하는 것과 관련된 자질구레한 내용에 시간을 버릴 필요가 없어 디자인 수준에서 초점을 맞출 수 있다.
  • 전문 용어를 사용하면 개발팀의 능력을 극대화 할 수 있다.
    • 디자인 패턴 용어를 모든 팀원이 잘 알고 있다면 오해의 소지가 줄어 작업을 빠르게 진행할 수 있다.
  • 전문 용어는 신입 개발자에게 훌륭한 자극제가 될 수 있다.
    • 선배가 디자인 패턴을 사용하면 디자인 패턴을 배울 동기가 부여될 수 있다.

디자인 패턴 사용법

라이브러리와 프레임워크는 개발 모델 전반에 걸쳐서 많은 영향을 미친다. 하지만 라이브러리와 프레임워크를 사용한다는 것이 이해하기 쉽고, 관리하기 쉬운 유연한 방법으로 애플리케이션의 구조를 만드는 데 도움을 주지는 못한다.

디자인 패턴은 라이브러리보다 더 높은 단계에 속한다.
디자인 패턴은 클래스와 객체를 구성해서 어떤 문제를 해결하는 방법을 제공하는데, 그런 디자인을 특정 애플리케이션에 맞게 적용하는 일은 개발자의 몫이다.

디자인 패턴은 라이브러리나 프레임워크가 도와주지 못하는 부분을 해결하는데 도움을 줄 수 있다.

  • 패턴을 완전히 익혀 두면 어떤 코드가 유연성 없이 엉망으로 꼬여있는 스파게티 코드라는 사실을 금방 깨닳을 수 있다.
  • 코드를 수정할 때 패턴을 적용하여 코드를 개선할 수 있다.

객체지향 디자인 원칙과 디자인 패턴

캡슐화, 추상화, 상속, 다형성을 잘 알고 있고 활용할 수 있다고 하더라도 유연하고, 재사용이 용이하고, 관리하기 쉬운 시스템을 쉽게 만들기는 어려운 일이다.

디자인 패턴은 간단하지만은 않은 객체지향 시스템 구축 방법들을 모아서 정의한 말그대로 패턴이다.

따라서 디자인 패턴을 잘 알고 있다면, 비교적 적은 수고로 제대로 작동하는 디자인을 만들 수 있게된다.

디자인은 예술이다.
장점이 있으면 단점도 있지만, 많은 사람이 오랜 시간 동안 고민해서 찾아낸 디자인 패턴을 잘 따른다면 훨씬 좋은 디자인을 만들 수 있다.

또한 패턴의 밑바탕에는 객체지향 패턴이 있으므로 원칙을 알고 있다면 문제에 딱 맞는 패턴을 찾을 수 없을 때에 적절한 디자인을 만드는데 도움을 줄 수 있다.

디자인 도구상자 안에 들어가야 할 도구들

  1. 객체지향 기초
    • 추상화
    • 캡슐화
    • 다형성
    • 상속
  2. 객체지향 원칙
    • 바뀌는 부분은 캡슐화 한다.
    • 상속보다는 구성을 활용한다.
    • 구현보다는 인터페이스에 맞춰서 프로그래밍한다. 등
  3. 객체지향 패턴
    • 전략패턴 등

시나리오

시스템을 처음 디자인 할때 표준 객체지향 기법을 사용하여 슈퍼클래스를 만든 다음, 그 클래스를 확장하여 서로 다른 종류의 클래스를 만들었다.

이후 새로운 기능을 요구하는 상황이 발생하여 슈퍼클래스에 해당 기능을 수행하는 메소드를 추가하였으나, 슈퍼클래스를 상속받는 모든 클래스들이 메소드가 적용됨에 따라 해당 기능이 필요없는 모든 클래스에서 해당 기능이 적용되 결국 오류를 만들게 되었다.

이에 따라 해당 기능이 필요없는 서브 클래스의 추가된 슈퍼클래스의 메소드를 오버라이드하여 사용할 수 없게 막았다.

하지만 이런 상황으로 인해 서브클래스에서 중복이 많이 발생할 수 있었고, 지속적으로 새로운 기능을 추가하기로 함에 따라 특정한 기능을 묶어 인터페이스를 설계하였다.

문제

서브클래스에 새로운 기능을 추가할 때 모든 서브클래스에 필요한 기능이 아니므로 상속으로 처리하는 것은 올바른 방법이 아니다.

  • 서브클래스에서 인터페이스를 구현하여 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드 관리에 커다란 문제가 생긴다.
  • 서브클래스에 필요한 모든 기능들에 대해서 인터페이스를 만드는 방식도 서브클래스마다 구현이 필요하므로 좋은 해결방법이 아니다.

이러한 문제는 디자인 패턴의 적용으로 해결할 수 있다.

문제를 명확하게 파악하기

위 시나리오에서 새로운 기능 추가를 위해 상속을 활용하는 것은 모든 서브클래스에서 한 가지 기능만 사용하도록 하는 방법이기 때문에 최선의 해결책이 아니다.

또한 인테페이스를 사용하는 방법은 괜찮아 보이지만, 인터페이스에는 구현된 코드가 없으므로 코드를 재사용할 수 없다는 문제점이 있다.

  • 한가지 행동을 바꿀때마다 그 행동이 정의되어 있는 서로 다른 서브 클래스를 전부 찾아서 코드를 일일이 고쳐야한다.
  • 그 과정으로 인해 새로운 버그가 생길 가능성이 높다.

바뀌는 부분과 그렇지 않은 부분 분리하기

  1. 변화하는 부분과 그대로 있는 부분을 분리하려면 2개의 클래스 집합을 만든다.
  2. 각 클래스의 집합에는 각각의 행동을 구현한 것을 전부 집어넣는다.

변화하는 기능을 슈퍼클래스에서 모두 분리하여 각 행동을 나타낼 클래스 집합을 새로 만들어야 한다.

flowchart LR
    a((슈퍼클래스))-->b
    a-->c

    subgraph 달라지는 기능
        b([메소드 구현 1])
        c([메소드 구현 2]) 
end

달라지는 기능을 디자인하는 방법

달라지는 기능을 구현하는 클래스 집합은 최대한 유연하게 만들고, 인스턴스에 기능을 할당할 수 있어야 한다. 이를 위해 각 행동은 인터페이스로 표현하고 인테페이스를 사용해 행동을 구현한다.

  • 시나리오에서는 슈퍼클래스에서 구체적으로 구현하거나 서브클래스 자체에서 별도로 구현하는 방법에서 항상 특정 구현에 의존한다.
  • 서브클래스는 인터페이스로 표현되는 행동을 사용하기 때문에 실제 행동 구현이 슈퍼클래스를 활용하는 서브클래스에게만 국한되지 않는다.
    • 인터페이스로 인해 서브클래스마다 해당 기능을 구현해야하는 문제점이 있다.

실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록 상속이 아닌 상위 형식(super type)에 맞춰 프로그래밍해서 다형성을 활용해야 한다는 점에서 인터페이스에 맞춰서 프로그래밍한다는 말은 사실 상위 형식에 맞춰 프로그래밍한다는 의미이다.

  • 변수를 선언할 때 보통 추상 클래스나 인테페이스 같은 상위 형식으로 선언해야 한다.
  • 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있다.

행동 통합하기

특정 행동을 슈퍼클래스 또는 서브클래스에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임한다.

classDiagram
    class Duck {
        + FlyBehavior flyBehavior
        + QuackBehavior quackBehavior
        + performQuack()
        - quack()
        swim()
        display()
        + performFly()
        - performFly()
    }

1
2
3
4
5
6
7
public abstract class Duck {
    QuackBehavior quackBehavior;
    
    public void performQuack() {
        quackBehavior.quack();
    }
}

특정 행동을 해고 싶을땐 인테페이스에 의해 참조되는 객체에서 동작하는 방식으로 위임할 수 있다.

동적으로 행동 지정하기

Setter를 이용하여 위임한 다른 클래스를 설정한다면 동적으로 변경될 수 있다.

전략 패턴

알고리즘군을 정의하고 캡슐화하여 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다. 전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

  • 여러 알고리즘 또는 동작을 동적으로 선택하고 사용해야 할 때
  • 클래스의 행동을 변경하고 확장하기 쉬운 구조를 갖추고자 할 때
  • 코드 중복을 방지하고 재사용성을 높힐때

구조

  1. 전략(Strategy): 다양한 알고리즘 또는 동작을 나타내는 인터페이스 또는 추상 클래스
    • 이 인터페이스를 구현하는 여러 전략 클래스가 존재함
  2. 전략 컨텍스트(Strategy Context): 전략 객체를 사용하는 클래스로, 전략을 변경하고 실행하는 역할을 수행
    • 컨텍스트 객체는 전략 객체를 가지며 필요에 따라 전략을 바꿀 수 있다.
  3. 전략 구체 클래스(Concrete Strategy): 전략 인터페이스를 구현한 구체 클래스들
    • 각 클래스는 특정한 알고리즘이나 동작을 구현

캡슐화된 행동 살펴보기

classDiagram
    class Duck {
        FlyBehavior flyBehavior
        QuackBehavior quackBehavior
        swim()
        display()
        performQuack()
        performFly()
        setFlyBehavior()
        setQuackBehavior()
    }
    
    class MallardDuck {
        display()
    }

    class RedheadDuck {
        display()
    }

    class RubberDuck {
        display()
    }

    class DecoyDuck {
        display()
    }

    Duck <|-- MallardDuck
    Duck <|-- RedheadDuck
    Duck <|-- RubberDuck
    Duck <|-- DecoyDuck
    
    class ImpFlyBehavior {
        fly()
    }
    
    class FlyWithWings {
        fly() // 나는 행동 구현
    }
    
    class FlyNoWay {
        fly() // 아무것도 하지 않음
    }

    ImpFlyBehavior<|--Duck
    ImpFlyBehavior<|--FlyWithWings
    ImpFlyBehavior<|--FlyNoWay
    
    class ImpQuackBehavior {
        quack()
    }
    
    class Quack {
        quack() // 소리 내는 행동 구현
    }

    class Squeak {
        quack() // 고무 오리 소리 구현
    }

    class MuteQuack {
        quack() // 아무 소리 내지 못함
    }

    ImpQuackBehavior<|--Duck
    ImpQuackBehavior<|--Quack
    ImpQuackBehavior<|--Squeak
    ImpQuackBehavior<|--MuteQuack

행동들을 알고리즘군(family of algorithms)으로 생각하고 위처럼 행동을 상속받는 대신 올바른 행동 객체로 구성되는 행동을 부여받기 위해 두 클래스를 합치는 것을 구성을 이용한다라고 표현한다.

정리

  • 객체지향 기초 지식만 가지고는 휼륭한 객체지향 디자이너가 될 수 없다.
  • 휼륭한 객체지향 디자인이라면 재사용성, 확장성, 관리의 용이성을 갖출 줄 알아야 한다.
  • 패턴은 훌륭한 객체지향 디자인 품질을 갖추고 있는 시스템을 만드는 방법을 제공한다.
  • 패턴은 검증받은 객체지향 경험의 산물이다.
  • 패턴이 코드를 바로 제공하는 것은 아니나, 디자인 문제의 보편적인 해법을 제공한다.
  • 패턴은 발명되는 것이 아니라 발견되는 것이다.
  • 대부분의 패턴과 원칙은 소프트웨어의 변경 문제와 연관되어 있다.
  • 대부분 패턴은 시스템의 일부분을 나머지 부분과 무관하게 변경하는 방법을 제공한다.
  • 많은 경우에 시스템에서 바뀌는 부분을 골라내서 캡슐화해야 한다.
  • 패턴은 다른 개발자와의 의사소통을 극대화하는 전문 용어 역할을 한다.