Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- leetcode
- 회사폐업
- 순열
- 알고리즘
- 자취준비
- 사회초년생
- 정보처리기사개정
- 실업급여
- 취업사실신고
- HeadFirstDesignPatterns
- 후니의쉽게쓴시스코네트워킹
- 생애첫계약
- 코딩테스트
- 튜터링
- 부분합알고리즘
- C++
- 동적계획법
- array
- 네트워크
- 전화영어
- 청년내일채움공제
- 캡쳐링
- IT기초
- 후니의쉽게쓴시스코라우팅
- 모여봐요동물의숲
- 실업인정인터넷신청
- 자료구조
- 프로그래머스
- 막대기자르기
- 정보
Archives
- Today
- Total
따봉도치야 고마워
Head First Design Patterns : (9)이터레이터와 컴포지트 패턴 본문
이터레이터(Iterator) 패턴
- 컬렉션 구현 방법을 노출시키지 않으면서도 모든 항목에 접근할 수 있게 해주는 방법을 제공 (반복 작업 캡슐화)
- 또한 각 항목에 접근하는 기능을 집합체가 아닌 반복자 객체에서 책임져 집합체 인터페이스 및 구현이 간단해짐
+ 컬렉션과 반복자
: 자바 Collection 인터페이스에선 집합체를 조작하기 위한 여러가지 유용한 메소드들을 포함하고 있음
: 그 중 하나가 iterator()
객체 마을 식당과 팬케이크 하우스의 합병
- 식당은 (점심) 메뉴를 ArrayList로, 하우스는 (아침메뉴를) 배열로 저장하고 있음.
- 두 가게의 메뉴 구현 방식이 달라 메뉴를 출력하고, 구별하는 웨이트리스 클래스를 만드는데 어려움이 생김
- 각 타입에 맞춰 두 번씩 반복해야 함 (식당이 추가되면 그만큼 더 반복)
- 각각에 대한 똑같은 처리를 위한 인터페이스를 구현한다면? Iterator
- 식당 메뉴와 하우스 메뉴에 대해 사용할 수 있는 구상 Iterator 클래스 생성 및 적용
//식당 메뉴에 대한 iterator 클래스
public class DinerMenuIterator implements Iterator{
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items){
this.items = items;
}
public Object next(){
MenuItem menuItem = items[position];
position ++;
return menuItem;
}
public boolean hasNext(){
//식당은 메뉴 개수를 정해놓기 때문에 마지막에 도달했는지 뿐 아니라
//현재 인덱스에 메뉴가 있는지도 확인을 해야함
if(position >= items.length || items[position] == null){
return false;
}
else{
return true;
}
}
}
//식당 메뉴에서 위의 Iterator 사용하기
public class DinerMenu{
static final int MAX_ITEMS = 6;
int numOfItems = 0;
MenuItem[] menuItems;
//식당 메뉴에 대한 Iterator를 생성해 리턴해줌
//클라이언트에선 이제 반복자를 써서 메뉴에 들어있는 항목들에 접근 가능
public Iterator createIterator(){
return DinerMenuIterator(menuItems);
}
//기타
}
- 웨이트리스 코드 수정
public class Waitress{
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;
..
public void printMenu(){
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
//각 반복자를 가지고 오버로드 된 printMenu 호출
printMenu(pancakeIterator);
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator){
//기존엔 ArrayList와 배열 타입에 맞춰 반복문을 두번 돌려야했지만
//iterator 인터페이스를 사용해 한 번만 순환하게 됨
while(iterator.hasNext()){
MenuItem menuItem = (MenuItem)iterator.next();
System.out.println(menuItem.getName());
..
}
}
}
- 여기까지의 클래스 다이어그램
+
자바에서 제공하는 Iterator 인터페이스를 사용하도록 수정하기
아직 똑같은 메소드를 사용하는 메뉴 클래스들이 추상화되지 않음
- dinnerMenuIterator에선 java.util.Iterator; 를 구현하도록 수정
- pancakeHouse에선 사실 개별 Iterator 클래스를 만들 필요가 없음 : ArrayList는 iterator()라는 메소드를 이미 제공함
- pancakeHouseMenu::createIterator() { return menuItems.iterator(); }로 수정하면 됨.
//PancakeHouseMenu Class
public Iterator createIterator(){
return menuItems.iterator();
}
//DinerMenuIterator Class
import java.util.Iterator;
public class DinerMenuIterator implements Iterator{
..
//자바 Iterator 인터페이스에 remove()가 정의되어 있기 때문에 구현해야 함.
public void remove(){
if(position <= 0){
throw new IllegalStateException("next()를 호출하지 않은 상태에선 삭제 불가");
}
if(items[position-1] != null){
for(int i=position-1; i<(items.length-1); i++){
items[i] = items[i+1];
}
items[items.length-1] = null;
}
}
}
- 통합 메뉴 인터페이스를 정의하고, 각 구상 메뉴 클래스에서 구현 + Waitress 코드 수정
//Menu Interface를 정의
public interface Menu{
public Iterator createIterator();
}
//Waitress 클래스에서 구상 메뉴 클래스를 사용하는 부분을 전부 Menu로 바꿔주기
public class Waitress{
Menu pancakeHouseMenu;
Menu dinerMenu;
..
}
최종적으로 완성된 클래스 다이어그램
개선점
- 각자 쓰던 코드를 수정하지 않고도 통합적인 처리가 가능해짐
- 각 메뉴 구현법이 캡슐화됨 - Waitress입장에선 메뉴 항목의 컬렉션이 무슨 타입인지 알 수 없음
- Iterator만 구현한다면 어떤 컬렉션이든 한 개의 순환문으로 처리가 가능
- "특정 구현이 아닌 인터페이스에 맞춰 프로그래밍한다"는 원칙을 지켜 Waitress와 구상 클래스 간 의존성 감소
Q&A
1. 다중 스레드 상황에서 같은 객체 컬렉션에 대해 여러 반복자가 있는 경우 remove()가 어떻게 작동하는지
- 따로 정의되어 있지 않기 때문에 컬렉션에 동시 접근하는 멀티 스레드 코드를 디자인할 땐 매우 유의해야 함
2. 내부 반복자와 외부 반복자는 무슨 뜻인지
- 예제에서 사용한 것이 외부 반복자 : 클라이언트에서 next()를 호출해 항목을 가져오고 작업을 제어
- 내부 반복자 : 반복자 자신에 의해 제어됨. 반복자한테 작업을 주고 다음 원소에 대한 작업을 반복자가 직접 처리
- 내부 반복자는 클라가 직접 제어할 수 없어 유연성이 떨어지지만, 할 일을 넘겨주기만 하면 나머지를 알아서 해 주기 때문에 편리
3. Hashtable처럼 정해진 순서가 없는 컬렉션에서의 반복 작업 순서는 어떻게 정해지는지
- 반복자에선 특별한 순서가 정해져 있지 않음. 컬렉션의 특성 및 구현하고 연관되어 있음
- 일반적으로 컬렉션 문서에 특별히 언급되어있지 않은 이상 순서에 대한 것은 가정하면 안 됨.
디자인 원칙
- 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.
- 어떤 클래스에서 맡고 있는 모든 역할들은 나중에 코드 변화를 불러올 수 있으므로, 한 클래스에선 한 가지 역할만 맡도록 하기
- ex) 집합체에서 컬렉션 기능과 반복자용 메소드를 모두 구현했다면 ? 두 가지 이유로 인해 클래스가 변경 될 것.
- 응집도(cohesion) : 한 클래스 또는 모듈이 특정 목적 또는 역할을 얼마나 일관되게 지원하는지를 나타내는 척도
- 응집도가 낮다는 건 서로 관련없는 기능들이 묶여있다는 것
컴포지트 패턴
- 컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있음
- 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있음
- 장점 : 클라이언트 단순화, 메소드 하나로 전체 구조에 대한 반복 가능
- 고려해야 할 점들
- 자식 간의 특별한 순서가 필요한 경우, 추가/제거/조회 시 더 복잡한 관리 방법을 사용해야함
- 복합구조가 너무 복잡하거나 많아져 많은 자원이 필요한 경우, 특정 작업에 대한 결과를 캐싱해 둘 수도 있음
- Component : 복합 객체내에 들어있는 모든 객체들에 대한 인터페이스를 정의 (복합노드 뿐 아니라 Leaf 에 대한 것도)
- Composite : 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장
- Composite에서 Leaf와 관련된 기능도 구현해야하지만 그런 경우 예외를 던지는 식으로 처리 가능
- Leaf : 자식이 없는 노드
Menu와 Waitress 코드 개선
- 지금 코드는 여러 메뉴를 서로 다른 독립적 객체로 다루고 있다는 문제가 있음
- 또한 서브 메뉴 추가나 기타 변경에 대해 자유롭지 못함
- 필요한 부분
- 메뉴, 서브메뉴, 메뉴항목 등을 모두 넣을 수 있는 트리 형태 구조
- 모든 항목, 각 항목에 대한 유연한 반복 작업
컴포지트 패턴 적용하기
- 구성요소 인터페이스 생성 - MenuComponent : Menu와 MenuItem 모두에 적용되는 인터페이스
- Menu와 MenuItem은 MenuComponent 추상 클래스에서 쓰일만한 메소드만 오버라이딩해서 사용
- 각각 사용하는 메소드가 다르기 때문에 기본적으로 모두 UnsupportedOperationException 에러를 반환하도록 함
- 자기 역할에 맞지 않은 메소드는 오버라이드 하지 않으면 됨
- Waitress에선 MenuComponent를 이용해 모두 접근 가능
public abstract class MenuComponent{
//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();
}
//MenuItem에서 작업을 처리하기 위해 사용하는 메소드
//이 중 몇 개는 Menu에서 사용하기도 함
public String 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();
}
}
- 메뉴 항목 구현
//MenuComponent 상속
public MenuItem extends MenuComponent{
String name;
String description;
boolean vegetarian;
double price;
//생성자와 getter 메소드는 기존과 동일함
public MenuItem(String name, String description, boolean vegetarian, double price){
..
}
public String getName(){
return name;
}
public String getDescription(){
return description;
}
public double getPrice(){
return price;
}
public boolean isVegetarian(){
return vegetarian;
}
public void print(){
System.out.print(" " + getName());
if(isVegetarian()){
System.out.print("(v)");
}
System.out.print(", " + getPrice());
System.out.println(" --" + getDescription());
}
- 메뉴 구현
//마찬가지로 MenuComponent 상속
public class Menu extends MenuComponent{
ArrayLisst menuComponents = new ArrayList();
String name;
String description;
public Menu(String name, String description){
..
}
public void add(MenuComponent menuComponent){
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent){
menuComponents.remove(menuComponent);
}
public void MenuComponent(int i){
return (MenuComponent)menuComponents.get(i);
}
//getter 구현, getPrice()와 isVegetarian()은 적합하지 않으므로 구현하지 않음
public String getName(){
..
}
public String getDescription(){
..
}
//Menu 클래스에선 해당 메뉴에 속하는 모든 서브Menu와 MenuItem을 출력해줘야함
//반복자를 사용해 각 자식의 print() 호출
public void print(){
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("---------------------");
Iterator iterator = menuComponents.iterator();
while(iterator.hasNext()){
MenuComponent menuComponent = (MenuComponent)iterator.next();
menuComponent.print();
}
}
}
- Waitress 수정
public class Waitress{
MenuComponent allMenus;
public Waitress(MenuComponent allMenus){
this.allMenus = allMenus;
}
//전체 계층구조를 출력하고 싶다면 최상위 메뉴의 print() 호출
public void printMenu(){
allMenus.print();
}
}
Q. 이렇게 되면 컴포지트 패턴에선 '계층 구조를 관리와 메뉴 관리' 두가지 역할을 한 클래스에서 처리하는 것 아닌가?
- 컴포지트 패턴은 단일 역할 원칙을 깨면서 투명성을 확보하기 위한 패턴
- Component 인터페이스에 Leaf와 자식들의 기능을 모두 넣으면서 클라이언트 입장에서 둘을 똑같은 방식으로 처리할 수 있음
- 물론 안정성이 떨어지긴함 (한 원소에 대해 무의미한/부적절한 작업을 실행하려고 할 수 있음)
- 즉 원칙은 상황에 따라 적절히 사용할 것 - 안정성과 투명성 사이 어딘가..
+ 복합반복자
- Waitress에서 채식주의자용 메뉴만 뽑아내거나, 복합 객체 전체에 대해 반복작업을 수행할 수 있도록 해보자
- 먼저, MenuComponent에 createIterator() 추가
//MenuComponent에 createIterator() 메소드를 추가하고
//Menu, MenuItem 에서 각각 구현
public class Menu extends MenuComponent{
..
public Iterator createIterator(){
return CompositeIterator(menuComponents.iterator());
}
}
public class MenuItem extends MenuComponent{
..
public Iterator createIterator(){
return new NullIterator();
}
}
- CompositeIterator : 어떤 복합 객체에 대해서도 반복작업을 할 수 있는 반복자
- NullIterator : MenuItem은 반복 작업을 할 대상이 없기 때문에 createIterator()의 별도 처리가 필요함
- createIterator()에서 널 리턴 -> 클라이언트에서 null check를 해줘야함
- hasNext()에서 무조건 false를 리턴하는 반복자 생성
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();
}
}
ex) allMenus.createIterator() -> { 팬케이크, 식당, 카페 } 메뉴 이터레이터 스택에 push
-> next() 호출 시 팬케이크 메뉴 객체 리턴 및 팬케이크 메뉴 항목에 대한 이터레이터 스택에 push
-> 한 번 더 next() 호출 시 팬케이크 메뉴 항목에 대한 이터레이터 조회
-> 팬케이크 메뉴 항목에 대한 조회가 끝나면 식당 > .. > 카페 순으로 반복
- 위의 반복자를 이용한 채식주의자 전용 메뉴 출력 함수
public void printVegetarianMenu(){
Iterator iterator = allMenus.createIterator();
System.out.println("\nVEGETARIAN MENU");
while(iterator.hasNext()){
MenuComponent menuComponent = (MenuComponent)iterator.next();
try{
if(menuComponent.isVegetarian()){
menuComponent.print();
}
}catch(UnsupportedOperationException e){}
//Menu의 경우 isVegetarian() 실행 시 예외 발생
//예외처리문을 작성해, 예외를 잡지만 반복작업이 지속될 수 있게 함
}
}
Q. try/catch 문을 프로그램 로직을 처리하는데 사용하는 것은 지양해야하지 않는지
- 맞음. Menu의 isVegetarian()이 무조건 false를 반환하도록 해도 되지만 해당 예제는 메소드 지원불가라는 점을 확실히 나타내기 위해 그냥 사용
연필을 깎으며
page 360
- ACDE
page 380
- Menu 인터페이스를 구현하기
- createIterator() 메소드 구현
- 기존 getMenu() 삭제
NOTE
제네릭(Generic)
- 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
- 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다.
'프로그래밍 > 공부' 카테고리의 다른 글
Head First Design Patterns : (11)프록시 패턴 (0) | 2020.10.05 |
---|---|
Head First Design Patterns : (10)스테이트 패턴 (0) | 2020.09.30 |
Head First Design Patterns : (8)템플릿 메소드 패턴 (0) | 2020.09.23 |
Head First Design Patterns : (7)어댑터 패턴과 퍼사드 패턴 (0) | 2020.09.21 |
Head First Design Patterns : (6)커맨드 패턴 (0) | 2020.09.17 |
Comments