Search
Duplicate

자바 디자인 패턴의 이해 - Gof Design Pattern[11~21]

목차

11. 데코레이터 패턴(Decorator Pattern)

개요

원천 객체(Object)에 기능을 추가해나가는 디자인 패턴.
개발을 하다보면 가끔 전체 클래스에 새로운 기능을 추가할 필요는 없지만, 특정 객체에는 새로운 기능이 추가되야 하는 경우가 있습니다.
예를 들어 Box라는 객체에는 좌우측 측면에 손잡이목적의 구멍(Hole)이 없지만 어떤 박스에는 필요 할수도 있습니다. 이런 경우 해당 박스에 새로운 속성이 추가되야 합니다.
일반적으로, 위와같은 기능및 속성추가를 하는 방법은 상속을 이용합니다. 말인즉슨, Box를 상속받는 BoxWithHole이라는 SubClass를 만드는 방법이죠.
하지만, 해당 방법은 적절한 방법은 아닌데, 그 이유는 손잡이구멍의 선택은 정적이기 때문입니다. 사용자는 구성요소를 언제 어디서 어떻게 손잡이를 구성할지 제어할 수 없기 때문입니다.
그래서 기존의 Box라는 객체를 감싸서 다른 기능(손잡이)을 추가해주는 객체를 장식자(Decorator)라고 합니다. 장식자는 자신이 둘러싼 속성과 기능도 동일하게 제공하기 때문에 장식자의 존재는 이를 사용하는 사용자에게 감춰집니다. 그리고 사용자가 기존 객체에 대한 요청을 중간에 가로채서 해당 객체에 전달하는데, 그 사이에 전&후처리로 다른 작업을 추가 할 수 있고, 이 경우 투명성이 존재하기에 장식자(Decorator)의 중첩이 가능하고 이를 통해 기능추가를 계속 할 수 있게 됩니다.

사용시기

객체의 타입과 호출 가능한 메소드를 그대로 유지하면서 객체에 새로운 기능을 추가할 때 사용한다.
탈부착 가능한 책임을 정의할 때 사용한다.
상속을 통해 서브 클래스를 계속 만드는 것이 비효율적일 때 사용한다.

예제

문자열의 주변에 장식을 만들어 표시하는 프로그램 (장식: -, *, |,...)
Search
이름
해설
1행으로 구성된 문자열 표시용의 클래스
'장식'을 나타내는 클래스
좌우에 장식을 붙이는 클래스
상하좌우에 장식을 붙이는 클래스
동작 테스트용의 클래스
COUNT6
예제 프로그램 클래스 다이어그램
코드
Display.java
StringDisplay.java
Border.java
SideBorder.java
FullBoder.java
Main.java

역할

1. Component(예제의 Display)

기능을 추가할 때 핵심이 되는 역할. 원천객체가 해당되며 개요에서 예시를 토대로하면 Box가 해당되고, 예제에서는 DIsplay가 해당됩니다.
Component 역할은 인터페이스(API)만을 결정합니다.

2. ConcreteComponent(예제의 StringDisplay)

Component역할의 인터페이스(API)를 구현하고 있는 구현클래스입니다.

3. Decorator(장식자)(예제의 Border)

Component와 동일한 인터페이스(API)를 가지며, 또한 Decorator역할이 장식할 원천객체 대상도 가지고 있습니다(예제의 display 필드).

4. ConcreteDecorator(구체적인 장식자)(예제의 SideBorder와 FullBorder)

장식(Border)을 구현하는 클래스입니다.

클래스 다이어그램

참고

1. 투과적인 인터페이스(API)

Decorator 패턴에서는 장식과 내용물을 동일시합니다.
예제를 보면 내용물을 나타내는 Display클래스와 장식을 나타내는 Border 클래스의 하위 클래스로 되어있는 곳에서 동일하게 표시되고 있습니다. 그 말은 장식클래스( + concreteComponent)는 원천객체와 동일한 인터페이스(API)를 가집니다. 장식을 아무리 추가하여 기능이 추가되더라도 내용물의 인터페이스(API)는 전혀 감출 수 없습니다.
이러한 것을 인터페이스(API)가 투과적 이라고 합니다. 이렇게 인터페이스가 투과적이기 때문에 Decorator 패턴에서는 Composite 패턴과 닮은 재귀적인 구조가 등장합니다.
(Decorator 객체 안에 내용물(Object)가 있을수도 있고 또 다른 Decorator가 있을 수도 있는 구조) 이런 구조가 유사하지만 Decorator 패턴은 기능을 추가해나가는 것에 목적을 두기때문에 두 패턴은 목적이 다릅니다.

2. 내용물을 바꾸지 않고 기능의 추가가 가능하다.

Decorator 패턴에서는 장식(Decorator)과 내용물(Object) 둘 다 공통의 인터페이스(API)를 가집니다. 그렇지만 장식을 추가할수록 기능은 추가됩니다.
크리스마스 트리를 예로들면 나무(Tree)에 여러가지 장식(양말, 별, 지팡이 등...)을 추가할 수 있지만, 그렇다고 나무자체가 높이가 달라지고 색상이 바뀌고 하지는 않습니다.
즉, 내용물을 변경하지 않고 기능을 추가할 수 있습니다.
Decorator 패턴에서는 위임을 사용하고 있습니다. '장식' 에 대한 요구는 '내용물'에 위임됩니다. 예제 코드에서 장식클래스에 요청된 getColumns나 getRows() 안을 보면 display.getColumns(), display.getRows()를 호출해 내용물에게 위임하는것을 확인할 수 있습니다.

3. 동적인 기능을 추가할 수 있다.

Decorator 패턴에서 사용되는 위임은 클래스 사이의 결합을 낮추기 때문에 framework의 소스를 본경하지 않고 오브젝트의 관계를 변경한 새로운 오브젝트를 만들 수 있습니다. (ex: 치즈라는 장식(Decorator)을 케익에 추가하면 치즈케익, 라면에 추가하면 치즈라면이 될 수 있습니다.)

구현시 고려할 부분

Components는 원천객체로 베이스가 되기에 최대한 공통적인 부분만을 명세한 작고 가볍게 규정하는게 좋습니다.
상속 구조를 통해 Decorator와 Component가 같은 인터페이스(API)를 갖게 해야 합니다.
투과적 인터페이스: Decorator로 계속 감싸도 Component의 메소드를 사용할 수 있어야 합니다.
코드를 수정하지 않아도 Decorator를 조합해 기능을 추가할 수 있도록 구현합니다.
비슷한 성질의 작은 클래스가 많이 만들어질 수 있다는 단점을 고려합니다.
구현하려하는 내용이 장식이 아닌 내용을 변경하려고 하는것은 전략패턴(strategy pattern)이 더 적절합니다.
Decorator 간에 서로 침범이 일어나면 안됩니다.(각각의 장식이 서로의 대해서 몰라도 상관없어야 합니다.)

12. 방문자 패턴(Visitor Pattern)

개요

데이터 구조와 처리를 분리하는 패턴
데이터 구조 안에는 많은 요소가 저장되어 있고, 그 각 요소에 대해서 '처리'해 간다고 할 때 '처리' 부분은 어디에 정의되야 할까요? 평범하게 생각하면 데이터 구조를 표시하고 있는 클래스 내에 정의를 할 것입니다. 하지만 그 '처리'가 여러 종류라면 새로운 처리가 필요할 때마다 데이터 구조의 클래스를 수정해야 합니다.
Visitor Pattern에서는 데이터구조와 처리를 분리하여 데이터 구조 내부를 순회하는 방문자(Visitor)클래스를 준비해서 그 클래스에게 처리를 위임합니다.
이렇게 설계를 할 경우 새로운 처리를 추가할 때는 새로운 '방문자(Visitor)'를 만들면 됩니다. 그리고 데이터 구조영역에서는 순회하려는 '방문자'에 대해 접근을 허용하면 됩니다.

예제

Composite Pattern에서 사용된 Directory 구조의 예를 사용하여 파일과 디렉토리로 구성된 데이터의 구조 내부를 방문자 객체가 돌아다니며 파일의 종류를 표시하는 프로그램을 만들어봅니다.
Search
이름
해설
파일과 디렉토리를 방문하는 방문자를 나타내는 추상클래스
VIsitor 클래스의 생성자를 받아들이는 데이터 구조를 나타내는 인터페이스
Visitor 클래스의 하위 클래스로 파일과 디렉터리의 종류를 나타내는 클래스
File과 Directory의 상위 클래스가 되는 추상 클래스(Acceptor 인터페이스를 구현)
파일을 나타내는 클래스
디렉토리를 나타내는 클래스
File에 대해서 add한 경우 발생하는 예외 클래스
동작 테스트용 클래스
COUNT8
예제 클래스 다이어그램
Visitor는 '방문자'를 나타내는 추상클래스이고 자기가 방문할 곳(FIle, Directory)에 의존합니다.
VIsitor클래스는 File과 Directory를 가지는 두개의 메소드를 오버로드해서 가지며 위임의 역할을 할 필드가 됩니다.
코드
Visitor.java
Element.java
ListVIsitor.java
Entry.java
File.java
Directory.java
FileTreatmentException.java
Main.java
실행결과

참고 - Visitor 와 Element의 상호 호출

