본문 바로가기
Lecture

자바 디자인 패턴: 반복자, 방문자, 연쇄책임 패턴

by Renechoi 2023. 7. 6.

 

1.  반복자 패턴 Iterator Pattern 

 

 

반복자 패턴이란? 

- 구조 안을 돌아다니며 처리하는 패턴 

- 객체 요소들의 내부 표현방식을 공개하지 않고 외부에서 객체를 순회하는 객체를 만든다.

- Java Collection Framework의 Iterator가 대표적이다. 

 

 

 

의도와 동기 

- 내부에서 객체의 순차적인 제공을 하지 않는 경우 
- 순회 구현 방식이 다르더라도 동일한 방식(메서드)로 순회할 수 있게 제공한다. 
- 동일한 방식으로 순회하는 것이 핵심

 

 

클래스 다이어그램 

 

집합 적인 객체가 있다고 했을 때, 이터레이터 인터페이스가 선언이 되어 있고 해당하는 구현체들이 그 집합체를 돌릴 수 있도록 구현한다. 

 

- Iterator: 요소에 접근하고 순회하는데 필요한 메서드 제공
- CocreteIterator: Iterator에 정의된 인터페이스를 구현하는 클래스
- Aggregate: Iterator 객체를 생성하는 인터페이스 정의
- ConcreteAggregate: 해당하는 ConcreteIteratir의 인스턴스를 반환하도록 Iterator 생성 인터페이스를 구현

 

 

 

예시 

팩토리를 써서 이터레이터를 만드는 예시를 만들어보자. 

 

 

먼저 다음과 같이 이터레이터를 선언한다. 

public interface Iterator {
    boolean hasNext();
    Object next();

}

 

집합체는 모두 이터레이터를 갖도록 선언한다. 

 

public interface Aggregate {
    Iterator iterator(int type);
    int getLength();
}

 

책과 책장 객체를 다음과 같이 선언하는데 그 전에 팩토리를 다음과 같이 만들자. 

 

public abstract class Factory {
    public final Iterator create(Aggregate list, int type) {
      return createProduct(list, type);
    }
    protected abstract Iterator createProduct(Aggregate list, int type);
}

 

 

팩토리의 create 메서드를 보면 집합체를 받아서 product를 생선하는데, 이때 이터레이터를 반환하게 해준다. 

 

해당하는 Factory의 구현체 

public class IteratorFactory extends Factory {

   private static IteratorFactory ifactory = new IteratorFactory();
   private IteratorFactory(){}
   
   public static IteratorFactory getInstance(){
      
      if(ifactory == null)
         ifactory = new IteratorFactory();
      return ifactory;
   }
   
   @Override
   protected Iterator createProduct(Aggregate bookShelf, int type) {
      if(type == Constant.FORWARD)
         return new BookShelfIterator(bookShelf);
      else if(type == Constant.REVERSE)
         return new ReverseIterator(bookShelf);
      else 
         return null;
   }

}

 

이 객체는 Factory의 기능을 해주면 되기 때문에 인스턴스를 계속 만들 필요가 없다. 따라서 싱글톤으로 설정한 것을 볼 수 있다. 

 

 

책 객체와 책장 객체를 만들자. 이때 책장 객체를 집합체로 설정하며, 따라서 책장은 Iterator 반환 메서드를 가져야 한다. 

 

public class Book {
    private String name = "";
    public Book(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public class BookShelf implements Aggregate {
    private Book[] books;
    private int last = 0;
    Factory factory = IteratorFactory.getInstance();
   
    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }
    public Book getBookAt(int index) {
        return books[index];
    }
    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }
    public int getLength() {
        return last;
    }
    public Iterator iterator(int type) {
        return factory.create(this, type);
    }
       
}

 

위의 Iterator 반환 메서드를 Factory에서 만드는데, 이때 Factory는 추상 Factory의 구현체로서 BookShelfIterator가 될 수도 있고 ReverseIterator가 될 수도 있게 한다. 어떤 종류를 만들지를 type으로 결정한다. 

 

 

이제 Iterator 구현체의 내부를 보면 다음과 같다. 집합체를 받아서 해당하는 집합체의 필드에 접근하고, 요소들을 꺼내주도록 한다. 이때 index를 사용하여 꺼내오고, 또 이를 저장함으로써 다음 요소의 존재 여부를 판단한다. 

 

public class BookShelfIterator implements Iterator{
    private BookShelf bookShelf;
    private int index;
    
    BookShelfIterator(Aggregate bookShelf) {
        this.bookShelf = (BookShelf)bookShelf;
        this.index = 0;
    }
    public boolean hasNext() {
        if (index < bookShelf.getLength()) {
            return true;
        } else {
            return false;
        }
    }
    public Object next() {
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }

}

 

 

reverse는 어떨까? index를 끝에서부터 시작하고 감소 로직으로만 바꿔주면 된다. 

 

public class ReverseIterator  implements Iterator {
   private BookShelf bookShelf;
    private int index;

