- 브리지(Bridge) 패턴
- 빌더(Builder) 패턴
- 책임 연쇄(Chain of Responsibility) 패턴
- 플라이웨이트(Flyweight) 패턴
- 인터프리터(Interpreter) 패턴
- 중재자(Mediator) 패턴
- 메멘토(Memento) 패턴
- 프로토타입(Prototype) 패턴
- 비지터(Visitor) 패턴
💡 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 한다.
즉, 구현과 더불어 추상화 부분까지 변경해야 한다면 이 패턴을 사용하면 된다.
만능 리모컨
을 만들기로 했다고 가정하자. 이 리모컨은 모든 TV를 대상으로 작동해야 한다.
사용자 인터페이스는 이미 추상화했기에 때문에 TV 모델마다 구상 클래스를 바꿔 쓸 수 있다.
하지만 사용자들이 제공하는 정보에 맞춰서 리모컨을 개선하다 보면 추상화 부분까지도 바꿔야 할 수도 있다.
이를 위해서는
구체적인 구현 부분과 추상화 부분을 모두 바꿀 수 있는
객체지향 디자인이 필요하다.
아래의 디자인을 사용하면 TV를 구현하는 부분만 변경 가능하고 사용자 인터페이스는 변경할 수 없다.
추상화된 부분과 구현 부분을 서로 다른 클래스 계층 구조로 분리해서 그 둘을 브리지로 연결했다.
따라서 양쪽을 서로 독립적으로 변경할 수 있다.
- 추상화된 부분에 들어있는 메서드는 구현 클래스에 있는 메서드를 통해서 구현된다.
- 구상 서브클래스는 추상 클래스의 메서드로 구현된다.
-
기능의 여러 변형을 가진 모놀리식 클래스를 나누고 정돈하려 할 때 유용하다.
- 모놀리식 : 모든 구성요소가 한 곳에 통합 되어 있는 형태
- e.g., 클래스가 다양한 데이터베이스 서버들과 작동할 수 있는 경우
- 따라서 여러 플랫폼에서 사용해야 하는 그래픽스와 윈도우 처리 시스템에서도 유용하게 쓰인다.
-
인터페이스와 실제 구현할 부분을 서로 다른 방식으로 변경해야 할 때 유용하다.
-
런타임에 구현을 전환해야 할 때에 사용할 수 있다.
- 브리지 패턴을 사용 시 필드에 새 값을 할당하기만 하면 추상화 내부의 구현 객체를 바꿀 수 있다.
- 전략 패턴과 혼동하지 않도록 주의하자.
- 구현과 인터페이스를 완전히 결합하지 않았기 때문에 구현과 추상화 부분을 분리할 수 있다.
- 추상화된 부분과 실제 구현 부분을 독립적으로 확장할 수 있다.
- 추상화 부분을 구현한 구상 클래스가 바뀌어도 클라이언트에는 영향을 끼치지 않는다.
- 새로운 추상화들과 구현들을 상호 독립적으로 도입할 수 있다. (
개방 폐쇄 원칙
) - 추상화의 상위 수준 논리와 구현의 플랫폼 세부 정보에 집중할 수 있다. (
단일 책임 원칙
)
- 결합도가 높은 클래스에 패턴을 적용하는 것이기 때문에 디자인과 코드가 복잡해진다.
이들은 매우 유사한 구조로 이루어져 있다.
모두 다른 객체에 작업을 위임하는 합성을 기반으로 하지만, 모두 다른 문제들을 해결한다.
특히 어댑터 패턴과의 차이점에 유의하자.
브리지 패턴
: 일반적으로 사전에 설계되며, 앱의 다양한 부분을 독립적으로 개발할 수 있도록 한다.어댑터 패턴
: 일반적으로 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 한다.
추상 팩토리
+ 브리지 패턴
- 이는 브리지에 의해 정의된 추상이 특정 구현들과만 작동할 수 있을 때 유용하다.
- 추상 팩토리는 이러한 관계들을 캡슐화하고 클라이언트 코드에서부터 복잡성을 숨겨줄 수 있다.
💡 복잡한 객체들을 단계별로 생성할 수 있도록 해준다.
제품을 여러 단계로 나눠서 만들도록 제품 생산 단계를 캡슐화하는 것이다.
이 패턴을 통해 같은 제작 코드로 객체의 다양한 유형들과 표현을 제작할 수 있게 된다.
많은 필드와 중첩된 객체들을 단계별로 초기화해야 하는 복잡한 객체를 생각해보자.
초기화 코드는 일반적으로 많은 매개변수가 있는 생성자 내부에 묻혀 있으며, 더 최악의 상황에는 클라이언트 코드 전체에 흩어져 있을 수도 있다.
예를 들어, House
객체를 만들려면 벽과 바닥을 만든 후 문을 설치하고, 창문도 맞춘 후 지붕도 만들어야 한다.
하지만 뒷뜰과 난방 시스템, 배관 및 전기 배선 등이 있는 더 큰 집을 원한다면 어떻게 해야 할까?
가장 간단한 해결책은 기초 House
클래스를 확장하고, 매개변수의 모든 조합을 포함하는 자식 클래스들의 집합을 만드는 것이다.
하지만 이 방식은 자식 클래스의 수도 너무 늘어날 뿐만 아니라
새로운 매개변수를 추가할 때마다 계층구조가 갈수록 복잡해질 것이기에 좋은 방법이 아니다.
다른 접근 방식으로는 기초 House
클래스에 House
객체를 제어하는 모든 가능한 매개변수를 포함한 거대 생성자를 만드는 것이다.
이 방식은 자식 클래스들의 필요성을 제거하지만,
모든 매개변수가 항상 필요한 것은 아니기 때문에 생성자 호출들의 코드가 매우 지저분해지게 된다.
빌더 패턴은 자신의 클래스에서 객체 생성 코드를 추출하여 builders
라는 별도의 객체들로 이동하도록 제안한다.
-
빌더는 객체 생성을 일련의 단계들로 정리한다. (
buildWalls
,buildDoors
, …)- 객체를 생성하고 싶으면 위 단계들을 빌더 객체에 실행하면 된다.
- 제품이 생성되는 동안 다른 객체들이 제품에 접근하는 것을 허용하지 않는다.
-
객체의 특정 설정을 제작하는 데 필요한 단계들만 호출하면 된다.
일부 건축 단계들은 다른 구현들이 필요할 수 있다. (e.g., 오두막 벽의 재료는 나무, 성벽의 재료는 돌)
이 경우에는 같은 건축 단계들의 집합을 다른 방식으로 구현하는 여러 다른 빌더 클래스를 생성하면 된다.
건축 프로세스 내에서 이러한 빌더들을 사용하여 다양한 종류의 객체를 생성할 수 있게 된다.
더 나아가서 제품을 생성하는 데 사용하는 빌더 단계들에 대한 일련의 호출을 Director
라는 별도의 크래스로 추출할 수 있다.
즉, Director
클래스는 제작 단계들을 실행하는 순서를 정의하고, Builder
는 이러한 단계들에 대한 구현을 제공하는 것이다.
휴가 계획 프로그램
을 만들어보자.
고객은 호텔, 입장권, 레스토랑, 특별 이벤트 등을 마음대로 선택해서 예약할 수 있다.
휴가 계획 프로그램을 만들려면 다음과 같은 구조가 필요하다.
- 휴가 계획은 날짜(첫째 날, 둘째 날) 별로 세운다.
- 각 날짜별로 호텔, 입장권, 식사, 특별 이벤트 등을 임의로 조합할 수 있다.
따라서 다양한 고객의 계획표를 표현할 수 있는 유연한 자료구조가 필요하다.
어떻게 하면 복잡한 계획표와 계획표를 만드는 단계가 서로 섞이지 않게 하면서 계획을 짤 수 있을까?
- 계획표 작성을 객체(빌더)에 캡슐화해서
- 클라이언트가 빌더에게 계획표 구조를 만들어 달라고 요청하도록 만들었다.
-
클라이언트가 빌더에게 계획표를 생성해 달라고 요청한다.
-
클라이언트는 몇 단계에 걸쳐서 계획표를 생성해 달라고 요청한 다음,
getVacationPlanner()
메서드를 호출해서 완성된 객체를 가져온다. -
클라이언트는 추상 인터페이스를 사용해서 계획표를 만든다.
-
구상 빌더는 실제 제품을 만들어서
vacation
이라는 복합 구조에 넣는다.
-
복합 객체 구조를 구축하는 용도로 많이 쓰인다.
-
점층적 생성자를 제거하기 위해 사용할 수 있다.
class Pizza { Pizza(int size) { ... } Pizza(int size, boolean cheese) { ... } Pizze(int size, boolena cheese, boolean pepperoni) { ... } // ...
-
-
일부 제품의 다양한 표현들을 생성할 수 있도록 하고 싶을 때 유용하다.
-
제품의 내부 구조를 클라이언트로부터 보호할 수 있다.
- 즉, 생성 단계들을 수행하는 동안 미완성 제품을 노출하지 않는다.
- 따라서 클라이언트 코드가 불완전한 결과를 가져오는 것을 방지한다.
- 제품의 비즈니스 로직에서 복잡 객체 생성 과정을 캡슐화하여 고립시킬 수 있다. (
단일 책임 원칙
) - 여러 단계와 다양한 절차를 거쳐 객체를 만들 수 있다
- 이와 다르게,
팩토리 패턴
은 한 단계에서 모든 것을 처리한다.
- 이와 다르게,
- 제품들을 단계별로 생성하거나, 생성 단계들을 연기하거나, 재귀적으로 단계들을 실행할 수 있다.
- 특히 재귀적으로 단계들을 호출할 경우 객체 트리를 구축해야 할 때 매우 유용하다.
- 클라이언트는 추상 인터페이스만 볼 수 있기에 제품을 구현한 코드를 쉽게 바꿀 수 있다.
- 팩토리를 사용할 때보다 객체를 만들 때 클라이언트에 관해 더 많이 알아야 한다.
- 패턴이 여러 개의 새 클래스들을 생성해야 하기 때문에 코드의 전반적인 복잡성이 증가한다.
- 복잡한 객체들을 단계별로 생성하는 데 중점을 둔다.
- 제품을 가져오기 전에 몇 가지 추가 생성 단계들을 실행할 수 있도록 한다.
- 관련된 객체들의 패밀리들을 생성하는 데 중점을 둔다.
- 제품이 생산된다면 그 즉시 반환한다.
복합체(Composite)
+ 빌더 패턴
복잡한 복합체 패턴 트리를 생성할 때 빌더를 사용할 수 있다.
빌더의 생성 단계들을 재귀적으로 작동하도록 프로그래밍할 수 있다.
싱글턴
+ 빌더 패턴
💡 핸들러들의 체인(사슬)을 따라 요청을 전달할 수 있게 해준다.
각 핸들러는 요청을 받으면 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지를 결정한다.
즉, 1개의 요청을 2개 이상의 객체에서 처리해야 한다면 이 패턴을 사용하면 된다.
사업이 대박나서 이메일들이 감당하기 힘들 정도로 많이 날아오기 시작했다고 해보자.
분석에 의하면 이메일은 크게 4가지로 분류할 수 있으며, 각각 다른 곳에 전달해야 한다.
- 팬 메일 : CEO에게 전달
- 항의 메일 : 법무 담당 부서에 전달
- 신규 설치 요청 메일 : 영업부로 전달
- 스팸 메일 : 삭제
인공지능 검출기를 만들어서 스팸 메일, 팬 메일, 항의 메일, 신규 설치 요청 메일을 감지할 수 있게 되었다.
이 검출기를 써서 메일을 분류하는 디자인을 만들어야 한다.
여기서는 주어진 요청을 검토하는 객체 사슬을 생성한다.
사슬에 들어있는 각 객체는 핸들러
역할을 하며, 객체마다 이어지는 객체가 있다.
그 사슬에 속해 있는 각 객체는 자기가 받은 요청을 검사해서 그 요청을 처리할 수 있으면 직접 처리하고,
그렇지 않으면 다음 객체에게 넘긴다.
-
이메일이 수신되면 첫 번째 핸들러인
SpamHandler
에게 전달된다.
(모든 이메일은 우선 첫 번째 핸들러에게 전달됨) -
SpamHandler가 처리할 수 없다면
FanHandler
로 넘긴다. -
FanHandler가 처리할 수 없다면
ComplaintHandler
로 넘긴다. -
ComplaintHandler가 처리할 수 없다면
NewLocHandler
로 넘긴다. -
NewLocHandler까지도 처리할 수가 없어 사슬 맨 끝까지 간 이메일은 처리되지 못한다.
(필요에 따라서 모든 요청을 처리하는 핸들러를 구현할 수 있음)
-
프로그램이 다양한 방식으로 다양한 종류의 요청들을 처리할 것으로 예상되지만, 정확한 요청 유형들과 순서들을 미리 알 수 없는 경우에 사용한다.
- 윈도우 시스템에서 마우스 클릭과 키보드 이벤트를 처리할 때 흔히 쓰인다.
-
특정 순서대로 여러 핸들러를 실행해야 할 때 사용한다.
-
핸들러들의 집합과 그들의 순서가 런타임에 변경되어야 할 때 유용하다.
- 핸들러 클래스들 내부의 참조 필드에 setter들을 제공하면, 핸들러들을 동적으로 삽입, 제거, 재정렬할 수 있다.
- 요청을 보낸 쪽과 받는 쪽을 분리할 수 있다.
- 요청의 처리 순서를 제어할 수 있다.
- 클라이언트 코드를 손상하지 않고 새로운 핸들러들을 도입할 수 있다. (
개방-폐쇄 원칙
) - 객체는 사슬의 구조를 몰라도 되고, 그 사슬에 들어있는 다른 객체의 직접적인 레퍼런스를 가질 필요가 없다.
→ 객체를 단순하게 만들 수 있다.
- 요청이 반드시 수행된다는 보장이 없다.
- 사슬 끝까지 갔는데도 처리되지 않을 수 있다.
- 이런 특성이 장점이 될 수 있긴 하다.
- 실행 시에 과정을 살펴보거나 디버깅하기가 힘들다.
이들은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룬다.
책임 연쇄 패턴
: 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달한다.커맨드 패턴
: 발신자와 수신자 간의 단방향 연결을 설립한다.중재자 패턴
: 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제한다.옵저버 패턴
: 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 한다.
두 패턴 모두 실행을 일련의 객체들을 통해 전달할 때 재귀적인 합성에 의존하지만, 몇 가지 결정적인 차이점이 있다.
- 핸들러들은 서로 독립적으로 임의의 작업을 실행할 수 있다.
- 요청을 언제든지 더 이상 전달하지 않을 수 있다.
- 객체의 행동을 확장하는 동시에 이러한 행동을 기초 인터페이스와 일관되게 유지할 수 있다.
- 요청의 흐름을 중단할 수 없다.
복합체(Composite)
+ 책임 연쇄 패턴
leaf 컴포넌트가 요청을 받으면 해당 요청을 모든 부모 컴포넌트들의 체인을 통해 개체 트리의 root까지 전달할 수 있다.
💡 각 객체에 모든 데이터를 유지하는 대신, 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 한다.
즉, 어떤 클래스의 인스턴스 하나로 여러 개의 ‘가상 인스턴스’를 제공하고 싶다면 이 패턴을 사용하면 된다.
조경 설계 애플리케이션에서 나무를 객체 형태로 추가해야 한다.
애플리케이션 내에서 나무들의 역할은 별로 중요하지 않은데
그저 X, Y 좌표를 가지고 있고, 나무의 나이에 따라 적당한 크기로 화면에 표현하면 된다.
Tree
인스턴스에는 나무의 상태가 저장된다.
문제는 어떤 사용자가 나무를 너무 많이 넣으려고 한다는 점이다.
테스트 결과, 나무를 많이 만들면 애플리케이션이 눈에 띄게 느려진다는 사실을 발견했다.
Tree
객체를 수 천 개 만드는 대신,
Tree
의 인스턴스는 하나만 만들고- 모든 나무의 상태를 클라이언트 객체가 관리하도록 하였다.
- 모든
가상 Tree 객체
의 상태가 2차원 배열에 저장된다. Tree 객체 인스턴스
에는 상태가 저장되어 있지 않다.
- 어떤 클래스의 인스턴스가 아주 많이 필요하지만 모두 똑같은 방식으로 제어해야 할 때 유용하게 사용된다.
- 실행 시에 객체 인스턴스의 개수를 줄여서 메모리를 절약할 수 있다.
- 여러 ‘가상’ 객체의 상태를 한곳에 모아 둘 수 있다.
- 이 패턴을 써서 구현해놓으면 특정 인스턴스만 다른 인스턴스와 다르게 행동하게 할 수 없다.
- 플라이웨이트 객체가 다른 컨텍스트에서 사용될 수 있기 때문에 이들이 해당 플라이웨이트 객체의 상태를 수정할 수 없는지를 확인해야 한다.
- 플라이웨이트 객체의 값을 변경하면 이들을 사용하는 곳에 영향을 줄 수 있다.
- 플라이웨이트 메서드를 호출할 때마다 컨텍스트 데이터의 일부를 다시 계산해야 한다면 CPU 주기 대신 메모리를 절약하고 있는 것일지도 모른다.
- 코드가 복잡해진다.
- 작은 객체들을 많이 만드는 방법을 보여준다.
- 전체 하위 시스템을 나타내는 단일 객체를 만드는 방법을 보여준다.
메모리를 절약하기 위해 복합체(Composite) 패턴
트리의 공유된 leaf 노드들을 플라이웨이트들로 구현할 수 있다.
💡 어떤 언어의 대해, 그 언어의 문법에 대한 표현을 정의하면서 그 표현을 사용하여 해당 언어로 기술된 문장을 해석하는 해석자를 함께 정의한다.
(언어의 인터프리터)
오리 시뮬레이션 게임을 아이들에게 프로그래밍을 가르쳐 주는 용도로 활용하고 싶다.
아이 1명당 오리 1마리를 정해 준 다음에 간단한 언어를 다음과 같은 식으로 가르쳐줄 것이다.
right; // 우회전
while(daylight)fly; // 낮 동안 계속 날아다니게 함
quack; // 꽥꽥 소리를 내게 함
다음과 같은 문법 규칙을 만들었다고 해보자.
expression ::= <command> | <sequence> | <repetition> // 프로그램 : 일련의 명령어와 시퀀스, 반복문으로 구성된 표현식
sequence ::= <expression> ';' <expression> // 시퀀스 : 세미콜론으로 여러 표현식을 연결해 놓은 것
command ::= right | quack | fly
repetition ::= while '(' <variable> ')'<expression> // while문 : 조건 변수와 표현식으로 구성됨
variable ::= [A-Z,a-z]+
아이들이 오리의 움직임을 프로그래밍하고 그 결과를 직접 확인할 수 있도록 위의 문법에 따라 만들어진 코드 해석용 인터프리터를 만들어야 한다.
인터프리터 패턴은 문법과 구문을 번역하는 인터프리터 클래스
를 기반으로 간단한 언어를 정의한다.
그리고 언어에 속하는 규칙을 나타내는 클래스를 사용해서 언어를 표현한다.
이 언어를 해석하려면 각 표현식에 대응하는 interpret() 메서드를 호출하면 된다.
이 메서드에 컨텍스트(파싱하고 있는 프로그램의 입력 스트림이 들어있음)도 전달되며,
입력된 내용을 확인하고 평가하는 작업도 이 메서드가 처리한다.
-
간단한 언어를 구현할 때 인터프리터 패턴이 유용하게 사용된다.
-
효율보다는 단순하고 간단하게 문법을 만드는 것이 더 중요할 경우에 유용하다.
-
스크립트 언어와 프로그래밍 언어에서 모두 쓸 수 있다.
- 문법을 클래스로 표현해서 쉽게 언어를 구현할 수 있다.
- 문법이 클래스로 표현되기 때문에 언어를 쉽게 변경하거나 확장할 수 있다.
- 클래스 구조에 메서드만 추가하면 프로그램을 해석하는 기본 기능 외에
예쁘게 출력하는 기능이나 더 나은 프로그램 확인 기능 같은 새로운 기능을 추가할 수 있다.
- 문법 규칙의 개수가 많아지면 아주 복잡해진다.
- 그럴 때에는 파서나 컴파일러 생성기를 쓰는 편이 낫다.
💡 객체 간의 직접 통신을 제한하고 중재자 객체를 통해서만 협력하도록 한다.
따라서 이 패턴을 통해서 서로 관련된 객체 간의 혼란스러운 의존 관계들(복잡한 통신과 제어 등)을 줄일 수 있다.
모든 가전제품에 Java가 내장되어 있는 자동화 주택이 있다.
고객이 더 이상 알람 시계의 5분만 더 버튼을 누르지 않으면 알람 시계에서 커피 메이커로 커피를 만들으라는 신호를 보낸다.
하지만 많은 고객이 주말에는 커피를 안 끓인다든지, 샤워 예정 시각 15분 전에 잔디밭 스프링클러를 자동으로 끈다든지 하는 등 더 나은 기능을 원하고 있다.
기능들을 추가하기 위해서는 객체가 할 일을 정리해야 한다.
어떤 객체에 어떤 규칙을 넣어야 할지를 결정하기가 점점 어려워지고 있고, 여러 객체를 서로 연관시키는 과정도 점점 복잡해지고 있다.
시스템에 중재자 패턴을 적용하면 가전제품 객체들을 훨씬 단순화할 수 있다.
- 상태가 바뀔 때마다 중재자에게 알려준다.
- 중자재에서 보낸 요청에 응답한다.
즉, 서로 독립적으로 작동해야 하는 컴포넌트들의 호출을 중재자 객체
를 통해 리다이렉션하여 그들이 간접적으로 협력하도록 하는 것이다.
이를 통해 컴포넌트들은 수십 개의 동료 컴포넌트들과 결합하는 대신, 단일 중재자 클래스에만 의존하게 된다.
중재자에는 모든 시스템을 제어하는 로직이 들어있다.
기존 가전제품에 새로운 규칙을 추가하거나 새로운 가전제품을 자동화 시스템에 추가하더라도 중재자만 고치면 된다.
-
일부 클래스들이 다른 클래스들과 단단하게 결합하여 변경하기 어려울 때 사용한다.
-
타 컴포넌트들에 너무 의존하기 때문에 다른 프로그램에서 해당 컴포넌트를 재사용할 수 없을 경우 사용한다.
-
서로 연관된 GUI 구성 요소를 관리하는 용도로 많이 쓰인다.
- 시스템과 객체를 분리함으로써 재사용성을 획기적으로 향상시킬 수 있다.
- 실제 컴포넌트들을 변경하지 않고도 새로운 중재자들을 도입할 수 있다. (
개방-폐쇄 원칙
) - 다양한 컴포넌트 간의 결합도를 줄일 수 있다.
- 제어 로직을 한 군데 모아놓았기 때문에 관리하기가 수월하다.
- 시스템에 들어있는 객체 사이에서 오가는 메시지를 확 줄이고 단순화할 수 있다.
- 디자인을 잘 하지 못하면 중재자 객체가 너무 복잡해질 수 있다.
- 중재자 객체가 전지전능한 객체(God object)로 발전할 수도 있다.
이들은 발신자와 수신자를 연결하는 다양한 방법을 다룬다.
(위의 책임 연쇄 패턴에서 각각에 대해서 정리하였음)
둘 다 밀접하게 결합된 많은 클래스 간의 협업을 구성하려고 한다.
- 객체들의 하위 시스템에 대한 단순화된 인터페이스를 정의하지만, 새로운 기능을 도입하지는 않는다.
- 하위 시스템 자체는 퍼사드를 인식하지 못한다.
- 하위 시스템 내의 객체들은 서로 직접 통신할 수 있다.
- 시스템 컴포넌트 간의 통신을 중앙 집중화한다.
- 컴포넌트들은 중재자 객체에 대해서만 알며, 서로 직접 통신하지 않는다.
- 이 패턴의 주목적은 시스템 컴포넌트들의 집합 간의 상호 의존성을 제거하는 것이다.
- 컴포넌트들은 단일 중재자 객체에 의존하게 된다.
- 이 패턴의 목적은 객체들 사이에 단방향 연결을 설정하는 것이다.
- 일부 객체는 다른 객체의 종속자 역할을 한다.
이 두 패턴을 동시에 사용하는 구현도 존재한다.
- 중재자 객체는 출판사의 역할을 맡는다.
- 컴포넌트들은 중재자의 이벤트들을 구독 및 구독 취소하는 구독자들의 역할을 맡는다.
💡 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해준다.
메멘토 패턴의 2가지 목적
- 시스템에서 핵심적인 기능을 담당하는 객체의 상태 저장
- 핵심적인 객체의 캡슐화 유지
앱은 작업을 실행하기 전에 객체들의 상태의 스냅샷
을 저장하며,
이 스냅샷은 나중에 객체들을 이전 상태로 복원하는데 사용할 수 있다.
상태 스냅샷은 객체의 모든 필드를 살펴본 후 해당 값들을 스토리지에 복사하는 것으로 생성된다.
하지만 이는 객체의 내용에 대한 액세스 제한이 완화되어 있는 경우에만 작동할 것이고,
대부분의 실제 객체들은 모든 중요한 데이터를 비공개 필드에 숨겨놓는다.
이 문제는 무시하고, 객체들이 상태들을 모두 공개했다고 가정해보자.
이렇게 하면 원하는 대로 객체들의 상태에 대한 스냅샷을 생성할 수 있지만, 여전히 심각한 문제들이 남아있다.
-
앞으로 일부 필드를 추가 또는 제거하거나 클래스들을 리팩터링하게 된다면
영향받은 객체들의 상태를 복사하는 역할을 맡은 클래스들을 변경해야 한다. -
다른 객체들이 스냅샷에서 데이터를 읽고, 스냅샷에 데이터를 쓸 수 있도록 하려면 해당 스냅샷의 필드를 공개해야 한다.
- 이렇게 한다면 객체들의 (비공개 포함) 모든 상태들이 노출될 것이다.
- 다른 클래스들은 스냅샷 클래스에 발생하는 모든 변경에 영향을 받게될 것이다.
즉, 클래스 내부의 세부 정보를 모두 공개하면 크래스가 너무 취약해지는데
반대로 클래스의 상태에 접근하지 못하게 되면 스냅샷을 생성할 수 없다는 교착 상태에 빠지게 된다.
위의 모든 문제는 캡슐화의 실패로 인해 발생한다.
저장하고자 하는 상태를 핵심 객체로부터 분리하여야 한다.
따라서 객체 상태의 복사본을 따로 저장하는 메멘토(Memento)
라는 특수 객체를 사용한다.
- 메멘토의 내용에는 메멘토를 생성한 객체를 제외한 다른 그 어떠한 객체도 접근할 수 없다.
- 다른 객체들은 메멘토들 또는 제한된 인터페이스를 사용해 통신해야 한다.
- 이러한 인터페이스는 스냅샷의 메타데이터를 가져올 수 있도록 하지만, 스냅샷에 포함된 원래 객체의 상태는 가져오지 않는다.
메멘토는 상태 스냅샷들의 생성을 해당 상태의 실제 소유자인 Originator
객체에 위임한다.
그러면 다른 객체들이 ‘외부’에서 객체의 상태를 복사하려 시도하는 대신,
자신의 상태에 대해 완전한 접근 권한을 갖는 객체가 자체적으로 스냅샷을 생성할 수 있다.
이러한 제한 정책을 사용하려면 일반적으로 Caretaker
라고 하는 다른 객체들 안에 메멘토 객체들을 저장한다.
Caretaker
들은 제한된 인터페이스를 통해서만 메멘토와 작업하기 때문에 메멘토 내부에 저장된 상태를 변경할 수 없다.Originator
는 메멘토 내부의 모든 필드에 접근할 수 있으므로 언제든지 자신의 이전 상태를 복원할 수 있다.
롤플레잉 게임 개발자라고 생각해보자.
캐릭터가 죽기 전에 저장해뒀던 곳에서부터 다시 시작할 수 있도록 세이브 기능
을 추가하려고 한다.
게임 상태는 매우 복잡한 데다가 아무나 몰래 코드를 건드릴 수 있게 해서는 안 된다.
아주 좋은 코드라고 할 순 없지만, 클라이언트가 메멘토 객체의 데이터에 접근할 수 없다는 사실은 확실하다.
-
객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷들을 생성하려는 경우에 사용한다.
-
객체의 필드들, getter, setter들을 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 때 유용하다.
- 메멘토는 객체 스스로가 자신의 상태의 스냅샷 생성을 담당하게 한다.
- 다른 객체는 스냅샷을 읽을 수 없기 때문에 원래 객체의 상태 데이터는 안전하다.
- 저장된 상태를 핵심 객체와는 다른 별도의 객체에 보관할 수 있어 안전하다.
- 핵심 객체의 데이터를 캡슐화된 상태로 유지할 수 있다.
- 복구 기능과 트랜잭션 기능을 구현하기가 쉬워진다.
- 클라이언트들이 메멘토들을 너무 자주 생성하면 앱이 많은 메모리를 소모할 수 있다.
- 상태를 저장하고 복구하는 데 시간이 오래 걸릴 수 있다.
- PHP, Python, JavaScript 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 내의 상태가 그대로 유지된다는 것을 보장할 수 없다.
- Java 시스템에서는 시스템의 상태를 저장할 때 직렬화를 사용하는 것이 좋다.
💡 코드를 그들의 클래스들에 의존시키지 않고 기존 객체들을 복사할 수 있도록 한다.
즉, 어떤 클래스의 인스턴스를 만들 때 자원과 시간이 많이 들거나 복잡하다면 이 패턴을 사용하면 된다.
어떤 객체의 정확한 복사본을 가지고 싶다면,
같은 클래스의 새 객체를 생성하여 원본 객체의 모든 필드들을 살펴본 후 해당 값들을 새 객체에 복사해야 한다.
하지만 이 방법으로는 문제들이 존재한다.
- 객체의 필드들 중 일부가
private
인 경우에는 위의 방식으로는 객체를 복사할 수 없다. - 객체의 복제본을 생성하려면 객체의 클래스를 알아야 하기 때문에, 코드가 해당 크래스에 의존하게 된다.
프로토타입 패턴은 실제로 복제되는 객체들에 복제 프로세스를 위임한다.
패턴은 복제를 지원하는 모든 객체(프로토타입)
에 대한 공통 인터페이스를 선언하며,
이 인터페이스를 사용하면 코드를 객체의 클래스에 결합하지 않고도 해당 객체를 복제할 수 있게 된다.
일반적으로 이러한 인터페이스에는 단일 clone
메서드만 포함된다.
clone
메서드의 구현은 모든 클래스에서 유사하다.
- 현재 클래스의 객체를 만든다.
- 이전 객체의 모든 필드 값을 새 객체로 전달한다.
(대부분의 프로그래밍 언어는 객체들이 같은 클래스에 속한 다른 객체의 private
필드들에 접근할 수 있도록 하므로 모든 필드를 복사하는 것이 가능함)
따라서 프로토타이핑은 다음과 같이 작동한다.
- 다양한 방식으로 설정된 객체들의 집합을 만든다.
- 설정한 것과 비슷한 객체가 필요한 경우 처음부터 새 객체를 생성하는 대신, 프로토타입을 복제하면 된다.
또다시 우리가 롤플레잉 게임 개발자라고 생각해보자.
이 게임에는 히어로가 동적으로 생성되는 지형을 따라서 여행을 하면 끊임없이 다양한 몬스터가 등장한다.
주변 환경에 맞춰서 몬스터의 특성이 바뀌는 기능이 필요하며, 헤비 유저들이 직접 몬스터를 만들 수 있게 기능을 추가하려고 한다.
몬스터 생성 과정을 따로 한 군데에 캡슐화하여 즉석에서 몬스터 객체 인스턴스를 생성하는 코드와 분리하면 좋을 것 같다.
-
기존 인스턴스를 복사하기만 해도 새로운 인스턴스를 만들 수 있다.
(Java에서는clone()
메서드를 사용하거나역직렬화
를 하면 됨) -
클라이언트 코드에서 어떤 클래스의 인스턴스를 만드는지 전혀 모르는 상태에서도 새로운 인스턴스를 만들 수 있다.
- 클라이언트에서 현재 상황에 맞는 새로운 몬스터를 원한다. 클라이언트는 몬스터의 종류를 알 수 없다.
- 레지스트리에서 적당한 몬스터를 찾아서 클론을 만든 다음, 그 클론을 리턴한다.
-
복사해야 하는 객체들의 구상 클래스들에 코드가 의존을 하면 안 될 때 사용한다.
-
각자의 객체를 초기화하는 방식만 다른 자식 클래스들의 수를 줄이고 싶을 때 유용하다.
-
시스템에서 복잡한 클래스 계층구조에 파묻혀 있는 다양한 형식의 객체 인스턴스를 새로 만들어야 할 때 유용하게 써먹을 수 있다.
- 클라이언트는 새로운 인스턴스를 만드는 과정을 몰라도 된다.
- 클라이언트는 구체적인 형식을 몰라도 객체를 생성할 수 있다.
- 상황에 따라서 객체들을 새로 생성하는 것보다 객체를 복사하는 것이 더 효율적일 수 있다.
- 순환 참조가 있는 복잡한 객체들을 복제하는 것은 매우 까다로울 수 있다.
- 상속을 기반으로 하지 않으므로 상속과 관련된 단점들이 없다.
- 하지만 복제된 객체의 복잡한 초기화가 필요하다.
- 상속을 기반으로 한다.
- 초기화 단계가 필요하지 않다.
상태를 기록에 저장하려는 객체가 간단하고, 외부 리소스에 대한 링크가 없거나 / 링크들이 있어도 이들을 재설정하기 쉬운 경우
프로토타입이 메멘토 패턴의 더 간단한 대안이 될 수 있다.
데코레이터 또는 복합체(Composite)
+ 프로토타입 패턴
데코레이터 및 복합체 패턴을 많이 사용하는 디자인들은 프로토타입을 사용하면 이득을 볼 수 있다.
복잡한 구조들을 처음부터 다시 건축하는 대신, 복제할 수 있기 때문이다.
💡 알고리즘들을 그들이 작동하는 객체들로부터 분리할 수 있도록 한다.
다양한 객체에 새로운 기능을 추가해야 하는데 캡슐화가 별로 중요하지 않다면 이 패턴을 사용하면 된다.
하나의 거대한 그래프로 구성된 지리 정보를 사용해 작동하는 앱을 개발하고 있다고 가정해보자.
그래프의 각 노드는 도시와 같은 복잡한 객체를 나타낼 수 있고, 산업들, 관광 지역들 등 더 세부적인 항목들도 나타낼 수 있다.
만약 노드들 사이에 도로가 있다면 노드들은 서로 연결된다.
각 노드 유형은 자체 클래스이지만 각 노드는 객체이다.
위의 그래프를 XML 형식으로 내보내는 작업을 구현하는 일을 맡았다고 해보자.
처음에는 각 노드 클래스에 내보내기 메서드를 추가한 다음,
재귀를 활용하여 그래프의 각 노드에 작업하며 내보내기 메서드를 실행할 계획이었다.
하지만 시스템의 설계자는 이런 방식으로 기존 노드 클래스들을 변경하는 것을 허용하지 않았다.
코드가 이미 프로덕션 단계에 있으며,
제안한 변경 사항들이 오류를 일으킬 수 있기 때문에 코드가 손상되는 위험을 감수하고 싶지 않다는 것이었다.
그리고 시스템 설계자는 노드 클래스들 내에 XML 내보내기 코드를 넣는 것이 적절한지에 대한 의문을 제기했다.
이 클래스들의 주 작업은 지리 데이터를 처리하는 것이므로, XML 내보내기 동작은 그곳에서 이상하게 보일 것이라고 했다.
또한, 위 기능이 구현된 후에도 마케팅 부서의 누군가가 데이터를 다른 형식으로 내보낼 수 있는 기능이나 다른 기능을 요청할 가능성이 있으며,
그렇게 된다면 우리는 다시 이 망가지기 쉬운 클래스들을 다시 한번 변경해야 한다는 문제가 있다.
비지터 패턴은 새로운 행동을 기존 클래스들에 통합하는 대신, Visitor
라는 별도의 클래스에 배치한다.
행동을 수행해야 했던 원래 객체는 Visitor
의 메서드 중 하나에 인수로 전달되어,
해당 메서드가 원래 객체 내에 포함된 모든 필요한 데이터에 접근할 수 있게 된다.
하지만 예를 들어 XML 내보내기의 경우, 실제 구현은 다양한 노드 클래스들에서 약간씩 다를 수 있기 때문에
Visitor 클래스는 단일 메서드를 정의하는 대신,
다음과 같이 메서드의 집합을 정의하여 각 메서드가 다른 유형의 인수를 받을 수 있도록 한다.
public interface Visitor {
String doForCity(City c);
String doForIndustry(Industry f);
String doForSightSeeing(SightSeeing s);
// ...
}
class ExportVisitor implements Visitor {
// ...
}
그러나 특히 전체 그래프를 다룰 때, 이러한 메서드를 정확히 어떻게 호출해야 할까?
이 메서드들은 시그니처들이 다르기 때문에 다형성을 사용할 수 없다.
따라서 주어진 객체를 처리할 수 있는 적절한 Visitor 메서드를 선택하려면 아래 의사코드처럼 먼저 그 클래스를 확인해야 한다.
foreach(Node node in graph)
if(node instanceof City)
exportVisitor.doForCity((City)node)
if(node instanceof Industry)
exportVisitor.doForIndustry((Industry)node)
// ...
메서드 오버로딩을 사용한다고 해도 노드 객체의 정확한 클래스를 사전에 알 수 없으므로,
오버로딩 매커니즘은 실행해야 할 올바른 메서드가 무엇인지 판단할 수 없다.
따라서 디폴트로 기초 Node
클래스의 객체를 받는 메서드를 선택하게 될 것이다.
비지터 패턴에서는 이 문제를 더블 디스패치라는 방법을 사용해서 해결한다.
클라이언트가 호출할 메서드의 적절한 버전을 선택하도록 하는 대신, 이 선택권을 Visitor에게 인수로 전달되는 객체에 위임한다.
이 객체들은 Visitor를 수락하고 해당 Visitor의 어떤 메서드가 실행되어야 하는지 알려준다.
// these are pseudocodes
// Client code
foreach(Node node in graph)
node.accept(exportVisitor)
// City
class City is
method accept(Visitor v)is
v.doForCity(this)
// ...
// Industry
class Industry is
method accept(Visitor v)is
v.doForIndustry(this)
// ...
이를 위해서는 노드 클래스들을 변경해야 했지만,
앞으로 코드를 다시 변경하지 않고도 다른 행동들을 추가할 수 있게 된다.
이제 모든 Visitor에 대한 공통 인터페이스를 추출하면 기존의 모든 노드가 앱에 도입하는 모든 Visitor와 함께 작동할 수 있다.
노드와 관련된 새로운 행동을 도입하려면 새 Visitor 클래스를 구현하기만 하면 된다.
Chapter 9에서의 통합 식당 예제를 상기해보자.
우리는 아래 그림처럼 복합체(Composite) 패턴
을 통해서 해당 문제를 해결하였다.
식당에서 주문하기 전에 영양 정보를 요구하는 손님이 늘었으며, 심지어 어떤 손님은 재료별 영양 정보까지 요구한다고 한다.
아래의 방법을 택하게 된다면 매번 메서드를 추가할 때마다 두 군데에 코드를 추가해야 한다.
또한, 만약 조리법이 들어있는 클래스를 추가하는 식으로 기본 애플리케이션을 고치게 된다면 세 군데에서 코드를 고쳐야 하여 매우 불편하다.
Traverser
객체는 복합체(Composite) 패턴
을 사용할 때,
복합 객체 내에 속해 있는 모든 객체에 접근하는 일을 도와주는 역할을 한다.
Visitor
객체에서 복합 객체 내의 모든 객체를 대상으로 원하는 작업을 처리하게 해주는 것이다.
- 각각의 상태를 모두 가져오면 클라이언트는 Visitor에게 각 상태에 맞는 다양한 작업을 처리하도록 요구할 수 있다.
- 새로운 기능이 필요하게 되면 Visitor만 고치면 된다.
-
Client
는 Visitor에게 복합 객체 구조 정보를 요구한다. -
Traverser
는 Visitor가 복합 객체 구조를 돌아다니는 데 도움을 준다. -
Visitor
는 모든 클래스의 getState() 메서드를 호출할 수 있어야 한다. -
복합 클래스에는 getState() 메서드만 있으면 된다. (대신 자기 자신의 상태를 외부에 노출해야 함)
-
복잡한 객체 구조(e.g., 객체 트리)의 모든 요소에 대해 작업을 수행해야 할 때 유용하다.
- Visitor 객체가 모든 대상 클래스들에 해당하는 같은 작업의 여러 변형들을 구현하도록 한다.
- 이로써 다양한 클래스들을 가진 여러 객체의 집합에 작업을 실행할 수 있도록 해준다.
-
비지터 패턴을 사용하여 보조 행동들의 비즈니스 로직을 정리할 수 있다.
- 이 패턴은 앱의 주 클래스들의 주 작업을 제외한 모든 다른 행동들을 비지터 클래스들의 집합으로 추출함으로써
그들이 주 작업에 더 집중하도록 만들 수 있게 해준다.
- 이 패턴은 앱의 주 클래스들의 주 작업을 제외한 모든 다른 행동들을 비지터 클래스들의 집합으로 추출함으로써
-
행동이 클래스 계층구조의 일부 클래스들에서만 의미가 있고, 다른 클래스들에서는 의미가 없을 때 유용하다.
- 이 행동을 별도의 Visitor 클래스로 추출한 후
관련 클래스들의 객체들을 수락하는 Visitor 메서드들만 구현하고 나머지는 비워두면 된다.
- 이 행동을 별도의 Visitor 클래스로 추출한 후
- 구조를 변경하거나 다른 클래스를 변경하지 않으면서도 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있다. (
개방-폐쇄 원칙
) Visitor
가 수행하는 기능과 관련된 코드를 한곳에 모아 둘 수 있으며, 같은 행동의 여러 버전을 같은 클래스로 이동할 수 있다. (단일 책임 원칙
)Visitor
객체는 다양한 객체들과 작업하면서 유용한 정보를 축적할 수 있다.- 이것은 객체 트리와 같은 복잡한 객체 구조를 순회하여 이 구조의 각 객체에 Visitor 패턴을 적용하려는 경우에 유용할 수 있다.
Visitor
를 사용하면 복합 클래스의 캡슐화가 깨진다.- 컬렉션 내의 모든 항목에 접근하는
Traverser
가 있기 때문에 복합 구조를 변경하기가 더 어려워진다. - 클래스가 요소 계층구조에 추가되거나 제거될 때마다 모든
Visitor
를 업데이트해야 한다. Visitor
들은 함께 작업해야 하는 요소들의 private 필드들 및 메서드들에 접근하기 위해 필요한 권한이 부족할 수 있다.
비지터 패턴은 커맨드 패턴
의 강력한 버전으로 취급할 수 있다.
비지터 패턴의 객체들은 다른 클래스들의 다양한 객체에 대한 작업을 실행할 수 있다.