Posts 학원 #34일차: iterator 디자인 패턴, static nested class
Post
Cancel

학원 #34일차: iterator 디자인 패턴, static nested class

오늘은 mini pms의 25 버전과 26-a 버전을 수행하였습니다. 25버전에서는 Iterator 디자인 패턴을 적용하고, 26-a 버전에서는 static nested class를 적용하였습니다.

객체 사용: 이전 버전 리뷰

200907-01

객체가 다른 객체를 사용한다는 것은 다른 객체의 메서드를 호출하다는 뜻이다. 클래스 이름으로 static 메서드를 호출하거나 인스턴스를 생성하여 인스턴스를 통해 메서드를 호출한다.

실무에서는 클래스와 인스턴스 모두 객체(Object)라고 부르기 때문에 문맥에 맞게 파악해야 한다.

AbstractList를 레퍼런스 변수로 선언한다. 이것은 AbstractList를 직접 사용겠다는 말이 아니라 AbstractList의 자손 클래스를 쓰겠다는 말이다. 이렇게 수퍼클래스 레퍼런스를 사용하면 유연성이 커진다.

200907-02

BoardHandler는 그저 목록을 다루는 기능(즉 메서드를 가지고 있는 객체)만 필요할 뿐이다. 그런 규칙(remove, add..)을 정해놓으면 더 유연하지 않을까? 이때 목록을 다루는 객체는 Collection이라고 한다. 그런데 어떤 객체를 호출하는지에 따라 사용법이 다르면 안된다. 사용법(메서드 시그너처, 호출 규칙)을 통일한 것이 인터페이스이다. 이전 버전에서 인터페이스 List를 정의함으로써 사용법을 통일하였다.

200907-03

이후 com.eomcs.util 패키지에 LinkedLsit를 상속받아 작성한 Stack과 Queue를 추가하였다. 이 자료구조들은 값을 꺼낼 때마다 호출하는 메서드가 다르다. Queue는 poll(), Stack은 pop(), LinkedList와 ArrayList는 get()이다. 즉, 목록을 따라가면서 값을 조회(순차적으로 조회한다; iterator)하는 방법이 컬렉션의 타입에 따라 다르다(호출하는 메서드가 다르다). 따라서 프로그래밍에 일관성이 없다는 문제점이 있다.

200907-04

순차적으로, 반복적으로 따라가면서 값을 조회하는 것을 영어로 하면 Iterator이다. 컬렉션의 타입이 다르더라도 목록을 따라가면서 값을 조회하는 방법을 같게 해야 한다. 이 뜻은 메서드 시그너처를 통일한다는 것이고, 호출 규칙을 정의한다는 것이다. 호출 규칙을 정의할 때 사용하는 문법이 Interface이다.

200907-05

그러면 클라이언트가 컬렉션을 사용할 때 Iterator 인터페이스의 사용 규칙에 따라 사용하면 된다.

200907-06

컬렉션이 제공한 대리자를 통해서 컬렉션의 값을 조회하자. 이 대리자를 iterator라고 부른다. 클라이언트는 대리자를 사용규칙에 따라 사용함으로써 프로그래밍의 일관성을 유지할 수 있다.

200907-12

실생활의 예를 들어서 Iterator 인터페이스를 이해해보자. 클라이언트가 비디오 가게 주인에게 “추천할 비디오 있나요?”라고 물어보고(hasNext()), “한 개 추천해 주세요”라고 부탁한다(next())고 해보자.

200907-13

실질적으로 비디오 가게 주인은 각각의 장르마다 알바생을 고용하여 고객을 상대하게 한다. 이때 고객은 알바생이 누군지 알 필요가 없다. 그저 비디오 가게 주인이 정해놓은 hasNext(), next()라는 사용규칙을 통해서 물어볼 것을 물어보면 되는 것이다. 만약 알바생에 따라 대화하는 방식이 다르다면 불편할 것이다. 프로그래밍도 마찬가지의 방법으로 일관성을 얻을 수 있다.

v.25 Iterator 디자인 패턴