하나의 디렉토리에 두 개의 파일이 있다고 가정할 때의 시퀀스 다이어그램을 통해 알아봅시다.
1.
Main 클래스에서 ListVisitor 인스턴스 생성
2.
Main에서 Directory의 accept 메소드 호출. 인자값으로는 ListVIsitor 인스턴스 전달
3.
Directory 의 인스턴스에서는 인자로 받은 ListVisitor의 visit(Directory) 메소드 호출
4.
visit(Directory) 로직 내부에서 디렉토리 내부를 조사해 파일의 accept 메소드 호출하며 인자값으로 자기자신(this)를 전달. ( 자기자신은 ListVisitor)
5.
File의 인스턴스는 인자로 전달받은 ListVisitor의 visit(File)메소드를 호출합니다.
6.
5번 항목의 File의 visit(File)로직이 완료되면 다음 Iterator가 다음 File을 next()로 꺼내와서 두 번쨰 파일의 accept메소드를 호춣합니다(4~5과정과 동일)
7.
이전과 동일하게 visit(File)의 메소드를 호출하여 완료되면 자신을 호출한 곳으로 돌아가며 Main까지 돌아갑니다.

역할

1. Visitor(예제의 Visitor)

Visitor는 데이터 구조의 구체적인 요소(ConcreteElement 역할)마다 visit(xxxx) 메소드를 선언합니다. 메소드는 xxxx를 처리하기 위한 메소드이고, 구현은 ConcreteVisitor가 합니다.

2. ConcreteVisitor(예제의 ListVisitor)

Visitor 의 인터페이스(API)를 구현합니다. 예제의 ListVisitor에서 currentdir 값이 변화하듯 visit을 수행하는 도중 ConcreteVisitor 역할의 내부 상태가 변화하는 일도 있습니다.

3. Element(예제의 Element)

Visitor역할의 방문할 곳을 나타내는 역할로, 방문자를 받아들이는 accept 메소드를 선언합니다. 메소드의 인자값으로는 Visitor역할이 전달됩니다.

4. ConcreteElement(예제의 File or Directory)

Element역할의 인터페이스(API)를 구현하는 역할입니다. 예제에서 File이나 Directory에서 accept 메소드를 오버라이딩해서 구현한것과 동일합니다.

5. ObjectStructure(예제의 Directory)

Element역할의 집합을 취급하는 역할입니다. ConcreteVisitor 역할이 각각의 Element역할을 취급할 수 있는 메소드를 구비하고 있습니다.

클래스 다이어그램

참고 - 더블 디스패치(double dispatch)

Element역할은 accept(Visitor)메소드로 Visitor를 받아들이고 Visitor는 visit(element)메소드로 element를 visit합니다. 이처럼 Visitor Pattern에서는 ConcreteElement역할과 ConcreteVisitor 역할 한쌍에 의해 실제 처리가 결정되는데 이런 것을 더블 디스패치(double dispatch: 이중 분리)라고 합니다.

참고 - 왜 이렇게 복잡하게 설계를 하는가?

굳이 Visitor Pattern을 사용해서 기능과 처리를 분리하고 재귀적으로 호출하며 어렵게 설계를 해야하는 이유는 무엇일까요?
'처리'를 데이터 구조에서 분리를 하는게 Visitor Pattern의 핵심인데, 데이터 구조는 요소를 집합으로 정리하거나 요소 사이를 연결해주는 중요한 역할을 합니다.
하지만, 구조를 유지하는 것과 구조를 기초로 처리를 정의하는것은 별개입니다.
만약, 처리내용을 FIle이나 Diretory 클래스의 메소드로 구현할 경우 새로운 기능('처리')이 추가될 때마다 클래스를 수정해야하는데, 이러면 클래스의 독립성도 떨어지고 단일 책임 원칙도 위배하게 됩니다.

참고 - 확장에 대해서는 열고, 수정에 대해서는 닫는다.

확장에대해서는 열려있지만, 수정에 대해서는 닫혀있어야 한다는 'The Open-Closed Principle(OCP)' 원칙이 있습니다.
이는, 클래스를 설계할 때 특별한 이유가 없는 한 확장을 허용해야 한다는 것인데, 이것이 확장에 대해서는 열려있다는 의미기도 합니다.
하지만, 확장을 허용한다고 확장을 할 때마다 기존의 클래스를 수정하는것은 곤란한데, 확장을 해도 기존의 클래스는 수정할 필요가 없는 것이 '수정에 대해서는 닫혀있다' 라는 의미가 됩니다.
결국, 기존의 클래스를 수정하지않고 기능추가 확장이 되야한다는 말입니다. 개발을 하게되면 시간이 흐를수록 기능을 확장해야하는 경우가 빈번한데 그럴때마다 이미 완성된 클래스를 수정하는 것은 프로그램의 완성도(품질)를 떨어트릴 위험이 있습니다.

참고 - ConcreteVisitor역할의 추가는 쉽고, ConcreteElement역할의 추가는 어렵다.

ConcreteVisitor 역할을 추가하는것은 쉽습니다. 구조를 담당하는 클래스(ConcreteElement)역할을 수정할 필요가 없기 때문입니다.
반면, ConcreteElement 역할의 추가는 어려운데 그 이유는, 기존의 File과 Directory 클래스에 더해 새로운 Entry의 하위 클래스인 Device 클래스를 만들기 위해서는
기존에 구현되있는 모든 Visitor 클래스에 visitor(Device)메소드를 만들어야 하고 하위 클래스(ConcreteVisitor)에도 visit(Device)메소드를 구현해야 하기 때문입니다.

13.책임사슬 패턴(Chain of Responsibility Pattern)

개요

여러 객체를 연결해서 연결된 객체를 순회하며 요청을 처리할 객체를 결정하는 방법
책임이라고 할 수도 있고, 요청이라고 할 수 있는데, 사용자의 요청을 처리할 객체를 직접 결정할 수 없는 경우, 복수의 객체를 사슬(chain)처럼 연결해 두면, 그 오브젝트(객체)의 사슬을 차례로 돌아다니면서 목적한 객체를 결정하는 방법입니다.
해당 패턴을 사용하면 '요청하는 쪽'과 '처리하는 쪽'의 연결을 유연하게 해서 각 객체를 부품으로 독립시킬 수 있습니다. 또한 상황에 따라서 요청을 처리할 객체가 변하는 프로그램에서도 대응할 수 있습니다.
학교의 수업시간을 예로들면, 수학시간에 수학선생님이 문제풀이를 학생한테 시킬 경우 7번에게 풀라고 시킵니다. 근데 7번에 공부를 안해와서 풀지못하면 그다음 8번 , 9번, 10번순으로 (혹은 선생님이 정한 규칙) 다른 학생들에게 문제를 풀어야 할 '책임'이 주어지고 해당 문제풀이를 처리할 수 있는 학생이 나타나면 문제는 해결됩니다.
이것을 책임 사슬 패턴이라 부릅니다.

예제

Search
이름
해설
발생한 트러블을 나타내는 클래스, 트러블 번호(number)를 가진다.
트러블을 해결하는 추상 클래스
트러블을 해결하는 구상 클래스(항상 '처리하지 않는다')
트러블을 해결하는 구상 클래스(지정한 번호 미만의 트러블을 해결)
트러블을 해결하는 구상 클래스(홀수 번호의 트러블을 해결)
트러블을 해결하는 구상 클래스(특정 번호의 트러블을 해결)
Support들의 사슬을 만들고 트러블을 발생시키는 동작 테스트용 클래스
COUNT7
예제 프로그램의 클래스 다이어그램
코드
Trouble.java
Support.java
NoSupport.java
LimitSupport.java
OddSupport.java
SpecialSupport.java
Main.java
Sequence Diagram

역할

1. Handler(처리자)의 역할 (예제의 support)

Handler는 요구를 처리하는 인터페이스(API)를 결정하는 역할을 합니다. '다음 사람'을 준비해 두고 자신이 처리할 수 없는 요구가 나오면 그 사람에게 떠넘기기를 합니다.
물론, '다음 사람'도 Handler 역할입니다. 예제 프로그램에서는 SUpport 클래스가 이 역할을 합니다.

2. ConcreteHandler(구체적인 처리자)의 역할(예제의 xxxSuport)

요구를 처리하는 구체적인 역할을 합니다.

클래스 다이어그램

참고 - 요청자와 처리자간의 연결을 유연하게 하자.

해당 패턴(Chain of Responsibility Pattern)의 핵심은 요청자와 처리자간의 연결이 유연해야 한다는 것입니다.
요청자(Client)의 역할을 요청을 하는 것이기에 최초의 사람(객체)에게 요청을 하면 결과값만 기대하면 되지 그 내부로직이나 어떤사람이 해야하는지 알 필요가 없습니다.
만일 이 패턴을 사용하지 않으면 요청에 대해 '이 요청은 이 객체가 처리해야 한다'는 정보를 어디선가 종합적으로 가지고 있어야 합니다. 그리고 이 정보를 요청자(Client)가 가지는 것은 좋은 생각이 아닙니다. 요청자가 처리자들의 역할 분담정보까지 자세히 알아야 하면 부품의 독립성이 떨어지기 때문입니다.

참고 - 동적으로 사슬의 형태를 바꾼다.

