비지니스 로직을 구현하다보면, 여러 상황에 마주칠 수 있습니다.


그 중 가장 많이 접하는 일은 아마 데이터를 자료구조에 담아 어떤 작업을 수행하도록 하는 것이라 생각합니다.


이와 관련된 내용으로 내부 구현 방법은 노출시키지 않으며 내부 데이터를 순회하는 반복자 패턴에 대해 다룬 적이 있습니다.



이번에 다룰 내용도 살짝 비슷한 내용입니다. @.@


종종 우리의 비지니스 작업 중 Tree 형태로 나타내고 싶은 경우가 종종 있습니다. 

오늘 다뤄볼 Composite 패턴은 객체들 간의 관계를 Tree 구조로 구성하여, 사용자가 단말노드(Leaf : 가장 하위 계층, 자식이 없는 노드)중간노드(Composite : 복합 계층, 자식이 있는 노드)를 동일하게 취급하여 다룰 수 있도록 해줍니다.


언제나 앞써, 개념적으로 나타내는 첫 설명은 어려운 듯 합니다. ㅡㅡ^

그렇다고, 예제를 바로 살펴보기 보다는 UML 을 먼저 살펴보고자 합니다.

 

 

위의 UML을 살펴보면, Tree 구조를 나타내기 위해 단말노드(Leaf)와 중간노드(Composite) 역할을 하는 하위개념를 두고 있으며, 중간노드는 1:N 관계로써 자식에 해당하는 Component 목록을 가지고 있습니다.


[Composite-Component 사이]에 관계가 존재하는 이유는 중간노드의 자식이 단말노드일 수도 있고 또 다른 중간노드일 수도 있기 때문이겠죠?

(즉, 구체적인 것에 의존하지 않게 하기 위함입니다. DIP 기억나나요???)


물론, Component 라는 상위개념을 둠으로써 클라이언트는 두 가지 하위개념을 동일하게 취급할 수 있습니다.


결국 Composite 패턴은 has-a 관계를 이용하여 트리 관계를 구성하고, 이들을 동일하게 취급하여 사용하는 것이 목적입니다.


이번 포스팅은 위에서 언급한 Composite 패턴의 두 목적을 충족할 수 있도록, 예제를 준비했습니다.

아래 내용에서는 회사의 조직도를 나타낼 것이며, 조직도를 나타내는 개념은 조직과 구성원정도로 나눌 생각입니다.

이렇게 나눈 하위개념을 지난 포스팅에서 다룬 반복자 패턴을 이용해서, 대표적인 트리 순회 알고리즘인 DFS(깊이우선탐색)로 순회하도록 해보고자 합니다.



1. 조직과 구성원의 상위 개념 Node. 


첫 번째로 기술할 개념은 조직과 구성원의 상위 개념인 Node 입니다.


두 개념의 상위 클래스이기 때문에, 두 하위 개념이 공통적으로 가져야할 것과 클라이언트가 하위 개념을 동일하게 취급할 수 있도록 추상적인 메소드 서명을 제공할 생각입니다.


여기에서 두 하위개념은 공통적으로 이름을 가진다고 가정할 생각입니다.

또한, 반복자를 적용하기 위해 Iterable 를 구현할 생각입니다.


구현을 해본다면, 아래와 같이 될 것 같습니다.


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
/**
 * 트리 구조의 노드 클래스 명시.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public abstract class Node implements Iterable<Node>{
 
    private String name;
 
    /**
     * 모든 노드 클래스는 이름을 가지고 있어야함.
     *
     * @param name
     */
    public Node(String name) {
        this.name = name;
    }
 
    /**
     * 이름출력.
     *
     * @return
     */
    public String getName() {
        return name;
    }
 
    /**
     * 노드를 추가.
     *
     * @param node
     */
    public abstract void addChild(Node node);
 
    /**
     * 자식 노드 출력.
     *
     * @return
     */
    public abstract List<Node> getChildList();
}
cs


Iterable 을 구현하도록 하였다면, 해당 반복자를 출력하는 Iterable::iterator 를 필수로 구현해야 합니다. 

하지만, 추상클래스이기 때문에 굳이 언급을 하지 않아도 되는군요.


구체적인 구현은 각 하위개념이 할 예정입니다.



2. 하위개념과 DFS.


이번에는 하위개념인 조직과 구성원, 그리고 DFS 를 수행하는 방법을 구현해볼 생각입니다.


먼저, 조직부터 살펴보죠.

조직은 Composite 역할을 담당하는 클래스로써, 위의 언급된 UML 처럼 트리의 자식인 Node 목록을 has-a 로 유지합니다.