    ReverseIterator(Aggregate bookShelf) {
        this.bookShelf = (BookShelf)bookShelf;
        this.index = bookShelf.getLength() -1;
    }
   public boolean hasNext() {
    if (index >= 0 ) {
            return true;
        } else {
            return false;
        }

   }
   @Override
   public Object next() {
       Book book = bookShelf.getBookAt(index);
        index--;
        return book;
   }

}

 

 

 

결론 

 

- 집합적인 객체를 순회할 수 있는 방법을 제공한다. 

- 다양한 순회 방법을 쉽게 사용할 수 있도록 해준다. 

 

 

 

2.  방문자(비지터) 패턴 Visitor Pattern 

 

 

방문자 패턴이란? 

- 구조 안을 돌아다니며 일을한다 -> 방문자 

 

 

- SOLID 원칙에 따르면 원래 객체의 일은 객체가 처리해야 하는 게 맞다. 

- 비지터 패턴은 클래스가 해야하는 일들을 밖에서 수행하도록 한다. 

- 기능을 구현하되, 모여 있는 패턴 

 

의도와 동기 

- 구문 트리의 예 -> 파싱 
- 문법적 처리를 위해 각 노드마다 수행해야 하는 기능을 따로 정의 해야함

- 유사한 오퍼레이션들이 여러 객체에 분산되어 있어 디버깅이 어렵고 가독성이 떨어지는 경우 -> 각 클래스별로 관련한 클래스를 모아서 Visitor를 별도로 만들어준다. 

 

-> 기존 

 

 

-> 변경 

 

 

 

각각의 visitor들이 방문을 하면 accept를 하여 해당 노드에 대한 기능이 수행되도록 한다. 

 

 

 

-> 클래스들이 자꾸 변할 때는 쓰면 안 된다. 

 

 

 

 

클래스 다이어그램 

 

- Visitor

각 객체에서 수행해야 할 기능을 선언한다.

메서드의 이름은 요청을 받을 객체를 명시한다.

 

- ConcreteVisitor

Visitor 클래스에 선언된 메서드를 구현한다.

각 메서드는 객체에 해당하는 알고리즘을 구현한다.

 

- Element

Visitor가 수행될 수 있는 Accept() 메서드를 선언한다.

 

- ConcreteElement

매개변수로 Visitor를 받아주는 Accept()메서드를 구현한다.

 

- ConcreteAggregate

해당하는 ConcreteIteratir의 인스턴스를 반환하도록 Iterator 생성 인터페이스를 구현

 

 

예시 

 

 

파일과 디렉토리가 자기의 속성을 보여줘야 할 때, 이를 Visitor로 빼서 구현하는 예제를 살펴보자. 

 

 

Visitor 추상 클래스를 다음과 같이 구현한다. 

 

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

 

파일이 다음과 같이 visit의 방문을 accept하면 기존에는 파일 내부에 가졌던 기능을 Visitor가 수행하도록 한다. 

 

public void accept(Visitor v) {
    v.visit(this);
}

 

public void visit(Directory directory) {   // 디렉토리를 방문했을 때 호출된다.
    System.out.println(currentdir + "/" + directory);
    String savedir = currentdir;
    currentdir = currentdir + "/" + directory.getName();
    Iterator<Entry> it = directory.iterator();
    while (it.hasNext()) {
        Entry entry = (Entry)it.next();
       // if(entry.getName() == "tmp")
       //  continue;
        
        entry.accept(this);
        
    }
    currentdir = savedir;
}

 

결론 

 

- 객체 안에 있어야 하는 기능들이 바깥으로 나와있는 상황이기 때문에 객체지향 관점에서 좋지는 않다. 

- 여러 요소에 대해 유사한 기능의 메서드를 한곳에 모아서 관리하게 되고, 여러 클래스에 걸친 메서드를 추가하기 용이함
- 각 클래스에 대한 기능이 자주 변경되거나 알고리즘의 변화가 많을때 사용하는것이 효율적임

 

 

 

 

3.  연쇄책임 패턴 Chain of Responsibility Pattern 

 

 

연쇄책임 패턴이란? 

- 해결할 수 있는 케이스마다 클래스를 따로 둔다

  -> 책임을 떠넘기기 

- 다수의 객체를 사슬처럼 연결

- 요청을 해결할 객체를 만날 때까지 객체 고리를 따라서 요청을 전달한다. 

  

-> 자기가 해결할 수 있는 범위를 딱 정해놓는 것 



연결 고리는 바뀔 수도 있고 우선 순위를 설정할 수도 있다. 

 

의도와 동기 

- 메세지를 보내는 객체와 이를 받아서 처리하는 객체들 간의 결합도를 줄이기 위함
- 하나의 요청에 대한 처리가 반드시 한 객체에서만 이루어지는것이 아닌 여러 객체가 조건이 맞으면 처리의 기회를 가지게 됨
- HELP 시스템 같은 경우 적절한 답을 찾을 때 까지 연결되어 해결할 수 있음



 

 

 

 

 

 

클래스 다이어그램 

 

- Handler

요청을 처리하는 인터페이스를 정의하고, 다음 번 처리자와의 연결을 구현한다.

연결고리에 연결된 다음 객체에게 다시 메세지를 보낸다.

 