예제에서는 Alice부터 Fred까지의 Support 들의 처리 순서가 고정되어 있습니다. 하지만 요구를 처리하는 ConcreteHandler역할의 오브젝트관계가 동적으로 변화하는 상황도 생각할 수 있습니다. Chain of Responsiblity Pattern과 같이 위임에 의해 떠넘기기를 실행하고 있으면 상황의 변화에 따라 ConcreteHandler역할을 재편할 수 있습니다.
해당 패턴을 사용하지 않고 특정 요구에 특정 처리라는 고정적인 관계가 정해지면 프로그램 실행 중 처리자 변경이 힘들어집니다.

참고 - 독립성확보 및 자신의 역할에 충실

ConcreteHandler가 떠넘기기를 통해 요청에 대해 다른 ConcreteHandler에게 떠넘긴다는 것은 자신은 자신이 할 수 있는 요청에 대해서만 집중하면 된다는 점입니다.
해당 패턴(Chain of Responsibility)을 사용하지 않을 경우 어느 한 객체에서 요구별 처리자를 모두 결정하는 방법으로 수행됩니다.
혹은, ConcreteHandler가 요청에대해 처리 여부를 판단 후 다른 객체에 위임하는 '일의 분담'까지 각각의 ConcreteHandler역할에서 구현되야 합니다.

참고 - 유연성과 지연성의 트레이드 오프

Chain of Responsibility Pattern을 통해 떠넘기기를 수행하면 객체간 독립성을 확보하며 로직의 유연성은 높아질 수 있지만, 떠넘겨지는 과정때문에 처리속도가 지연될 수 있습니다. 아무래도 미리 어느 처리자가 처리를 할 지까지 다 정해둔 경우와 비교해서 속도차이가 날 수밖에 없습니다.
그렇기에 우리는 요구와 처리간의 관계가 고정적이고 속도가 중요한지, 변동적이고 속도는 비교적 덜 중요한지 그때그때 상황에 맞게 패턴의 사용여부를 결정하면 됩니다.

14. 퍼사드 패턴(Facade)

개요

프로그램은 시간이 자날수록 커지게 됩니다. 간단한 회원가입에도 encrypt, 비밀번호 재확인, remember-me 부터 추가적으로 신기술에 나옴에 따라 클래스와 메서드 그리고 로직이 추가됩니다. 이런 프로그램에서 클래스를 사용할 때는 클래스 간의 관계를 정확히 파악 후 올바른 순서대로 메소드를 호출해야 합니다.
프로그램이 거대할수록 처리를 맡기려면 그안에 있는 많은 클래스를 적절하게 제어해야 합니다. Facade Pattern은 이런 처리를 실행하기 위한 '창구' 역할입니다.
Facade는 프랑스어의 façade를 어원으로 하는 '건물의 정면' 이라는 의미인데, 즉, 프로그램의 정면에서 요구를 받아서 전해주는 역할이라 할 수 있습니다.
Facade Pattern 은 복잡하게 얽혀 있는 것을 정리해서 높은 레벨의 인터페이스(API)를 제공합니다.
Facade 역할은 시스템 외부에는 단순한 인터페이스(API)를 보여주고 시스템 내부에 있는 각 클래스의 역할이나 의존관계를 생각해 정확한 순서로 클래스를 이용합니다.

예제

⇒ 웹페이지를 작성하는 프로그램
Search
패키지
이름
해설
Database
메일 주소에서 사용자 이름을 얻는 클래스
HtmlWriter
HTML 파일을 작성하는 클래스
PageMaker
메일 주소에서 사용자의 웹 페이지를 작성하는 클래스
Main
동작 테스트용 클래스
COUNT4
예제 클래스 다이어그램
코드
Database.java
HtmlWriter.java
PageMaker.java
Main.java
실행결과

역할

1. Facade(예제의 PageMater)

시스템을 구성하고 있는 그 밖의 많은 역할에 대한 창구 역할로써 외부에서 사용하기 편한 최대한 단순한 인터페이스(API)를 제공합니다.

2. 그외의 역할

시스템을 구성하는 다른 많은 역할들은 각각의 임무를 실행하지만 Facade에 대해 신경쓰지 않습니다.

클래스 다이어그램

참고 - Facade 역할은 무엇일까?

Facade는 복잡한 것을 단순하게 보여줍니다. 그렇다면 '복잡한 것'은 무엇일까요? 내부에서 실행되고 있는 많은 클래스의 관계 혹은 사용법이 '복잡한 것'을 말합니다.
결국 핵심은 인터페이스(API)를 적게하는 일입니다. 예제를 예시로 들더라도 HtmlWriter클래스의 모든 메소드가 인터페이스로 제공된다면 사용자는 매번 title 메소드를 시작으로 다른 메소드들을 각각 호출하며 페이지를 구성해야하고 심지어 title메소드를 먼저 써야한다는 제약조건을 모르면 문제가 발생하고 그 원인을 찾느라 디버깅을 해야합니다.
클래스 혹은 메소드가 너무 많이 오픈되어 제공되면 사용자는 무엇을 어떻게 어떤순서로 사용을 해야할 지 망설이게 됩니다. 망설인다는 것은 잘 못된 메소드나 클래스를 사용할 수 있다는 것이고 그렇기에 인터페이스(API)가 적은 Facade 역할을 생각합니다.

참고 - 재귀적인 Facade 패턴의 적용

클래스간의 창구역할인 Facade를 구현했다면 좀 더 생각을 확장하여 패키지간의 창구역할인 Facade역시 충분히 가능합니다.
Facade역할을 가진 클래스의 집합이 여러개 있다고 가정할 때 이 클래스의 집합을 정리해서 새로운 Facade 역할을 만들 수 있습니다. 즉, Facade Pattern을 재귀적으로 적용하는 것인데, 시스템의 규모가 크고 Facade패턴이 여러군데 적용되어 있을수록 편리해질 수 있습니다.

15. 중재자 패턴(Mediator)

개요

'사공이 많으면 배가 산으로 간다' 라는 말이 있습니다. 하나의 배에 사공이 10명이 있다고 하면 사공이 서로서로에게 자기주장을하고 지시를 하며 혼란한 상황이 발생합니다.
말 그대로 목적지로 가지못하고 이상한 산에 도착할 수 있죠. 이럴 때 중간에서 사공들의 의견과 요청을 종합하여 지시를 내려줄 중개인이 필요합니다.
이렇게 중개인이 있으면 사공은 모두 중개인에게 보고를 하고 중개인이 사원들에게 지시를 내릴 수 있게되면서 혼란이 사라지게 될 수 있습니다.
Mediator Pattern에서는 중개인을 mediator(조정자), 각 회원을 colleague(동료)라고 합니다.

예제

⇒ 로그인 다이얼로그를 만들업보시다. 해당 다이얼로그에서는 접속 유저가 Guest 인지 Login인지 선택이 가능하며 Usernamer과 Password를 입력할 수 있습니다.
요구사항
1.
게스트상태를 클릭하면 사용자명과 암호를 입력할 수 없도록 한다.
2.
로그인 상태를 클랙하면 사용자명과 암호를 입력할 수 있도록 한다.
3.
사용자명에 문자가 입력되지 않았을 때, 패스워드는 입력할 수 없다.
4.
사용자명에 문자가 하나라도 있으면 패스워드를 입력할 수 있다.
5.
사용자명 과 암호 양쪽이 다 입력되어 있어야만, OK버튼을 누를 수 있습니다.
6.
Cancel버튼은 언제나 누를 수 있습니다.
Search
이름
해설
'중개인'의 인터페이스(API)를 결정하는 인터페이스
'회원'의 인터페이스(API)를 결정하는 인터페이스
Colleague 인터페이스를 구현, 버튼을 나타내는 클래스
Colleague 인터페이스를 구현, 텍스트 입력을 실행하는 클래스
Colleague 인터페이스를 구현, 체크박스(여기선 라디오박스)를 나태는 클래스
Mediator 인터페이스를 구현, 로그인 다이얼로그를 나타내는 클래스
동작 테스트용 클래스
COUNT7
예제 클래스 다이어그램
예제 시퀀스 다이어그램
코드
Mediator.java
Colleague.java
ColleagueButton.java
ColleagueTextField.java
package mediator; import java.awt.*; import java.awt.event.TextEvent; import java.awt.event.TextListener; public class ColleagueTextField extends TextField implements TextListener, Colleague { private Mediator mediator; public ColleagueTextField(String text, int columns) throws HeadlessException { super(text, columns); } @Override public void setMediator(Mediator mediator) { this.mediator = mediator; } @Override public void setColleagueEnabled(boolean enabled) { setEnabled(enabled); setBackground(enabled?Color.white : Color.lightGray); } public void textValueChanged(TextEvent event) { mediator.colleagueChanged(); } }
Java
복사
텍스트의 내용이 변할 때 textValueChanged 메소드에서 캐치하기 위해서 TextListener 인터페이스도 구현하고 있습니다.
textValueChanged는 텍스트의 내용이 변했을 때 AWT의 프레임워크 쪾에서 호출됩니다.
ColleagueCheckbox.java
LoginFrame.java
Main.java
실행결과

역할

1. Mediator(예제의 Mediator)

Colleague역할과 통신을해서 조정을 실행하기 위한 인터페이스(API)를 결정합니다.

