3. 디자인 패턴

디자인 패턴 에는 늘 비용이 따라온다. 즉 디자인 패턴을 적용했을때 과도한 설계가 될 수도 있다는 것이다. 디자인 패턴을 통해 얻는 이점(예를들면 확장성)과 그 비용 사이에서 균형을 잘 맞춰야 한다.

또한 고객의 요구사항은 언제나 변할 수 있으므로 개발자는 처음부터 완벽한 설계를 할 수 없다.

3.1. 전략 패턴

전략 패턴 이란 클래스의 여러가지 동작을 표현하기 위해 다양한 종류의 클래스를 상속하지 않고 전략 인터페이스에만 의존하여 전략을 선택할 수 있는 디자인 기법이다. 알고리즘을 독립시키는 패턴을 뜻한다. 전략 패턴은 객체의 다형성을 이용하여 코드의 중복없이 알고리즘을 재사용할 수 있게 한다.

예를들어 인턴, 대리, 부장이라는 Employee를 상속받은 객체가 있고 각 객체에 다른 월급을 주고 싶을때 전략 패턴이 사용된다. 이때 Salary라는 인터페이스를 연관(Association) 시킨 후 Salary를 상속 받은 Salty, Generous중 하나를 주입하는 것이다. 각 클래스의 의미는 Salty는 적은 월급 Generous는 일반적인 월급이라고 할 수 있다. 이는 전략을 주입하는것과 비슷하다.

이처럼 조건에 따라 다른 전략을 사용하고 싶을때 사용하는 패턴이다. 참고

../_images/strategy.png

3.2. MVC 패턴

프로그램을 크게 Model, View, Controller 구성요소로 나누는 패턴이다.

  • Model은 데이터를 수정하거나 어떤 동작을 수행할 수 있다.
  • View는 Model에서 데이터를 읽어와서 사용자에게 보여준다.
  • Controller는 사용자의 요청을 처리하기위해 Model의 상태를 바꾼다.

이외에도 Service 계층을 두고 1개 이상의 모델과 함께 특정 작업을 처리하기도 한다.

3.3. SRP(Single Reposponsibility Principle)

단일 책임의 원칙은 하나의 클래스가 2가지 이상의 책임(변경의 이유)을 지지 않아야 한다는 원칙이다. 이는 소프트웨어 경직성(설계의 문제로 변경이 다른 변경을 유발하는 현상)을 제거하기 위해 사용되며 소프트웨어 설계에서 하는 일의 대부분이다.

3.4. OCP(Open Closed Principle)

개방 패쇄 원칙은 소프트웨어 개체(모듈, 클래스, 함수)는 확장에 열려있어야 하고, 변경에는 닫혀있어야 한다는 원칙이다. OCP에서는 경직성 문제를 제거 하기 위해 기능을 수정할때 추상화 기술과 이를 활용한 확장을 통해 기능을 수정하는 것을 권장한다.

3.5. LSP(Liskov Substitution Principle)

리스코프 치환 원칙은 부모 타입이 사용되는 문맥에서 부모타입을 자식타입으로 치환하는 것이 가능해야한다는 원칙이다. 예를들어 LSP를 위반한다면 자식 클래스가 생길때 마다 분기 코드를 작성하게 된다. 즉, 기능이 파생 클래스에 닫혀있지 않는 것을 의미하므로 개방폐쇄원칙을 위반하게 된다. 결론적으로 경직성 문제가 발생한다. 사용자 관점에서 클래스 설계를 확인해서 LSP를 위반하는지 찾을 수 있다. 사용자의 기대와 다르면 이는 LSP를 위반한 것이다. 개발자는 TDD를 통해서 예방할 수 있다.

3.5.1. 정사각형 예시

대표적으로 직사각형 <= 정사각형 관계에서 setWidth()라는 함수가 있다고 할때 LSP 위반이 발생할 수 있다. 사용자 입장에서 width와 height에 대한 불변식이 서로 다르기 때문이다. 여기에 계약에 의한 설계(Design by contract)를 대입해보면 다음과 같다.

직사각형의 사후조건은 너비 == 새로운 너비 && 높이 == 이전 높이 이고 정사각형의 사후조건은 이 조건과 동등하거나 더 강할 수 없다.

따라서 파생 클래스는 같거나 더 강한수준의 사후조건을 가져야 하며, 같거나 약한수준의 사전조건을 가져야 한다.

3.6. ISP(Interface Segregation Principle)