- ConcreteHandler

책임져야 할 메세지를 처리한다.

처리못하는 메세지는 다음 수신자에게 전달한다.

 

- Client

ConcreteHandler 객체에게 필요한 요청을 보낸다.

 

 

예시 

 

Help 시스템을 생각해보자. 트러블에 대한 해결 요청을 받는데, 각각 종류가 다를 수 있다. 이것을 하나의 클래스에서 처리하면 너무 복잡하다. 

 

 

고객센터에서 Support를 해주는 케이스로 예제를 살펴보자. 

 

먼저 Support 클래스를 정의한다. 

 


public abstract class Support {
    private String name;                    // 트러블 해결자의 이름
    private Support next;                   // 떠넘기는 곳
    public Support(String name) {           // 트러블 해결자의 생성
        this.name = name;
    }
    public Support setNext(Support next) {  // 떠넘길 곳을 설정
        this.next = next;
        return next;
    }
    public final void support(Trouble trouble) {  // 트러블 해결 순서
        if (resolve(trouble)) {
            done(trouble);
        } else if (next != null) {
            next.support(trouble);
        } else {
            fail(trouble);
        }
    }
    public String toString() {              // 문자열 표현
        return "[" + name + "]";
    }
    protected abstract boolean resolve(Trouble trouble); // 해결용 메소드
    protected void done(Trouble trouble) {  // 해결
        System.out.println(trouble + " is resolved by " + this + ".");
    }
    protected void fail(Trouble trouble) {  // 미해결
        System.out.println(trouble + " cannot be resolved.");
    }
}

 

간단하게 문제 클래스를 정의한다. 숫자를 가지고 있고 그 숫자에 대해 맞는지 틀리는지 여부로 트러블의 해결 여부를 판단한다고 설정하는 것이다. 

 

public class Trouble {
    private int number;
    public Trouble(int number) {  
        this.number = number;
    }
    public int getNumber() {     
        return number;
    }
    public String toString() {     
        return "[Trouble " + number + "]";
    }
}

 

추상클래스로서 해결이 되는 경우와 해결되지 않았을 때 넘길 수 있는 메서드를 정의한다. 

 

이 추상 클래스를 확장한 다음과 같은 구체 클래스들이 있다. 

 

public class LimitSupport extends Support {
    private int limit;                              // 이 번호 미만이면 해결 할수 있다.
    public LimitSupport(String name, int limit) {   // 생성자
        super(name);
        this.limit = limit;
    }
    protected boolean resolve(Trouble trouble) {         // 해결용 메소드
        if (trouble.getNumber() < limit) {
            return true;
        } else {
            return false;
        }
    }
}

public class OddSupport extends Support {
    public OddSupport(String name) {                // 생성자
        super(name);
    }
    protected boolean resolve(Trouble trouble) {    // 해결용 메소드
        if (trouble.getNumber() % 2 == 1) {
            return true;
        } else {
            return false;
        }
    }
}

 

public class SpecialSupport extends Support {
    private int number;                                 // 이 번호만 해결할 수 있다.
    public SpecialSupport(String name, int number) {    // 생성자
        super(name);
        this.number = number;
    }
    protected boolean resolve(Trouble trouble) {     // 해결용 메소드 
        if (trouble.getNumber() == number) {
            return true;
        } else {
            return false;
        }
    }
}

 

 

이처럼 각각의 상황에 따라 해결할 수 있는 객체가 있고, 즉 객체별로 다른 책임을 갖는다. 

 

 

문제가 들어 왔을 때 support들이 체인을 연결하는 메서드는 다음과 같다. 

 

public Support setNext(Support next) {  // 떠넘길 곳을 설정
    this.next = next;
    return next;
}
public final void support(Trouble trouble) {  // 트러블 해결 순서
    if (resolve(trouble)) {
        done(trouble);
    } else if (next != null) {
        next.support(trouble);
    } else {
        fail(trouble);
    }
}

 

이를 다음과 같이 사용할 수 있다. 

 

Support alice   = new NoSupport("Alice");
Support bob     = new LimitSupport("Bob", 100);
Support charlie = new SpecialSupport("Charlie", 429);
Support diana   = new LimitSupport("Diana", 200);
Support elmo    = new OddSupport("Elmo");
Support fred    = new LimitSupport("Fred", 300);
// 연쇄의 형성
alice.setNext(bob).setNext(charlie).setNext(diana).setNext(elmo).setNext(fred);

 

 

다양한 트러블을 발생시켜보자. 

 

// 다양한 트러블 발생
for (int i = 0; i < 500; i += 33) {
    alice.support(new Trouble(i));
}

 

 

 

결론 

 

 

- 객체들 간의 결합도가 적어진다. 요청을 처리하는 객체와 요청을 보내는 객체가 서로 모를 수 있다.

- 연결순서는 상황에 따라 바뀌거나 추가 삭제될 수 있다. 즉 객체의 책임을 추가, 변경, 확장할 수 있다.

- 메세지가 항상 수신된다는것을 보장할 수 없다.

 

 

 

 


참고자료

- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)

반응형