2. ConcreteMediator(예제의 LoginFrame)

Mediator역할의 인터페이스(API)를 구현해서 실제의 조정을 실행합니다.

3. Colleague(예제의 Colleague)

Mediator역할과 통신을 실행할 인터페이스(API)를 결정합니다.

4.ConcreteColleague(예제의 ColleagueButton, ColleagueTextField, ColleagueCheckbox)

Colleague역할의 인터페이스(API)를 구현합니다.

클래스 다이어그램

참고 - 분산이 화를 부를 때

예제를 보면 모든 변경에 대한 로직은 colleagueChanged 메소드로 집결되기 때문에 메소드 내부에 버그가 생길 수 있습니다. 하지만 이것은 큰 문제가 되지 않습니다.
예제에서 모든 요소에 대한 표시의 유효/무효에 대한 로직은 모두 colleagueChanged 메소드로 집결 되기 때문에 해당 메소드만 디버깅하면 됩니다.
하지만, 만약 로직이 ColleagueButton, ColleagueTextField, ColleagueCheckbox로 분산되어있으면 디버그할 범위만 넓어지게 됩니다.
OOP에서는 한 곳에 로직이 집중되는 것을 피해 분산시키는 경우가 많지만 이런 예제와 같은 경우 로직을 분산시키는것은 좋지 못합니다.
항상 분산시킬것과 집중시킬것을 잘 구분해 설계를 하지 않으면 오히려 생산성을 떨어트리고 디버그의 난이도를 높힐 수 있습니다.

16. 옵저버 패턴(Observer)

개요

관찰 대상의 상태가 변화하면 관찰자에게 알려주는 패턴.
객체 사이에 일 대 다의 의존 관계를 정의해 두어, 어떤 객체의 상태가 변할 때 그 객체에 의존성을 가진 다른 객체들이 그 변화를 통지받고 자동으로 갱신될 수 있게 만듭니다.

예제

Search
이름
해설
관찰자를 나타내는 인터페이스
수를 생성하는 오브젝트를 나타내는 클래스
랜덤으로 수를 생성하는 클래스
숫자로 수를 표시하는 클래스
간이 그래프로 수를 표시하는 클래스
동작 테스트용 클래스
COUNT6
예제 클래스 다이어그램
코드
Observer.java
NumberGenerator.java
RandomNumberGenerator.java
DigitObserver.java
GraphObserver.java
Main.java
실행결과

역할

1. Subject(관찰 대상자)(예제의 NumberGenerator)

관찰되는 대상을 나타내며 관찰자인 Observer역할을 등록 및 삭제하는 메소드를 가지고 있습니다. 또 '현재의 상태를 취득하는' 메소드도 선언되어 있습니다.

2. ConcreteSubject(구체적인 관찰 대상자)의 역할(예제의 RandomNumberGenerator)

ConcreteSubject는 구체적으로 '관찰되는 대상'을 표현하는 역할입니다. 상태가 변화하면 그것이 등록되어 있는 Observer역할에 전합니다.

3. Observer(관찰자)(예제의 Observer)

Observer는 Subject 역할로부터 '상태가 변했습니다' 라고 전달 받는 역할을 합니다. 이를 위한 메소드는 update입니다.

4. ConcreteObserver(구체적인 관찰자)(에제의 DigitObserver, GraphObserver)

ConcreteObserver는 구체적인 Observer입니다. update 메소드가 호출되면 그 메소드 안에서 Subject 역할의 현재 상태를 취득합니다.

클래스 다이어그램

참고 - 교환가능성의 등장

디자인 패턴의 목적 중 하나는 클래스를 재이용 가능한 부품으로 만드는 일입니다.
Observer Pattern에서는 상태를 가지고 있는 ConcreteSubject역할과 상태 변화를 전달 받는 ConcreteObserver역할이 등장했습니다. 그리고 이 두 역할을 연결하는것이 인터페이스(API)인 Subject역할과 Observer역할입니다.
RandomNumberGenerator클래스는 현재 자신을 관찰(구독)하고있는 것이 DigitObserver클래스의 인스턴스인지 GraphObserver클래스의 인스턴스인지 알 필요가 없습니다.
하지만, observers필드에 저장되어있는 observer들이 Observer 인터페이스를 구현하고 있다는것을 알고 있습니다. 그말은 Observer 인터페이스를 구현한다는 말이고 update메소드를 호출할 수 있다는 의미입니다.
반대로, 각각의 Observer구현체들 역시 자신이 관찰하고있는 것이 RandomNumberGenerator클래스의 인스턴스인지 다른 NumberGenerator 하위클래스의 인스턴스인지 신경쓸 필요 없이 NumberGenerator의 하위클래스의 인스턴스이고, getNumber메소드를 가지고 있다는 것만 알면 됩니다.
이처럼 추상 클래스나 인터페이스를 사용해서 구현 클래스로부터 추상 메소드를 분리하고, 인수로 인스턴스를 전달할 때, 필드에서 인스턴스를 저장할 땐 구현 클래스의 형태가 아닌 추상 클래스나 인터페이스의 형태로 해두면 교환이 쉬워집니다.

참고 - 순서

Subject역할에는 복수의 Observer 역할이 등록되어 있습니다. 예제에선 notifyObservers 메소드를 통해 먼저 등록한 Observer의 update가 먼저 호출됩니다.
일반적으로 ConcreteObserver역할의 클래스를 설계할 땐 update 메소드의 호출순서가 영향을 끼치면 안됩니다. 원래 각 클래스의 독립성이 보장되면 의존성의 혼란은 별로 발생하지 않습니다만, 다음과같은 상황에서는 주의를 해야합니다.

참고 - Observer의 행위가 Subject에 영향을 줄 때

만약, RandomNumberGenerator에서 notifyObservers가 호출되고, GraphObserver에서 update가 호출되는데 , 이 update 로직 도중 Subject에 영향을 주게된다면,
Subjects인 RandomNumberGenerator에서는 다시 notifyObservers를 호출하면서 무한 루프가 발생할 수 있습니다. 그렇기 때문에 Observer역할에게 '현재 Subject역할로부터 전달받고 있는중인지 아닌지'를 나타내는 플래그 변수를 하나 가지게 하는 것이 좋습니다.(ex: boolean isUpdate)

참고 - 갱신정보의 인자값 즉시 제공

예제에서는 number라는 필드에 값을 변경하면 Observer에서 getNumber를 통해 number값을 알아내어 사용했습니다. 하지만, 특정 상황에서(예제프로그램) update 메소드의 인수로 갱신된 수(number)자체를 제공해도 상관없습니다.
void update(NumberGenerator generator, int number);
Java
복사

참고 - '관찰' 보다는 '전달' (Publish-Subscribe Pattern)

Observer는 '관찰자'라는 의미이지만 능동적으로 '관찰'하는게 아닌 Subject역할로부터 '전달'되는 것을 수동적으로 기다리고 있습니다. 그래서 신문사가 신문을 발행하고 구독자고 발행된 신문을 구독해 받는것처럼 Publish-Subscribe 패턴이라고도 합니다.

참고 - Model/View/Controller(MVC)

MVC에서 Model과 View의 관계는 Observer 패턴에서 Subject 와 Object 역할의 관계에 대응합니다. Model은 '표시 형식에 의존하지 않는 내부 모델' 을 조작하고 View는 '어떻게 보일 것인지'를 관리하고 있는 부분입니다. 일반적으로 하나의 Model 에 복수의 View가 대응합니다.

17. 메멘토 패턴( Memento Pattern)

개요

⇒ 객체의 상태 정보를 저장하고 사용자의 필요에 의해 원하는 시점의 데이터를 복원할 수 있는 패턴
우리가 자주 사용하는 단축키중엔 ctrl + z 이 있습니다. 텍스트 에디터의 undo(실행취소 or 되돌리기) 기능인데, 내가 채팅창에 할 말을 작성하다가 undo 기능을 사용하면 작성전의 상태로 복원할수도있고 실수로 지웠을 때 역시 복원할 수 있습니다. 객체지향 프로그램에서 undo기능을 사용하기 위해선 인스턴스가 가지고 있는 정보를 저장해 둘 필요가 있습니다. 다만, 저장만 할게 아니라 저장한 정보로부터 인스턴스를 원래의 상태로 되돌려야 합니다.
여기서 고려해야할 부분이 있는데, 인스턴스를 복원하기 위해서는 인스턴스 내부의 정보를 자유롭게 접근할 수 있어야 하는데, 접속이 제한되야 하는 부분마저 접근이 허용되면
클래스 내부 구조에 의존한 코드가 프로그램의 여기저기로 흩어져 수정이 어려워지고, 이것을 캡슐화의 파괴라고 합니다.
이번장에서는 캡슐화의 파괴에 빠지지 않고 저장과 복원을 실행하는 Memento Pattern을 알아봅니다.

예제