인터페이스 분리 원칙은 자신의 서브 클래스 때문에 원치 않은 상속과 사용하지 않는 메소드에 강제로 의존하지 않아야 한다는 원칙이다. 이는 인터페이스를 분리하고 다중 상속이나, Adapter를 통해 분리된 인터페이스의 서브 클래스간 통신을 하게하여 해결할 수 있다.

그 예제로 Door, Timer, TimerClient 문제가 있다. 상황은 Timer에 TimerDoor를 등록하여 울리게 하는 것이다. Door은 TimerClient를 TimerDoor은 Door를 상속한다고 가정해보자. TimerDoor은 2개의 기능을 갖고 있는 Door을 상속하게 된다. 이것은 ISP를 위반하며 문제를 일으킨다. Timer나 TimerClient의 변경이 있을 경우 모든 Door의 자식 타입에 변경의 문제가 발생하게 된다. TierClient 인터페이스를 구현할 필요 없이 TimerDoor의 어댑터를 통해 Timeout()이 호출되도록 하거나, 다중 상속으로 해결할 수 있다.

그 외에는 Shape, TextShape, TextView 문제가 있다.

3.7. DIP(Dependency Inversion Principle)

의존성 역전 원칙은 전통적으로 사용되는 저수준 모듈에 대한 의존성을 제거하고 추상 클래스나 인터페이스에 의존성을 갖도록 하는 원칙이다. 이는 저수준 모듈과의 결합을 줄여서 저수준 모듈이 변경되어도 크게 영향을 받지 않으며, 도메인, 사용자 요구에 맞는 일관성 있는 메서드 사용을 유도할 수 있다.

Robert Martin는 처음 DIP를 Open Closed Principle과 Liskov Substitution Principle를 합한 개념이라고 설명하였다.

3.8. IOC(Inversion of Control)

제어의 역전은 프로그래머가 작성한 코드를 작성한 코드 내에서 직접 제어하는 것이 아닌 프레임워크가 제어하도록 하는 원칙이다. 프레임워크가 확장성을 갖는 프로그램 뼈대가 될 수 있게 도와준다. 프레임워크의 기본 특징 중 하나이다.

일반적으로 프래임워크는 특정 구현체를 생성하고 프로그램에 주입하는 역할을 하기도 한다. 이때 IOC 패턴을 DI 라는 이름으로 부르기도 한다.

Hollywood’s Law의 Don’t call us, we’ll call you는 IOC의 특징을 잘 설명한다.

3.9. DI(Dependency injection)

의존성 주입은 객체에서 의존하는 객체 생성에 대한 관심을 분리하고 외부에서 의존성을 주입하는 패턴을 뜻한다. 직접 의존성 객체를 생성 및 주입하는 것은 복잡하고, 오류가 발생하기 쉽고, 테스트하기 어렵게 한다. 반면, IOC컨테이너를 통해 의존성을 주입할 경우 특정 클래스에 대한 의존성을 제거할 수 있다. 이는 애플리케이션이 실행되는 다양한 환경에 유연하게 대처할 수 있게 도움을 준다.

DI 방법은 크게 3가지가 있다. 인터페이스를 이용하여 주입하거나, 생성자나 setter를 이용하여 주입하는 방법이다. IOC 컨테이너는 대표적으로 Spring, Guice 같은 프레임워크가 있다.

단순하게 생각해도 아래의 Consumer 클래스를 선언할때, 그 객체의 의존성을 생성해야하고 의존성의 의존성 객체도 생성해야한다. 사용하기 너무 불편하다. 객체의 라이프 타임도 관리가 안된다.:

class D{
}

class C{
  D d;
  public C(D d){
    this.d = d;
  }
}

class B{
  C c;
  public A(C c){
    this.c = c;
  }
}

class A{
  B b;
  public A(B b){
    this.b = b;
  }
  ...
}

class Consumer{
  void consume(){
    C c = new C(new D())
    B b = new B(c);
    A a = new A(b);
    a.use();
  }
}

DI 패턴은 아예 클래스 내부에서 객체를 생성하는 Compile-time dependency를 사용하거나, 주입할 스태틱 객체를 갖는 팩토리 클래스를 만드는 패턴을 대체한다. DI와 유사한 패턴으로는 Service Locator 패턴이 있다.

3.10. Service Locator

DI과 비슷한 용도로 사용되며 일반적으로 서비스 객체를 검색하는데 사용하는 Registry 객체이다. 객체를 주입 받으려면 DI 패턴과 달리 Locator 객체에 의존성을 가져야 한다. 여러가지 방식으로 구현 가능하다. 서비스 객체를 제공하는 메서드를 구현하거나, 기능별 인터페이스를 구현하는 방법과 문자열과 같은 키값을 주어 동적으로 원하는 객체를 가져오는 방법이 있다.

