반복자 패턴
상황 예시: 아침 메뉴와 점심 메뉴 합병
아침 메뉴 : List<MenuItem> breakfastItems
점심 메뉴 : MenuItem[] lunchItems
아침 메뉴와 점심 메뉴를 모두 출력하고 싶은 상황
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem item = breakfastItems.get(i);
System.out.println(item.getName());
}
for (int i = 0; i < lunchItems.length; i++) {
MenuItem item = breakfastItems[i];
System.out.println(item.getName());
}
각 메뉴는 서로 다른 구현 방식을 사용하고 있으므로, 2개의 서로 다른 순환문을 사용해야 함
따라서 반복을 캡슐화하기 위해 요소 접근 방식 인터페이스를 통일
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
public MenuItem next() {
MenuItem menuItem = items[position];
position = position + 1;
retur menuItem;
}
public boolean hasNext() {
// ..
}
}
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
// ...
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
public void printMenuWithIterator() {
LunchMenuIterator lunchIterator = lunchMenu.getIterator()
BreakfastMenuIterator breakfastIterator = breakfastMenu.getIterator()
System.out.println("아침 메뉴")
printMenu(breakfastIterator)
System.out.println("점심 메뉴")
printMenu(lunchIterator)
}
public void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
// ...
}
}
- 각 자료구조로 구현된 컬렉션읜 순회를 Iterator 인터페이스로 구현 → 구상 클래스에 대한 의존성 제거
- 따라서 어떤 컬렉션이든 반복자만 구현하면 1개의 순환문으로 처리 가능
자바와 코틀린에서 List 인터페이스는 iterator() 를 기본 제공. 그러나 배열은 기본으로 제공되는 iterator 가 없다.
따라서 배열의 경우에는 직접 Iterator 구현체를 만들어서 구현해주면 됨
public class DinerMenuIterator implements Iterator<MenuItem> {
// ...
}
클라이언트에서 메뉴에 들어있는 항목의 반복자를 획득할 수 있게 해주는 메뉴 인터페이스 통일
public interface Menu {
public Iterator<MenuItem> createIterator();
}
- 아침 메뉴와 점심 메뉴 모두 위 인터페이스를 구현하여, iterator 를 생성할 수 있는 메서드를 구현하도록 강제할 수 있음
컬렉션
객체를 모아 놓은 것. 다양한 자료구조에 컬렉션을 보관할 수 있는데, 어떤 자료구조를 사용하든 결국 컬렉션은 컬렉션
컬렉션을 집합체(aggregate) 라고도 부름
반복자 패턴
- 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공
- 컬렉션 객체 안에 들어있는 모든 항목에 접근하는 방식이 통일되어 있으면 종류에 관계없이 모든 집합체에 사용할 수 있는 다형적인 코드를 만들 수 있음
- 또한 모든 항목에 접근하는 작업을 컬렉션 객체가 아닌 반복자 객체가 맡게됨 → 집합체는 객체 컬렉션 관리에 전념, 반복자를 통해 항목 접근 수행 → 집합체 인터페이스와 구현이 간단해짐
위 예시에서 보았던 클래스와 매칭
- Aggregate = Menu
- ConcreteAggregate = breakfastMenu, lunchMenu
- Iterator = Iterator<MenuItem>
- ConcreteIterator = LunchMenuIterator, BreakfastMenuIterator
Client 는 반복자 덕분에 구상 클래스로부터 분리되었음 → 각 메뉴가 어떤 자료구조로 구현되었는지 신경 쓸 필요가 없음. 따라서 메뉴 인터페이스와 반복자만 신경쓰면 됨 → "구현보다는 인터페이스에 맞춰서 프로그래밍" 원칙 성립!
ConcreteAggregate 에는 객체 컬렉션이 들어 있으며, 그 안에 들어있는 컬렉션을 Iterator 로 리턴하는 메서드를 구현
ConcreteIterator 에서 각 메뉴의 반복자를 만들어 줌
특징
- 외부 반뽁자를 사용한다.
- 반복자에는 특별한 순서가 정해져 있지 않아서, 접근 순서는 사용된 컬렉션의 특성 또는 구현 방식에 따라 달라진다.
- 집합체의 Iterator 인터페이스에 다른 기능을 추가하고 싶다면, Iterator 인터페이스를 확장해서 쓰면 됨
- 모든 Collection 은 Iterable 인터페이스를 구현한다.
- Iterable 은 Iterator 인터페이스를 구현하며, 반복자를 리턴하는 iterator() 제공
- 그 외에도 forEach(), spliterator() 메서드 구현
모든 컬렉션에서 Iterator 제공 + forEach 호출 가능 + 향상된 for 문 사용 가능(e.g for(MenuItem item: menus) { } )
단일 역할 원칙
어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.
- 클래스를 고치는 일은 최대한 피해야 함 → 코드 변경 시 여러 문제가 발생 가능
- 코드를 변경할 만한 이유가 2가지가 되면, 그만큼 클래스를 나중에 고쳐야 할 가능성이 커짐
- 따라서 하나의 역할은 하나의 클래스에서만 맡아야 한다.
- = 이것을 응집도가 높다고 말할 수 있다.
- 이 원칙을 제대로 지키려면 디자인을 열심히 살펴보고, 시스템이 커짐에 따라 클래스가 바뀌는 부분의 역할이 2가지 이상이 아닌지 생각해봐야 함
응집도(Cohesion)
한 클래스 또는 모듈이 특정 목적이나 역할을 얼마나 일관되게 지원하는지를 나타내는 척도
응집도가 높다는 것 = 연관된 기능이 묶여 있다는 것
응집도는 단일 역할 원칙보다 좀 더 광범위한 용도로 쓰임
=> 응집도가 높다 = 서로 연관된 기능을 수행한다 = 단일 책임 원칙을 지킨다.
컴포지트 패턴
상황예시: 기존 메뉴 안에 서브 메뉴를 추가
반복자 패턴을 사용했지만, 반복자를 여러 개를 사용하여 각 메뉴를 출력해야 하는 문제가 여전히 남아 있음
그러려면 먼저 메뉴, 서브메뉴, 메뉴 항목 등을 모두 넣을 수 있는 트리 형태의 구조가 필요함 → 즉, 아이템을 대상으로 반복 작업을 수행할 수 있어야 함
컴포지트 패턴
객체를 트리구조로 구성해서 부분-전체 계층구조를 구현 → 클라이언트에서 개별 객체(leaf)와 복합 객체(composite)를 똑같은 방법으로 다룰 수 있다. 즉, 클라이언트를 단순화시킬 수 있다는 것이 가장 큰 장점.
객체들을 모아서 관리할 때 매우 유용하게 사용 가능
개별 객체(leaf)
- 자식이 없는 원소
- 안에 들어있는 원소의 행동을 정의
- 복합 객체의 원소에 해당하는 행동을 구현
public class MenuItem extends MenuComponent {
String name;
String description;
boolean vegetarian;
double price;
public MenuItem() { ... }
public getName() { ... }
public String getDescription() { ... }
public double getPrice() { ... }
public boolean isVegetarian() { ... }
public void print() { ... }
}
복합 객체(composite)
- 자식이 있는 원소
- 자식이 있는 구성 요소의 행동을 정의, 저장
- 개별 객체 뿐만 아니라, 다른 복합 객체를 저장할 수 있음
- 출력 시 가지고 있는 각 객체들에게 출력 책임을 넘김
- 복합 객체 내 복잡한 계산 결과를 임시로 저장하는 캐시를 사용할 수도 있음
public class Menu extends MenuComponent {
List<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
String name;
String description;
public Menu() { ... }
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public void print() {
for (MenuComponent menuComponent: menuComponents) { // 향상된 for 순환문: 반복자 패턴 적용
menuComponent.print();
}
}
public getName() { ... }
public String getDescription() { ... }
}
객체 인터페이스(Component)
- 개별 객체와 복합 객체 모두에 적용되는 공통 인터페이스
- 개별 객체와 복합 객체를 똑같은 방법으로 처리 가능
- 자기 역할에 맞지 않는 상황을 기준으로 예외를 던지도록 기본 구현 제공
- 관점에 따라 예외를 발생시키지 않고 로직으로 풀어쓸 수도 있다
- e.g. 개별 객체는 하위 객체를 0 개 포함하는 복합 객체로 볼 수 있다.
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
최상위 클래스(복합 클래스 활용)
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
- 최상위 메뉴 구성 요소만 넘겨주면, 전체 메뉴에 대한 출력 가능
정리
위 예에서 봤듯이, 컴포지트 패턴은 개별 객체의 로직과, 개별 객체를 관리하는 복합 객체의 로직을 모두 포함하고 있다.
이는 복합 객체 클래스가 두 가지 책임을 지닌다고 볼 수 있는데, 단일 역할 원칙을 깨는 대신 투명성을 확보하는 패턴이라고 할 수 있다. 즉, 어떤 원소가 복합 객체인지 개별 객체인지를 구분하지 않기 때문에 클라이언트 입장에서는 투명하게 보인다는 것이다. 덕분에 두 객체 모두를 일관적으로 처리할 수 있게 된다. 그러나 클라이언트가 어떤 원소를 대상으로 무의미하거나 부적절한 작업을 처리할 수도 있기 때문에 안전성은 약간 떨어진다.
이런 문제는 디자인상의 결정 사항에 속하는데, 다른 방향으로 디자인하여 각 방법에 따라 얻고자 하는 내용이 달라질 수 있다. 예를 들어, 안전성을 조금 더 추구한다면 여러 역할을 서로 다른 인터페이스로 분리하여 사용할 수 있는데, 그렇게 되면 투명성이 떨어지게 되고, 코드에서 조건문이라든가 instanceof 연산자 같은 것을 추가로 사용해야 한다.
따라서 원칙을 상황에 따라 적절하게 사용하는 것이 중요하다.
디자인 원칙에서 제시하는 가이드를 따르되, 그 원칙이 디자인에 어떤 영향을 끼칠지 항상 고민해보고 적용해보자.
→ 영향도에 따라 원칙을 선택 취사하자
참고: 컴포지트 패턴 과 데코레이터 패턴
둘 다 재귀적인 합성에 의존하여 하나 또는 불특정 다수의 객체들을 정리하는 패턴이다.
컴포지트 패턴
- 자신의 자식들의 결과를 요약 (퍼사드 느낌?!)
데코레이터 패턴
- 자식 컴포넌트가 하나만 존재
- 래핑된 객체에 추가 책임들을 추가
'Read Book > 헤드퍼스트 디자인 패턴' 카테고리의 다른 글
11장. 프록시 패턴 (0) | 2023.07.02 |
---|---|
10장. 상태 패턴 (0) | 2023.07.02 |
8장. 템플릿 메서드 패턴 (0) | 2023.05.14 |
7장. 어댑터 패턴과 퍼사드 패턴 (1) | 2023.05.14 |
6장. 커맨드 패턴 (0) | 2023.04.18 |