과일을 모으는 주사위 게임을 만듭니다.
요구사항
1.
이 게임은 자동적으로 진행됩니다.
2.
게임의 주인공은 주사위를 던져 나온 수가 다음 상태를 결정합니다.
3.
좋은 수가 나오면 주인공의 돈이 증가합니다.
4.
나쁜 수가 나오면 돈이 감소합니다.
5.
특별히 좋은 수가 나오면 주인공이 과일을 받습니다.
6.
돈이 없어지면 종료합니다.
Search
패키지
이름
해설
Memento
Gamer의 상태를 나타내는 클래스
Gamer
게임을 실행하는 주인공의 클래스. Memento의 인스턴스를 만든다
Main
게임을 진행시키는 클래스. Memento의 인스턴스를 저장해 두고, 필요에 따라 Gamer의 상태를 복원한다.
COUNT3
코드
Memento.java
Gamer.java
Main.java
실행결과
예제 시퀀스 다이어그램

역할

1. Originator(작성자)(예제의 Gamer)

자신의 현재 상태를 저장하고 싶을 때 Memento역할을 만듭니다.
또한, 이전의 Memento역할을 전달받으면 그 Memento역할을 만든 시점의 상태로 돌리는 처리를 실행합니다.

2. Memento(스냅샷)(예제의 Memento)

Originator역할의 내부 정보를 가지고 누구에게도 공개하지 않으며 관리합니다.
Memento 역할은 다음 두 종류의 인터페이스(API)를 가지고 있습니다.
wide interface - 넓은 인터페이스(API)
⇒ 오브젝트 상태를 원래 상태로 돌리기 위한 정보를 모두 얻을 수 있는 메소드의 집합입니다. 넓은 인터페이스(API)는 Memento 역할의 내부 상태를 속속들이 들어내기 때문에 이것을 사용하는 것은 Originator역할 뿐이어야 합니다.
narrow interface - 좁은 인터페이스(API)
⇒ 외부의 Caretaker역할에게 보여주는 좁은 인터페이스(API)이며 할 수 있는 일에는 한계가 있고 내부 상태가 외부에 공개되는 것을 방지합니다.
이 두 종류의 인터페이스(API)를 구별해서 사용해 오브젝트 캡슐화의 파괴를 방지합니다.

3. Caretaker(관리인)(예제의 Main)

현재의 Originator역할의 상태를 저장하고 싶을 때 그것을 Originator에게 전합니다. Originator역할은 그것을 받아 Memento역할을 만든 뒤 Caretaker역할에게 전달합니다.
Caretaker역할은 미래에 필요에 대비해 그 Memento역할을 저장해 둡니다.
하지만, Caretaker역할은 Memento역할의 좁은 인터페이스(narrow interface api)만 사용할 수 있으므로 Memento 내부정보에 접근할 수 없습니다.
그저 Memento 역할을 한 덩어리의 블랙박스로 통쨰로 저장해 둘 뿐입니다.

클래스 다이어그램

참고 - 두 개의 인터페이스(API)와 액세스 제어

Memento 패턴에 등장하는 두 가지의 인터페이스(API)를 실현하기 위해 예제에서는 Java의 접근제어자를 사용했습니다.
예제프로그램에서는 이 접근제어자를 통해 narrow interface인 경우에는 public을 주고(ex: getMoney()) 그외에는 default 를 줬습니다.
그로인해 Caretaker역할에서는 getMoney를 통해 소지금을 얻어오는 것을 제외하고는 memento를 조작하거나 핸들링할 수 없게 됩니다. 더하여 public이 가장 접근 범위가 넓은데도 narrow interface 즉, 좁다고 얘기하는 이유는 범위가 단순히 다른 곳에서 접근이 가능하다는 의미가아닌, 내부상태를 조작할 수 있는 정도가 좁다는 의미입니다.
생성자를 통한 Memento역할의 생성도 되지않기 때문에 Gamer역할에게 부탁을 해야합니다. 이와 같이 액세스 제어를 이용해 캡슐화의 파괴를 막을 수 있습니다.

참고 - Memento의 갯수는?

예제에서는 1개만 가지고 있었습니다. 하지만 배열이나 컬렉션을 통해 여러개를 가지도록 할 수도 있습니다.

참고 - Caretaker와 Originator 역할을 분리하는 이유

어째서 Memento역할을 구현하여 undo기능을 구현해야할까요 Originator안에 바로 이전값들을 저장해도 되는데 말이죠.
Caretaker역할을 어느 시점에서 스냅샷을 찍을지를 결정하고 언제 undo를 할지를 결정하는 Memento역할을 저장합니다. 한편, Originator역할은 Memento역할을 만드는 일과 제공된 Memento 역할을 사용해서 자신의 상태를 원래 상태로 돌리는 일을 수행합니다.
즉, 서로 역할분담을 하고있는 것인데 이처럼 역할 분담을 하게 되면 여러 단계의 undo를 실행하고자 하거나, undo 기능 뿐 아닌 현재 상태를 파일에 저장하는 기능이 추가및 적용될 때도 Originator역할을 변경할 필요가 없어집니다.

18. 상태 패턴(State Pattern)

개요

'상태'를 클래스로 표현하는 패턴
객체(Object)를 표현함에 있어 구체적인 '사물'이 현실에 존재하는 경우도 있고 아닌 경우도 있습니다. 경우에 따라서는 이런게 클래스가 될수 있는가 싶은 것들도 클래스로 할 때가 있는데, State 패턴에서는 '상태'를 클래스로 표현합니다. state는 '상태(사물의 모양이나 형편)'를 의미합니다. 현실세계에서 우리는 다양한 사물의 '상태'에 대해서 생각합니다.
이번에는 '상태'를 클래스로 표현하여 클래스를 교체하며 상태의 변화를 표현하고 새로운 상태의 추가역시 합니다.

예제

시간마다 경비 상태가 변화하는 금고경기 시스템
요구사항
프로그램상의 1초를 현실 세계의 1시간으로 가정합니다.
금고는 1개가 있습니다.
금고는 경비센터와 접속되어 있습니다.
금고에는 비상벨과 일반통화용 전화가 접속되어 있습니다.
금고에는 시계가 설치되어 있어 현재의 시간을 감시하고 있습니다.
주간은 09:00~16:59, 야간은 17:00~23:59 및 0:00~8:59
금고는 주간에만 사용할 수 있습니다.
주간에 금고를 사용하면 경비센터에 사용기록이 남습니다.
야간에 금고를 사용하면 경비센터에 비상사태로 통보가 됩니다.
비상벨은 언제나 사용할 수 있습니다.
비상벨을 사용하면 경비센터에 비상벨 통보가 됩니다.
일반통화용의 전화는 언제나 사용할 수 있습니다(야간은 녹음만 가능)
주간에 전화를 사용하면 경비센터가 호출됩니다.
야간에 전화를 사용하면 경비센터의 자동응답기가 호출됩니다.
구성도
State Pattern을 사용하지 않는 일반적인 구조
경비시스템클래스 { 금고사용시_호출_메소드() { if(주간){ 경비센터에_이용_기록 } else if (야간) { 경비센터에_비상사태_통보 } } 비상벨_사용시_호출_메소드() { 경비센터에_비상벨_통보 } 일반_통화시_호출_메소드() { if (주간) { 경비센터의_호출 } else if (야간) { 경비센터의_자동응답기_호출 } } }
JavaScript
복사
위 의 클래스 설계역시 문제는 없습니다.
상태를 메소드내부에서 조사하고있습니다 (if(야간))
State Pattern은 상태를 메소드안에 기술하는 것을 클래스에서 기술하도록 구조를 변경합니니다.
Search
이름
해설
금고의 상태를 나타내는 인터페이스
State를 구현하고 있는 클래스, 주간의 상태를 나타낸다.
State를 구현하고 있는 클래스. 야간의 상태를 나타낸다.
금고의 상태변환을 관리하고 경비센터와 연락을 취하는 인터페이스
Context를 구현하는 클래스, 버튼이나 화면표시 등의 사용자 인터페이스를 갖는다.
동작 테스트용 클래스.
COUNT6
예제 클래스 다이어그램
예제 시퀀스 다이어그램
코드
State.java
DayState.java
NightState.java
Context.java
SafeFrame.java
Main.java
실행결과

역할

1. State(상태)(예제의 State)

상태를 나타냅니다. 상태가 변할 때마다 다르 동작을 하는 인터페이스(API)를 결정합ㄴ다. 이 인터페이스(API)는 상태에 의존한 동작을 하는 메소드의 집합이 됩니다.

2. ConcreteState(구체적인 상태)(예제의 DayState, NightState)

구체적인 각각의 상태를 표현하는 역할. State역할로 결정되는 인터페이스(API)를 구체적으로 구현합니다.

3.Context(상황, 전후관계, 문맥)(예제의 Context, SafeFrame)

현재의 상태를 나타내는 ConcreteState 역할을 가집니다. 또한 State Pattern 의 이용자에게 필요한 인터페이스(API)를 결정합니다.
좀 더 정확히는 Context인터페이스는 인터페이스(API)를 결정하는 부분을 담당하고, SafeFrame클래스가 ConcreteState역할을 가지는 부분을 담당합니다.

클래스 다이어그램

참고 - 분할해서 통치해라(divide and conquer)

복잡하고 규모가 큰 문제 혹은 프로그램은 그대로 해결하려하기보단, 우선 작은 문제로 나누고 또 나눠서 하나하나의 모듈들이 쉽게 해결가능한 수준까지 나눠 해결해야 합니다.
State Pattern에서는 각각의 '상태'를 각각의 클래스로 표현해 문제를 분할했습니다. 조건과 규모가 커질수록 조건은 많아지고 분기문은 많아집니다. 하지만 이번처럼 State Pattern을 사용하면 각각의 '상태'를 클래스고 표현해 복잡한 프로그램을 분할합니다.