3.11. DI vs Service Locator

쉽게 테스트 구현체를 주입할 수 있다. 객체에 어떤 의존성이 있는지 쉽게 확인할 수 있다. 인터페이스, 메서드, 생성자와 같은 DI 매커니즘을 통해 쉽게 확인 가능하다.

DI는 IOC 개념을 사용하기 때문에 직관적이지 않고 디버깅을 통해 문제를 분석하기 어려운 경우가 있다. 한번 의존성이 구성되면 추가적인 서비스 객체를 가져올 수 없다.

쉽게 테스트 구현체를 등록할 수 있다. 코드 호출을 통해 직관적이고 명시적으로 객체를 얻어올 수 있다.

Locator 객체에 의존성을 가져야 하기 때문에 상황에 따라 단점이 될 수 있다. 또한 의존성을 확인하기 위해 Locator의 메서드를 호출하는 소스코드를 찾아야 하는 번거로움이 있다.

다른 사람이 만드는 애플리케이션에서 고유의 Locator를 사용하고 있고 동시에 내가 만든 Locator에 의존하는 클래스를 사용하려 한다면 Adapter로 두개의 Locator 연결해야한다. 이는 Locator의 단숨함을 해칠 수 있다.

3.12. 위임 패턴

다른 객체에 일을 위임하는 설계 패턴을 뜻한다. 안드로이드에서 터치이벤트를 처리하기 위해 버튼의 구체적인 역할을 OnClickListener에 위임하였다.

3.13. 퍼사드 패턴

퍼사드 패턴은 기존의 여러 패키지의 독점 중개인을 만들어서 강제로 정책을 적용한다. 복잡하고 일반적인 모듈을 추상화 하고 간단한 인터페이스를 제공할때 제공하며, 사용자 입장에서는 복잡한 인터페이스는 퍼사드에 의해 완벽하게 가려지며 보이지 않게 된다.

예를들어 java.sql 패키지 같은 경우 DB에 연결하거나 DB를 조작할때 사용하며, 다양한 클래스로 구성되어 있으며 사용 방법이 복잡하며 반복적이다. DB 클래스를 이용해서 java.sql의 복잡한 인터페이스를 숨기고 간단하게 쉽게 사용할 수 있도록 한다.

3.14. 미디에이터 패턴

퍼사드 패턴과 달리 은밀하고 강제적이지 않은 방식으로 정책을 적용한다.

예를들어 스마트폰에서 A라는 모듈에 새로운 메세지가 왔을때 B라는 모듈에 알림이 나타나도록 정책을 적용할 수 있다. 이러한 정책은 강제 사항이 아닌 숨어서 동작한다.

3.15. 팩토리 패턴

팩토리 패턴

객체를 사용하는 측에서 사용하려는 클래스의 인터페이스만 정의하며 실제로 어떤 객체를 생성할지 구체적인 팩토리에 맡기는 패턴이다. 예를들어 워드에서 특별한 형식의 문서를 만들때 그 타입만 넘겨주면 타입에 관련된 구체적인 클래스를 반환한다.

장점

  • 단지 인터페이스만 알고 있으면 되기 때문에 클래스와의 강결합을 줄여준다. 어플리케이션과 구체적인 클래스를 분리시킨다. 팩토리 패턴을 적용하면 생성될 객체의 클래스를 수정하여도 상위 모듈의 재컴파일이 필요하지 않다.
  • 프로젝트 초기에 클래스가 자주 수정될 경우 사용하기 좋다.
  • 구체적인 팩토리를 여러개 만들어서 용도에 맞게 사용할 수 있다.

단점 (사용하기 안좋은 경우)

  • 생성하려는 클래스의 종류가 부모를 상속하는 형태가 아니라면 굳이 쓸필요가 없는 패턴이다.
  • 확장이 아닌 결합도를 줄이기 위해 사용하는 패턴이다.

3.16. 추상 서버 패턴(Abstract Server Pattern)

클래스에 직접 연관을 갖지 않고 중간에 인터페이스를 통해 클래스를 사용하는 패턴을 뜻한다.

스탠드 내부에서 돌아갈 소프트웨어를 설계할때 어떤 점을 고려해야할까? 1990년에 많은 논쟁이 있었고 많은 사람들이 아이디어를 제시하였고 우월성을 입증하려 노력하였다.

../_images/lamp1.PNG

