Featured image of post 9. 컬렉션 잘 관리하기 - 반복자 패턴과 컴포지트 패턴

9. 컬렉션 잘 관리하기 - 반복자 패턴과 컴포지트 패턴

헤드 퍼스트 디자인 패턴

객체를 컬렉션에 추가하는 방법은 정말 다양하고, 클라이언트가 컬렉션에 들어있는 모든 객체에 일일이 접근하고 싶어하는 날이 올 것이다.
그런 날이 오더라도 클라이언트에게 전부 보여 줄 필요는 없으며, 객체 저장 방식을 보여 주지 않으면서도 클라이언트가 객체에 일일이 접근할 수 있게 해줄 수 있다.
그리고 한 방에 멋진 자료 구조를 만들 수 있는, 객체들로 구성된 슈퍼 컬렉션을 제공할 수 있다.

반복자 패턴

반복자(iterator) 패턴은 컬렉션의 요소를 순차적으로 접근할 수 있는 방법을 제공하여 반복을 캡슐화한다.

이 패턴을 통해 컬렉션의 내부 표현 방식에 독립적으로 요소에 접근할 수 있으며, 클라이언트 코드는 컬렉션 내부 구조의 세부 사항을 알 필요가 없게된다.

효과

  • 컬렉션 객체의 모든 항목에 접근하는 방식이 통일되므로 종류에 관계 없이 모든 집합체에 사용할 수 있는 다형적인 코드를 만들 수 있다.
  • 모든 항목에 일일이 접글ㄴ하는 작업을 컬렉션 객체가 아닌 반복자 객체가 맡게된다.
    • 집합체의 인터페이스와 구현이 간단해지고 집합체는 객체 컬렉션 관리에만 전념할 수 있다.

구조 알아보기

classDiagram
    direction LR
    class Aggregate {
        << interface >>
        createIterator()* Iterator
    }
    
    class ConcreateAggregate {
        createIterator()
    }
    
    class Iterator {
        << interface >>
        hasNext()* Boolean
        next()* Object
        remove()* void
    }
    
    class ConcreateIterator {
        hasNext()
        next()
        remove()
    }
    
    class Client {
        
    }
    
    Aggregate <|.. ConcreateAggregate
    Iterator <|.. ConcreateIterator
    
    Aggregate <|-- Client
    Client --|> Iterator

    ConcreateAggregate --|> ConcreateIterator
  • Aggregate
    • 인터페이스를 통해 클라이언와 객체 컬렉션의 구현을 분리할 수 있다.
  • Iterator
    • 모든 반복자가 구현해야 하는 인터페이스를 제공한다.
    • 컬렉션에 들어있는 원소에 돌아가면서 접근할 수 있게 해 주는 메소드를 제공한다.
  • ConcreteAggregate
    • 객체 컬렉션이 들어있다.
    • 그 안에 들어있는 컬렉션을 Iterator로 리턴하는 메소드를 구현한다.
    • 모든 ConcreteAggregate는 그 안에 있는 객체 컬렉션을 대상으로 돌아가며 반복 작업을 처리할 수 있게 해주는 ConcreteIterator의 인스턴스를 만들 수 있어야 한다.
  • ConcreteIterator
    • 반복 작업 중에 현재 위치를 관리를 담당한다.

단일 역할 원칙

어떤 클래스에서 맡고 있는 모든 역할은 나중에 코드 변화를 불러올 수 있다.
즉 역할이 2개 이상 있으면 바뀔 수 있는 부분이 2개 이상이 된다는 의미이다.

집합체 내부 컬렉션 관련 기능과 반복자용 메소드 관련 기능을 전부 구현한다면 2가지 이유로 클래스가 바뀔 수 있다.

  • 컬렉션이 어떤 이유로 변경
  • 반복자 관련 기능이 변경

이러한 이유로 어떤 클래스가 바뀌는 이유는 하나 뿐이어야 한다.

응집도(cohesion)
클래스 또는 모듈이 특정 목적이나 역할을 얼마나 일관되게 지원하는지를 나타내는 척도이다.

  • 응집도가 높다는 것은 서로 연관된 기능이 묶여있다는 것을 의미
  • 응집도가 낮다는 것은 서로 상관없는 기능들이 묶여있다는 것을 의미

Java Iterable 인터페이스 알아보기

자바의 모든 컬렉션 유형에서 Iterable 인터페이스르 구현한다.

classDiagram
    class Iterable {
        << interface >>
        iterator()* Iterator
        +forEach()*
        +spliterator()*
    }

    class Iterator {
        << interface >>
        hasNext()* Boolean
        next()* Object
        +remove()* void
    }
    
    class Collection {
        << interface >>
        add()*
        addAll()*
        clear()*
        contains()*
        containsAll()*
        equals()*
        hashCode()*
        isEmpty()*
        iterator()*
        remove()*
        removeAll()*
        retainAll()*
        size()*
        toArray()*
    }
    
    Iterable <|-- Collection
  • 어떤 클래스에서 Iterable을 구현한다면 그 클래스는 iterator() 메소드르 구현한다.
  • 메소드는 Iterator 인터페이스를 구현하는 반복자를 반환한다.
  • 이 인터페이스는 컬렉션에 있는 항목을 대상으로 반복 작업을 수행하는 방법을 제공하는 forEach() 메소드가 기본으로 포함된다.