참고 - 상태에 의존한 처리

SafeFrame 클래스의 setClock메소드는 Main클래스로부터 시간이 변경될 때(반복문의 순회)마다 호출되고 있습니다. 그리고 이 메소드는 시간을 인자값으로받아 출력 후 state.doClock(this,hour)으로 책임을 위임하고 있습니다. 즉, 시간의 설정을 '현재의 상태에 의존한 처리'로 취급하고 있다는 말입니다. 좀 더 설명하자면 state는 항상 같은값이 아닌 시간이 변경되서 기준을 충족할 때마다 주간과 야간 상태클래스로 변경하기 때문이죠.
이처럼 State Pattern에서는 '상태에 의존한 처리'를 프로그램에서 표현하는방법은 다음과 같습니다.
추상 메소드로 선언후 인터페이스 명세(API)
구현 메소드로 구현 후 각각의 클래스로 한다.

참고 - 상태전환은 누가 관리해야 하는가?

State Pattern에서 상태전환을 어디서 관리하는지에 대해서는 고려해봐야 할 부분입니다.
예제에서는 Context역할의 SafeFrame 클래스에서 상태전환 메소드(changeState)를 구현했습니다. 그리고 이 메소드를 호출하는 부분은 ConcreteState역할인 DayState나 NightState클래스입니다. 다시말하면 예제에서는 '상태전환'을 '상태에 의존한 동작'으로 간주하고 있습니다. 이 방법은 장점과 단점이 있습니다.
장점은 상태전환의 시기를 하나의 클래스 내에서 가지고 있다는 점입니다. (ex: DayState 클래스가 상태전환되는 시기는 DayState 클래스 코드를 보면 된다.)
단점은 하나의 ConcreteState역할이 다른 ConcreteState역할을 알아야 한다는 점입니다. (ex: doClcok메소드 context.changeState(NightState.getInstance());)
이것의 문제는 클래스 사이의 의존관계를 깊게한다는 점입니다.
그럼, 모든 상태전환을 Context 역할의 SafeFrame클래스에 맡길수도 있는데 이 경우 ConcreteState역할의 독립성이 높아지지만 Context가 모든 ConcreteState역할을 알아야하는 문제가 발생합니다. 해결책으로는 Mediator Pattern을 쓰거나 혹은 State Pattern대신 상태의 테이블(표)를 사용해 설계하는 방식이 있습니다.
(테이블은 '입력과 내부상태'를 기초로 '출력과 다음 내부상태'를 얻을 수있는 테이블)

참고 - 자기모순에 빠지지 않는다.

예제에서는 상태를 SafeFrame클래스의 state 필드로 관리했습니다. 이처럼 state 필드의 값이 시스템의 상태를 확실히 결정하기 때문에 자기 모순에 빠지지 않지만,
상태가 복수의 변수값의 집합으로 표현한다면 자기 모순 및 불균형이 일어날 수 있으니 주의해야 합니다.

참고 - 새로운 상태를 추가하는 것은 간단하다.

State 인터페이스를 구현하는 ConcreteState 클래스를 만들어 구현하면되기에 상태추가는 어렵지 않습니다.
다만, 상태전환은 다른 ConcreteState 역할과 접점이 되기에 주의해야 할 필요는 있습니다.
그리고 완성된 State Pattern에 새로운 '상태의존의 처리'를 추가하는 것은 곤란합니다. 이 말은 State 역할이 인터페이스에 메소드를 추가한다는 것을 의미하며 이를 구현하는 모든 ConcreteState역할에 구현이 필요하기 때문입니다. 아이러니하게도 이렇게 될 때 컴파일시점에서 구현이 안된 ConcreteState역할에서느 에러가 나기때문에 놓쳐서 구현을 못할 일은 없습니다.

19. 플라이웨이트 패턴(Flyweight Pattern)

개요

인스턴스를 공유시키면서 불필요한 인스턴스를 생성하지 않게 하는 패턴
권투에서 'Flyweight'급은 가장 체중이 가벼운 체급을 말합니다. 디자인 패턴에서 'Flyweight'역시 각각의 객체를 '가볍게'하는 기법인데, 객체는 실물이 존재하지않기 때문에 따로 무게가 있는것은 아니고 '메모리의 사용량'을 가볍게 한다는 의미로 쓰입니다.
자바에서는 new 키워드를 통해 인스턴스를 생성하는데, 이 인스턴스는 각각 메모리를 차지하게되고, 이 인스턴스가 많을수록 메모리의 사용량을 올라간다는 말인즉슨, 무거워진다는 말이 되겠죠. 그렇기에 인스턴스를 최대한 필요한 곳끼리 공유를 시켜서 인스턴스의 생성을 막아 메모리의 사용량을 가볍게 하는 기법입니다.

예제

큰 문자가 저장된 여러가지의 text파일들을 읽어 메모리에 저장 후 출력하는 프로그램을 만듭니다.
Search
이름
해설
'큰 문자'를 나타내는 클래스
BigChar의 인스턴스를 공유하면서 생성하는 클래스
BigChar를 모아서 '큰 문자열'을 나타내는 클래스
동작 테스트용 클래스
COUNT4
예제 클래스 다이어그램
코드
BigChar.java
BigCharFactory.java
BigString.java
Main.java
실행결과

역할

1. Flyweight(예제의 BigChar)

평소대로 취급하면 프로그램이 무거워지기 때문에 공유하는 것이 좋은 것을 나타내는 역할

2. FlyweightFactory(예제의 BigCharFactory)

FlyweightFactory역할을 만드는 공장의 역할로 Flyweight를 Factory에서 만들면 인스턴스가 공유됩니다.

3. Client(예제의 BigString)

FlyweightFactory 역할을 사용해서 Flyweight 역할을 만들고 그것을 이용하는 역할입니다.

클래스 다이어그램

참고 - 여러 장소에 영향을 미친다.

Flyweight의 가장 큰 특징은 인스턴스의 공유입니다. 동일한 클래스의 인스턴스의 중복생성을 막아 프로그램을 가볍게 하기 위함인데요. 이로인해 주의해야 할 점은 하나의 인스턴스를 여러 곳에서 공유되고 있기 때문에 인스턴스가 변경되면 여러 장소에 영향을 미친다는 것입니다. 이 부분은 장점과 단점이 공존합니다.
하나만 변경해도 전체가 변경되니 전체적으로 새로운 기능이 추가되거나(색상을 흰색→노랑색으로 바꾼다)해야 한다면 일일이 모든 인스턴스를 바꿀 게 아닌 이 인스턴스만 바꿔주면 되겠죠. 반대로 특정 메소드만 폰트의 색만 바꾸고 싶을 때 주의해야 할 부분이 늘어납니다. 이 경우 색의 정보를 BigChar가 아닌 BigString이 관리하게되면 3번째 출력되는 문자열은 노랑색으로 출력 하게끔 로직을 추가하면서 관리가 가능해집니다.
무엇을 선택할지는 그때그때 사용 목적에 따라 결정하며 Flyweight Pattern을 사용하여 클래스를 설계한 사람은 무엇을 공유 시킬지 고려해야 합니다.

참고 - intrinsic 와 extrinsic

Search
종류
설명
장소나 상황에 의존하지 않기 때문에 공유할 수 있다.
장소나 상황에 의존하기 때문에 공유할 수 없다.
COUNT2
이전 참고에서 얘기한 공유시키는 정보와 공유시키지 않는 정보에는 각각 이름이 붙어 있습니다.
공유시키는 정보는 intrinsic한 정보라 합니다. 이는 인스턴스를 어디서 가지고 있더라도 변하지 않고, 상태에 의존하지 않는 정보를 뜻합니다.
예제의 BigChar의 필드 값은 BigString의 어디에서도 변하지 않습니다. 그렇기 때문에 BigChar의 폰트데이터는 intrinsic한 정보가 됩니다.
다음으로 공유시키지 않는 정보는 extrinsic한 정보라 합니다. 인스턴스를 두는 장소나 상황에 따라 변하며, 상태에 의존하는 정보라는 의미이며 두는 장소에 따라 변하는 정보를 공유시킬 수는 없습니다. 예를 들어 , BigChar의 인스턴스가 BigString의 몇 번째 문자인가 하는 정보는 BigChar가 놓이는 장소에 따라 변하기에 BigChar에게 제공할 수 없습니다.
그래서 이 정보는 extrinsic한 정보가 됩니다.

참고 - 인스턴스와 garbage collection