단순히 Switch와 Light라는 2가지 객체만 사용할 경우 발생할 수 있는 문제는 다음과 같다. 먼저 DIP, OCP라는 원칙을 위배한다. 상위 정책 인터페이스에 의존하는 것이 아니고 구체적인 클래스에 의존하기 떄문이다. 또한 항상 Light 객체를 끌고 다녀야 하고 Light외에 다른 객체를 제어할 수 있도록 확장하기 어렵다.

다른 객체까지 다루기 위해 Switch를 상속받아 확장할 수 도 있지만, Light에 대한 의존성 문제가 여전히 존재하므로 DIP를 위배한다.

추상 서버 패턴은 간단하다. 연관된 클래스를 직접 참조하지 않고 Switchable이라는 인터페이스를 통해 참조하여 의존성 역전(DIP)을 시키는 것이다. 이렇게 되면 OCP 원칙 역시 충족된다.

../_images/lamp2.PNG

인터페이스란 파생 클래스보다 클라이언트와 더 강한 논리적 구속력(연관)을 갖는 개체이다. 클라이언트는 인터페이스가 반드시 필요하지만, 파생 클래스는 항상 인터페이스와 연관되어 있을 필요는 없다. 이는 인터페이스와 파생 클래스 사이에 논리적 구속력이 상대적으로 약하다고 할 수 있다.

따라서 인터페이스는 클라이언트의 소유라 할 수 있다.

어떤 개발자들은 물리적 구속력(상속)이 논리적 구속력보다 강하다고 여기기도 한다. 하지만 논리적 구속력을 더욱 신경 쓰고 설계하였을때 직관적이고 좋은 설계가 나온다.

3.17. 인터페이스 변경

인터페이스는 자주 변경되어서는 안된다. 예를들어 자바 버전이 올라가면서 하위 호환성을 고려하지 않고 JDBC Connection 인터페이스를 수정하였다면, 모든 파생 클래스의 구현이 수정되어야 하며 클라이언트 소스코드도 수정이 되어야 한다.

이는 모든 클라이언트/벤더 개발자가 불편해할 것이며 쉽게 자바 버전을 올릴 수 없게 할 것이다. 인터페이스의 변경은 다른 코드의 변경을 강제한다.

3.18. 어댑터 패턴(Adapter Pattern)

어떤 라이브러리의 소스코드를 수정하지 않고 사용자가 요구하는 인터페이스를 구현하기 위해 사용하는 디자인 패턴이다.

기존 소스코드를 수정할 수 없거나, 아예 소스코드가 존재하지 않을때 사용할 수 있다. 이때 호환성이 없는 객체는 Adaptee라 불린다.

예를들어 상위 수준 정책이 의존하는 인터페이스를 어댑터가 구현하게하고, 내부적으로 호환성이 없는 인터페이스(Adaptee)에게 작업을 위임한다.

탄스타플(TANSTAAFL)은 There ain’t No Such Thing As A Free Launch의 약자이다. 추상 서버 패턴이나 단순한 클래스 연관으로 충분한 상황이라면 굳이 어댑터 패턴을 사용할 필요는 없다. 어댑터 패턴은 위임 객체 연결, 어댑터 객체 메모리 등 추가적인 자원을 소모한다.

3.19. 템플릿 메서드 패턴

부모 클래스에서 메서드 호출의 흐름을 제어하고 자식 클래스에서 구체적인 내용을 작성하는 패턴이다. 예를들어 HTML문서를 생성할때 템플릿 메서드를 활용하면 좋다. 템플릿 메서드에서 head, body 등의 필요한 태그를 생성하는 함수를 이용해 흐름을 작성한다.

장점

  • 템플릿을 통해 코드의 중복을 줄인다.
  • 템플릿 메서드를 이용해 리팩토링이 쉽다.

3.20. 옵저버 패턴

다른 객체의 변화를 관찰하는 패턴이다. OCP, LSP, DIP 등 3가지 원칙을 만족 시키며 Subject와 Observer로 역할이 나뉘어진다. Subject는 추상 클래스로(구체화를 못 하도록) 다른 객체에 알림을 줄때 사용하는 클래스이며, Observer는 관찰하고 싶을때 사용하는 인터페이스이다.

동작하는 방식은 pull-model, push-model가 있다. pull-model은 observer에게 전달할 데이터의 종류가 간단할때, push-model은 복잡하거나 단순히 pull-model로는 가져올 수 없을때 사용한다.

../_images/observer-pull.PNG ../_images/observer-push.PNG