Design Pattern 배우기
Design Pattern은 Gang of Four (GoF: 중국 문화 혁명의 무소불위 권력자 사인방에서 따온 것이라 한다)가 1995년에 발표한 책[1]으로 부터 소개 되어 유명해 졌다. 이 책이 고전인 것이 30년을 넘어서도 계속해서 사람들이 읽고 배우고 있다. 나도 몇번의 시도를 하였지만, 업무를 하면서는 배우지 못했다. 결국은 강의를 들으면서 어느 정도 감은 잡았다고 할 수 있다. 하지만, 이것은 몸에 익히기 까지 해야 하는 것이라 돌아 보고 또 돌아봐야 하는 것이라고 생각된다.
강의를 들을 때, 교제로 사용한 책이 GoF의 책이 아니였다. 바로, Head First Design Pattern이었다. [2] 이 책은 주요한 Design Pattern을 Story와 연결하여 예를 들어 잘 설명하고 있다. 또한 이 예와 연결해서 Java Code도 제공하고 있다. 예를 들자면, Strategy는 Duck 예제, Observer는 기상 모니터링, Decorator는 커피 가게, Factory는 Pizza 가게, Command는 리모콘, State는 Gumball Machine 등 Pattern을 떠올리면 그 예제의 이이기와 요구 사항들이 떠오른다.
즉, 접근법이 쉽고 예를 잘 설명하고 있다. 그렇기 때문에, Design Pattern을를 처음 접하는 사람에게 추천한다. 하지만 여기서 만족하고 멈춰서면 안된다. GoF의 Design Pattern을 사전 처럼 참고하는 것이 좋다. Head First를 읽어 보고 해당하는 내용을 GoF 책을 보면, 잘 정리되어 있기 때문에 또 다른 것을 배우게 된다. 즉, Head First로 친숙해 지고, GoF의 Design Pattern으로 정리하는 방식이다. 그래서도, 쉽지 않으므로, 혼자 공부하기 보다는 Study 혹은 강의를 먼저 듣기를 권한다. Internet에서 아직 좋은 강의를 아직 보지 못해서 안타까울 뿐이다.
여기서는, Design Pattern을 하나 하나 살펴 보기 보다는 그 뒤에 깔려 있는 Object-Oriented 설계의 Principles들을 살펴 본다.
"The critical design tool for software development is a mind well educated in design principles. It is not the UML or any other technology."
- Craig Larman
이는 Craig Larman의 조언에 따라 내가 본 책들에서 주요하게 강조하는 원칙들을 정리해 본다. 위의 말 처럼 Design Pattern을 이해하기 이전에 Object Oriented(OO) Principle을 바탕으로 Design Pattern을 이해하면, 왜 그렇게 구조를 가지는에 대해서 잘 이해할 수 있다 생각하기 때문이다.
GRASP Design Principles
GRASP은 General Responsibility Assignment Software Pattern의 약자이다.[3] 이 Concept은 Design Pattern의 것은 아니지만, 기본적인 OO Principles이므로 살펴 본다. 아래는 GRASP의 Summary 내용이다. 요약을 읽어 보면 바로 알수 있지만, Class의 Responsibility를 어떻게 할당하는가에 관한 원칙이다.
"After identifying your requirements and creating a domain model, then add methods to the appropriate classes, and define the messaging between the objects to fulfill the requirements"
- Craig Larman
위와같이, OO Design할 때 method를 class에 할당하고 다른 Class들과 messaging을 통해서 연계 시키는 것이 설계이다. 거꾸로 이미 구현된 시스템이 이러한 원칙들을 잘 따르고 있는지 살펴 보는 것도 중요할 수 있다.
내용 측면에서는 우선적으로는 Information Expert, Creator와 Controller까지는 살펴 본다. 그리고, Loose Coupling, High Cohesion 또한 기본적인 개념이므로 따로 논의 하고 넘어 가보자. 나머지는 아래 표를 참고하자.
Information Expert
Information Expert는 결국 Responsibility를 그 정보를 필요로 하는 class에 할당한다는 것이다. 아래의 예는 [2]에 있는 Information Expert의 예이다. Board 게임 예제 인데, 아래 그림과 같이 Board에 주사위 정보(Square)를 제공(getSquare)한다. Board는 Square의 정보(Information)을 가지고 사용하는 Object로서 Expert가 된다. 즉, Board가 Square object를 Aggregate하게 되면서, Responsibility를 가지는 Object가 되는 것이다.
Creator
Creator는 어떤 Object를 생성하는 Class가 Responsibility를 가진다는 것이다. 원칙으로 보면, B class가 A class에 대해서 아래 중 한가지 혹은 그 이상에 해당되면 적용 되는 것으로 볼 수 있다.
- B "contains" or compositely aggregates A
- B records A
- B closely uses A
- B has the initializing data for A
Controller
"UI 계층을 넘어서 시스템의 동작의 조정(Control)을 수신하고 조정하는 하는 첫번째 Object가 무엇인가?"라는 질문에 대한 솔루션을 제공하는 원칙이다. 시스템을 대표하는 시스템 이름을 가진 Object혹은 Root Object을 두는 것이다. 또는 Use Case Scenario별로 해당되는 Object(Use Case Controller 혹은 Session Controller)를 두는 것이다.
아래 그림은 [2]에 있는 예제이다. Monopoly Game에서 UI Layer인 Monopoly JFrame 이후에 시스템을 대표하는 Object를 두는데, 이 것이 Controller인 MonopolyGame Class가 된다.
Loose Coupling & High Cohesion
Loose Coupling은 Object들 간의 Dependency를 낮게 만드는 쪽으로 Responsibility를 할당하라는 원칙이다. 아래 그림을 보면, Register가 Payment가 Create를 하고 직접 쓰지 않고 Sale에 할당하는 구조이다. 이는 Register가 Sale과 Payment에 모두 관계하는 구조를 가지고 있다. 그러므로 Dependency가 높다. 이를 아래 그림 처럼 Register는 Sale만 관여하고 Sale이 Payment를 관여하게 바꾸면 Dependency가 줄어 들게 된다.
Cohesion은 응집도로 해석된다. Functional Cohesion이라고도 한다. Element가 Responsibility에 대해서 얼마나 강하게 관련되고 집중되는지의 척도로 정의 된다. 위의 예에서 Coupling을 낮추는 동작 중에서, Sale에서 Payment를 생성하는 동작이 결국은 Cohesion을 높이는 역할까지 수행한다고 볼 수 있다.
그래소, Loose Coupling과 High Cohesion은 일반적으로 같이 움직이는 경향이 있다. Loose Coupling의 최대치는 아무 Dependency를 가지지 않는 것이다. 하지만, 이것은 비현실적이다. 즉, 하나의 Dependency만 가지는 것이 가장 좋을 것이다. Cohesion은 관련이 있으면 포함되게 하는 것이 좋다. 하지만, 여기 저기 관련되는 Coupling은 나빠지니 그 부분도 또한 조심해야 한다.
SOLID Design Principles
SOLID는 아래 원칙들의 앞글자를 따서 만든 원칙이다[4]. 상세 내용을 살펴 보면 알게 되지만, 결국 Software가 계속 바뀐다는 가정하에 이러한 원칙들이 적용된다. 즉, 변경에 대해서 원칙들이 지켜져야 하고 이것들 때문에 Design Pattern들이 나오게 된다.
- Single Responsibility Principle (SRP)
- Open-Close Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
하나씩 살펴 본다.
Single Responsibility Principle (SRP)
하나의 Class의 수정해야 하는 이유가 단 한가지여야 한다는 것이다. 그래서, Responsibility를 고쳐야 하는 이유로 정의하기도 한다. Class가 한가지 역할 이상을 하게 되면 변경되어야 하는 이유가 늘기 때문에 좋지 않다는 것이다. 만일 여러 Responsibility가 있다면 분리된 Class가 되어야 한다는 것이 이 원칙이다.
그렇다면, Responsibility를 어떻게 구분할까? 이 부분도 Software가 변경되어야 할 때, 드러나기 쉽다. 중요한 것은 이 원칙이 있다하더라도 꼭 쪼개어야 하는지는 고민해야 하는 부분이다.
Open-Close Principle (OCP)
이 원칙은 확장에는 열려 있고(Open to Extension), 변경에는 닫혀 있다는(Close to Modification) 원칙이다. 이상하게 들릴 수 있다. 하지만, 이원칙은 변경을 Localization하는 원칙이다. 다른 Pattern에도 많이 적용되지만, Strategy Pattern과 Template Pattern이 이를 가장 잘 활용하는 Pattern이다.
좀 더 다르게 이야기 하면, 변경되어야 하는 부분을 추상화(Abstraction)으로 분리하여 다른 Class들은 영향을 받지 않도록 하고, 계속 변경되는 부분은 이를 기반으로 새롭게 구현하도록 하여 확장이 가능하게 하는 원칙이다.
아래 그림에서는 Strategy Pattern과 Template Pattern을 적용한 OCP 원칙을 보여 준다. Client-Server 구조에서 Server의 변경에 Client가 영향을 받지 않게 하기 위해서 연동하는 부분을 추상화(여기서는 Interface 사용)하였다. 그리고, 이를 통해서 Server를 연동하게 하였다. Template Pattern의 예는 Policy를 추상화화 하여 Implementation들이 변경하더라도 서로 영향을 받지 않도록 분리하는 역할을 한다.
Liskov Substitution Principle (LSP)
LisKov Substitution Principle은 Java 혹은 C++ 언어를 배웠다면 어렵지 않게 이해할 수 있다. 즉, Super Class의 타입을 통해서 Derived Class를 대체 가능해야 한다는 원칙이다.
위의 코드와 같이 구현되어 있는 경우는 이 원칙을 잘 따른다고 볼 수 있다. 하지만, 만약에 함수 f()가 Ctype을 받았을 경우 오동작하게 되면 원칙을 위배하게 되는 것이다. 또한, f()가 Ctype의 내용을 가지고 내용을 접근한다고 하면 Open-Close Principle도 위배하게 된다.
Interface Segregation Principle
Client는 사용하지 않는 method들에 강제로 의존적이지 않아야 한다는 원칙이다. 여기서는 Fat interface를 말하는데, 결국 사용하지 않는 method도 포함하는 interface를 말한다. 이러한 경우는 이를 분리하는 것이 좋다.
[4]에서는 Security Door에 Timer를 연계하는 예를 보여 준다. 기존 문에 Timer를 장착하고 일정 시간이 지나면 알람을 울리게 하는 문이다. 특별히 고민 없이 한다면, Door가 Timer를 상속하게 하는 방법이 있을 수 있다. 하지만, 모든 Door가 Timer를 가지는 것이 아니기 때문에 부적절하다. 이를 오른쪽과 같이 DoorTimer를 Aggregate하게 하거나 아니면, 다중 상속을 하게 하는 방법도 가능하다. Java에서는 다중 상송이 안되므로, Interface를 가지게 하면 된다.
Dependency Inversion Principle(DIP)
"Don't call us, we'll call you"라고 이야기 하면서, 에이전트를 찾아 가는 것이 아니라, 찾아 오게 만들어야 한다는 의미에서 Hollywood Principle이라고도 불린다. 예로 든 것이, Layered 구조에서 상위 Level이 하위 Level에 Dependency를 가지는 구조이다. 아래 그림은 Policy -> Mechanism -> Utility 방향으로 Dependency를 가지는 원래 구조에 DIP를 적용한 모습이다. 하위 구조의 Dependency를 interface로 추출하여 상위 Level에 두고 하위 Level이 이를 상속하게 하여 Dependency를 Inversion시켰다. 이렇게 함으로써, 상위 Level은 하위 Level의 변화에도 영향을 받지 않게 된다. 사족이지만, 하위 Level은 상위 Level의 변화에 영향을 받지 않았다.
OO Basic and OO Principles
Head First에 나오는 기본 원칙들이다. 책에서도 이것 후에 Design Pattern들이 설명된다. 여기서는 Pattern에 가기 전에 기본이되는 2가지 기본과 원칙에 대해서 살펴 본다. 이미 나온 것들과 연결되는 것이 많아서 간단히 언급하고 연결해 본다.
OO Basics
Head First Design Pattern에서 이야기 하는 Object Oriented Basics는 "Abstraction, Encapsulation, Polymorphism, Inheritance" 이다.
Abstraction (추상화): 이 부분은 OO Paradigm에서 Object를 표시하기 위해서는 Object의 data와 이를 다루는 method로 표시하는데 이것이 추상화라고 할 수 있다.
Encapsulation(캡슐화): 크게 이야기 하면, 추상화에서 이야기한 Data와 Method를 하나로 묶어 주는 것이 캡슐화라고 할 수 있다. 좀 더 좁게 이야기 하면, Class의 외부로 보이는 부분을 제한하고 내부의 상세 구현은 감추는 것을 말할 때도 있다. 이 때에는 정보 은닉(Information Hiding)이라고 표현하기도 한다.
Polymorphism(다형성): 같은 이름으로 여러 것들을 Refer할 수 있게 해주는 개념이다. Overloading과 Overriding이 있다. Overloading은 Static한 Polymorphism으로 Compile할 때 결전된다. Class내에서 같은 이름를 가지지만, 인자가 다른 경우이다. Overriding은 상속 과계에서 Runtime에 Dynamic하게 매핑이 되는 Polymorphism을 이야기 한다.
OO Principles
Head First Design Pattern에서는 개별 Pattern을 설명하면서, 여러 Principle들을 설명하고 있다. 이미 설명한 GRASP이나 SOLID 혹은 OO Basics를 기반으로 한 것이다.
Encapsulate what varies: 캡슐화는 같이 묶어 준다는 의미이다. 즉, 변화하는 것과 변화 하지 않은 것을 분리해서 묶어 준다고 이해할 수 있다.
Favor composition over inheritance: 상속은 비용이 비싸다. 이를 Aggregation 혹은 Composition으로 가능한 것인지 정말 Inheritance를 채용할 것인지 고민해봐야한다.
Program to interfaces, not implementations: Concrete한 구현을 보고 프로그램하지 말고, 추상화 된 Interface를 보고 하는 것이 Dependency를 줄이는 방법이다.
Strive for loosely coupled designs between objects that interact: 연동하는 object들 간의 느슨하게 연결된(coupled) 설계를 사용하라고 번역할 수 있겠다. 이는 연동에 최소한의 interaction을 가지는 구조로 설계하라는 GRASP의 Loose Coupling과 일맥상통한다.
Classes should be open for extension but closed for modification: SOLID의 OCP와 일맥상통한다.
Depend on abstraction. Do not depend on concrete classes: LSP와 일맥 상통한다.
Only talk to your friends: Don't talk to stranger라고도 한다. 다른 Object와 Dependency를 최소화 하는 의미로 생각할 수 있다.
Don't call us, we'll call you: SOLID의 DIP와 일맥상통한다.
A class should have only one reason to change: SOLID의 SRP와 일맥 상통한다.
돌아보며: Design Pattern
Design Pattern의 Principle들을 돌아 보았다. 결국은 Object Oriented Principle들을 돌아보았다. 결국은 Design Pattern도 Object-Oriented Design의 반복되는 문제에대한 솔루션을 정리해 놓은 것이므로 일맥 상통한다. 배경이 되는 원칙들을 잘 이해하면 이 Pattern들도 잘 이해할 수 있다.
정말로 중요한 것은 이 원칙들을 아는 것이 아니다. 결국은 Software에 변화되는 요구 사항에 Software를 관리하기 편하고 유연한 구조를 가지게 하는 적절한 원칙과 그를 기반으로 하는 Design Pattern을 적용하는 것이 중요하다.
나중에 기회가 되면, Design Pattern들도 하나 둘 씩 정리해 볼 생각이다. 실재로 여기에는 이러한 Pattern을 써야겠네, 혹은 다른 사람의 코드에서 이런 패턴을 썼네 판단되어 참고가 필요하면 책을 꺼낼 수도 있지만, Reference Sheet로 잘 정리된 것이 있어 계속 참고하고 있다.[5] 요즘도 계속해서 이를 참조하게 된다.
References
[1] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, "Design Patterns: Elements of Reusable Object-Oriented Software," Adisson-Wesley
[2] Eric Freeman and Elsabeth Freeman, "Head First Design Patterns," OReilly
[3] Craig Larman, "Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development," Third Edition.
[4] Robert C. Martin, "Agile Software Development, Principles, Patterns, and Practices," Pearson
[5] Design Pattern Quick Reference: http://www.mcdonaldland.info/2007/11/28/40/
'Object-Oriented Design' 카테고리의 다른 글
UML 상세 설계: University 예제 (0) | 2019.06.23 |
---|---|
Design Pattern: Factory Method (0) | 2018.09.02 |
Refactoring: Bad Smell & Techniques (0) | 2018.08.26 |
UML 두 번째: Sequence Diagram (0) | 2018.08.25 |
UML 첫 번째: Class Diagram 요약 (0) | 2018.07.14 |