BigCharFactory에서는 HashMap을 통해 pool 에서 BigChar의 인스턴스를 관리하고 있습니다. 이처럼 인스턴스를 관리하게되면 이 인스턴스들, 즉 관리되고 있는 인스턴스는 garbage collection되지 않는다는 점을 주의해야 합니다. garbage collection을 실행할 때 각 인스턴스는 garbage인지 아닌지 판정이 이뤄지지만, 이처럼 외부에서 참조되고 있는 인스턴스는 '사용중'으로 판단해 garbage로 판정되지 않습니다.
예제에서 pool 필드에 BigChar인스턴스들을 관리하고 있는데, 단, 한 번 혹은 몇 번 호출된 후 방치되고 있는 인스턴스역시 관리되고있는 인스턴스로 판정되어 garbage로 판정되지 않는다는 의미입니다. 예제에서는 짧은 시간 적은 인스턴스를 관리하고 종료되었기에 문제가 되지 않았지만, 오랜 시간 동작하거나 적은 메모리에서 동작하는 프로그램인 경우 이 점을 고려해야 합니다.
인스턴스를 명시적으로 삭제할 수는 없지만, 인스턴스에 대한 참조를 없애 방치할 수는 있습니다. 예를들어 HashMap으로부터 해당 인스턴스를 포함하는 엔트리를 삭제하면 참조가 해제되며 방치되고, garbage collection에 의해 처리됩니다.
gabage collection : Java 의 프로그램은 new에 의해 메모리를 확보하는데, 많은 메모리를 확보하다보면 메모리가 부족해지게되고 이 때JVM에서는 garbage collection 처리를 시작합니다. 이것은 자신의 메모리 공간(heap 영역)을 조사해서 사용되지 않는 인스턴스를 해제하여 메모리의 빈 영역을 늘리는 처리입니다. 이 기능 덕분에 Java 프로그래머는 new한 인스턴스를 방치해도 됩니다

20. 프록시 패턴(Proxy Pattern)

개요

필요해지면 만드는 패턴
'Proxy'의 의미는 '대리인' 입니다. 대리인은 말 그대로 누군가를 대신(대리)한다는 의미이며, 본인이 아니라도 가능한 일을 대리인이 할 수 있습니다. 하지만, 대리인의 역량을 벗어나는 일이 발생하면 본인이 와서 일 처리를 해야 합니다. 오브젝트(객체)지향에서는 '본인'도 '대리인'도 오브젝트(객체)가 됩니다. JPA의 Layloading과도 관련이 있는데, 매번 실제 DB를 조회해서 엔티티를 만들어 반환하는 것이 아닌 따로 쓸 일이 없는데 다른 엔티티에 묶여서 같이 조회되었지만, 따로 참조될일은 없는 경우 틀만 같은 프록시 객체만 있어도 됩니다. 그리고 이렇게 프록시 객체로 처리를 하게되면 불필요한 DB조회를 막아 성능도 올라갈 수 있습니다.

예제

이름을 가지는 프린터 프로그램을 만들어 봅니다.
프린터는 화면에 문자열을 표시하는 기능만 있고, 이름을 가지고 있으며, 이름의 set/get 시점에서는 실제 프린터 클래스의 인스턴스가 생성되지 않고 PrinterProxy에서 대신 처리합니다. 실제로 출력되는 시점에서 PrintProxy는 Printer 클래스의 인스턴스를 생성합니다.
Printer 클래스의 인스턴스 생성에 시간이 오래 걸린다는 것을 전제로 개발합니다.
Search
이름
해설
이름있는 프린터를 나타내는 클래스(본인)
Printer와 PrinterProxy 공통의 인스턴스
이름있는 프린터를 나타내는 클래스(대리인)
동작 테스트용 클래스
COUNT4
예제 클래스 다이어그램
예제 시퀀스 다이어그램
코드
Printer.java
Printable.java
PrinterProxy.java
Main.java
실행결과

역할

1. Subject(주체)(예제의 Printable)

Proxy 역할과 RealSubject 역할을 동일시하기 위한 인터페이스(API)를 결정합니다.
Subject 역할이 있는 덕분에 Client 역할은 Proxy 역할과 RealSubject 역할의 차이를 의식할 필요가 없습니다.

2. Proxy(대리인)(예제의 PrinterProxy)

Client 역할의 요구를 할 수 있는 만큼 처리를 합니다. 만약 자신만으로 처리할 수 없으면 Proxy역할은 RealSubject역할에게 처리를 맡깁니다.
Proxy역할은 정말 RealSubject역할이 필요해질 때 해당 역할을 생성합니다. Proxy역할은 Subject 역할에서 정해지는 인터페이스(API)를 구현합니다.

3. RealSubject(실제의 주체)(예제의 Printer)

'대리인'인 Proxy가 감당할 수 없는 일이 발생했을 때(예제의 print()) 등장하는 것이 RealSubject의 역할입니다. 이 역할도 Subject역할에서 정해져 있는 인터페이스(API)를 구현합니다.

4. Client(의뢰인)(예제의 Main)

Proxy Pattern을 이용하는 역할

클래스 다이어그램

참고 - 대리인을 사용해서 속도 올리기

Proxy Pattern에서는 Proxy 역할이 대리인이 되어 처리할 수 있는 일만 대신합니다. 예제에서는 강제로 5초를 지연시키는 작업뿐이기에, 체감이 잘 안될 수 있지만 규모가 거대한 시스템을 생각해보면 됩니다. 예를 들어, 축구팀이라는 Football_Team 클래스내부에 소속된 선수들이 12명이 있다고 합니다. 그럼, 여기서 이 축구선수에는 해당하는 팬들이 10만명이 될수도 100만명이 될 수도 있는데, 축구팀을 호출할 때마다 해당 팬들에 대한정보까지 다 가져오게되면 성능이 떨어질 수밖에없습니다. 정말 해당팀의 소속된 선수의 팬들정보를 집계및 조회할때만 실제로 RealSubject를 생성하면 됩니다. Proxy 패턴은 이런 경우에 힘을 발휘합니다.

참고 - 대리인과 본인을 분리할 필요가 있을까

PrinterProxy를 생성하지 않고 처음부터 Printer 클래스안에 지연평가의 기능을 넣을수도 있습니다. 하지만, 두 역할을 분리해서 만들면 개별적으로 수정할 수 있게되고 이는 State Pattern에서 말했던 분할해서 통치하라(Divide and Cunquer)와 일맥상통합니다.
그리고, 만일 지연평가를 일으키지 않고 즉시 생성하고싶다면 Printer클래스를 바로 new로 생성해도 됩니다. RealSubject와 Proxy는 모두 Subject를 구현하고 있기 때문에 Client는 안심하고 두 역할을 교체해가며 사용할 수 있습니다.

참고 - 투과적이란?

PrinterProxy클래스와 Printer 클래스는 같은 Printable 인터페이스를 구현합니다. 그렇기에 Client인 Main클래스는 실제로 호출하는 곳이 PrinterProxy이든 Printer 클래스이든 상관하지 않습니다. 이런 경우, PrinterProxy 클래스는 '투과적'이라고 할 수 있습니다. 사람이 백화점진열대의 진열장유리 뒤로 상품을 볼 수 있는 것처럼, Main 클래스와 Printer 클래스사이의 PrinterProxy클래스가 놓여있어도 문제가 되지 않습니다.

참고 - HTTP Proxy

HTTP Proxy는 HTTP서버(웹 서버)와 HTTP 클라이언트(웹 브라우저) 사이에서 웹 페이지의 캐싱(cashing)을 실행하는 소프트웨어입니다. 이것도 Proxy 패턴을 적용해서 생각 할 수 있습니다.
여기서는 페이지의 캐싱(cashing)에 대해 얘기해봅시다.
웹 브라우저가 어떤 웹 페이지를 표시할 때 일일이 원격지에 있는 웹 서버에 액세스해서 그 페이지를 가져오는 것이 아닌, HTTP Proxy가 캐쉬해서 어떤 페이지를 대신해서 취득합니다.
최신 정보가 필요하거나 페이지의 유효기간이 지났을 때 웹 서버에 웹 페이지를 가지러 갑니다.
Client역할 ⇒ 웹 브라우저
Proxy역할 ⇒ HTTP Proxy
RealSubject 역할 ⇒ 웹 서버

참고 - Proxy 종류

