[CS300 #184] UML 기초 — 클래스 다이어그램과 시퀀스 다이어그램만 제대로 읽어도 된다
컴퓨터공학 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” 경로 중 어디서 타임아웃이 났는지 설명할 때, 시퀀스 다이어그램 한 장이 로그 수백 줄보다 빨리 전달된다.
- 배치 다이어그램은 인프라 그림으로 쓰인다. 어떤 컴포넌트가 어느 노드(물리 장비, 컨테이너)에 올라가는지를 그리는 것이 배치 다이어그램의 원래 용도다.
확인 문제
- 클래스 다이어그램에서
-와#가시성 기호의 뜻은? - 집합(aggregation)과 합성(composition)을 구분하는 기준은 무엇인가?
- 다중성
1..*는 무엇을 뜻하는가? - 시퀀스 다이어그램에서 조건 분기를 나타내는 프레임 이름은?
- 위 예제 스크립트가
Order.lines를 합성으로 그린 근거는 무엇이고, 그것이 항상 맞지 않은 이유는?
풀이
-는 private,#은 protected.- 부분의 생명주기가 전체에 묶여 있는지다. 전체가 사라질 때 부분도 사라지면 합성, 부분이 독립적으로 남으면 집합이다.
- 1개 이상.
alt.- 리스트 타입 필드라는 문법적 단서만 보고 합성이라고 가정했다. 생명주기 의미는 코드 문법에 드러나지 않아, 공유되는 객체 목록일 수도 있다.
더 읽을거리 (References)
- Object Management Group, Unified Modeling Language (UML) 2.5.1 명세
- Mermaid, Class diagrams
- Mermaid, Sequence diagrams
- Martin Fowler, UML Distilled: A Brief Guide to the Standard Object Modeling Language, 3rd ed., Addison-Wesley, 2003 (서지 정보)