1. 개요

들어가기 전에 (추천사)

UML은 표기법이며 다른 목표를 이루기 위한 수단일 뿐이다. 우리가 UML 지상주의에 빠지지 않도록 '왜' 모델링을 하는지 아는 것이 '어떻게' 모델링을 하는지 아는 것보다 더 중요하다고 한다. 또 다이어그램과 그것에 대응하는 자바 코드를 함께 보여줌으로써 무엇 때문에 다이어그램을 그리는지 늘 일깨워준다.

소프트웨어 프로젝트에 내재된 실타래와 같은 복합적인 문제들은 기술에만 국한된 것은 아니다. 업무 영역을 얼마나 잘 이해하고 있는지도 중요하다. 이런 복잡함을 풀기 위해서는 경험이 제각기 다른 팀원들과 업무 요구사항들을 정의하는 협업, 투자를 통해서 기대효과를 가시적으로 얻고자 하는 스폰서들에게 필요한 무언가가 필요했다. 복잡함을 단순함으로 혼돈의 비즈니스를 가지런한 비즈니스의 묶음으로 만들 수 있는 군더더기 없는 실용성이었다.

좋은 설계는 하늘에서 떨어지는 것이 아니라 어떤 문제를 놓고 여러 차례 힘들게 반복(iteration)을 거치면서 진화한다는 것이다.

개요

UML은 무엇이며 언제 쓰이는가?

UML(통합 모델링 언어)은 소프트웨어 개념을 다이어그램으로 그리기 위해 사용하는 시각적인 표기법이다.

  • 문제 도메인(problem domain)

  • 소프트웨어 설계 제안

  • 이미 완성된 소프트웨어 구현에 대한 다이어그램

위와 같은 경우 UML을 사용한다.

마틴파울러의 3가지 차원의 UML

마틴 파울러는 이러한 서로 다른 세가지 차원을 각각 다음과 같이 정의했다.

  • 개념(conceptual)

    • 사람이 풀고자하는 문제 도메인 안에 있는 개념과 추상적 개념을 기술하기 위한 속기용 기호

  • 명세(specification)

    • 소스코드로 바꾸려고 그리는 것

  • 구현(implementation)

    • 이미 있는 소스코드를 설명하려 그리는 것

이 책에서는 명세구현 차원을 다룬다. 이 두 차원은 소스코드와 직접적으로 관련이 있기 때문에 반드시 일정한 규칙과 의미론을 지켜야 한다. 이 다이어그램들을 그릴 때는 모호성이 거의 없도록 하고 형식도 잘 맞춰야 한다.

반면 개념 차원의 다이어그램은 소스코드와 그렇게 관계가 깊지 않으며, 오히려 '사람의 자연 언어'와 더 관련이 있다. 이 차원의 다이어그램은 의미론적 규칙에 그다지 얽매이지 않으며, 따라서 의미하는 바도 모호하거나 해석에 따라 달라질 수 있다.

위 다이어그램은 개념차원 다이어그램으로 AnimalDog라는 두 실체가 일반화(generalization) 관계로 연결된 것을 그림으로 보여준다. 해석은 우리집 개는 동물이라고 명확히 밝히는 것일 수도 있고, 개가 동물의 왕국에서 한자리 한다고 명확하게 밝히는 것일 수도 있다. 즉, 이 다이어그램은 사람의 말처럼 어떻게 해석하느냐에 따라 의미가 달라질 수 있다.

반면, 똑같은 다이어그램이라도 개념 차원에 있을 때보다 '명세 차원이나 구현 차원에 있을 때' 훨씬 의미가 명확해진다.

다이어그램만 가지고 다이어그램이 어떤 차원에 속하는지 알 수 없다는 것은 큰 불행이다. 프로그래머와 분석가 사이의 의사소통에서 큰 오해가 생길 수 있다. '개념 차원 다이어그램은 소스코드를 정의하지 않으며', 해서도 안 된다. 그리고 문제에 대한 해결책을 기술하는 명세 차원 다이어그램도 문제 자체를 기술하는 개념 차원 다이어그램과 비슷해야 할 이유가 전혀 없다.