1.
Virtual Proxy(가상 프록시)
: 이번 챕터에서 소개한 Proxy 패턴. 정말 인스턴스가 필요한 시점에서 생성/초기화를 실행합니다.
2.
Remote Proxy(원격 프록시)
: RealSubject 역할이 네트워크의 상대 쪽에 있음에도 자신의 옆에 있는 것처럼(투과적으로) 메소드를 호출할 수 있습니다. (ex: Java의 RMI(Remote Method Invocation)
3.
Access Proxy
: Access Proxy는 RealSubject역할의 기능에 대해서 액세스 제한을 설정한 것. 정해진 사용자이면 메소드 호출을 허가하지만 그 외에는 에러로 처리하는 Proxy입니다.

21. 커맨드 패턴(Command Pattern)

개요

명령 혹은 요청 자체를 캡슐화 하는 패턴.
요청 자체를 객체로 캡슐화하여 인자값으로 여러 명령(Command)들을 수행시킬 수 있습니다. 더하여 로깅, Undo기능 역시 지원 가능합니다.

예제

간단한 그림그리기 프로그램을 만드어 봅니다.
마우스를 드래그하면 빨간 점의 열이 그려지고, clear 버튼을 누르면 초기화됩니다.
Search
패키지
이름
해설
Command
'명령'을 표현하는 인터페이스
MacroCommand
'복수의 명령을 모은 명령'을 표현하는 클래스
DrawCommand
'점 그리기 명령'을 표현하는 클래스
Drawable
'그리기 대상'을 표현하는 인터페이스
DrawCanvas
'그리기 대상'을 구현하는 클래스
Main
동작 테스트용 클래스
COUNT6
예제 클래스 다이어그램
코드
Command.java
MacroCommand.java
DrawCommand.java
Drawable.java
DrawCanvas.java
Main.java
실행결과
예제 시퀀스 다이어그램

역할

1. Command(명령)(예제의 Command)

명령의 인터페이스(API)를 정의하는 역할입니다.

2. ConcreteCommand(구체적 명령)(예제의 MacroCommand, DrawCommand)

Command 역할의 인터페이스(API)를 실제로 구현하고 있는 역할

3. Receiver(수신자)(예제의 DrawCanvas)

Command 역할이 명령을 실행할 때 대상이 되는 역할입니다. 명령을 받아들이는 수신자라고 불러도 됩니다.
예제에서 DrawCommand의 명령을 받아들이고 있는 것은 DrawCanvas입니다.

4.Client(의뢰자)(예제의 Main)

ConcreteCommand 역할을 생성하고, 그 사이에 Receiver 역할을 할당합니다. Main 클래스는 마우스의 드래그에 맞춰 DrawCommand의 인스턴스를 생성하지만, 그 사이에 Receiver 역할로 DrawCanvas의 인스턴스를 생성자에게 전달합니다.

5. Invoker(기동자)(예제의 Main, DrawCanvas)

명령의 행동을 개시하는 역할. Command역할에서 정의되는 인터페이스(API)를 호출하는 역할
예제에서는 Main과 DrawCanvas가 이 역할을 합니다. 두 클래스 모두 Command 인터페이스의 execute 메소드를 호출하고 있습니다.

클래스 다이어그램

시퀀스 다이어그램

참고 - 명령이 가지고 있어야 할 정보는?

예제인 DrawCommand에서 '명령'은 그림을 그리는 점의 위치정보를 가지고 있었습니다. 어떤 명령이냐에 따라 추가로 점의 크기나, 색 형태 등의 정보가 추가될 수도 있습니다.
이처럼 '명령'에 어느정도 어떤 정보를 가질지는 목적에 따라 다릅니다.
DrawCommand는 좌표값인 Position외에 Drawable 타입의 drawable 필드도 가지고 있습니다. 이는 그림을 그리는 대상(Receiver)를 가르키는데, 예제에서는 DrawCommand의 인스턴스도 하나일뿐이며 모든 그림그리기 동작은 이 인스턴스가 대응하기에 drawable 필드가 큰의미가 없습니다.
지만, 그림그리기의 대상(Receiver)가 하나 이상이 된다면 ConcreteCommand인 DrawCommand에서는 이와같은 필드(drawable)가 있음으로써 Receiver의 역할및 기능(API)을 알고 있기 때문에 ConcreteCommand역할을 어디서 누가 관리하더라도 execute 할 수 있습니다.

참고 - 어댑터

예제의 Main 클래스에서는 여러 인터페이스중 세 개의 인터페이스(마우스클릭, 마우스드래그, 윈도우종료)정도만이 구현되어 사용되거 나머지는 실제로 사용되지 않고 있습니다. 프로그래밍을 간결하기 위해 어댑터라는 클래스들이 java.awt.event 패키지에 준비되어 있습니다. (ex: MouseMotionListener ⇒ MouseMotionAdapter, WindowListener ⇒ WindowAdapter)
이 어댑터 클래스들은 각각 Listener를 구현하고 이 인터페이스가 요구하는 메소드를 모두 구현하고 있습니다.(아무 동작을 하지않는 default method로 정의되어있습니다.)
따라서 MouseMotionListener 클래스의 하위 클래스를 만들어 필요한 메소드만을 구현해서 목적을 달성할 수 있습니다.
+Java의 익명 내부 클래스(anonymouse inner class)라는 기구와 조합해서 어댑터를 사용하면 더 깔끔하게 구술할 수도 있습니다.
사용예

22. 인터프리터 패턴(Interpreter Pattern)

개요

프로그램이 해결하고자 하는 문제를 간단한 미니 언어로 표현하고, 구체적인 문제를 해당 미니 언어로 기술된 '미니 프로그램'으로 표현하는 기법.
미니 언어라는 문법을 클래스화 한 구조로써 미니 언어로 정의되있는 일련의 언어를 분석 및 파싱하는 패턴.
⇒ 사용 예: SQL 구문 해석(jdbcTemplate의 createQuery, shell command 해석기, 통신 프로토콜)
미니 프로그램은 그 자체로는 동작하지 않고 Java언어로 통역(Interpreter)역할을 하는 프로그램을 만들어서 분석을 해줍니다.
문법 규칙이 많아질수록 복잡해지고 무거워지기 때문에, 너무 많아진다 싶을 때는 컴파일러 생성기를 쓰거나 파서를 쓰는게 좋습니다.

미니언어

1. 미니 언어의 명령

무선조종 자동차를 예시로 미니 언어에 대해 간단하게 알아봅시다.
자동차의 기능 중 3가지인 전진, 우회전, 좌회전을 미니언어로 정의하면 아래와 같습니다.
GO : 앞으로 1미터 전진
RIGHT: 우회전
LEFT: 좌회전
그리고 미니 프로그램은 위와같은 명령을 조합해서 자동차를 움직이도록 합니다.
더하여 프로그램의 편의성을위해 동작을 반복하는 반복 명령어 REPEAT이 있다고 합니다.

2. 미니 프로그램의 예

미니 언어로 자동차를 4미터 전진후 다시 되돌아오는 프로그램을 구현한다고 하면 코드는 아래와 같습니다.
PROGRAM REPEAT 4 GO END REPEAT 2 LEFT END REPEAT 4 GO END END
Java
복사
프로그램의 시작과 끝을 알 수 있도록 PROGRAMEND로 구분을 합니다.
프로그램은 1미터 전진을 4회 반복 후 2회 좌회전을 하여 U턴을 한 뒤 다시 1미터 전진을 4회 반복하여 원점으로 돌아옵니다.

3. 미니 언어의 문법

미니프로그램의 예에서 정의한 4미터 전진 후 되돌아오는 프로그램을 BNF (Backus Normal Form)으로 표기해봅니다.
<program> ::= program <command list> <command list> ::= <command>* end <command> ::= <go command> | <uturn command> <go command> ::= repeat <number> go <uturn command> ::= <number> left
Plain Text
복사
<program> ::= program <command list> ⇒ 프로그램이라는<program>을 정의합니다. 그리고 뒤에 <command list>가 이어진 것으로 정의합니다.
<command list> ::= <command>* end ⇒ <command>가 *회 반복후 종료(end)합니다.
<command> ::= <go command> | <uturn command> ⇒ command는 <go command> 이거나 또는 <uturn command>중 하나라는 의미입니다.
<go command> ::= repeat <number> go ⇒ <go command>는 <number>회만큼 go를 반복합니다.
<utern command> ::= <number> left ⇒ <utern command>는 left를 <number>횟수만큼 반복합니다.

예제

미니 언어를 구문해석한 프로그램을 만들어 봅니다.
위에서 미니 프로그램의 예를 설명하며 각 부분을 분해해서 설명했습니다. 단지 문자열로 구성된 문자 프로그램을 분해해서 각 부분이 어떤 구조로 되어 있는지를 해석하는 것이 구문해석입니다.
미니 프로그램 구문 트리 - program repeat 4 go end repeat 2 left end repeat 4 go end end
Search
이름
해설
구문 트리의 '노드'가 되는 클래스
<program>에 대응하는 클래스
<command list>에 대응하는 클래스
<command>에 대응하는 클래스
<repeat command>에 대응하는 클래스
<primitive command>에 대응하는 클래스
구문해석을 위한 전후 관계를 나타내는 클래스
구문해석 중의 예외 클래스
동작 테스트용 클래스
COUNT9
예제 클래스 다이어그램
코드
Node.java
ProgramNode.java ⇒ [BNF] <program> ::= program <command list>
CommandListNode.java ⇒[BNF] <command list> ::= <command>* end
CommandNode.java
RepeatCommandNode.java ⇒ [BNF] <repeat command> ::= repeat <number><command list>
PrimitiveCommandNode.java ⇒[BNF]<primitive command> ::= go | right | left
Context.java
ParseException.java
Main.java
program.txt
실행 결과

역할

1. AbstractExpression(추상적인 표현)(예제의 Node)

구문 트리의 노드에 공통의 인터페이스(API)를 결정하는 역할입니다. 공통이 되는 API는 예제에서는 parse라는 메소드가 쓰였지만, interpret이라고 쓰일수도 있습니다.

2. TerminalExpression(종착점 표현)(예제의PrimitiveCommandNode)

BNF의 Terminal Expression에 대응하는 역할입니다.

3. NonterminalExpression(비종착점 표현)(예제의 ProgramNode, CommandNode, RepeatCommandNode, CommandListNode)

BNF의 Nonterminal Expression에 대응하는 역할입니다.

4. Context(문맥, 전후 관계)(예제의 Context)

인터프리터가 구문해석을 실행하기 위한 정보를 제공하는 역할입니다.

5. Client(의뢰자)(예제의 Main)

구문 트리를 조립하기위해서 TerminalExpression과 NonterminalExpression을 호출하는 역할을 합니다.

클래스 다이어그램