헤드 퍼스트 디자인 패턴
2022-07-20
도서명 | 헤드퍼스트 디자인패턴 |
---|---|
지은이 | 에릭 프리먼, 엘리자베스 롭슨 |
옮김 | 서환수 |
출판사 | 한빛미디어 |
ISBN | 9791162245262 |
금액 | 36,000원 |
출판일 | 2022년 03월 16일 발행(개정판) |
페이지수 | 656쪽 |
참고 | Sungho’s BlogHead First Design Patterns |
디자인 패턴을 알아야 하는 이유
- 보통 소프트웨어를 유지 및 보수하는 데는 소프트웨어를 개발하는 만큼의, 혹은 더 많은 양의 비용과 노력이 들기 때문에 확장성, 혹은 유지 보수성은 매우 중요하다.
- 디자인 패턴을 잘 활용하면 확장성있는 유연한 소프트웨어를 작성하는데 도움이 된다.
- 또한, 디자인 패턴을 알면 다른 개발자와 소프트웨어의 설계에 대해 이야기 할 때 장황한 설명 없이 특정 패턴의 이름을 말하는 것만으로도 어떤 구조를 말하는 건지 의도를 빠르고 명확하게 전달할 수 있다는 점에서 커뮤니케이션 실수를 줄이고 생산성을 높이는데 큰 도움이 된다.
- 게다가 이미 존재하는 수많은 복잡한 시스템이나 프레임 워크, 혹은 API 들은 대부분 디자인 패턴을 활용하여 작성 되었다.
- 그렇기 때문에 디자인 패턴을 알면 소스 코드를 조금만 훑어봐도 어떤 패턴을 사용하여 구현된건지 금방 눈치 챌 수 있어 큰 그림을 이해하는데 도움이 된다.
자주 쓰이는 디자인 패턴 15가지
Strategy 패턴
- 오리 시뮬레이션 게임 구현하기
- 방법 1 (only 상속)
- Duck 이라는 슈퍼 클래스를 만들고 꽥꽥대기(quack), 수영하기(swim) 등의 공통된 행위를 메소드로 정의한다.
- 오리의 종류에 따라 화면에 표시하는 방식이 다르므로 display 라는 추상 메소드를 둔다.
- 다양한 오리의 클래스들이 Duck 클래스를 상속받아 각자의 기능을 정의한다.
- 하지만 만약 오리에 나는 기능을 추가하기위해 날기(fly) 라는 새로운 행위를 메소드로 정의한다면 잠재적인 문제가 발생한다.
- 일부 서부클래스들은 슈퍼클래스의 일부, 혹은 전체 행위를 할 수 없어야 하는 경우다. 예를 들어, 장난감 오리 를 나타내는 RubberDuck 이라는 서브클래스는 날면 안되기 때문에 fly 메소드를 오버라이딩하여 아무것도 하지않도록 해야하며, 나무를 깎아 만든 오리장식 을 나타내는 DecoyDuck 라는 서브클래스는 나는것은 물론 꽥꽥대지도 말아야하기 때문에 fly 와 quack 메소드 모두 오버라이드 하여 아무 행위도 하지않도록 해야한다. 즉, 재사용을 목적으로 상속을 사용하는 것은 좋지만, maintenance 측면에서는 그다지 좋지 않다는 것이다
- 방법 2 (+ interface)
- 그렇다면 Interface 는 어떨까?
- display 나 swim 같은 공통된 성질 및 행위는 Duck 이라는 슈퍼클래스에 두고 상속받도록 하되, fly, quack 등의 optional한 behavior 는 interface 로 정의하고, 각각의 오리 subclass 들이 필요한 behavior 를 impplements 하여 스스로 각자의 기능을 정의하도록 하는 방법이다.
- 필요한 행위만 골라 쓸 수 있다는 점은 분명 장점이지만, 행위 코드의 재사용이 불가능해지기때문에 코드의 중복이 발생한다.
- 만약, fly 라는 행위에 대한 요구사항이 조금이라도 발생한다면, 이를 구현하고있는 모든 subclass 들의 코드들을 모두 변경해주어야 한다는 문제도 있다.
-
결국 이 방식은 이전의 inheritance 를 사용한 방식의 일부 문제는 해결주지만 코드 재사용의 이점을 없애 또다른 maintenance nightmare 를 만든다.
- 여기서 중요한 법칙이 나온다. 프로그래밍을 할 때 절대 변하지 않는 법칙은, 프로그램은 항상 변한다는것이다. 규모가 커지거나 기능이 변경될 수가 있다는 것이다. 그러므로 변하는 부분을 Encapsulation(캡슐화) 하여 변하지 않는 부분으로 부터 분리해야한다. 그러면 분리한 부분이 다른 것으로 대체되거나 확장되어도 변하지 않는 부분에 영향을 끼치지 않게 된다. (SOLID 의 OCP 와도 일맥상통한다) 결과적으로 코드 변경 시 의도치않은 결과가 줄고, 유연함이 생긴다.
-
방법 3 (Strategy Pattern)
- 그렇다면, optional 한 behavior 들을 캡슐화하여 오리 subclass 를 생성할 때 행위를 정의해 줄 수는 없을까?
- 행위들을 interface 로 정의하고, 이를 implements 하는 구체적인 행위들을 정의한뒤, setter method 를 통해 초기화 해준다.
- 여기서 또다른 Design Principal 이 등장하는데, behavior 를 전달받는 오리 class에서 behavior 의 type 을 구체적인(인터페이스를 구현하는 클래스)타입이 아니라 인터페이스의 타입으로 사용하는 것이다.
- 이렇게되면 런타임에 행위를 다른 것으로 동적으로 변경해줄 수도 있게된다.
- 오리 클래스는 행위의 구체적인 구현을 알 필요가 없어진다.
- 이 원칙은 Program to an interface, not an implementation 이다.
- 만약 행위를구체적인 타입으로 선언할 경우 변경에 대한 여지가 사라지게 된다.
-
예를 들어, 다음과 같은 클래스가 있다고 할 때
Dog d = new Dog(); d.bark();
보다는
Animal animal = new Dog(); animal.makeSound();
또 이것 보다는
a = getAnimal(); a.makeSound();
로 구현하는게 좋다. 왜냐면 두번째 형식도 코드에 특정 클래스 타입이 등장하기 때문이다
스트레티지 패턴의 키 포인트는 behavior(행동) 를 delegate(대리자) 한다는 것이다 기존에 오리 subclass에 있던 fly 함수대신 performFly 함수를 선언하고 내부에서 behavior class 에 정의된 fly 함수를 호출한다 그럼 오리 subclass는 behavior(행동) 의 디테일을 신경쓰지않아도 된다
- 여기서 우리는 IS-A 관계인 inheritance(계승) 대신 HAS-A 관계인 composition(구성) 을 사용한 것인데, 여기서 또다른 Design Principle(원칙) 이 등장한다.
- Favor composition over inheritance(상속보다 구성 선호)
- inheritance(계승) subclass 가 superclass 강하게 결합되며, 컴파일 타임에 subclass 의 성격이 정해져버리는 단점이 있어서 좋지 않을 때도 있다
- composition(구성) 을 사용하면 runtime 에 behavior(행동) 를 변경할 수가 있어져, 더 유연한 설계를 가능하게 한다
- composition 은 여러 디자인패턴에서 사용된다
- 방법 1 (only 상속)
- 추가 설명
- OO basics 를 잘 따른다고, 항상 extensible(확장 가능한) & flexible(유연한) & reusable(재사용 가능한) & maintainable(유지 가능한) 한 설계를 보장해주진 않는다. OO 의 속성들이 항상 명확하진 않기때문에 OO basics 를 따른 설계들 중에서도 오랫동안 검증된 방식의 모음이 디자인 패턴인것
- 개발 도중과 완료 이후 중에 더 시간을 많이 들어가는 것은 개발 완료 이후이기 때문에, reuse(재사용) 보단 extensibility(확장성) 와 maintainability( 유지 보수성) 를 더 신경써야한다.
- reuse(재사용) 를 달성하는 방법은 상속 이외에도 다양한 방법들이 있기때문에 꼭 상속을 사용할 필요는 없다
- Strategy Pattern 에 적용되는 Design Principles(원칙)
- 변경되는 부분을 변경되지 않는 부분과 분리해라(Identify the aspects of your application that vary and seperate them from what stays the same)
- 구현체가 아니라 인터페이스를 사용하여 프로그래밍해라(Program to an interface, not an implementation)
- 상속보다 구성을 선호해라(Favor composition over inheritance)
Observer 패턴
객체들간의 일대다 의존관계를 만들어 하나의 객체가 변하면 이 객체에 의존하는 객체들이 모두 알림을 받고 자동으로 업데이트 되는 디자인 패턴
- 날씨 앱 만들기
- 초기버전
class WeatherData { public void measurementsChanged() { float temp = getTemperature(); float humidity = getHumidity(); float pressure = getPressure(); currentConditionsDisplay.update(temp, humidity, pressure); statisticsDisplay.update(temp, humidity, pressure); forecastDisplay.update(temp, humidity, pressure); } }
- 문제점
- display 가 추가될 때마다 코드를 변경해야함
- display 를 런타임에 추가/삭제할 수 없다
- 인터페이스가 아닌 구체적인 구현으로 코딩하고있다
- 변화하는부분을 캡슐화하지 못했다
- 옵저버 패턴 적용
- Subject 라는 데이터를 관리하는 단 하나의 대상을 두고 변경시에만 동기화를 하여 Observer 들이 공유하도록 함
- 굳이 모든 대상이 데이터를 관리하지 않아도 됨
- notify 순서에 의존하지마라
- 장점
- 어떤 클래스이던간에 상관없이 Observer 인터페이스를 구현하기만하면 데이터 변경시 알림을 받을 수 있다
- Subject 는 각 Observer 들이 실제로 어떤 클래스인지 알 필요 없이 일관된 인터페이스로 데이터 변경 알림을 줄 수 있다
- 새로운 Observer 를 추가해야 하는 경우에도 Subject 는 코드의 변경 없이 이를 구행할 수 있음
- 초기버전
- Design Principal
- Strive for loosely coupled designs between objects that interact
- 느슨하게 결합된 설계는 더 유연한 코드를 만든다
- 일반적인 observer pattern
- Subject 와 Observer 로 이루어짐
- push 방식(subject 만 observer 에게 데이터를 보내줄 수 있음)
- Java built-in observer pattern
- 특징
- Observable 과 Observer 로 이루어짐
- 일반적인 옵저버 패턴의 Subject interface 와는 다르게 Observable 이 클래스임
- 장점
- push 방식과 pull 방식(옵저버가 직접 subject/observable 의 데이터를 가져가는 방식) 모두 지원
- setChanged() 와 changed 필드를 통해 유연함을 추가했다(잦은 데이터가 변경으로 notify 를 너무 자주하는경우, 특정 수준 이상으로 크게 변경되었을 경우만 setChanged() 를 호출하여 notify 하도록 가능. pretected 로 선언되어있어서 observer 가 호출하는게아니라 subject 자신이 호출하는것임)
- 주요 메소드를 상속받기 때문에 직접 구현할 필요없다
- 단점
- setChanged 가 protected 여서 서브클래스 내부가 아니라면 Composition 방식으로는 호출할수가 없다
- class 를 상속받는방식이어서 우리 나름대로 로직을 구현할수없다
- 특징
- java.util Observer 는 Deprecated
- 자바9 부터 java.util.Observable/Observer 가 deprecated 되었다
-
https://stackoverflow.com/questions/46380073/observer-is-deprecated-in-java-9-what-should-we-use-instead-of-it
- 이유
- Observable 이 serializable 을 구현하짇 않앗고, 모든 멤버가 private 이라 serialize 하지 못한다
- Thread safety 하지 않음
- 뭐가 변했는지 안알려줌 단순히 변했다는것만 알수있음
- 모든 Observable 이 똑같다. instanceof 로 검사하여 어떤 타입으로 타입캐스트하는 로직을 구현해야한다
- 그밖에 버그 및 레거시 코드 관리의 어려움 등
- 대안
- java.beans 에 있는 PropertyChangeSupport 와 PropertyChangedListener (그리고 PropertyChangedEvent)
- Listeners 는 많은 타입이 있고 callback method 도있고 casting 할 필요도없다
- PropertyChangeListener 추천
- Listener 도 역시 Observer Pattern 이다!
- pull 방식 vs push 방식
- push 방식
- 장점
- 데이터 변경시에만 알림을 받을 수 있어서 효율적임
- 단점
- 모든 옵저버에게 일관된 방식으로 데이터를 줘야하기 때문에 옵저버 입장에서는 필요하지 않은 데이터일지라도 모두 받아야함
- subject 가 관리하는 새로운 데이터가 추가될 경우 observer 들에게 변경된 데이터를 notify 하는 부분의 코드의 변경이 불가피함
- 장점
- pull 방식
- 장점
- 각 옵저버들은 필요한 데이터만 getter 메소드를 통해 받아올 수 있음
- subject 가 관리해야하는 새로운 데이터가 추가되어도 notify 해주는 부분의 코드 변경없이 getter 만 만들어 주면됨
- 단점
- 데이터가 변경되지 않았을 때도 매번 getter 를 통해 데이터를 직접 가져가야함
push 보단 pull 이 더 적절한 방식으로 여겨진다
- 장점
- push 방식
- 실생활 예
- Java swing 의 JFrame 에서 JButton 과 ActionListener
Decorator 패턴
- 커피 가격 계산기 만들기
- 각 커피 종류에 재료를 더할 수 있는 모든 경우에 대해 클래스를 만들고 Beverage 라는 super 클래스를 상속
- 문제점
- 클래스가 너무 많아짐
- 문제점
- 각 재료 추가 여부를 boolean field 로 가지고 있도록 구현
- 재료들에 대한 추가 비용을 계산하는 cost 메소드를 Beverage 에서 정의
- subclass 들은 cost 를 오버라이드하고 super.cost() 와 자신들의 가격을 더한 값을 반환한다
- 문제점
- 가격이 바뀌면 코드 바꿔야함
- 새로운 재료가 추가되면 슈퍼클래스의 필드 선언부와 cost 메소드 코드 바꿔야함
- 기존의 재료와는 어울리지 않는 음료가 추가될 수 있음(e.g. Tea 클래스인데 hasWhip 메소드가 있으면 이상함)
- 같은 재료를 여러개 필요로 하는 경우엔?
- Decorator Pattern
- Decorator 패턴은 4가지로 구성된다
- Abstract Component
- Concrete Components
- Abstract Decorator
- Concrete Decorators
- Decorator 는 Wrapper Class 다
- Object Composition 과 Delegation 을 통해 기존의 코드를 변경하지 않고 런타임에 기능을 추가할 수 있음
- Inheritance 의 단점을 해결
- 이를 사용하는 Client 는 내부적으로 데코레이터를 사용하는지 알필요가 없다
- 런타임에 동적으로 기능도 추가할 수 있는 상속으로 기능을 확장하는 것의 유연한 대체제의 역할
- OCP 의 대표적인 예
- 문제점
- 너무 많은 작은 클래스들을 만들어 직관적이지 못한 코드가 될 수가 있다.
- 하지만 동작 원리를 알면 금방 이해할 수 있다
- Decorator 패턴은 4가지로 구성된다
- 각 커피 종류에 재료를 더할 수 있는 모든 경우에 대해 클래스를 만들고 Beverage 라는 super 클래스를 상속
Factory Method 패턴 & Abstract Factory 패턴
-
Factory 패턴은 크게 Factory method 패턴과 Abstract Factory 패턴, 이렇게 두가지가 있다.
- Factory method pattern
- super-class 에서 특정 object 를 반환하는 abstract 메소드를 둠으로써, sub-class 에게 object 의 생성을 위임하는 패턴
- DIP(Dependency Inversion Principal) 을 실천하는 강력한 테크닉 중 하나
- Concrete 한 class 들에 직접 의존하는 것이 아니라 Object 를 생성하는 역할을 factory mothod 에게 위임함으로써 DIP 를 실천
- Abstract factory pattern
- 서로 관련이있는 Object 들을 생성하는 메소드들을 가진 interface 를 정의하고, 이를 구현하는 class 들을 만든 뒤, object 를 필요로 하는 class 에 주입하여, 이를 사용하게 하는 패턴
- 객체의 생성을 interface 로 추상화하였기 때문에, 런타임에도 자유롭게 팩토리를 바꿔 쓸 수가 있다
- 사실 abstract factory 역할을 하는 interface 안에 factory method 있다.
- 공통점
- Factory Pattern 임
- 둘 모두 Object creation 을 encapsulate 하여 앱이 구체적인 구현으로부터 loosely coupled 되도록함
- 왜냐하면 factory 를 사용하는 client 입장에서는 concrete 한 class 를 몰라도 되며, high-level 의 abstract 만 가지고 동작하기 떄문
- 차이점
- Factory method 는 Inheritance 을 통해 object 를 생성하며, Abstract factory 는 Object composition 을 통해 생성
-
Factory method 는 하나의 object 의 생성만을 담당할 수 있으며, Abstract factory 는 object 의 family (서로 관련있는 여러개의 object)의 생성을 담당할 수 있음
- +추가 : Simple Factory
- 정확히는 pattern 은 아니지만, seperation of concern 으로 object creation 부분을 별도의 클래스(와 한 메소드)로 분리한 방식
- 말 그대로 구현이 단순하여 유용할 수 있음
Singleton 패턴
- 특징
- single instance 와 global access point 제공
- private 생성자라 상속불가
- global variables 와 다른점은 lazy initialization 이 가능
- class loader 여러개일 땐 그 개수만큼의 instance 가 생기므로 주의
- 멀티스레드일때 volatile static 으로 instance 변수 선언해야함
- volatile 은 CPU 캐시가 아니라 메모리에 저장하라는 뜻. 캐시에 저장하면 스레드마다 서로 사용하는 데이터가 다를 수 있기 때문
- Java 1.2 이전 버전에선 GC 가 싱글톤을 죽일 수 있다
- Java 1.5 이전 버전에선 volatile 키워드가 double-checked locking 이 제대로 동작하지 않도록하는 JVM implementation 을 가지는 경우가 많음. 고로 멀티스레드에서 동작 제대로 안할 수 있음
- 구현방식
- 공통
- instance 를 담을 static field
- private default constructor
- instance 를 리턴해줄 static method
- 싱글스레드
- static factory method 에서 null 체크를 하여, null 일 때만 new 로 static field 초기화. 그다음 instance 를 리턴
- 멀티스레드
- class 가 로드되는 순간 초기화 (static 으로 선언된 instance 에 new 키워드로 초기화) (lazy initialization 아님)
- lock (synchronized static method)
- double-checked lock (null-check, lock, and additional null-check)
- 그 외 책에 나와있지 않은 방법
- enum 으로 구현할 수도 있음 (by Effective Java 3rd, item3)
- LazyHolder 라는 방법도 있음 (static nested class) (현재까지 가장 완벽하다고 평가받는 이디엄이라고 한다)
- 둘 다 멀티스레드에서도 잘 동작
- 공통
Command 패턴
- Request(Command) 를 Object 로 Encapsulate 하여 해당 Request 를 큐에 넣거나, 로깅을 할 수 있게도 하며, 실행취소도 가능하게 하는 디자인 패턴
- Command 라는 인터페이스를 통해 실제 액션을 수행하는 클래스와 명령을 내리는 클래스 간의 결합도를 낮춤
- 명령을 내리는 클래스는 실제 액션이 어떻게 동작하는지 몰라도 되게 하는것
- 런타임에서 Command 를 구현하는 클래스간의 교체를 통해 동적으로 action 을 변경할 수도 있음
- 리모컨을 예로들면 리모컨의 구현을 가능한한 단순하게 만들 수 있으며, 새로운 vendor 의 제품이 추가되거나 기존의 제품이 변경되더라도 리모컨은 변경될 필요가 없음
- 리모컨의 슬롯은 실제 제품의 동작 방식에 대해 알 필요가 없음
-
단순히 버튼이 눌렸을 때 슬롯에 연결된 제품의 정의된 동작을 수행하라는 명령만 내리면 됨
-
비유
Client Invoker Command Receiver Customer Waitress Order Chef User Universal Remote Control Device Driver Device -
웨이터가 주문서라는 중간매개체에 의해 요리사와 Decoupled 되는것
- 만능 리모컨 만들기 예제
- 각 소켓마다 on/off 버튼이 있는 리모컨
- 각 소켓에 디바이스를 연결할 수 있다.
- 이전 명령 실행취소 기능 추가
- Command interface 에 undo() 를 추가한다
- 모드가 여러개 있는 디바이스(선풍기의 강풍/약풍/미풍/정지) 지원 추가
- 하나의 소켓에 특정 디바이스의 한가지 모드로 설정할 수 있는 기능을 넣을 수 있다
- 매크로 기능 추가
- Command 의 배열을 가진 또다른 Concrete Command 를 정의하여 invoker 에 셋팅한다
- execute() 에서 반복문을 통해 들고있는 모든 Command 들의 execute() 를 호출한다
- 실행취소 기능은, 반복문을 통해 들고있는 모든 Command 들의 undo() 를 호출한다.
- 실행취소(Undo)가 여러번 가능한 기능 추가
- request 의 히스토리를 관리
- Redo 도 여러번 가능한 기능 추가
- 각 소켓마다 on/off 버튼이 있는 리모컨
- 활용 예
- Job queue, schedulers, thread pools
- Logging requests
- 예를 들어, 스프레드시트 앱에서 변경사항이 발생할 때마다 파일 전체를 저장하는 것은 현실적으로 힘들다
- 그러므로 매번 해당 변경 사항만 따로 store 하고, 만약 프로그램이 죽었다가 다시 살아났을 경우, 저장했던 command 들을 다시 load 하여 복구하는것
- 해당 기능을 확장해서 Transactional 한 기능을 구현할 수도 있다
Adapter 패턴 & Facade 패턴
- 두 패턴 모두 기존의 interface 를 다른 interface 로 대체하여 Clients 와 이들이 사용하는 system 과의 커플링을 낮추는 것을 목적으로 한다.
- 하지만 둘의 목적과 분명히 다르다
Pattern | Intent |
---|---|
Decorator | Doesn’t alter the interface, but adds responsibility |
Adapter | Converts one interface to another |
Facade | Makes an interface simpler |
- Adapter Pattern
- 비유
- 실제 세계의 어댑터란 110V 의 플러그를 220V 의 콘센트에 끼우는 것처럼 110V 플러그를 220V 콘센트가 기대하는 형태로 알아서 바꿔주는 것
- 우리가 어떤 소프트웨어를 만들었는데 기존의 벤더에 맞게 만들었기 때문에 지금까진 잘 동작했지만, 새로운 벤더의 모듈과는 맞지 않을 때, 기존의 코드를 바꾸고싶지 않고, 벤더의 코드를 변경할 수는 없기 때문에, 어댑터라는 중간 매체를 만들어서 둘을 연결하는것
- 오리 시뮬레이션 게임
- 오리 객체가 부족해서 칠면조 객체를 오리객체로서 사용하고 싶을 때
- Object Adapter vs Class Adapter
-
Object Adapter
-
Class Adapter
- 다중 상속이 가능해야 구현할 수 있으므로 Java 에선 쓸 수 없다
-
- 실제 세계의 예
- Enumeration 과 Iterator
- Enumeration 은 초기 Collections 타입인 Vector, Stack, Hashtable 등에서 사용되었다
- Iterator 는 이후 버전에서 생긴 새로운 Collections 클래스들을 위해 생겼다
- Enumeration 은 hasMoreElements(), nextElements() 메소드를 가졌고
- Iterator 는 hasNext(), next(), remove() 메소드를 가졌다
- 이제는 항상 Iterator 만을 사용하도록 하고싶기 때문에 EnumerationIterator adapter 를 만든다
- hashMoreElements() 는 hasNext() 와 목적이 같고, nextElements() 는 next() 와 목적이 같다.
- 고로, 어댑터의 hashNext() 와 next() 가 호출될 경우 delegate 하면 된다.
- 하지만 Enumeration 은 remove 을 지원하지 않기 때문에 Adapter 에서 next() 가 호출되면 Exception 을 던져야한다
- 이것은 Adapter 가 완벽하지는 않다는 것을 보여준다
- 하지만 잘 문서화되고, Client 가 조심한다면 Adapter 는 굉장히 합리적인 솔루션이다
- 아무튼 자바는 Iterator 를 사용하는 방향으로 가고있기에 client code 에는 Enumeration 에 의존하는 많은 레거시가 존재한다
- 그렇기 때문에 Iterator 를 Enumeration 으로 변환해주는 어댑터도 꽤 유용하다
- Enumeration 과 Iterator
- 비유
- Facade Pattern
- Adapter 는 incompatible 한 interface 간의 변환을 위한 것
- 반면, Facade 는 기존의 복잡한 interface 를 단순화하기 위해 interface 를 교체하는것
- Client 는 단 하나의 Friend 인 Facade 만을 통해서 subsystem 을 조작한다(단 하나의 Friend 를 갖는것은 OOP 에서 좋은것이다)
- Facade 가 관리하는 subsystem 은 Principle of Least Knowledge 를 지키도록 해야한다
- 만약 subsystem 이 너무 복잡하고, 너무 많은 클래스가 섞인다면, 추가적인 Facades 들을 만들 수도 있다
- 단점으로는 복잡도가 감소하고 유지보수성이 증가하지만, 런타임 퍼포먼스가 감소하거나 Wrapper 클래스가 많아질 수 있다
-
참고로 Facade[fəˈsɑːd] 는 퍼사드로 읽는다
- Principle of Least Knowledge
- Principle of Least Knowledge - talk only to your immediate friends
- 데메테르의 법칙(The law of demeter) 이라고도 불리나 선호하지는 말자 (이름만 봐선 의미를 알수없고, 법칙까진 아니다)
- 서로 강하게 결합된 많은 클래스들을 가지고, 이 중 하나가 변경 되었을 때 이 변화가 다른 부분에 cascade 되는것을 막는다
- 여러 클래스들 간에 많은 의존성이 존재할 경우, 이는 유지보수하기 어렵고, 다른 사람이 이해하기 어려운 fragile 한 system 이 된다
- 호출해도 되는 메소드
- 자신이 가진 메소드
- 메소드의 파라미터로 전달되는 객체가 가진 메소드
- 메소드가 생성하거나 인스턴스화 하는 객체의 메소드
- 멤버 변수의 메소드
- 예
public class Car { Engine engine; // other instance variables public Car() { // initialize engine, etc. } public void start(Key key) { Doors doors = new Doors(); boolean authorized = key.turns(); // 파라미터로 넘어온 객의 메소드는 호출해도 된다 if (authorized) { engine.start(); // 인스턴스 변수의 메소드는 호출해도 된다 updateDashboardDisplay(); // 객체 내의 다른 메소드는 호출해도 된다 doors.lock(); // 직접 생성했거나 인스턴스화한 객체의 메소드는 호출해도 된다 } } public void updateDashboardDisplay() { // update display } }
- 호출하지 말아야 하는 메소드
- 다른 메소드가 return 한 객체의 메소드
- 예
- 위의 원칙을 적용하지 않은 코드
public float getTemp() { Thermometer thermometer = station.getThermometer(); return thermometer.getTemperature(); }
- 원칙을 적용한 코드
public float getTemp() { return station.getTemperature(); }
-
이렇게 하면 우리가 의존하는 클래스의 개수를 줄여준다
놀랍게도 다음의 코드는 Principle of Least Knowledge 를 위반하지만
public House { WeatherStation station; // other methods and constructor public float getTemp() { return station.getThermometer().getTemperature(); } }
다음의 코드는 Principle of Least Knowledge 를 위반하지 않는다
```java public House { WeatherStation station; // other methods and constructor public float getTemp() { Thermometer thermometer = station.getThermometer(); return getTempHelper(thermometer); } public float getTempHelper(Thermometer thermometer) { return thermometer.getTemperature(); } } ```
- 참고자료
- Android 의 RecyclerViewAdapter 는 디자인 패턴에서 말하는 Adapter Pattern 을 사용한것인가
- https://stackoverflow.com/questions/41626980/are-android-adapters-an-example-of-adapter-design-pattern
- 상반되는 의견이 있지만 아니라고봄
- 애초에 목적과 구조가 다르다
- 우선 어댑터 패턴은 한 인터페이스(Adaptee)를 다른 인터페이스(Target Interface)로 변환하는 것이 목적이다
- Adapter Pattern 에서는 Adapter 내에서 Composition 을 통해 function call 을 delegate 하지만, 안드로이드에서는 그렇지 않다
- Android 의 RecyclerViewAdapter 는 디자인 패턴에서 말하는 Adapter Pattern 을 사용한것인가
Template Method 패턴
superclass 가 알고리즘을 정의하고, 단계별로 실행될 메소드 등 로직을 정해놓으면 subclass 에게 일부 로직의 구현만을 맡기는 패턴
- 프레임워크에서 많이 사용된다.
- 프레임워크는 개발자에게 일부 로직의 구현만 위임하고, 그 로직들이 언제 어떤 로직이 실행될지에 대한 흐름은 직접 제어하기 때문이다. (e.g. Android 의 Activity lifecycle callback)
- factory method 는 template method 의 특수한 형태다
-
예시
public abstract class CaffeineBeverage { final void prepareRecipe() { // Template Method boilWater(); brew(); pourInCup(); if(customerWantsCondiments()) { addCondiments(); } } abstract void brew(); abstract void addCondiments(); void boilWater() { // Concrete Method System.out.println("Boil water"); } void pourInCup() { // Concrete Method System.out.println("Pour in cup"); } boolean customerWantsCondiments() { // Hook return true; } }
-
일부 디테일만 다른 여러 비슷한 로직들을 추상화시켜 공통된부분만 분리시킨 뒤 캡슐화하여 template method 로 만들고 디테일은 subclass 에게 위임하면 중복 코드를 많이 줄일 수 있으며, 로직의 변경도 한 곳에서 관리할수있다.
- Template Method
- 알고리즘이 정의되어있는 메소드다
- Template Method 의 일부 Step 은 아직 정의되지 않아 subclass 가 무조건 구현해야만 한다.
- final 로 선언하여 알고리즘이 변경되지 않도록 한다
- Concrete Method
- Template Method 를 포함하는 클래스 내 이미 구현되어있는 메소드
- Subclass 에서 이를 호출하기도한다
- final 로 선언한다
- Hook
- Optional 한 step 을 정의 할 수 있다
- Subclass 가 꼭 구현하지 않아도된다
- 보통 아무 로직이 없거나 default 로직이 있다.
- boolean 을 반환하는 hook 을 만들어 template method 내의 if문에서 호출하면 조건부 실행제어가 가능하다
- 특정 Step 의 실행 직전/후에 subclass 가 react 할 수 있는 통로로 사용될 수 있다
- 사용 예
- JFrame 의 paint
- Applet 의 paint
- 항상 상속의 형태인것은 아니다
- Arrays.sort 는 설계자가 모든 클래스에 대해서 사용할 수 있게 하기 위해 static method 로 정의하였고, 자바의 배열은 상속이 불가능하다는 점으로인해 interface 를 사용했다
- 정렬되는 item 에게 비교 로직의 구현을 맡긴다
- Comparable 의 item 이 compareTo 만 구현하면 이를 호출하여 사용하는것은 sort 내에서 호출하는 mergeSort 내에서 한다.
public static void sort(Object[] a) { Object aux[] = (Object[]) a.clone(); mergeSort(aux, a, 0, a.length, 0); } private static void mergeSort(Object src[], Object dest[], int low, int high, int off) { for (int i = low; i < high; i++) { for (int j = i; j > low && ((Comparable) dest[j - 1]).compareTo((Comparable) dest[j]) > 0; j--) { swap(dest, j, j - 1); } } return; }
- Hollywood Principle
- Don’t call us, we’ll call you
- Template Method Pattern 이 사용하는 설계 원칙이다
- high-level 컴포넌트가 low-level 컴포넌트의 로직의 호출 타이밍을 정하고, low-level 컴포넌트는 단지 로직을 정의하기만 하는 원칙이다.
- 실생활 예시
- JFrame
public class MyFrame extends JFrame { public MyFrame(String title) { super(title); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setSize(300, 300); this.setVisible(true); } public void paint(Graphics graphics) { // Hook super.paint(graphics); String msg = “I rule !!”; graphics.drawString(msg, 100, 100); } public static void main(String[] args) { MyFrame myFrame = new MyFrame(“Head First Design Patterns”); } }
- Applets
public class MyApplet extends Applet { String message; public void init() { message = "Hello World, I’m alive !"; repaint(); // Concrete Method of Applet class } public void start() { message = "Now I’m starting up..."; repaint(); } public void stop() { message = "Oh, now I’m being stopped..."; repaint(); } public void destroy() { // applet is going away... } public void paint(Graphics g) { // Hook g.drawString(message, 5, 15); } }
- JFrame
Iterator 패턴 & Composite 패턴
-
Iterator Pattern
aggregate object(data 의 collection 을 들고있는 object) 의 세부 구현을 드러내지 않으면서 그것의 elements 들을 순차적으로 접근할 수 있는 방법을 제공하는 디자인 패턴
- SRP(Single Responsibility Principle)
- 하나의 클래스는 변경될 하나의 이유를 가져야한다
- Cohesion
- high cohesion : 하나의 클래스가 하나의 기능을 함
- low cohesion : 하나의 클래스가 서로 관련이 적은 여러 기능을 함
- 즉, SRP 를 잘 지키면 high cohesion, 안 지키면 low cohesion 이 됨
- iterator pattern 은 iteration 기능을 캡슐화하여 aggregate 으로 부터 분리함으로써 SRP 를 지킬 수 있게 한다.
- aggregate 은 data 들의 collection 을 가지고 있는 class 라고 보면된다
- 왜냐하면 분리하기 전에는 collection 의 세부 구현과 iteration 이라는 두가지 책임을 가지기 때문이다
- iteration 은 client 가 aggregate 의 세부 구현을 몰라도 되게 함으로써 둘 사이의 커플링을 낮춘다
- SRP(Single Responsibility Principle)
-
Composite pattern
객체들이 트리구조를 이루도록 하여 Client 가 Composites(내부노드)와 Individual Objects(리프노드)를 구분하지 않고 사용할 수 있도록 하는 디자인 패턴
- client 가 단 한번의 메소드 호출만으로도 모든 노드에 대해 특정 동작을 수행하게 함으로써 client 가 해야할 일을 단순하게 만들어 준다는 것이 가장 큰 장점이다
- Iterator pattern 과 함께 사용할 수 있다(재귀적으로 구현)
- internal iteration vs external iteration
- internal iteration 은 functional programming 에서처럼 client 가 collection 에 대해 operation 을 정해주면 내부적으로 iteration 을 수행하는것
- external iteration 은 client 가 직접 iteration 을 수행하는것
- Composite Pattern 에서 Iterator Pattern 을 같이 사용하여 CompositeIterator 를 만들면, 서브 트리를 순회하는 iterator 만 재귀적으로 사용하는 것이 아니라, Client 가 전체 노드를 순회하는 iterator 를 얻을 수 있다.
- 이를 활용한 예로, 전체 노드들 중 특정 조건에 해당하는 노드만 뽑아 내는 것이 가능하다
State 패턴
Object 의 Behavior 를 내부 state 의 변화에 따라 바꿀 수 있는 패턴
- 각각의 State 들을 class 로 구현한다
-
공통된 행위의 종류를 interface 로 빼고, concrete 클래스들이 이를 구현하도록 한다.
- 장점
- 여러개의 if 문을 사용하여 현재 state 에 따른 behavior 를 결정하는 것(procedural programming)이 아닌
- 각각의 state 를 class 로 만들고, 현재 state 를 composition 하는 방식으로 구현하여
- 로직에서 달라지는 부분인 각 state 의 behavior 들을 캡슐화할 수 있고
- Open-Closed Principal 을 잘 지키기 때문에
- 특정 state 의 로직이 변경되거나 새로운 state 가 추가되었을 때 유리하다.
- 단점
- 클래스의 수는 더 많아진다
- 특징
- State 클래스들은 여러 Context 들 사이에서 공유될 수 있다.
- Strategy 패턴 과의 차이점
- State 패턴은 Strategy 패턴과 클래스 다이어그램은 같다.
- Behavior 를 런타임에 동적으로 변경시킨 다는 점에서는 유사하다
- 그러나 의도가 다르다.
- Strategy pattern 은 behavior 의 변화를 context 가 컨트롤한다
- State pattern 은 내부적으로 정의된 state transition 에 따라 behavior 가 변경된다
Proxy 패턴
Proxy Pattern 은 어떤 객체로의 접근을 제어하기 위해 대리자(proxy)를 제공하는 패턴
- Proxy 는 특정 객체(Real Subject)가
- Remote machine 에서 실행되거나
- 생성하는데 비용이 많이 들거나
- 접근을 제한할 때(권한에 따라 접근을 허용할 때)
- 유용하다
- 여러가지 형태로 활용이 된다
- 대표적인 예
- 그외의 예
- Caching Proxy
- Firewall Proxy
- Smart Reference Proxy
- Synchronization Proxy
- Complexity Hiding Proxy(Facade Proxy)
- Copy-On-Write Proxy(Virtual Proxy 의 변형)
- 단점은 클래스의 수가 더 많아 질 수 있다
- 다른 Wrapper 형식의 패턴들과 마찬가지로.
- Client 가 Proxy 를 쓰게 하는 방법
- 흔한 방법중 하나는 Factory method 를 만들어서 기존의 subject 를 wrapping 하는 proxy 를 만들어서 반환
- Remote Proxy
- Client 와 Remote object 사이의 interaction 을 제어한다
- RMI(Remote Method Invocation) 에서 Remote Proxy 를 사용한다
- Stub(Proxy) 과 Skeleton 이 각각 client helper 와 server helper 의 역할을 하여 통신을 담당한다
- Virtual Proxy
- 인스턴스의 생성에 비용이 많이 드는 경우 사용
- 예를 들어, swing 에서 icon 가 network 상의 이미지를 띄울 때 사용
- Hibernate 는 지연 로딩을 구현하기 위해 프록시 객체를 사용(DB 에서 데이터를 가져오는 것은 비용이 많이 듬)
- 임시적으로 ‘image is being loaded…’ 같이 임시 메시지를 띄우고
- 서버에서 이미지를 가져오면, 그제서야 real subject 를 생성하는 식
- Protection Proxy
- Caller 에 기반하여 접근을 제어하는 프록시
- Java 의 Dynamic Proxy API 를 사용하여 구현
- 데이팅 앱을 예로 들면, 자신의 개인정보는 자신만 변경할 수 있어야 하고, 자신의 평가점수는 타인만 변경할 수 있어야 하기 때문에 접근 권한을 제어하기위해 Protection Proxy 가 필요
- Proxy.newProxyInstance() 로 런타임에 생성
- 런타임에 생성해서 Dynamic Proxy
- 사용법
- InvocationHandler 를 작성(java.lang.reflect.InvocationHandler 인터페이스를 구현)
- Real subject 를 생성자에 넣어 전 단계에 작성한 InvocationHandler 를 인스턴스화
- Proxy.newProxyInstance() 의 마지막 인자로 InvocationHandler 객체를 넣어주며 dynamic proxy 생성
- proxy 를 사용하여 원하는 메소드를 호출
- RMI 사용법
- java.rmi.Remote 를 상속받는 Remote interface 를 만들어 메서드를 정의한다
- 각 메서드는 RemoteException 을 throw 해야한다
- 이 interface 를 구현하고 UnicastRemoteObject 를 상속받는 concrete class 를 만든다
- 서버측에서는 Naming.rebind() 를 통해 RMI Registry 에 service 에 해당하는 stub 이 등록되게 한다
- 클라측에서는 Naming.lookup() 을 통해 입력한 서버의 RMI Registry 에 접근하여 stub 을 받아온다
- 이 때 앞서 정의한 Remote interface 타입으로 변환하여 사용한다
- 이 때 stub 이 Remote proxy 의 역할을 한다
- Java 5 부터는 rmic 로 stub 만들 필요 없이 Dynamic Proxy 를 사용하면 동적으로 생성된다.
Proxy 는 Real Subject 의 대역이다 client 는 Proxy 를 통해 Real Subject 에 접근한다 Subject 라는 동일한 인터페이스를 구현하기 때문에 Proxy 를 사용해서 Real Subject 에 접근할 수 있다 Proxy 는 가끔 Real Subject 의 생성과 소멸을 담당한다
- Proxy 패턴과 Decorator 패턴
- Object 를 wrapping 하여 function call 을 delegate 한다는 점에서 비슷
- 하지만 둘의 목적이 다름
- Decorator 패턴은 이름에서도 알 수 있듯이 기능을 추가하는것이 목표
- Proxy 패턴은 access 를 control 하는 것이 목표
- Decorator 패턴은 단순히 다른 object 를 wrapping 하지만
- Proxy 패턴은 사실상 wrapping 이 아닌경우가 있음
- Virtual Proxy 의 경우 proxy 를 처음 사용하는 시점에는 real subject 가 아예 존재하지 않기도 함
- Remote Proxy 의 경우 object 가 local 이 아닌 remote machine 에 존재함
Compound 패턴
Compound 를 사전에서 찾아보면 복합체, 혼합물 등의 뜻이 나온다. 그렇다면 Compound 패턴은 무엇일까?
-
Compound 패턴이란?
MVC 패턴의 상호작용
컴파운드(Compound) 패턴은 이름 그대로 여러 디자인 패턴이 혼합된 디자인 패턴을 말한다. 하지만 단순히 여러 패턴이 사용되었다고 해서 컴파운드 패턴인 것은 아니다. 여러 패턴이 사용되는 동시에 일반적인 문제를 해결하는데 반복적으로 사용될 수 있어야 한다. Compound Pattern 의 대표적인 예가 바로 그 유명한 MVC Pattern 이다.
-
MVC 패턴이란?
MVC 는 Model-View-Controller 의 약자로서, 역할에 따라 3개의 컴포넌트로 분리하고 여러 디자인 패턴을 적용하여 재사용성을 높인 대표적인 컴파운드 패턴의 예다. 그렇다면 MVC 패턴에서 사용된다는 여러 디자인 패턴은 대체 무엇일까? 전통적인 MVC 패턴에서는 다음 3가지 패턴이 사용된다.
- 옵저버(Observer) 패턴
- Model 의 상태가 변경 되었을 때 Controller, 혹은 View 에게 이 사실을 알리는데 사용된다.
- 컴포지트(Composite) 패턴
- View 를 구성하는 컴포넌트들은 계층 구조를 이룬다. (e.g. Java Swing 의 JFrame/JLabel 등, Android 의 View/ViewGroup, HTML 의 DOM)
- 스트래티지(Strategy) 패턴
- Controller 의 핵심 기능을 인터페이스로 분리하여 View 가 이 인터페이스를 통해 Controller 를 구성(Composition) 한다. 그렇기 때문에 View 는 Controller 를 갈아 끼우며 기능을 변경할 수 있다.
또한, 필요에 따라 어댑터(Adapter) 패턴 을 함께 사용할 수도 있다.
MVC 패턴은 사용되는 곳에 따라(모바일, 웹 등) 여러 프레임워크에서 다양한 형태로 변형되어 적용되곤 한다.
하지만 이번 섹션에서는 변형되지 않은 전통적인 MVC 패턴에 대해 알아보고, 이어지는 섹션에서 웹 버전의 MVC 인 JSP Model 2 에 대해 알아본다.
그렇다면 전통적인 MVC 의 3가지 컴포넌트에 대해 알아보자.
-
모델(Model)
Model 은 애플리케이션의 핵심 로직과 데이터를 가지고 있는 컴포넌트다.
Controller 가 Model 에게 상태 변경을 요청하면 Model 은 일련의 과정을 거쳐 자신의 상태를 변경하게 되고
상태 변경이 완료되면 이를 Controller 와 View 에게 알린다.
그런데 Model 의 한가지 큰 특징은 Controller 와 View 에 대해 알지 못한다는 것이다.
이게 가능한 이유는 위에서 언급한 것처럼 Observer 패턴을 사용하기 때문인데
조금 설명을 하자면, Controller 와 View 는 Observer 라는 인터페이스를 구현하고
이 타입을 통해 Model 을 subscribe 하므로 Model 입장에서 이들은
단순히 모두 똑같은 Observer 일 뿐이지 각 Observer 가 실제로 어떤 객체인지는 알 필요가 없다.
클래스 다이어그램을 그려보면 다음과 같다
MVC 패턴의 클래스 다이어그램
이를 통해 Controller 와 View 에 대한 Model 의 결합(Coupling)을 느슨하게 하고, 이들의 재사용성을 높이게 된다.
참고로, 전혀 다른 비즈니스 로직을 가지는 Model 에 Adapter 패턴을 활용하여 기존의 View 와 Controller 를 그대로 재사용할 수도 있다.
-
뷰(View)
View 는 사용자와의 상호작용을 담당하며, 크게 2가지 역할을 수행한다.
첫째는 사용자에게 화면을 보여주는 것이다. GUI 환경이라면 버튼이나 체크박스 등이 될 수 있고, CLI 환경이라면 텍스트가 될 것이다.
Model 로 부터 자신의 상태가 변경 되었다는 알림을 받을 때마다 Model 에게 데이터를 받아, 이를 기반으로 화면을 업데이트한다.
Model 의 데이터를 기반으로 변경하는 것 뿐만 아니라, 단순히 Controller 요청에 따라 화면을 변경하기도 한다.
둘째는 사용자의 입력 이벤트를 받는 것이다. 정확히는 사용자의 이벤트를 받아 Controller 에게 전달한다.
-
컨트롤러(Controller)
Controller 는 View 와 Model 사이의 중재자이다.
Controller 는 View 로 부터 사용자의 입력 이벤트를 받으면 다시 View 에게 화면 업데이트를 요청할 수도 있고
Model 에게 상태 변경을 요청할 수도 있다. 위에서 말한 것처럼 Model 은 상태 변경이 완료되면 Observer 패턴을 통해 Controller 와 View 에게 이를 알린다.
- 옵저버(Observer) 패턴
-
JSP Model 2 란?
JSP Model 2의 구조
JSP Model 2 란 MVC 패턴을 웹 애플리케이션에 맞는 형태로 적용시킨 것이다.
즉, Spring 과 같은 Web Framework 에서 사용하는 MVC 패턴은 JSP 를 사용하지 않는다고 하더라도
사실상 전통적인 MVC 패턴 보다는 이 JSP Model 2 에 해당한다고 할 수 있다.
(좀 더 구체적으로 말하자면, Spring MVC 에서 사용하는 패턴은 Servlet(DispatcherServlet)에서 HTTP 요청을 처리하는 것을 제외한 Controller 로직을 분리한 구조로, Front Controller 패턴 이라고 부른다.)
사실 JSP Model 1 도 있는데 Model 1 에서는 View 와 Controller 의 역할이 분리되지 않고 하나의 컴포넌트에서 담당하는 모양을 하고있다.
Model 2 의 등장으로 웹 애플리케이션 개발 단위는 View 에 해당하는 JSP pages 와 Controller 에 해당하는 Servlet 이 완벽하게 분리되었기 때문에 HTML 과 약간의 JSP 에 대한 지식을 가진 웹 퍼블리셔와 전문적인 소프트웨어 지식을 가진 개발자의 역할을 분리하여 생산성을 높이는데 기여했다.
그럼 Model 2 에서는 MVC 의 각각의 컴포넌트가 전통적인 MVC 와 어떻게 다른지 알아보자
-
Java Bean (Model)
Model 2 에서의 Model 이 전통적인 MVC 에서의 Model 과 가장 큰 차이점은
Model 2 에서는 View 로 부터 사용자의 입력 이벤트가 들어올 때, 네트워크(HTTP)를 통해 들어온다는 것이다.
그렇기 때문에 Model 의 상태 변경이 완료되었을 때 View 에게 이를 알리지 않는다.
단지 Controller 의 요청이 있을 때만 Model 이 Java Bean 으로서 Controller 를 통해 View 에게 전달될 뿐이다.
-
JSP (View)
Controller 를 통해 Model 로 부터 전달받은 Java Bean 내 데이터를 기반으로 화면을 구성한다.
-
Servlet (Controller)
HTTP 요청에 따라 Model 에 상태 변경을 요청하고 상태가 변경된 Model 을 View 에 전달한다.
HTTPServlet 클래스를 상속받아 doGet(), doPost(), doPut(), doDelete() 등의 메소드를 Override 하여
HTTP 요청을 받으며, 메소드 인자로 전달된 HttpServletRequest 객체를 사용하여 요청과 함께 전달된 데이터를 읽어들인다.
-
덜 쓰이지만 유명한 패턴 9가지
Bridge 패턴
Builder 패턴
Chain of Responsibility 패턴
Flyweight 패턴
Interpreter 패턴
Mediator 패턴
Memento 패턴
Prototype 패턴
Visitor 패턴
이 책의 좋은점
어떤 설계가 좋고, 왜 좋은지 설명해준다. 또한 비슷한 디자인 패턴들을 비교하며 공통점과 차이점에 대해 명확히하며 자칫하면 헷갈릴 수도 있고, 의문이 드는 점들을 잘 해결해준다.