Stack, Queue, List에서 목록을 조회하는 기능을 캡슐화하여 그 사용 규칙을 Iterator 인터페이스로 정의하였습니다. 그리고 Stack, Queue, List에서 사용규칙에 따라 반복자를 구현하고, 값을 꺼낼 때 반복자를 사용하였습니다.

반복자(Iterator) 패턴

객체 목록을 관리하는 컬렉션(collection)에서 목록 조회 기능을 별도의 객체로 캡슐화하는 설계 기법이다. 컬렉션의 관리 방식(data structure)에 상관 없이 일관된 목록 조회 방법을 제공할 수 있다. 컬렉션을 변경하지 않고도 다양한 방식의 목록 조회 기법을 추가할 수 있다.

실습

1단계 - Iterator 인터페이스를 정의한다.

  • 데이터 목록을 조회하는 기능을 캡슐화하여 인터페이스로 정의한다.
1
2
3
4
public interface Iterator<E> {
    boolean hasNext();
    E next();
}

2단계 - Iterator 구현체를 정의한다.

ListIterator 클래스 작성

  • List 구현체의 목록을 조회하는 기능을 수행한다.
  • ArrayListLinkedList 모두 같은 인터페이스를 갖기 때문에 각각 별개로 반복자(Iterator) 를 만들 필요는 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.NoSuchElementException;

public class ListIterator<E> implements Iterator<E> {
    List<E> list;
    int cursor;
    
    ListIterator(List<E> list) {
        this.list = list;
    }
    
    @Override
    public boolean hasNext() {
        return cursor < list.size();
    }
    
    @Override
    public E next() {
        if (cursor == list.size())
            throw new NoSuchElementException();
        return list.get(cursor++);
    }
}

3단계 - 모든 List 구현체가 Iterator 객체를 리턴하도록 규칙을 추가한다.

List 인터페이스에 iterator() 메서드 추가

  • 컬렉션의 반복자를 리턴해주는 규칙을 추가하는데, 컬렉션이 목록을 다루는 방식이 다르기 때문에 서브 클래스가 반드시 구현해야 하는 추상 메서드를 정의한다.
1
2
3
4
public interface List<E> {
    // ..
    Iterator<E> iterator();
}

4단계 - 모든 List 구현체가 Iterator 객체를 리턴하도록 iterator() 메서드를 구현한다.

  • AbstractList 클래스 변경
    • List 인터페이스에 추가된 iterator() 규칙을 구현한다.
    • ArrayListLinkedList 는 이 클래스를 상속 받기 때문에 수퍼 클래스인 AbstractList에서 iterator() 를 구현하면 된다.
1
2
3
4
5
6
7
public abstract class AbstractList<E> implements List<E> {
    // ..
	@Override
    public Iterator<E> iterator() {
        return ListIterator<E>(this);
    }
}

5단계 - XxxHandler 에서 목록을 조회할 때 Iterator 를 사용한다.

handler 패키지 멤버 클래스의 list() 메서드를 변경한다.

사용 목적에 따라 Iterator를 사용할 수도 있고, 사용하지 않을 수도 있다.

  • 전체 목록을 조회할 때: Iterator 객체를 사용한다.
  • 목록의 일부만 조회: 인덱스를 직접 다루는 이전 방식을 사용한다.
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
public class BoardHandler {
    List<Board> boardList;
    
    public BoardHandler(List<Board> list) {
        this.boardList = list;
    }
    
    // 전체 목록을 조회하는 메서드
    public void list() {
        System.out.println("[게시글 목록]");
        // Iterator 객체 사용
        Iterator<Board> iterator = boardList.iterator();
        
        while(iterator.hasNext()) {
            Board board = iterator.next();
            System.out.printf("%d, %s, %s, %s, %d\n",
          		board.getNo(),
          		board.getTitle(),
         		board.getWriter(),
          		board.getRegisteredDate(),
          		board.getViewCount());
        }
    }
    
    //..
}