Node 목록을 관리를 위해, Node::addChild 와 Node::getChildList 를 구현할 것이며, 구체화 클래스이기 때문에 Iterable::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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
 * 조직도를 나타내는 노드.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class Organization extends Node {
 
    private ArrayList<Node> nodeList;
 
    /**
     * 조직도 구성.
     *
     * @param name
     */
    public Organization(String name) {
        super(name);
 
        nodeList = new ArrayList<>();
    }
 
    /**
     * 자식 Node 를 추가.
     *
     * @param node
     */
    @Override
    public void addChild(Node node) {
        nodeList.add(node);
    }
 
    /**
     * 자식 Node 목록을 출력.
     *
     * @return
     */
    @Override
    public List<Node> getChildList() {
        return nodeList;
    }
 
    /**
     * 반복자 전략을 출력.
     *
     * <pre>
     *     반복자는 본인을 담은 목록의 반복자 전략을 출력.
     * </pre>
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
 
        ArrayList<Node> arrayList = new ArrayList<>();
        arrayList.add(this);
 
        return new DFSIterator(arrayList.iterator());
    }
}
cs


주목해 볼 것은 반복자의 구현인 Organization::iterator 메소드 입니다. 

이 곳에서 보이는 DFSIterator 는 DFS 방식으로 트리를 순회하기 위해, 따로 제작한 전략 클래스입니다. 


생각하고 있는 꿈은 이 곳의 전략 클래스를 교체하는 방식으로, 언제든 순회방식을 바꾸고자 합니다만,,,,

일단 DFSIterator 부터 구현해보죠...


아래는 DFS 알고리즘을 구현한 전략 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
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
62
/**
 * DFS 로 작동하는 반복자 정의.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class DFSIterator implements Iterator<Node>{
 
    private Stack<Iterator<Node>> nodeStack = new Stack<>();
 
    /**
     * 생성자 구현.
     *
     * @param nodeIterator
     */
    public DFSIterator(Iterator<Node> nodeIterator) {
        this.nodeStack.push(nodeIterator);
    }
 
    /**
     * 노드스택이 비어있지 않고, 다음 반복자가 비어있지 않을 때 다음 순회를 할 수 있음!!
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        if (nodeStack.isEmpty()) {
            return false;
        } else {
            return nodeStack.peek().hasNext();
        }
    }
 
    /**
     * 순회 방법을 구현.
     *
     * @return
     */
    @Override
    public Node next() {
 
        Node node;
        {
            // 첫 노드를 뽑는다.
            Iterator<Node> nodeIterator = nodeStack.peek();
 
            node = nodeIterator.next();
 
            if (!nodeIterator.hasNext()) {
                // 다음 데이터가 없다면, 스택에서 제거.
                nodeStack.pop();
            }
 
            if (!node.getChildList().isEmpty()) {
                // 자식이 존재한다면, 자식 목록의 반복자를 노드스택에 넣는다.
                nodeStack.push(node.getChildList().iterator());
            }
        }
 
        return node;
    }
}
 
cs


이번 포스팅에서는 DFS 알고리즘을 포스팅하는 것이 아니기 때문에, 구체적인 알고리즘 전략을 언급하지는 않습니다.

(구글링을 조금 해보면, DFS 알고리즘은 금방 찾을 수 있을 것입니다. @.@)


이 포스팅에서 생각해볼 점은 반복자를 구현함으로써, 반복하는 방법을 숨긴 상태로 클라이언트는 목적에 맞게 순회할 수 있음을 알면 더 좋을 듯 싶습니다.

(다시 한번 언급해보는 반복자 패턴의 목적입니다. @.@)



마지막으로 구현할 것은 Leaf 에 해당하는 구성원입니다.

구성원은 자식을 가지지 않으며, 반복자 또한 필요없어보입니다.


이에 따라, 구현된 구성원 클래스는 다음과 같습니다.


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
/**
 * 구성원을 나타내는 노드.
 *
 * Created by Doohyun on 2017. 8. 27..
 */
public class Member extends Node {
 
    public Member(String name) {
        super(name);
    }
 
    /**
     * 구성원 추가 구현 시, 어떤 것도 하지 않음.
     *
     * @param node
     */
    @Override
    public void addChild(Node node) {
        // NOTING WORK
    }
 
    /**
     * 빈 목록 출력.
     *
     * @return
     */
    @Override
    public List<Node> getChildList() {
        return Collections.emptyList();
    }
 