앞으로 등장하는 모든 다이어그램은 명세 차원이거나 구현 차원이며, 되도록 대응하는 소스코드와 함께 제시된다. 위에서 본 개념 차원 다이어그램이 마지막으로 보는 개념차원 다이어그램이 될 것이다.

다이어그램의 유형

UML의 주요 다이어그램

  • 정적 다이어그램(static diagram)

    • 클래스, 객체, 데이터 구조와 이것들의 관계를 그림으로 표현해서 소프트웨어 요소에서 변하지 않는 논리적 구조를 보여준다.

  • 동적 다이어그램(dynamic diagram)

    • 실행 흐름을 그림으로 그리거나 실체의 상태가 어떻게 바뀌는지 그림으로 표현해서 소프트웨어 안의 실체가 실행 도중 어떻게 변하는지 보여준다.

  • 물리적 다이어그램(physicla diagram)

    • 소스파일, 라이브러리, 바이너리 파일, 데이터파일 등의 물리적 실체와 이것들의 관계를 그림으로 표현해서 소프트웨어 실체의 변하지 않는 물리적 구조를 보여준다.

예제 TreeMap 소스

public class TreeMap <K extends Comparable<K>, V> {
    TreeMapNode<K, V> topNode = null;

    public void add(K key, V value) {
        if(topNode == null) {
            topNode = new TreeMapNode<>(key, value);
        }
        else {
            topNode.add(key, value);
        }
    }

    public V get(K key) {
        return topNode == null ? null : topNode.find(key);
    }
}

class TreeMapNode <K extends Comparable<K>, V> {
    private final static int LESS = 0;
    private final static int GREATER = 1;
    private final K itsKey;
    private V itsValue;
    private final TreeMapNode<K, V>[] nodes = new TreeMapNode[2];

    public TreeMapNode(K key, V value){
        this.itsKey = key;
        this.itsValue = value;
    }

    /**
     * 해당 TreeMapNode 로부터 key 를 찾아 내려감
     * compareTo() 메소드의 결과 값이 0 이라면 키를 찾은 것이기 때문에 반환
     * @param key
     * @return value
     */
    public V find(K key) {
        if (key.compareTo(itsKey) == 0) {
            return itsValue;
        }

        return findSubNodeForKey(selectSubNode(key), key);
    }

    /**
     * 찾는 키와 현재 TreeMapNode 의 key 를 비교하여,
     * 왼쪽 노드(LESS) 혹은 오른쪽 노드(GREATER)로 안내함
     * @param key
     * @return 0 || 1
     */
    private int selectSubNode(K key) {
        return (key.compareTo(itsKey) < 0) ? LESS : GREATER;
    }

    /**
     * 현재 TreeMapNode 의 nodes 를 뒤져보고 node 가 null 이라면 null 을 반환
     * 만일 node 가 존재한다면, 해당 node 부터 find 를 시작
     * @param node
     * @param key
     * @return value
     */
    private V findSubNodeForKey(int node, K key) {
        return nodes[node] == null ? null : nodes[node].find(key);
    }

    /**
     * 해당 TreeMapNode 를 기준으로 TreeMap 에 값을 추가
     * 항상 topNode 를 기준으로 추가해야 규칙이 어긋나지 않음
     * selectSubNode() 메소드를 이용하여,
     * TreeMapNode 로 들어온 값이 현재 TreeMapNode 가 갖고 있는 value 보다 큰지 작은지 판단
     * 동일하다면 사실상 아무런 작업도 안 함
     * @param key
     * @param value
     */
    public void add(K key, V value) {
        if(key.compareTo(itsKey) == 0) {
            itsValue = value;
        } else {
            addSubNode(selectSubNode(key), key, value);
        }
    }

    /**
     * node 의 위치 [LESS, GREATER] 중 하나를 받아서
     * 해당하는 node 의 위치에 값이 있다면, add()를 다시 호출
     * 값이 없다면, 해당하는 node 의 위치에 새로운 TreeMapNode 를 생성함
     * @param node
     * @param key
     * @param value
     */
    private void addSubNode(int node, K key, V value) {
        if (nodes[node] == null) {
            nodes[node] = new TreeMapNode<>(key, value);
        } else {
            nodes[node].add(key, value);
        }
    }