컴포지트 패턴

컴포지트 패턴(Composite Pattern)은 객체들을 트리 구조로 구성하여 개별 객체와 복합 객체(그룹화된 객체)를 동일하게 다룰 수 있도록 하는 구조적인 디자인 패턴 중 하나이다.

이 패턴을 사용하면 클라이언트 코드가 단일 객체와 복합 객체를 구별하지 않고 일관된 방식으로 다룰 수 있다.

  • 객체의 구성과 개별 객체를 노드로 가지는 트리 형태의 객체 구조를 만들 수 있다.
  • 이런 복합 구조를 사용하면 복합 객체와 개별 객체를 대상으로 똑같은 작업을 적용할 수 있다.
    • 복합 객체와 개별 객체를 구분할 필요가 거의 없어진다.
classDiagram
    class Client {
        
    }
    
    class Component {
        << abstract >>
        operation()*
        add(Component)*
        remove(Component)*
        getChild(int)*
    }
    
    class Leaf {
        operation()
    }
    
    class Composite {
        operation()
        add(Component)
        remove(Component)
        getChild(int)
    }
    
    Client --> Component
    
    Component <|-- Leaf
    Component <|-- Composite
    Component <-- Composite

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 1. Component 인터페이스
interface Component {
    void operation();
}

// 2. Leaf 클래스 (단일 객체)
class Leaf implements Component {
    private String name;

    public Leaf(String name) {
        this.name = name;
    }

    @Override
    public void operation() {
        System.out.println("Leaf " + name + " operation");
    }
}

// 3. Composite 클래스 (복합 객체)
class Composite implements Component {
    private String name;
    private List<Component> children = new ArrayList<>();

    public Composite(String name) {
        this.name = name;
    }

    public void add(Component component) {
        children.add(component);
    }

    public void remove(Component component) {
        children.remove(component);
    }

    @Override
    public void operation() {
        System.out.println("Composite " + name + " operation");
        for (Component child : children) {
            child.operation();
        }
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        // Leaf 객체 생성
        Leaf leaf1 = new Leaf("Leaf 1");
        Leaf leaf2 = new Leaf("Leaf 2");

        // Composite 객체 생성 및 Leaf 객체 추가
        Composite composite = new Composite("Composite 1");
        composite.add(leaf1);
        composite.add(leaf2);

        // 두 개의 Leaf와 Composite를 모두 동일한 방식으로 다룸
        composite.operation();
    }
}

컴포지트 패턴은 한 클래스에서 계층구조를 관리하는 일과 관련 작업을 처리하는 일 2가지 역할을 수행한다.

컴포지트 패턴은 단일 역할 원칙을 깨는 대신 투명성을 확보하는 패턴이라고 할 수 있다.

투명성(transparency)
Component 인터페이스에 자식들을 관리하는 기능과 잎으로써의 기능을 전부 넣어서 클라이언트가 복합 객체와 잎을 똑같은 방식으로 처리할 수 있도록 만들 수 있다.
이를 통해 어떤 원소가 복합 객체인지 잎인지가 클라이언트에게 투명하게 보인다.

Component 클래스에는 두 종류의 기능이 모두 들어있다 보니 안전성은 약간 떨어진다.

이런 문제는 디자인상의 결정 사항에 속하며, 다른 방향으로 디자인해서 여러 역할을 서로 다른 인터페이스로 분리할 수도 있다.

  • 어떤 원소에 부적절한 메소드를 호출하는 일이 일어나지 않을 것이고, 컴파일 중 혹은 실행 중 문제가 생기는 일을 예방할 수 있다.
  • 그 대신 투명성이 떨어지게 되고, 코드에서 조건문이라든가 instanceof 연산자 같은 걸 써야한다.

정리

  • 반복자 패턴
    • 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항복에 접근하는 방법을 제공한다.
  • 컴포지트 패턴
    • 객체를 트리 구조로 구성해서 부분-전체 계층 구조를 구현한다.
    • 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.

  • 반복자를 사용하면 내부 구조를 드러내지 않으면서도 클라이언트가 컬렉션 안에 들어있는 모든 원소에 접근하도록 할 수 있다.
  • 반복자 패턴을 사용하면 집합체를 대상으로 하는 반복 작업을 별도의 객체로 캡슐화할 수 있다.
  • 반복자 패턴을 사용하면 컬렉션에 있는 모든 데이터를 대상으로 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있다.
  • 반복자 패턴을 쓰면 반복 작업에 똑같은 인터페이스를 적용할 수 있으므로 집합체에 있는 객체를 활용하는 코드를 만들 때 다형성을 활용할 수 있다.
  • 한 클래스에는 될 수 있으면 한가지 역할만 부여하는 것이 좋다.
  • 컴포지트 패턴은 개별 객체와 복합 객체를 모두 담아 둘 수 있는 구조를 제공한다.
  • 컴포지트 패턴을 사용하면 클라이언트가 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.
  • 복합 구조에 들어있는 것을 구성 요소라고 부른다.
    • 구성 요소에는 복합 객체와 잎 객체가 있다.
  • 컴포지트 패턴을 적용할 때는 여러 장단점을 고려해야 한다.
    • 상황에 따라 투명성과 안정성 사이에서 적절한 균형을 찾아야한다.