6단계 - Stack 객체에 들어 있는 값을 꺼내 줄 Iterator 구현체를 준비하고 리턴한다.

  • Iterator를 구현하는 StackIterator 클래스 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StackIterator<E> implements Iterator<E> {
    Stack<E> stack;
    
    StackIterator(Stack<E> stack) {
        this.stack = stack;
    }
    
    @Override
    public boolean hasNext {
        return !stack.empty();
    }
    
    @Override
    public E next() {
        if (stack.empty())
        	throw enw NoSuchElementException();
        return stack.pop();
    }
}
  • Stack 클래스 변경: LinkedList 로 부터 상속 받은 iterator() 를 서브 클래스 역할에 맞게 오버라이딩 한다.
1
2
3
4
5
6
7
8
9
10
11
12
public class Stack<E> extends LinkedList<E> {
    //..
    public Iterator<E> iterator() {
        // 스택은 pop() 하면 데이터가 제거되기 때문에 복제본 만들어 사용
        try {
            return new StackIterator<E>(this.clone());
        } catch(Exception e) {
            // 복제할 때 오류가 생기면 메서드를 호출한 쪽에 실행 오류 던진다.
            throw new RuntimeException("스택 복제하는 중에 오류 발생!");
        }
    }
}

7단계 - history 명령을 처리할 때 Iterator 를 사용하여 명령을 조회하고 출력한다.

App.printCommandHistory() 메서드를 변경한다.

  • Stack 객체로부터 값을 직접 꺼내지 않고 Iterator 객체를 통해 값을 꺼낸다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void printCommandHistory(StackIterator<String> iterator) {
    try {
        int count = 0;
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
            count++;
            
            if ((count % 5) == 0 && Prompt.inputString(":").equalsIgnoreCase("q") {
                break;
            }
        }
    } catch (Exception e) {
        System.out.println("history 명령 처리 중 오류 발생!");
    }
}

8단계 - Queue 객체에 들어 있는 값을 꺼내 줄 Iterator 구현체를 준비하고 리턴한다.

Stack에서 한 것과 마찬가지로 Iterator 인터페이스를 구현한 QueueIterator 클래스를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class QueueIterator<E> implements Iterator<E> {
  Queue<E> queue;
  
  public QueueIterator(Queue<E> queue) {
    this.queue = queue;
  }
  
  @Override
  public boolean hasNext() {
    return queue.size() > 0;
  }
  
  @Override
  public E next() {
    if (queue.size() == 0)
      throw new NoSuchElementException();
    return queue.poll();
  }
}

LinkedList 로 부터 상속 받은 iterator() 메서드(반복자 객체를 리턴하는 메서드)를 서브 클래스인 Queue 역할에 맞게 오버라이딩 한다.

1
2
3
4
5
6
7
8
9
10
11
public class Queue<E> extends LinkedList<E> {
  //..
  @Override
  public Iterator<E> iterator() {
    try {
      return new QueueIterator<E>(this.clone());
    } catch (Exception e) {
      throw new RuntimeException("큐를 복제하는 중에 오류 발생");
    }
  }
}

9단계 - history2 명령을 처리할 때 Iterator 를 사용하여 명령을 조회하고 출력한다.

  • App.printCommandHistory() 메서드의 파라미터의 타입을 Iterator로 변경해, history와 history2 명령을 모두 처리할 수 있게 한다. (자료구조에 상관 없이 값을 꺼낼 수 있게 한다.) printCommandHistory2() 는 삭제한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void printCommandHistory(Iterator<String> iterator) {
    try {
        int count = 0;
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
            count++;
            
            if ((count % 5) == 0 && Prompt.inputString(":").equalsIgnoreCase("q") {
                break;
            }
        }
    } catch (Exception e) {
        System.out.println("history 명령 처리 중 오류 발생!");
    }
}

BoardHandler 입장에서는 자신이 사용하는 객체가 구체적으로 어떤 객체인지 알 필요가 없어진다. 일상에서도 마찬가지이다. 택시를 탈 때 기사가 누구인지 정확히 알 수 없어도 ‘운전기사’라는 레퍼런스로 소통할 수 있다. 그 사람이 누군지 정확히 알지 않아도 우리는 소통할 수 있다. “저기요”, 아주머니”와 같은 보다 추상적인 명칭으로 부르면 되기 때문에 일상이 편해진다.

