다른 챕터에 비해 이번 챕터는 내용이 많다ㅡ_ㅡ. 휴
우선 어떤 상황을 가정하겠다.
서로 다른 클래스 A, B에서 각자의 방식으로 구현된 배열들을 Clinet 에서 출력하는 코드를 작성할 때,
- A 클래스의 배열은 ArrayList, B 클래스의 배열은 기본 배열로 구현되었다고 예를 들겠다. -
다음과 같은 방법을 쓸 수 있다.
for(int i=0; i<arrayListItems.size(); i++){
MenuItem menuItem = (MenuItem) arrayListItems.get(i);
// 출력
}
for(int i=0; i<arrayItems.length; i++){
MenuItem menuItem = arrayItems[i];
// 출력
}
배열 타입 때문에 코드 자체는 조금 다르지만 명백하게 같은 일을 하고 있다.
이렇게 구상 클래스에 맞춰서 코딩하면 코드 중복 및 수정, no 캡슐화와 같은 폐해가 뒤따른다.
그래서 !
이를 해결하기 위해 '반복을 캡슐화'할 것이다.
1. Iterator 패턴
- Iterator (반복자) 라는 인터페이스를 통해 Array, List, HashTable 은 물론이고
어떤 종류의 컬렉션(또는 집합체, Aggregate)에 대해서도 반복자를 구현할 수 있다.
다음과 같이 인터페이스를 직접 만들어도 되고,
public interface Iterator {
boolean hasNext();
Object next();
}
아니면 java.util.Iterator 를 사용해도 된다. (위의 인터페이스에서 void remove()만 추가하면 된다)
참고로 ArrayList 의 메소드 중에는 반복자를 리턴하는 iterator() 라는 메소드가 있다.
하지만 기본 Array 에서는 iterator()가 없으니
아까 위에서 기본 배열 방식으로 구현한 B 클래스를 위한 IteratorB 클래스를 만든 후,
그 클래스에 java.util.Iterator 를 import 해서 Iterator 인터페이스를 구현해보겠다.
import java.util.Iterator;
public class IteratorB implements Iterator {
MenuItem[] arrayItems;
int position = 0;
public IteratorB(MenuItem[] arrayItems) {
this.arrayItems = arrayItems;
}
public Object next() {
MenuItem menuItem = arrayItems[position];
position = position + 1;
return menuItem;
}
public boolean hasNext() {
if ( position >= arrayItems.length || arrayitems[position] == null ) {
return false;
} else {
return true;
}
}
public void remove() {
if ( position <= 0 ) {
throw new IllegalStateException("can't remove");
}
if ( arrayItems[position-1] != null ) {
for( int i = position-1; i < (arrayItems.length-1); i++ ) {
arrayItems[i] = arrayItems[i+1];
}
}
}
}
Tip. remove()가 필요없다면 예외를 던질 수도 있다.
public void remove() {
throw new UnsupportedOperationException("Unsupprted Operation 'remove()'");
}
반복자를 구현했으면 이 반복자를 사용하기 위해 객체를 생성해줘야 한다.
ArrayList 로 구현한 A 클래스에서는
public Iterator createIterator() {
return arrayListItems.iterator();
}
Array 로 구현한 B 클래스에서는
public Iterator createIterator() {
return new IteratorB(arrayItems);
}
자, 반복자 패턴을 위한 멍석을 다 깔아놓았으니 Client 클래스 코드에서 출력 코드를 작성해보자.
public class Client {
A arrayListItems;
B arrayItems;
public Client ( A arrayListItems, B arrayItems ) {
this.arrayListItems = arrayListItems;
this.arrayItems = arrayItems;
}
public void printMenuItems() {
Iterator iteratorA = arrayListItems.createIterator();
Iterator iteratorB = arrayItems.createIterator();
printMenuItems(iteratorA);
printMenuItems(iteratorB);
}
public void printMenuItems(Iterator iterator) {
while ( iterator.hasNext() ) {
MenuItem menuItem = (MenuItem) iterator.next();
// 출력
}
}
여기서 끝난 게 아니다. 아직 Client 가 A, B 라는 구상 클래스를 참조하고 있다.
화룡점정으로 집합체 클래스인 A, B 의 인터페이스를 통일시켜주자.
아까 위에서 A, B 클래스 각자 createIterator() 를 구현했다.
public interface Aggregate {
public Iterator createIterator();
}
Aggregate 라는 인터페이스를 이처럼 작성하고
A, B 클래스 선언부에 implements Aggregate 만 추가해주면 통일끝.
그러면 Client 는 구상 클래스가 아닌 인터페이스를 참조할 수 있다.
public class Client {
Aggregate arrayListItems;
Aggregate arrayItems;
public Client ( Aggregate arrayListItems, Aggregate arrayItems ) {
this.arrayListItems = arrayListItems;
this.arrayItems = arrayItems;
}
// 이하 생략
}
휴, 힘들다. 코드가 여기저기 튀어나와 복잡하니 정리해보겠다.
클래스 다이어그램을 보자.
이 클래스 다이어그램으로 위에서 설명했던 것이 정리가 된다.
(참고로 Factory Method 패턴의 다이어그램과 상당히 유사한 형태이다)
* Iterator Pattern :
컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에
하나씩 접근할 수 있게 해주는 방법을 제공한다.
이것이 바로 반복을 캡슐화하는 패턴이다.
Iterator 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을
컬렉션 객체(Concrete Aggregate)가 아닌 반복자 객체에서 맡게된다는 장점이 있다.
이렇게 하면 컬렉션의 인터페이스 및 구현이 간단해지고,
컬렉션은 반복작업에서 손 떼고 컬렉션 관리에만 전념하면 된다.
* 단일 역할 원칙
- 어떤 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.
즉, 한 클래스에는 1가지 역할만 맡도록 해야 한다.
Iterator 패턴의 경우 반복작업과 컬렉션 관리를 신경쓰는 컬렉션을
관리라는 역할에만 충실할 수 있도록 도와주었으니 이 원칙을 준수한다.
+ 응집도(cohesion) :
한 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도.
응집도가 높다는 것은 일련의 서로 연관된 기능이 묶여있다는 것을,
응집도가 낮다는 것은 서로 상관 없는 기능들이 묶여있다는 것을 뜻한다.
※ 마무리 Tip ※
(1) HashTable 의 경우 iterator() 를 지원하는 자바 컬렉션이지만
① hashTableItems.iterator() -> (X)
② hashTableItems.values().iterator -> (O)
HashTable 이 <key, value> 로 구성되있다는 점을 떠올리면 ② 가 왜 옳은지 알 수 있다.
(2) JAVA 컬렉션 프레임워크에서는 ListIterator 라는 반복자 인터페이스도 제공하는데,
이 인터페이스에는 Iterator 의 메소드 외에도 previous() 를 비롯해 몇 개 더 추가되어 있다.
그러니 next() 때문에 한 방향으로만 접근해야 되는지에 대한 걱정은 안 해도 된다.
2. Composite Pattern
- Iterator 패턴과 Composite 패턴을 꼭 묶어서 생각할 필요는 없다 -
Iterator 패턴 덕분에 출력 코드를 for 문으로 도배하는 것을 막을 수 있었다.
하지만 Client 클래스의 printMenuItems() 를 보자.
public void printMenuItems() {
Iterator iteratorA = arrayListItems.createIterator();
Iterator iteratorB = arrayItems.createIterator();
print(iteratorA);
print(iteratorB);
}
새로운 Concrete Aggregate 가 추가될 때마다 여기에 코드를 추가해주어야 한다.
이는 'OCP(Open Closed Principle) 원칙 - 확장은 open, 코드 변경은 close -' 에 위배된다.
게다가 집합체(Aggregate) 안에 또 다른 집합체를 갖고 있을 경우 골치가 아파진다.
ex) array = { 1, 2, 3, {7, 8, 9} }
자, 더 유연한 방법으로 아이템에 대해서 반복작업을 수행할 수 있어야 한다.
리팩토링이 필요한 시기라 할 수 있다.
우선 골치 아파지기 전에 다음과 같이 Tree 구조를 사용하는 게 좋을 듯 싶다.
* Composite Pattern :
객체들을 트리(Tree) 구조로 구성하여 부분-전체 계층구조를 구현한다.
이 패턴을 이용하면 Client 에서 개별 객체와 복합 객체(Composite)를 똑같은 방법으로 다루도록 할 수 있다.
정의 내린 김에 Composite 패턴의 클래스 다이어그램도 보자.
복합 객체(Composite) 에는 구성요소(Component) 가 들어가있다.
구성요소는 2 종류로 나뉜다.
하나는 복합 객체, 다른 하나는 잎(Leaf)이다.
복합 객체에는 일련의 자식들(Children)이 있고, 그 자식노드 역시 복합객체 or 잎일 수 있다.
그리고 잘 알다시피 잎은 자식이 하나도 없다.
즉, 재귀적인 구조를 갖는다.
이제 패턴을 이용해서 아까의 문제로 돌아가서 적용해보자.
아래 다이어그램에서 웨이트리스(Waitress) 는 Client 다.
원래 책의 예제에서는 아까 언급했던 A, B 클래스가 서로 다른 음식점의 메뉴(Menu)에 해당한다.
MenuComponent 가
MenuItem(Leaf) 와 Menu(Composite) 에서 쓰이는 인터페이스 역할을 한다는 점을 기억해두자.
MenuComponent 의 기본 구현을 해보자면,
public abstract class MenuComponent
{
// Composite's Method
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException(); // body 는 이하 모두 동일
}
public void remove(MenuComponent menuComponent) { }
public MenuComponent getChild(int i) { }
// Leaf, Composite's Method
public String getName() { }
public String getDescription() { }
public double getPrice() { }
public boolean isVegitarian() { }
public void print() { }
}
MenuItem 은 위 클래스 다이어그램 만으로도 충분하므로 Menu 코드만 보겠다.
Menu 코드에서 print() 를 자세히 보면 재귀적인 방법으로 출력하는 것을 볼 수 있다.
public class Menu extends MenuComponent
{
ArrayList menuComponents = new ArrayList();
// 생성자 코드
// 기타 메소드
public void print() {
Iterator iterator = menuComponents.iterator();
while ( iterator.hasNext() ) {
MenuComponent menuComponent = (MenuComponent) iterator.next();
menuComponent.print();
}
}
Client 코드는 간단하다.
public class Client
{
MenuComponent allMenus;
public Client (MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void print() {
allMenus.print();
}
}
테스트 코드에서는
MenuComponent allMenus = new Menu(); 를 선언하고
여러 메뉴 항목들을 만들어 allMenus 에 추가 한 다음(add()),
Client client = new Client(allMenus); -> client.print(); 하면 말끔하다.
Q. Composite 패턴에서 계층 구조 관리와 메뉴와 관련된 작업을 처리하는데,
단일 역할 원칙에 위배되는 건 아닌가?
- Composite 패턴은 단일 역할 원칙을 깨지만 대신에 투명성(Transparency)을 확보하는 패턴이다.
Component 인터페이스에 계층 구조 관리 기능과 잎(Leaf)의 기능을 전부 집어넣음으로써
Client 에서 복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있도록 할 수 있다.
어떤 것이 복합 객체고 잎 노드인지가 Client 입장에서는 투명하게 느껴진다.
2가지 기능을 하다보니 안정성은 약간 떨어지나
(Client 입장에서는 구분을 못 하므로 어떤 객체에 부적절한 메소드를 호출하는 일이 있을 수 있다)
만약 쪼개서 디자인한다면 투명성이 떨어져 코드에 조건문, instanceof 를 써야할 수도 있다.
모든 디자인 원칙을 준수하는 건 어렵기 때문에 상황에 따라 원칙을 적절하게 사용해야한다.
* 복합 객체(Composite) 에 대한 반복자(Iterator) 구현
끝난 게 아니다. createIterator() 가 남아있다.
인터페이스인 MenuComponent 에 이 메소드를 추가할 것이다.
그러면 Menu 와 MenuItem 클래스에서 추가로 구현하자.
Menu 는
public Iterator createIterator() {
return new CompositeIterator(menuComponents.iterator());
}
MenuItem 는 NullIterator 객체를 리턴하는데
NullIterator는 Iterator 를 구현한 클래스이고 Command 패턴에서 봤던 Null 객체와 같은 개념이다.
public Iterator createIterator() {
return new NullIterator();
}
마지막 코드다. CompositeIterator 클래스이다.
import java.util.*;
public class CompositeIterator implements Iterator
{
Stack stack = new Stack();
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
public Object next() {
if (hasNext()) {
Iterator iterator = (Iterator) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();
if (component instanceof Menu) {
stack.push(component.createIterator());
}
return component;
} else {
return null;
}
}
public boolean hasNext() {
if (stack.empty()) {
return false;
} else {
Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
재귀때문에 조금 빡칠 수도 있다.
이해하고 보면 별 것도 아니니 인내를 가지고 이해하자.
- Composite 패턴의 가장 큰 장점은 클라이언트를 단순화 시킬 수 있다는 점이다.
아까 투명성 얘기에서 말했다시피 클라이언트는 복합 객체인지, 잎 객체인지 신경쓰지 않아도 된다.
올바른 객체에 대해서 올바른 메소드를 호출하고 있는지 확인하려고
여기저기 if 문을 사용하지 않아도 된다는 이야기이다.
그리고 메소드 하나만 호출하면 전체 구조에 대해서 반복해서 작업을 처리할 수 있는 경우도 자주 있다.
- 복합 구조가 너무 복잡하거나 복합 객체 전체를 Traveling 하는 데
너무 많은 자원이 필요한 경우에는 복합 노드를 Caching 해두는 것도 도움이 된다.
예를 들어 복합 객체에 있는 모든 자식노드에서 어떤 계산을 하고, 그 모든 자식노드들에 대해서
반복적인 작업을 수행해야 한다면 계산 결과를 임시로 저장하는 캐시를 만들어 속도를 향상시킬 수 있다.
* 객체지향의 원칙 :
1) 바뀌는 부분은 캡슐화 한다.
2) 상속보다는 구성을 활용한다.
3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.
4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).
6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).
7) 친한 친구들하고만 이야기한다(최소 지식 원칙).
8) 먼저 연락하지 마세요. 저희가 연락 드리겠습니다(헐리우드 원칙).
9) 어떤 클래스가 바뀌게 되는 이유는 한 가지 뿐이어야만 한다(단일 역할 원칙).
* 이 장에서의 정리 :
- Iterator 패턴 -
. 반복자(Iterator)를 이용하면 내부 구조를 드러내지 않으면서도 클라이언트로부터
컬렉션 안에 들어있는 모든 원소들에 일일이 접근하도록 할 수 있다.
. 집합체에 대한 반복 작업을 별도의 객체로 캡슐화 할 수 있다.
. 컬렉션에 모든 데이터에 대해서 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있다.
- Composite 패턴 -
. 개별 객체와 복합 객체를 모두 담아둘 수 있는 구조를 제공한다.
. 클라이언트에서 개별 객체와 복합 객체를 동일한 방법으로 다룰 수 있다.
. 복합 구조는 구성 요소로 이루어진다. 구성요소에는 복합 객체와 잎 노드가 있다.
. 상황에 따라 투명성(Transparency)과 안전성 사이에서 적절한 평형점을 찾아야 한다.
# Pattern 짧게 복습 #
- Observer : 어떤 상태가 변경되었을 때 일련의 객체들에게 이를 알려준다.
- Adapter : 하나 이상의 클래스의 인터페이스를 변환한다.
- Facade : 일련의 클래스들에 대한 인터페이스를 단순화시킨다.
- Iterator : 컬렉션의 구현을 드러내지 않으면서도 컬렉션에 있는 모든 객체들에 대해 반복 작업이 가능하다.
- Composite : 클라이언트에서 객체 컬렉션과 개별 객체를 같은 방식으로 처리할 수 있다.
(이미지 및 기타 출처 :
http://stevenjsmin.tistory.com/77
http://sadiles.blog.me/10110397609
http://blog.lukaszewski.it/2013/11/19/design-patterns-composite/
http://blog.lukaszewski.it/2013/10/14/design-patterns-iterator/
http://www.cnblogs.com/zhuqiang/archive/2012/04/27/2474233.html
http://www.cnblogs.com/zhuqiang/archive/2012/05/03/2481288.html)
P.S. 이번 포스팅 겁나 힘들다 흑ㅠ_ㅠ
RECENT COMMENT