    /**
     * 반복자를 구현하지 않음.
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
        return null;
    }
}
cs


그러나 한번 고려해볼 점은 구성원 클래스는 Iterable 를 구현하는 클래스이고, 아래와 같은 코드가 가능함을 의미합니다.


1
2
3
for (Node node : new Member("강현지")) {
    System.out.println(node.getName());
}
cs


이 상황에서 Member 클래스의 반복자는 Null 이기 때문에 NullPointerException 이 발생할 것입니다.

즉, 한 단위에 대해서 반복하는 전략 클래스를 제공해야할 것 같습니다.


이로써, 구현한 LeafIterator 는 다음과 같습니다.


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
/**
 * 단말노드 반복자.
 *
 * Created by Doohyun on 2017. 9. 3..
 */
public class LeafIterator implements Iterator<Node> {
 
    private Node node;
    private Boolean hasNext = true;
 
    /**
     * 오직 한 개의 목록을 가짐.
     *
     * @param node
     */
    public LeafIterator(Node node) {
        this.node = node;
    }
 
    /**
     * 한번 사용하고 끝내도록 구현.
     *
     * @return
     */
    @Override
    public boolean hasNext() {
        return hasNext;
    }
 
    /**
     * 다음을 수행한 후, hasNext 를 거짓으로 돌림.
     *
     * @return
     */
    @Override
    public Node next() {
 
        hasNext = false;
 
        return node;
    }
}
cs


오직, 한번만 순회할 수 있도록 플래그를 두어 반복자 클래스를 제작했습니다.

즉, Leaf 인 구성원 클래스는 이 반복자를 사용하도록 해야합니다.


1
2
3
4
5
6
7
8
9
10
11
12
public class Member extends Node {
 
    /**
     * Leaf 반복자를 사용하도록 변경.
     *
     * @return
     */
    @Override
    public Iterator<Node> iterator() {
        return new LeafIterator(this);
    }
}
cs



3. Composite 패턴 사용.


앞써, 만든 조직과 구성원 클래스를 이용하여 트리구조를 구성해 볼 생각입니다.

이렇게 구성된 조직은 for-loop 을 통해, DFS 알고리즘으로 순회하도록 할 것입니다.


테스트 코드는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Organization 웹솔센터 = new Organization("웹솔센터");
 
// 자식노드로 구성원을 추가하고 있음.
Organization 웹솔2팀 = new Organization("웹솔2팀");
웹솔2팀.addChild(new Member("남두현"));
웹솔2팀.addChild(new Member("유덕형"));
 
// 자식노드로 구성원을 추가하고 있음.
Organization 웹솔3팀 = new Organization("웹솔3팀");
웹솔3팀.addChild(new Member("강현지"));
웹솔3팀.addChild(new Member("유형주"));
 
// 자식노드로 조직을 추가하고 있음. (즉, 구체적인 것에 의존하지 않는 DIP)
웹솔센터.addChild(웹솔2팀);
웹솔센터.addChild(웹솔3팀);
 
// 클라이언트는 단순히 for-loop 만 사용하면 됨)
for (Node node : 웹솔센터) {
    System.out.println(node.getName());
}
cs


작성하고 보니, 결과를 잊었군요.

물론 아래와 같이 깊이우선탐색을 잘 수행하고 있습니다. @.@


1
2
3
4
5
6
7
8
9
// CONSOLE RESULT
//
// 웹솔센터
// 웹솔2팀
// 남두현
// 유덕형
// 웹솔3팀
// 강현지
// 유형주
cs


위의 코드는 첫 번째 목적인 객체들 간의 관계를 Tree 형태로 잘 나타내고 있습니다.

Node 라는 추상적인 것에 Organization 이 의존하도록 하여, 조직이든 구성원이든 자식으로 추가할 수 있습니다.


또한 반복자를 이용함에 있어서, 어떤 하위개념이든 동일하게 사용할 수 있습니다.

단순하게 for-loop 만 이용하면 되죠.


살짝 복잡해보이지만 데이터 구조를 트리 형태로 가진다는 것을 제외하면, 클래스의 목적에 따라 다형적으로 구현하고 있습니다.

UML 역시 다른 디자인패턴과 그리 달라보이지 않습니다. @.@


계속하여 언급되는 다형성의 중요성과 사용사례를 접하면 언젠가 OOP 고수가 될 수 있지 않을까요?

(저는 아직 부족함을 느끼고 있지만, 언제나 공부할 때마다 깨달음은 얻는 것 같습니다.^^;)


이 글이 많은 분들께 도움이 되었으면 좋겠습니다.



반응형
Posted by N'