프로그래밍도 일상 생활과 똑같다. 이렇게 될 경우 직접적인 종속이 발생하지 않는다. BoardHandler 가 쓰는 객체는 LinkedList 객체가 맞지만, LinkedList가 아니더라도 LinkedList와 같은 규칙으로 만든 객체를 사용한다면 이 규칙에 따라 메서드를 호출하면 되는 것이다.

한편 AbstractList의 경우 ListIterator를 직접 사용하기 때문에 이 객체에 대해 정확하게 알아야 한다. 그러나 프로젝트에서 ListIterator를 직접 사용하는 클래스는 AbstractList뿐이다. 따라서 일반 패키지 레벨 클래스(top level class)로 선언하지 말고 중첩 클래스로 바꿔 유지 보수를 용이하게 하자.

26-a. 중첩 클래스 : 스태틱 중첩 클래스(static nested class)

현재 작성한 Iterator 구현체를 보면,

  • ListIteratorAbstractList 컬렉션에서만 사용된다.
  • StackIteratorStack 컬렉션에서만 사용된다.
  • QueueIteratorQueue 컬렉션에서만 사용된다.

이런 경우, Iterator 구현체가 사용되는 컬렉션 클래스 안에 두는 것이 유지보수에 더 좋다. 즉 사용되는 위치 가까이에 두는 것이 코드를 더 읽기 쉽게 하고 관리하기 편하게 만든다. 특히 Iterator 구현체가 컬렉션의 멤버를 사용하고 있는데, Iterator 구현체가 컬렉션의 멤버가 되면 컬렉션의 멤버에 바로 접근할 수 있어 목록 조회가 한결 편해진다.

이렇게 컬렉션의 목록을 조회하는 Iterator 구현체를 중첩 클래스 로 정의하면, 캡슐화를 통해 복잡한 구현 로직을 외부에 노출하지 않는 효과가 있다. 즉 외부에서는 Iterator 구현체를 직접 사용하지 않기 때문에, 나중에 Iterator 구현체가 변경되더라도 영향을 받지 않는다.

중첩클래스

다른 클래스 안에 정의된 클래스이다.

종류

스태틱 중첩 클래스 는 스태틱 멤버로 정의한 클래스다. 스태틱 멤버이기 때문에 인스턴스 멤버(필드나 메서드)에는 접근할 수 없다. 비록 다른 클래스 안에 있지만 일반 패키지 클래스(top-level class)처럼 사용할 수 있다.

논스태틱 중첩 클래스 스태틱 멤버가 아닌 중첩 클래스이다. 보통 내부 클래스(inner class)라 부른다. 인스턴스 멤버(필드나 메서드)처럼 사용한다. 그래서 바깥 클래스의 인스턴스 멤버를 직접 접근할 수 있다. 인스턴스 멤버이기 때문에 바깥 클래스의 인스턴스를 참조하는 this 내장 변수를 갖고 있기 때문이다. 따라서 inner class 를 사용하려면 바깥 클래스의 인스턴스를 먼저 생성해야 한다. inner class에는 메서드 안에 정의하는 로컬 클래스(local class)와 이름 없이 정의하는 익명 클래스(anonymous class) 도 있다.

용도와 특징

  • 특정 클래스의 작업을 도와주는 작은 크기의 클래스를 정의
  • 클래스가 사용되는 곳에 위치하기 때문에 코드를 읽기 쉽고 관리하기가 쉽다.
  • 다른 클래스 안에 위치하기 때문에 캡슐화가 더 좋아진다. 복잡한 코드는 감추고 외부로부터의 접근은 줄이고 단순화시켜서 코드를 더 관리하게 쉽게 만든다는 의미다. 또한 바깥 클래스의 멤버에 대한 접근은 더 쉬워진다.

훈련 내용

  • ListIterator 구현체를 AbstraceList 클래스 안에 스태틱 중첩 클래스로 정의한다.
  • StackIterator 구현체를 Stack 클래스 안에 스태틱 중첩 클래스로 정의한다.
  • QueueIterator 구현체를 Queue 클래스 안에 스태틱 중첩 클래스로 정의한다.

실습

