컴퓨터공학 300 주제 시리즈의 184번째 글이다. 전체 지도는 여기.

한 줄 요약

UML(Unified Modeling Language)은 소프트웨어의 구조와 동작을 그림으로 나타내는 표준 표기법이다. OMG 가 관리하며, 현행 2.5.1 판은 14종의 다이어그램을 정의한다. 실무에서 가장 많이 쓰는 것은 클래스 다이어그램과 시퀀스 다이어그램이다.

왜 필요한가

코드 1만 줄을 읽어서 “주문은 고객 하나에 속하고, 주문 항목 여러 개를 가지며, 결제 수단은 카드와 계좌이체 중 하나다” 를 파악하는 데는 시간이 걸린다. 같은 정보를 상자 다섯 개와 선 네 개로 그리면 10초면 읽는다.

그림은 누구나 그릴 수 있다. 문제는 각자 다르게 그린다는 것이다. 어떤 사람은 화살표를 “호출한다” 로, 어떤 사람은 “상속한다” 로 쓴다. UML 은 선 모양 하나하나에 뜻을 정해 둔 공통 문법이다. 문법이 같으면 설계 리뷰에서 그림을 두고 다투지 않고 설계를 두고 다툴 수 있다.

핵심 개념

14종의 다이어그램

UML 2.5.1 명세는 다이어그램을 크게 두 갈래로 나눈다.

분류 다이어그램
구조(structure) 클래스, 객체, 패키지, 컴포넌트, 복합 구조, 배치(deployment), 프로파일
행위(behavior) 유스케이스, 액티비티, 상태 머신
행위 중 상호작용(interaction) 시퀀스, 커뮤니케이션, 상호작용 개요, 타이밍

다 외울 필요는 없다. 클래스(정적 구조)와 시퀀스(시간 순서의 메시지 흐름), 그리고 상태 머신과 유스케이스 정도를 읽을 줄 알면 대부분의 문서를 소화할 수 있다.

클래스 다이어그램

클래스 하나는 세 칸짜리 상자로 그린다.

┌──────────────────────┐
│ Order                │  ← 이름
├──────────────────────┤
│ - id: int            │  ← 속성
│ - status: Status     │
├──────────────────────┤
│ + total(): int       │  ← 연산
│ + cancel(): void     │
└──────────────────────┘

가시성 기호

기호 뜻
+ public
- private
# protected
~ package

관계 표기 — 이 표가 클래스 다이어그램의 핵심이다.

관계 표기 뜻 예
연관(association) 실선 ──> 한 객체가 다른 객체를 안다 주문 → 고객
집합(aggregation) 빈 마름모 ◇── 전체-부분, 부분은 독립적으로 존재 가능 팀 ◇── 선수
합성(composition) 채운 마름모 ◆── 전체-부분, 전체가 사라지면 부분도 사라짐 주문 ◆── 주문 항목
일반화(generalization) 빈 삼각형 실선 ──▷ 상속 카드결제 ──▷ 결제수단
실체화(realization) 빈 삼각형 점선 ..▷ 인터페이스 구현 구현 클래스 ..▷ 인터페이스
의존(dependency) 점선 화살표 ..> 잠깐 사용한다(매개변수, 지역 변수) 서비스 ..> 유틸

집합과 합성의 차이는 생명주기다. 주문을 지우면 주문 항목도 의미가 없으니 합성이다. 팀이 해체돼도 선수는 남으니 집합이다. 집합은 명세상 의미가 약해 실무에서는 그냥 연관으로 그리는 경우도 많다.

다중성(multiplicity) 은 관계 끝에 숫자로 적는다. 1(정확히 하나), 0..1(없거나 하나), * 또는 0..*(0개 이상), 1..*(1개 이상).

시퀀스 다이어그램

시간이 위에서 아래로 흐르고, 참여자 사이의 메시지를 화살표로 그린다.

 사용자        OrderService        PaymentGateway      DB
   │ 주문하기()      │                     │              │
   │───────────────>│                     │              │
   │                │ pay(amount)         │              │
   │                │────────────────────>│              │
   │                │      승인번호        │              │
   │                │<- - - - - - - - - - │              │
   │                │ INSERT order                       │
   │                │───────────────────────────────────>│
   │   주문번호      │                     │              │
   │<- - - - - - - -│                     │              │

실선 화살표는 호출(동기 메시지), 점선 화살표는 응답이다. 조건 분기는 alt, 반복은 loop, 선택적 실행은 opt 라는 이름의 프레임(combined fragment)으로 감싼다. 시퀀스 다이어그램은 “이 요청 하나가 몇 개의 서비스를 거치는가” 를 보여 주기 때문에 장애 분석이나 성능 병목을 설명할 때 특히 유용하다.

상태 머신과 유스케이스

  • 상태 머신: 객체가 가질 수 있는 상태와 전이 조건을 그린다. 주문의 생성됨 → 결제됨 → 배송중 → 완료, 결제됨 → 취소됨 같은 흐름이다. 허용되지 않은 전이(배송중 → 생성됨)가 눈에 보이게 된다.
  • 유스케이스: 행위자(사람 모양)와 시스템이 제공하는 기능(타원)을 연결한다. 요구사항 분석 단계에서 범위를 합의할 때 쓴다.

텍스트로 그리는 UML

요즘은 그림 도구보다 텍스트로 다이어그램을 적고 렌더링하는 방식이 많다. 코드 리포에 함께 넣어 버전 관리할 수 있기 때문이다. Mermaid 의 클래스 다이어그램 문법에서 상속은 <|--, 합성은 *--, 집합은 o--, 연관은 -->, 의존은 ..>, 실체화는 ..|> 로 쓴다. 시퀀스 다이어그램도 같은 방식으로 적는다.