    public K getItsKey() {
        return itsKey;
    }

    public V getItsValue() {
        return itsValue;
    }
}

이 부분은 책과 좀 다르게 제네릭을 사용하였으며, 메소드마다 간략한 설명을 달아놓았다.

자주 쓰이는 다이어그램

클래스 다이어그램

위는 TreeMap 클래스의 클래스 다이어그램이다.

기본 클래스를 위와 같이 표현한다고 했을 때, TreeMap에는 add()get()이라는 public 메소드가 있음을 알 수 있다.

그리고 위 클래스 다이어그램에서는 단순히 attribute를 네모 안에 가둬놓는 것이 아니라, 화살표로 표현하여 topNode라는 필드가 TreeMapNode라는 타입을 가진다고 말하고 있다.

또한 TreeMapNode는 총 3개의 화살표가 존재하는데, 각각 nodes, itsKey, itsValue이다.

  • nodesTreeMapNode라는 자신의 타입을 그대로 다시 가진다.

  • itsKeyComparable이라는 인터페이스 타입을 가진다.

  • itsValueObject 타입을 가진다.

현재는 책에 있는 코드가 아닌 Generics를 이용한 코드로 변환했기 때문에

타입파라미터를 이용해 위와 같이 표현해보았다.

클래스 다이어그램 안에 있는 미묘한 내용을 읽는 법은 나중에 다루고, 지금은 아래의 규칙만 인지해두자.

  • 사각형은 클래스를 나타낸다.

  • 화살표는 관계를 나타낸다.

    • 이 다이어그램에서는 모든 관계는 연관(association)이다. 연관은 한쪽 객체가 다른쪽 객체를 참조하며, 그 참조를 통해 그 객체의 메소드를 호출하는 것을 나타내는 단순한 데이터 관계이다.

    • 연관 위에 쓴 이름은 참조를 담는 변수의 이름과 대응된다.

    • 화살표 옆에 쓴 숫자는 보통 이 관계를 맺음으로써 생기는 인스턴스의 개수를 나타낸다. 만약 이 숫자가 1보다 크다면 어떤 컨테이너를 사용한다는 뜻인데, 컨테이너로 대개 배열을 사용한다.

  • 클래스 아이콘은 여러 구획으로 나뉠 수도 있다. 첫 번째 구획에는 언제나 클래스 이름을 쓴다. 다른 구획에는 각각 함수와 변수를 쓴다.

  • <<interface>> 표기법은 Comparable이 인터페이스임을 나타낸다.

  • 설명한 표기법은 반드시 써야 하는 것이 아니라 선택해서 쓸 수 있다.

객체 다이어그램

객체 다이어그램은 시스템 실행 중 어느 순간의 객체와 관계를 포착해서 보여준다. 한 순간의 메모리 상태를 스냅 사진으로 찍어둔 것이라고 생각해도 좋다. 이 다이어그램에서 객체는 사각형으로 표현되며, 이름 밑에 밑줄이 있다. 콜론(:) 다음에 나오는 이름은 이 객체가 속한 클래스의 이름이다. 객체마다 아래 구획에 그 객체의 itsKey 변수의 값이 나와 있는 것에 주목하라.

객체 사이의 관계는 연결(link)이라 하며, 연결은 이전의 연관에서 유도된다. 연결마다 nodes 배열의 두 원소에 이름이 각기 붙은 것을 볼 수 있다.

시퀀스 다이어그램

위 시퀀스 다이어그램은 TreeMap.add() 메소드가 어떻게 구현되는지 기술한다.

허수아비는 여기서 알려지지 않은 메소드 호출자를 의미한다. 이 호출자가 TreeMap.add()를 호출한다. topNode 변수가 null일 경우, TreeMap은 응답으로 새로운 TreeMapNode를 생성하고 그것을 topNode에 할당한다. 그렇지 않은 경우에는 topNodeadd 메시지를 보낸다.

대활호([])안의 표현식은 가드(guard)라고 하며, 어떤 경로를 따라가야 할지 알려 준다. TreeMapNode 아이콘에 닿은 화살표는 생성(construction)을 나타낸다. 한쪽 끝에 원이 그려진 작은 화살표는 데이터 토큰(data token)이라고 하고, 이 경우에 이 데이터 토큰은 생성자의 인자를 나타낸다. TreeMap 아래 홀쭉한 사각형은 활성 상자(activation)라고 부르는데, add() 메서드가 실행되는데 시간이 얼마나 걸리는지를 보여준다.