1단계 - ListIterator 구현체를 스태틱 중첩 클래스로 정의한다.

ListIterator 구현체를 AbstractList 클래스의 static nested class로 정의한다.

  • 이 클래스 자체는 외부에 공개하지 않기 때문에 private으로 접근을 막는다.
  • 외부에서는 iterator()가 리턴한 객체가 ListIterator 인스턴스인지 알 필요가 없다.
  • 그냥 리턴 받은 객체를 Iterator 규칙에 따라 사용하면 되는 것이다.
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
31
32
33
34
35
public abstract class AbstractList<E> implements List<E> {
  protected int size;
  
  @Override
  public int size() {
    return size;
  }
  
  @Override
  public Iterator<E> iterator() {
    return new ListIterator<E>(this);
  }
  
  // 스태틱 중첩 클래스로 정의한 Iterator 구현체
  private static class ListIterator<E> implements Iterator<E> {
    List<E> list;
    int cursor;
    
    public ListIterator(List<E> list) {
      this.list = list;
    }
    
    @Override
    public boolean hasNext() {
      return cursor < list.size();
    }
    
    @Override
    public E next() {
      if (cursor == list.size())
        throw new NoSuchElementException();
      return list.get(cursor++);
    }
  }
}

2단계 - StackIterator 구현체를 스태틱 중첩 클래스로 정의한다.

StackIterator 구현체를 Stack 클래스의 스태틱 중첩 클래스 로 정의한다.

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
31
32
33
34
35
36
37
public class Stack<E> extends LinkedList<E> {
  //..
  @Override
  public Stack<E> clone() throws CloneNotSupportedException {
    Stack<E> newStack = new Stack<>();
    Object[] values = this.toArray();
    for (Object value : values) {
      newStack.push((E) value);
    }
  }
  
  @Override
  public Iterator<E> iterator() {
    try {
      return new StackIterator<E>(this.clone());
    } catch (Exception e) {
      throw new RuntimeException("스택 복제하는 중에 오류 발생!");
    }
  }
  
  private static class StackIterator<E> implements Iterator<E> {
    Stack<E> stack;
    public StackIterator(Stack<E> stack) {
      this.stack = stack;
    }
    @Override
    public boolean hasNext() {
      return !stack.empty();
    }
    @Overrdie
    public E next() {
      if (stack.empty())
        throw new NoSuchElementException();
      return stack.pop();
    }
  }
}

3단계 - QueueIterator 구현체를 스태틱 중첩 클래스로 정의한다.

QueueIterator 구현체를 Queue 클래스의 스태틱 중첩 클래스 로 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Queue<E> extends LinkedList<E> {
  private static class QueueIterator<E> implements Iterator<E> {
    Queue<E> queue;
    
    public QueueIterator(Queue<E> queue) {
      this.queue = queue;
    }
    
    @Override
    public boolean hasNex() {
      return queue.size() > 0;
    }
    
    Override
    public E next() {
      if (queue.size() == 0)
        throw new NoSuchElementException();
      return queue.poll();
    }
  }
}

중첩클래스의 종류

특정 클래스 안에서만 사용되는 클래스가 있다면 중첩 클래스로 선언한다. 즉 노출 범위를 좁히는 것이 유지보수에 좋다.

클래스의 종류

  • top level class
  • nested class
    • static nested class: 바깥 클래스의 인스턴스에 종속되지 않는 클래스로, top-level class와 동일하게 사용된다.
    • non-static nested class (= inner class): 바깥 클래스의 인스턴스 없이 생성될 수 없다 (종속된다)
      • local class: 특정 메서드 안에서만 사용되는 클래스이다.
      • anonymous class: 클래스의 이름이 없어 생성자가 없고, 따라서 정의하는 동시에 인스턴스를 생성해야 한다. 단 한 개의 인스턴스만 생성해서 사용할 경우 익명 클래스를 적용하면 좋다.
This post is licensed under CC BY 4.0 by the author.

[Java] 백준 #2440, 2441, 2442, 2443: 별찍기 - 3, 4, 5, 6

학원 #35일차: 중첩 클래스: static nested class, inner class, local class

Loading comments from Disqus ...