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 자바)
'Lecture' 카테고리의 다른 글
자바 코드 리팩토링: 매직넘버, 제어 플래그, 널 문제, 분류 코드 문제, 분기문 문제 (0) | 2023.07.06 |
---|---|
자바 디자인 패턴: 플라이웨이트, 프록시, 명령, 해석 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 상태 패턴, 옵저버 패턴, 메멘토 패턴, 파사드 패턴, 중재자 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 데코레이터 패턴, 콤포지트 패턴, 어댑터 패턴 (0) | 2023.07.05 |
자바 디자인 패턴: 템플릿 메서드, 팩토리 메서드, 전략 패턴, 브릿지 패턴 (0) | 2023.07.05 |