협력 다이어그램

위는 topNode != null일 때를 보여주는 협력 다이어그램이다.

협력 다이어그램의 정보는 시퀀스 다이어그램에 담긴 정보와 똑같다. 하지만 시퀀스 다이어그램은 메시지를 보내고 받는 순서를 명확히 하는 것이 목적인 반면, 협력 다이어 그램은 객체간의 관계를 명확히 하는 것이 목적이다.

객체들은 연결이라 불리는 관계로 맺어지며, 어떤 객체가 다른 객체에 메시지를 보낼 수 있다면, 두 객체 사이에 연결이 있다고 말한다. 이 연결 위로 지나다니는 것이 바로 메시지다. 메시지는 작은 화살표로 그리며, 메시지 위에는 메시지 이름과 시퀀스 숫자, 그리고 이 메시지를 보낼 때 적용하는 모든 가드를 적는다.

호출의 계층 구조는 시퀀스 숫자에서 볼수 있는 점(.)을 사용한 구조로 알 수 있다. TreeMap.add() 메서드는 TreeMapNode.add() 메서드를 호출하는 식인데, 여기서 메시지 1.1번은 메시지 1번이 호출한 메서드에서 처음 보내는 메시지를 나타낸다.

상태 다이어그램

UML에는 유한 상태 기계(finite state machine)를 나타내기 위한 방대한 표기법이 들어 있다.

유한 상태 기계란 어떤 상태를 다른 상태로 변환하는 방법을 기술한다. 이 기계에는 유한한 상태가 있는데, 이 중에 하나는 시작 상태(start state)이고, 또 끝 상태(final state)가 하나 이상 있다. 상태끼리 심벌로써 표시된 호로 연결되는데, 이 심벌은 입력의 한 문자에 해당된다. 입력 스트링에 대하여, 시작 상태에서 출발해서 각 입력 심벌에 따라 순서대로 상태를 변환하여, 모든 입력 심벌이 끝났을 때 끝 상태에 놓이면 이 입력은 정해진 언어에 속하는 것으로 간주된다.

위 그림은 지하철 개찰구 상태 기계를 상태 다이어그램으로 표현한 것이다. 기계에는 Locked(잠김)Unlocked(풀림)이라는 두가지 '상태'가 있고, 두가지 '이벤트'를 받을 수 있다.

coin(표) 이벤트는 사용자가 개찰구에 표를 넣었음을 뜻하고, pass(지나감) 이벤트는 사용자가 개찰구를 통해 지나감을 뜻한다.

화살표는 전이(transition)라고 부른다. 이 전이 화살표에는 전이를 일으키는 이벤트와 전이가 수행하는 행동을 레이블로 단다. 전이가 일어나면 시스템의 상태가 바뀐다.

  • Locked 상태에서 coin 이벤트를 받으면, Unlocked 상태로 가고 Unlock 함수를 호출한다.

  • Unlocked 상태에서 pass 이벤트를 받으면 Locked 상태로 가고 Lock 함수를 호출한다.

  • Unlocked 상태에서 coin 이벤트를 받으면, 그대로 Unlocked 상태에 남아 있으면서 Thankyou 함수를 호출한다.

  • Locked 상태에서 pass 이벤트를 받으면, 그대로 Locked 상태에 남아 있으면서 Alarm 함수를 호출한다.

이런 다이어그램은 시스템의 행동방식을 파악할 때 유용하다. 어떤 사용자가 표를 넣은 다음 아무 이유 없이 다시 표를 넣는 것처럼 예상하지 못한 경우에 시스템이 어떻게 행동해야 하는지 탐색할 기회를 마련해준다.

결론

이 장에서 본 다이어그램만으로도 UML을 그리는 대부분의 목적을 달성하는데 충분하며, 대부분의 프로그래머는 이 장에서 본 UML 지식 정도만 가지고도 충분히 잘 살아갈 수 있다.

Last updated