UML: 소프트웨어 구조와 동작을 표준 표기법으로 모델링하는 시각 언어
UML(Unified Modeling Language)은 객체지향 소프트웨어의 구조와 행위를 표준화된 다이어그램으로 표현하는 모델링 언어다. 클래스 다이어그램, 시퀀스 다이어그램 등 주요 다이어그램의 역할과 읽는 법을 정리하고, 텍스트 기반 도구(PlantUML)와 Python 코드로 다이어그램과 구현을 대응시켜 본다. 실무에서 설계 논의와 문서화에 어떻게
By 송민성
1. 개념
UML(Unified Modeling Language, 통합 모델링 언어)은 소프트웨어 시스템의 구조와 동작을 그래픽 표기법으로 표현하기 위한 표준 모델링 언어다. 객체 관리 그룹(OMG, Object Management Group)이 관리하며, ISO/IEC 19501로도 표준화되어 있다. UML은 프로그래밍 언어가 아니라 표기법(notation) 이다. 코드를 실행하는 것이 아니라 설계 의도를 사람 사이에 전달하는 것이 목적이다.
UML 2.x의 다이어그램은 크게 두 부류로 나뉜다.
| 분류 | 다이어그램 | 표현하는 것 |
|---|---|---|
| 구조(Structure) | 클래스, 객체, 컴포넌트, 배치(Deployment), 패키지 등 | 시스템을 이루는 요소와 정적 관계 |
| 행위(Behavior) | 유스케이스, 시퀀스, 활동(Activity), 상태(State Machine), 커뮤니케이션 등 | 시간에 따른 동작과 상호작용 |
실무에서 가장 자주 쓰는 것은 클래스 다이어그램, 시퀀스 다이어그램, 유스케이스 다이어그램, 상태 다이어그램, 활동 다이어그램, 컴포넌트/배치 다이어그램이다.
2. 왜 사용하는가
- 의사소통: 코드를 읽지 않고도 구조와 흐름을 팀원, 기획자와 공유할 수 있다.
- 설계 검토: 구현 전에 책임 분리와 의존 방향의 문제를 싸게 발견한다.
- 문서화: 복잡한 도메인이나 레거시 시스템의 전체 그림을 남긴다.
- 추상화 수준 선택: 같은 시스템을 개념 수준, 설계 수준, 구현 수준으로 다르게 그릴 수 있다.
웹 개발 관점에서는 API 호출 흐름(시퀀스), 도메인 모델(클래스), 주문 상태 전이(상태 다이어그램)를 논의할 때 특히 유용하다.
3. 동작 원리
3.1 클래스 다이어그램의 관계 표기
클래스는 이름, 속성, 연산의 세 칸 사각형으로 그린다. 가시성은 +(public), -(private), #(protected), ~(package)로 표기한다.
| 관계 | 선 모양 | 의미 | 코드상 대응 |
|---|---|---|---|
| 연관(Association) | 실선 | 객체가 다른 객체를 알고 있음 | 필드로 참조 |
| 집합(Aggregation) | 빈 마름모 | 약한 소유, 부분이 독립적으로 존재 | 외부에서 주입받은 참조 |
| 합성(Composition) | 채운 마름모 | 강한 소유, 전체가 사라지면 부분도 소멸 | 내부에서 생성하고 생명주기를 관리 |
| 상속(Generalization) | 빈 삼각형 실선 | is-a 관계 | 클래스 상속 |
| 구현(Realization) | 빈 삼각형 점선 | 인터페이스 구현 | 인터페이스/추상 클래스 구현 |
| 의존(Dependency) | 점선 화살표 | 일시적 사용 | 메서드 인자, 지역 변수 |
다중성(multiplicity)은 1, 0..1, *, 1..* 형태로 선 끝에 표기한다.
3.2 시퀀스 다이어그램
참여자(participant)를 가로로 배치하고 각자의 생명선(lifeline)을 세로로 내린다. 메시지는 위에서 아래로 시간 순서대로 흐른다. 동기 호출은 실선 화살표, 반환은 점선 화살표로 그린다. 조건과 반복은 alt, opt, loop 같은 결합 프래그먼트(combined fragment)로 감싼다.
3.3 상태 다이어그램
객체가 가질 수 있는 상태(state)와 이벤트(event)에 의한 전이(transition)를 표현한다. 전이는 이벤트 [가드조건] / 액션 형식으로 쓴다.
4. 코드 예제
텍스트로 다이어그램을 정의하는 PlantUML 예제와, 이를 구현한 Python 코드를 나란히 놓는다. 먼저 클래스 다이어그램이다.
@startuml
interface PaymentGateway {
+charge(amount: int): bool
}
class Order {
-id: int
-status: OrderStatus
+pay(gateway: PaymentGateway): None
+total(): int
}
class OrderItem {
-name: str
-price: int
-qty: int
}
class CardGateway
Order "1" *-- "1..*" OrderItem
Order ..> PaymentGateway : uses
CardGateway ..|> PaymentGateway
@enduml위 다이어그램을 Python으로 구현한 코드다. 합성은 Order가 OrderItem을 내부에서 소유하는 형태로, 구현 관계는 Protocol 만족으로, 의존은 메서드 인자로 나타난다.
from dataclasses import dataclass, field
from enum import Enum
from typing import Protocol
class OrderStatus(Enum):
CREATED = "CREATED"
PAID = "PAID"
FAILED = "FAILED"
class PaymentGateway(Protocol):
def charge(self, amount: int) -> bool: ...
class CardGateway:
def charge(self, amount: int) -> bool:
print(f"카드 결제 요청: {amount}원")
return amount > 0
@dataclass
class OrderItem:
name: str
price: int
qty: int
@dataclass
class Order:
id: int
items: list[OrderItem] = field(default_factory=list) # 합성: Order가 생명주기를 소유
status: OrderStatus = OrderStatus.CREATED
def add_item(self, name: str, price: int, qty: int) -> None:
self.items.append(OrderItem(name, price, qty))
def total(self) -> int:
return sum(i.price * i.qty for i in self.items)
def pay(self, gateway: PaymentGateway) -> None: # 의존: 인자로만 사용
ok = gateway.charge(self.total())
self.status = OrderStatus.PAID if ok else OrderStatus.FAILED
if __name__ == "__main__":
order = Order(id=1)
order.add_item("키보드", 50000, 1)
order.add_item("마우스", 20000, 2)
order.pay(CardGateway())
print(order.status) # OrderStatus.PAID다음은 같은 결제 흐름을 시퀀스 다이어그램으로 그린 것이다.
@startuml
actor Client
participant "OrderService" as S
participant Order
participant PaymentGateway as G
Client -> S : pay(orderId)
S -> Order : pay(gateway)
Order -> G : charge(total)
alt 승인
G --> Order : true
Order --> S : PAID
else 거절
G --> Order : false
Order --> S : FAILED
end
S --> Client : 결과
@enduml상태 다이어그램은 주문 상태 전이를 명확히 한다.
@startuml
[*] --> CREATED
CREATED --> PAID : 결제 승인
CREATED --> FAILED : 결제 거절
FAILED --> CREATED : 재시도
PAID --> [*]
@endumlPlantUML 파일을 이미지로 변환하는 명령은 다음과 같다. Java와 plantuml.jar가 필요하다.
java -jar plantuml.jar order.puml # order.png 생성
java -jar plantuml.jar -tsvg order.puml # SVG로 생성5. 시간 복잡도 또는 성능 특성
UML은 알고리즘이 아니므로 시간 복잡도는 해당하지 않는다. 대신 비용 특성은 다음과 같다.
- 작성 비용: 다이어그램이 상세할수록 작성과 유지 비용이 커진다. 코드와 다이어그램이 이중 관리되면 불일치가 생긴다.
- 텍스트 기반 도구의 이점: PlantUML, Mermaid 같은 텍스트 정의 방식은 Git으로 버전 관리하고 diff를 볼 수 있어 유지 비용이 낮다.
- 추상화 수준: 모든 클래스를 그리면 읽기 어려워진다. 핵심 도메인만 골라 그리는 것이 효율적이다.
6. 실무 사용 사례
- 설계 리뷰: 새 기능의 도메인 모델을 클래스 다이어그램으로 그려 PR 전에 합의한다.
- API 흐름 문서화: 인증, 결제처럼 여러 서비스가 얽힌 흐름을 시퀀스 다이어그램으로 남긴다.
- 상태 머신 정의: 주문, 배송, 구독 상태 전이를 상태 다이어그램으로 정리해 누락된 전이를 찾는다.
- 아키텍처 소통: 컴포넌트/배치 다이어그램으로 서비스 경계와 배포 단위를 설명한다.
- 문서 내장: Mermaid는 GitHub 마크다운에서 바로 렌더링되므로 README나 ADR(Architecture Decision Record)에 다이어그램을 넣기 쉽다.
7. 주의할 점
- 집합과 합성 구분의 모호함: 실무에서 두 관계를 혼동하기 쉽다. 팀 내에서 기준(생명주기를 함께하는가)을 정해 두거나, 불확실하면 연관으로 단순화한다.
- 과도한 상세화: 구현 세부사항까지 그리면 코드보다 먼저 낡는다. 목적에 맞는 수준에서 멈춘다.
- 다이어그램과 코드의 불일치: 오래된 다이어그램은 없는 것보다 해롭다. 생성 가능한 부분은 코드에서 자동 생성하거나 변경 시 함께 갱신하는 규칙을 둔다.
- 화살표 방향 오독: 의존 화살표는 "의존하는 쪽 → 의존 대상" 방향이다. 상속 삼각형은 부모 쪽에 붙는다.
- 모든 다이어그램을 그릴 필요는 없다: 문제를 가장 잘 드러내는 한두 가지를 고르는 것이 낫다.
- 스케치 vs 청사진: 설계 논의용 비공식 스케치와 코드 생성용 정밀 모델은 목적이 다르다. 대부분의 웹 개발 팀에서는 전자로 충분하다.
8. 핵심 정리
- UML은 소프트웨어 구조와 행위를 표현하는 표준 시각 표기법이며, 구조 다이어그램과 행위 다이어그램으로 나뉜다.
- 클래스 다이어그램의 관계(연관, 집합, 합성, 상속, 구현, 의존)는 코드의 필드, 생명주기, 상속, 인터페이스, 인자 사용과 대응해서 읽는다.
- 시퀀스 다이어그램은 시간 순서의 메시지 흐름을, 상태 다이어그램은 이벤트에 의한 상태 전이를 표현한다.
- PlantUML, Mermaid 같은 텍스트 기반 도구를 쓰면 Git으로 관리할 수 있어 코드와의 불일치를 줄이기 쉽다.
- 목적에 맞는 추상화 수준과 다이어그램 종류를 고르는 것이 UML을 실무에서 잘 쓰는 핵심이다.