Contents
1. 배경
2. 개방-폐쇄 원칙(Open-closed principle, OCP)
3. 단일 책임 원칙(Single Responsibility principle, SRP)
4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
5. 리스코프 원칙(Liskov Substitution Principle, LSP)
6. 의존 관계 역전의 원칙(Dependency Inversion Principle, DIP)
1. 배경
소프트웨어 설계에 있어 객체와 객체간의 유기적 관계를 효과적으로 구성하는 것은 유지보수 및 관리에 있어 상당히 중요하다. 하지만, 요구사항 변경과 그로 인한 의존성 관리가 불가피해 짐에 따라 설계가 다음의 네 가지 증상을 보이며 무너지는 것을 볼 수 가 있다.
- Rigidity (연쇄적 변경으로 인해 변경자체를 허용 하지 않음)
- Fragility (변경으로 인해 다발적으로 설계가 붕괴)
- Immobility (재사용을 하려고 하였을 때, 필요 없는 부분이 너무 커 코드를 버리고 재작성)
- Viscosity (여러 변경 방법 중 현재 설계방법을 유지시켜주는 변경 방법을 찾기가 쉽지 않음)
이런 증상을 원초적으로 해결하기 위해 설계에 대한 원칙과 패턴을 만들 필요가 있게 되었고, 다음 Section 부터 그에 대해 자세하게 설명 할 것이다.
2. 개방-폐쇄 원칙(Open-closed principle, OCP)
소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수)는 확장(Extension) 에 대해서는 개방되어야 하지만,
변경(Change) 에 대해서는 폐쇄되어야 한다.
Dynamic polymorphism
① 변하는(확장되는) 것과 변하지 않는(폐쇄 되어야 하는 것)을 엄격히 구분한다.
② 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.
인터페이스는 서비스 내용을 추상화 하는 형태로 제공하므로 적당한 추상화 level 선택이 중요하다.
Static polymorphism
- Templates 나 generics 이용
[그림1] 읽는 대상의 타입이 확장, 변경될수록 복잡도는 증가한다
[그림2] 다형성을 이용한 호출로 인해 상속 관계에서 자식 클래스의 실제 타입에 상관없이 상위 클래스를 통해 충분히 읽고 싶은 것을 읽을 수 있다.
사례) GCC 컴파일러, 미들웨어, 커맨드 패턴
- GCC 컴파일러
[그림3] 컴파일러 OCP 계층
- 미들웨어
Client/Sever에서 미들웨어로의 전환은 변하지 않는 서비스를 독립시키려는 이유에서이다. 트랜잭션이나 분산 시스템, 메시징 서비스 같은 미들웨어적인 기능은 비대해지는 서버와 클라이언트 모두에게 부담이 됐다. 또한 이 모듈의 목적도 어느 정도 정리되어 있어 변하지 않는 모듈로도 손색이 없다. 따라서 자련스럽게 closed의 영역으로 분리됐다.
하지만, 미들웨어도 완결된 시스템이 아니기 때문에 분리되는 과정에서 미들웨어 인터페이스를 만들게 된다. 즉 미들웨어 자체의 버전 업그레이드나 내부 모듈의 확장 여지를 남기기 위해 (변하지 않아서 분리됐음에도 불구하고) 자신의 인터페이스를 클라이언트와 서버에 제공한다.
여기서 레이어 시스템의 양방향 OCP 전략을 볼 수 있는데, 한 레이어의 인터페이스는 관계하는 양쪽을 위해 두 개의 인터페이스로 정의하게 된다. 가령 OSI 레이어에서 각 레이어는 위, 아래를 위한 두 개의 인터페이스를 갖는다. 이로써 그 레이어 내부의 확장 및 변경으로부터 외부에 전달되는 충격을 무력화한다. 마치 이더넷 카드를 다른 제품으로 바꾼다 하더라도 그 이더넷 카드는 하위 망 계층과 상위 드라이버 계층에 동일한 인터페이스를 준수하기 때문에 동작하는 데 아무 문제가 생기지 않는다.
- 커맨드 패턴
텍스트 에디터 Copy 란 명령은 여러 방법으로 실행할 수 있다. 아이콘, 메뉴에서 선택, 오른쪽 마우스, 단축키 등을 통해 사용할 수 있다. 하지만 명령을 요청하는 방법이 다를 뿐이지 처리하는 핸들러는 하나다. 이렇게 요청자와 처리자의 관계가 복잡해지는 객체 관계에서 그 처리를 단순화하기 위해 커맨드 패턴을 사용한다. 즉 명령을 실행하는 방식이 어떤 방법이든지 Copy 기능을 구현한 (execute() 메쏘드로) CopyCommand라는 객체를 생성하여 처리자에 전달하면 처리자는 CopyCommand가 어떤 작업을 하는지 알 필요 없이 execute()를 실행하여 객체 관계를 단순화할 수 있다. Command 객체는 실제 작업과 정보를 캡슐화하여 처리자에 전달된다. 따라서 복잡도는 낮아진다. 이렇게 처리하는 방식은 스트럿츠에서도 적용되는데, 스트럿츠 Action 클래스는 커맨드 패턴의 좋은 사례가 된다. 커맨드 패턴은 이렇게 호출을 요청하는 요청자와 그 요청을 처리하는 처리자 간에 Command 라는 실제 처리 로직을 캡슐화한 객체를 통해 의존성을 분리하는 패턴이다.
[그림4] 커맨드 패턴
3. 단일 책임 원칙(Single Responsibility Principle, SRP)
하나의 클래스는 하나의 책임만을 가져야 한다.
'변경'의 거북함을 조장하는 요소는 서로 다른 '책임' (= Concern) 이 혼재해 있다는 데 있다.
책임이란 '변경을 위한 이유' 이다. 만약 하나의 클래스에 변경을 위한 두 가지 이상의 이유가 있다면 그 클래스는 한 가지 이상의 책임을 갖고 있는 것이다.
이 원칙을 위반할 경우 Bad smell 이 생길 수 있음 즉,
첫째, 소외되는 메소드가 존재한다.
둘째, 무관한 메소드에 변경이 발생할 경우 불필요한 변경 임팩트가 전달된다. (컴파일, 테스트, 재배포)
* SRP 위반의 bad smell
① 여러 원인에 의한 변경(Divergent change) - Tangling in aspect-oriented programming
한 클래스를 여러 가자 디른 이유로 고칠 필요가 있을 때 발생한다. 즉, 하나의 클래스에 여러 책임이 혼재하고 있어서 하나의 책임의 변화가 다른 책임에 영향을 준다. 해결 리팩토링 방법
- Extract Class는 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 하는 것이다. 액티브
오브젝트 패턴에서 데이터 맵퍼 패턴으로의 진화가 대표적인 사례가 된다. 여기서 관건은 책임만 분리하는 것이 아니라 분리
된 두 클래스간의 관계의 복잡도를 줄이도록 설계하는 것이다.
- 만약 Extract Class 된 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 갖고 있다면 Extract Superclass를 사용할 수
있다. Extract Class 된 각각의 클래스들의 공유되는 요소를 부모 클래스로 정의하여 부모 클래스에 위임하는 기법이다. 따
라서 각각의 Extract Class들의 유사한 책임들은 부모에게 명백히 위임하고 다른 책임들은 각자에게 정의할 수 있다.
② 산탄총 수술(Shotgun surgery) - Scattering in aspect-oriented programming
'여러 원인에 의한 변경'과 상반된 의미를 가지며 변경이 있을 때 여러 클래스를 수정해야 하는 증상이다. 수정을 했음에도 불구하고 확인하지 못한 부분이 존재 할 수 있다는 가능성이 산탄총 수술의 위험요소가 된다. 산발적으로 흩뿌려져 있는 정보들을 한군데로 모아 응집성을 높이는 것이 중요하다.
사례) Enterprise Patterns - 액티브 오브젝트 패턴, 데이터 맵퍼 패턴(DAO 패턴), 식별자 맵 패턴
- 액티브 오브젝트 패턴 : 하나의 매소드에서 두 가지 책임을 분리시켰을 뿐이지, 하나의 클래스에서 두 가지 책임을 분리시키지는 않는다.
- 데이터 맵퍼 패턴(DAO:Data Access Object) : 마이크로소프트에서 4GL 언어 아키텍처 작업 당시 객체 단위 DB 접근 인터페이스로 제인한 DB 접근 객체 인터페이스. 클래스에 속해있는 다른 한 책임을 데이터 맵퍼 클래스로 분리시킴으로써 액티브 오브젝트 패턴의 단점을 극복.
- 식별자 맵 패턴 : 식별자 맵을 등록하여 한번 load된 객체는 두 번째 load 요청부터 식별자 맵에서 가져온다. (식별자 맵 : DB를 통해 얻어온 객체를 캐시하는 맵)
4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
일반적인 단일 인터페이스보다는, 구체적인 다수의 인터페이스가 낫다
SRP가 클래스 분리를 통해 변화에의 적응성을 획득하는 반면, ISP에서는 인터페이스 분리를 통해 같은 목표에 도달한다. 만약 어떤 클래스를 이용하는 클라이언트가 여러 개 있고, 이들이 해당 클래스의 특정 부분집합만을 이용한다면 이들을 따로 인터페이스로 빼내어 클라이언트가 기대하는 메시지만을 전달할 수 있도록 하는 것이다.
인터페이스 분리 원칙 (Adapter 패턴 - Class Adapter, Object Adapter)
첫째, 미리 구현된 클라이언트의 변경을 주지 말아야 한다.
둘째, 두 개 이상의 인터페이스가 공유하는 부분은 재사용을 극대화 해야 한다.
셋째, 서로 다른 성격의 인터페이스를 명백히 분리해야 한다.
[그림5] ISP를 적용하기 전 모습 - 하나의 인터페이를 여러 클라이언트에서 부분적으로 사용하고 있다
[그림 6] ISP를 적용하고 난 후 모습 - 각 클라이언트에 필요한 기능만 인터페이스로 뽑아내어 사용하고 있다
자바 스윙이 제공하는 클래스. JTable에는 많은 역할들이 혼재되어 있지만 모두 제공해야 하기 때문에 인터페이스 분리를 통해 특정 역할만을 이용할 수 있도록 해준다. 즉, Accessible, CellEditorListener, ListSelectionListener, Scrollable, TableColumnModelIstener, TableModelListener 등 여러 인터페이스 구현을 통해 서비스를 제공하는 것이다.
- EAI(Enterprise Application Integration) 인터페이스
기업 내, 기업 간의 서로 다른 애플리케이션 인터페이스를 통합하기 위해 제안된 기술. 인터페이스는 여러 타입과 여러 방식이 사용될 수 있으며 수준에 따른 EAI 인터페이스를 통해 인터페이스 개념을 확장할 수 있다.
[그림7] 통합이 무시된 기업 시스템(왼쪽, 노란색) 과 EAI로 구축된 기업 시스템(오른쪽, 빨간색)
5. 리스코프 대체 원칙(Liskov Substitution Principle, LSP)
기반 클래스는 서브 클래스로 대체 가능해야 한다.
상속 관계에서 부모와 자식 간에는 IS-A 관계가 성립해야 한다. 이는 자식이 부모의 메소드 중 일부를 거부하면 안 된다는 것을 의미한다. LSP는 올바른 상속 구조가 갖춰야 할 특성을 가이드해주며 OCP의 기반이 된다. 원칙을 지키지 않으면 Refused Bequest 이라는 bad smell 이 생긴다.
- Design by Contract, DBC
Derived class가 Base class를 대신하려면 Pre-condition, Post-condition 등을 명료하게 정의해야 한다.
① Base class 보다 조건이 까다로운 pre-condition이 있으면 안된다.
② Base class 보다 조건이 부족한 post-condition이 있으면 안된다.
즉, derived method 는 base method 와 동일해야 한다.
- IS-A 관계에 대한 경험 법칙
① 만약 Base 클래스와 Derived 클래스가 똑같은 일을 한다면 둘을 한 클래스로 표현하고 이들을 구분할 수 있는 필드를 둔다.
② 똑같은 연산을 제공하지만, 이들을 약간씩 다르게 구현한다면 공통의 인터페이스를 만들고 이를 구현한다. (인터페이스 상속)
③ 공통된 연산이 없다면 완전 별개인 2개의 클래스를 만들면 된다.
④ 만약 Derived 클래스가 추가적으로 무언가를 더 구현한다면 구현 상속을 사용한다.
- Refused Bequestbad smell
증상
① 부모를 상속한 자식 클래스에서 메소드를 지원하는 대신 예외가 발생한다.
② 자식 클래스가 예외를 발생시키지는 않지만 아무런 일도 하지 않는다.
③ 클라이언트가 부모보다는 자식을 직접 접근하는 경우가 많다.
해결책
① 혼동될 여지가 없고 여러 트레이드 오프를 고려해 선택한 것이라면 그대로 놔둔다. 단, 트레이드 오프와 프로그램의 범용성의 한계에 대해서 스스로 인지하고 있어야 한다.
② 다형성을 위한 상속 관계가 필요없다면 Replace Inheritance with Delegation을 한다. 상속은 깨지기 쉬운 기반 클래스 등을 지니고 있으므로 IS-A 관계가 성립되지 않는다. LSP를 지키기 어렵다면 상속 대신 합성(Composition)을 사용하는 것이 좋다.
③ 상속 구조가 필요하다면 Extract Subclass, Push Down Field, Push Down Method 등의 리팩토링 기법을 이용하여 LSP를 준수하는 상속 계층 구조를 구성한다.
6. 의존 관계 역전의 원칙(Dependency Inversion Principle, DIP)
클라이언트는 구체 클래스가 아닌 인터페이스나 추상 클래스에 의존해야 한다.
Concrete 클래스는 변화 가능성이 많기 때문에 의존하게 될 경우 유지비용이 많이 든다. 그래서 상대적으로 변화 가능성이 적은 Abstract 클래스에 의존해야 한다. Abstract 클래스에 의존하는데는 IOC(Inversion of Control)가 중추 역할을 한다. 통제권이 역전 되면 응답을 확인하는 작업에서 자유로워지고, 이로 인해 다른 작업을 할 수 있는 기회비용을 확보 할 수 있다. - 클라이언트의 역할을 단순화
- 통제권의 역전(Inversion of Control, IOC)
주 통제권이 호출자에게서 프레임워크로 역전. 이 때, Inverse는 통제권에 대한 의존성에 대한 것만 의미하는 것이 아니라 인터페이스 소유권에 대한 것도 의미한다.
- Hook 메소드
미리 정의해 둔 인터페이스. Inverse를 위한 매개 포인트가 되며 확장성을 확보하는 기능도 한다.
사례) 통신 프로그래밍 모델, 이벤트 드리븐(콜백 & JMS 모델)
- 통신 프로그래밍 모델
Send & Recv 모델 : 각 쓰레드에서 send()를 실행하면 recv()가 올 때까지 기다려야 한다. 때문에, 다른 일을 할 수없어 낭비가 심하다.
Polling 모델 : recv()를 전담하는 쓰레드를 따로 만들어 읍답을 확인하고 싶은 시점에 클라이언트가 응답을 확인한다. 만약, 원하는 시점에 서버의 응답이 없을 경우 응답이 있을 때까지 확인해야 하는 오버헤드가 있다. [그림8] Send & Recv 모델과 폴링 모델
비동기 모델 : 메시지를 send()한 후, recv()하는 대신 서버의 응답을 처리하는 Hook 메소드를 Reply DeMuxer에 등록한다. (구조적 프로그램에서는 함수 포인터를 등록하지만 객체지향 프로그램에서는 커맨드 오브젝트를 등록한다) recv()를 담당하는 쓰레드는 서버로부터 응답을 접수하면 대응하는 Hook 메소드를 실행한다. - DIP 원칙 따름 [그림9] 비동기 모델
- 이벤트 드리븐 (콜백 & JMS 모델)
① 콜백(Callback) : 서버가 비동기적으로 클라이언트에게 정보를 전달하는 Hook 메소드 이다. 서버에게 장시간의 작업들을 할당하고 클라이언트가 각 작업의 결과에 대한 중간보고를 비동기적으로 받고싶을 때 유용하다. recv() 쓰레드가 서버의 역할로 전이된 형태를 갖는다. [그림10] 콜백 모델
② JMS 토픽 모델 : MOM 아키텍처에서 Publish/Subscribe 메시징 모델로 멀티캐스팅 같은 그룹 메시징을 제공할 때 유용하다. (클라이언트/서버에서 메시지 기반으로 패러다임이 변경)
Subscriber들은 Topic 제공자에게 자신을 등록한다. Publisher가 Topic 제공자에게 메시지를 전송하면 JMS Topic 제공자는 등록된 Subscriber들에게 메시지를 멀티캐스팅한다. [그림11] Publish/Subscribe 모델
참조자료
[1] "Design Principles and Design Patterns", Robert C. Martin, http://www.objectmentor.com
[2] "다시 보면 크게 보이는 개방-폐쇄 원칙", 최상훈, 마이크로소프트웨어 2005년 1월호
[3] "헤어져서 행복해진 사례연구, 단일 책임 원칙", 최상훈, 마이크로소프트웨어 2005년 2월호
[4] "Design Patterns: Elements of Reusable Object-Oriented Software", Erich Gamma, Richard Helm, John Vissides, Addison Wesley. 1994
[5] "Patterns of Enterprise Application Architecture", Martin Fowler
[6] "상대에 대한 조그만 배려, 인터페이스 분리의 원칙", 최상훈, 마이크로소프트웨어 2005년 3월호
[7] "복잡성과 단순성이 상생하는 미학", 최상훈, 마이크로소프트웨어 2005년 4월호