직접 해 보기

파이썬 클래스를 읽어 Mermaid 클래스 다이어그램 텍스트를 자동으로 만드는 스크립트다. 코드에서 그림을 만드는 리버스 엔지니어링의 아주 작은 버전이다.

import inspect
from dataclasses import dataclass, field, fields

@dataclass
class Customer:
    name: str
    email: str

@dataclass
class OrderLine:
    product: str
    qty: int
    price: int
    def subtotal(self) -> int:
        return self.qty * self.price

@dataclass
class Order:
    customer: Customer                                     # 연관
    lines: list[OrderLine] = field(default_factory=list)   # 합성
    def total(self) -> int:
        return sum(l.subtotal() for l in self.lines)

class PaymentMethod:
    def pay(self, amount: int) -> bool:
        raise NotImplementedError

class CardPayment(PaymentMethod):                          # 일반화
    def pay(self, amount: int) -> bool:
        return amount > 0

def to_mermaid(classes):
    out = ["classDiagram"]
    for c in classes:
        out.append(f"    class {c.__name__} {{")
        if hasattr(c, "__dataclass_fields__"):
            for f in fields(c):
                t = getattr(f.type, "__name__", str(f.type))
                out.append(f"        +{t} {f.name}")
        for name, fn in inspect.getmembers(c, inspect.isfunction):
            if not name.startswith("_") and fn.__qualname__.startswith(c.__name__ + "."):
                ret = fn.__annotations__.get("return")
                out.append(f"        +{name}() {getattr(ret, '__name__', '')}".rstrip())
        out.append("    }")
    for c in classes:
        for base in c.__bases__:
            if base in classes:
                out.append(f"    {base.__name__} <|-- {c.__name__}")
        if hasattr(c, "__dataclass_fields__"):
            for f in fields(c):
                args = getattr(f.type, "__args__", ())
                if f.type in classes:
                    out.append(f"    {c.__name__} --> \"1\" {f.type.__name__}")
                elif args and args[0] in classes:
                    out.append(f"    {c.__name__} *-- \"0..*\" {args[0].__name__}")
    return "\n".join(out)

print(to_mermaid([Customer, OrderLine, Order, PaymentMethod, CardPayment]))

실행 결과(Python 3.12):

classDiagram
    class Customer {
        +str name
        +str email
    }
    class OrderLine {
        +str product
        +int qty
        +int price
        +subtotal() int
    }
    class Order {
        +Customer customer
        +list lines
        +total() int
    }
    class PaymentMethod {
        +pay() bool
    }
    class CardPayment {
        +pay() bool
    }
    Order --> "1" Customer
    Order *-- "0..*" OrderLine
    PaymentMethod <|-- CardPayment

이 출력을 Mermaid 를 지원하는 마크다운 뷰어에 붙이면 그림이 된다. 주의할 점이 있다. 코드만 보고는 lines 가 합성인지 집합인지 알 수 없다. 스크립트는 “리스트로 들고 있으면 합성” 이라고 가정했을 뿐이다. 생명주기라는 의미는 코드 문법에 드러나지 않으므로, 자동 생성 그림은 설계자의 의도를 다시 확인해야 한다.

현업에서는

  • 전부 그리지 않는다. 모든 클래스를 다이어그램으로 옮기면 아무도 유지하지 않는 그림이 된다. 핵심 도메인 객체 5~10개, 또는 장애가 났던 요청 흐름 하나처럼 “말로 설명하기 어려운 부분” 만 그린다.
  • 설계 문서와 PR 에 텍스트 다이어그램을 넣는다. Mermaid·PlantUML 텍스트는 리포에 함께 들어가 코드 리뷰를 받는다. 그림이 코드와 같이 바뀌므로 낡은 그림이 남지 않는다.
  • 시퀀스 다이어그램은 장애 회고의 단골이다. 홈랩 클러스터에서 “Ingress → Service → Pod → 외부 DB” 경로 중 어디서 타임아웃이 났는지 설명할 때, 시퀀스 다이어그램 한 장이 로그 수백 줄보다 빨리 전달된다.
  • 배치 다이어그램은 인프라 그림으로 쓰인다. 어떤 컴포넌트가 어느 노드(물리 장비, 컨테이너)에 올라가는지를 그리는 것이 배치 다이어그램의 원래 용도다.

확인 문제

  1. 클래스 다이어그램에서 - 와 # 가시성 기호의 뜻은?
  2. 집합(aggregation)과 합성(composition)을 구분하는 기준은 무엇인가?
  3. 다중성 1..* 는 무엇을 뜻하는가?
  4. 시퀀스 다이어그램에서 조건 분기를 나타내는 프레임 이름은?
  5. 위 예제 스크립트가 Order.lines 를 합성으로 그린 근거는 무엇이고, 그것이 항상 맞지 않은 이유는?

풀이

  1. - 는 private, # 은 protected.
  2. 부분의 생명주기가 전체에 묶여 있는지다. 전체가 사라질 때 부분도 사라지면 합성, 부분이 독립적으로 남으면 집합이다.
  3. 1개 이상.
  4. alt.
  5. 리스트 타입 필드라는 문법적 단서만 보고 합성이라고 가정했다. 생명주기 의미는 코드 문법에 드러나지 않아, 공유되는 객체 목록일 수도 있다.

더 읽을거리 (References)