Post

복잡한 설계, 한 번에 이해하기 - 클래스 다이어그램과 시퀀스 다이어그램으로 요구사항 명세 작성하기

복잡한 설계, 한 번에 이해하기 - 클래스 다이어그램과 시퀀스 다이어그램으로 요구사항 명세 작성하기

요구사항만으로는 부족했다

새로운 서비스 설계 과정에서 요구사항만 정리하면 충분하다고 생각했습니다.

1
2
3
4
- 사용자는 상품을 주문할 수 있다
- 주문 시 재고가 차감된다
- 쿠폰이 있으면 할인이 적용된다
- 포인트로 일부 금액을 결제할 수 있다

문서로 정리하고 바로 구현에 들어갔습니다. 그런데 구현 단계에서 문제가 생겼습니다.

  • 구조와 역할이 불명확해졌습니다: OrderService가 어디까지 담당해야 하는가? OrderFacade와의 경계는?
  • 데이터 흐름이 혼란스러워졌습니다: 할인 금액은 어디서 계산되고 어디로 전달되는가?
  • 협력 관계가 모호해졌습니다: CouponOrder를 알아야 하는가, OrderCoupon을 알아야 하는가?

요구사항은 “무엇을” 설명하지만, 다이어그램은 “어떻게”를 명확하게 합니다.

클래스 다이어그램: 정적 구조 설계

클래스 다이어그램은 시스템의 정적 구조를 보여줍니다. 클래스 간의 관계, 속성, 메서드를 시각화합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+------------------+       +------------------+
|      Order       |       |    OrderItem     |
+------------------+       +------------------+
| - id: Long       |1    * | - id: Long       |
| - userId: Long   |------>| - productId: Long|
| - status: Status |       | - quantity: int  |
| - totalAmount    |       | - price: Money   |
+------------------+       +------------------+
        |
        | uses
        v
+------------------+       +------------------+
|   OrderFacade    |------>|  ProductService  |
+------------------+       +------------------+
| + order(cmd)     |------>|  CouponService   |
+------------------+       +------------------+
                    ------>|  PointService    |
                           +------------------+
                    ------>|  OrderService    |
                           +------------------+

클래스 다이어그램을 그리면서 발견한 것들입니다.

  1. OrderOrderItem의 관계는 1:N입니다. OrderItemOrder 없이 존재할 수 없습니다.
  2. OrderFacade는 4개의 서비스에 의존합니다. 이건 의도된 설계입니다 (Facade 패턴).
  3. 각 서비스는 자신의 도메인 객체만 다룹니다.

시퀀스 다이어그램: 동적 흐름 시각화

시퀀스 다이어그램은 시간 순서에 따른 객체 간 메시지 교환을 보여줍니다. “주문 생성” 흐름을 시퀀스 다이어그램으로 그려보면 이렇습니다.

1
2
3
4
5
6
7
8
9
10
11
12
Client          OrderFacade      ProductService   CouponService   PointService   OrderService
  |                 |                 |                |               |               |
  |--order(cmd)---->|                 |                |               |               |
  |                 |--deductStock--->|                |               |               |
  |                 |<--ok------------|                |               |               |
  |                 |--apply(couponId)---------------->|               |               |
  |                 |<--discountAmount-----------------|               |               |
  |                 |--deduct(userId, point)-------------------------->|               |
  |                 |<--ok--------------------------------------------|               |
  |                 |--create(cmd, discount)----------------------------------------->|
  |                 |<--OrderResult---------------------------------------------------|
  |<--OrderResult---|

시퀀스 다이어그램을 그리면서 발견한 것들입니다.

  1. 흐름이 명확해졌습니다. 재고 -> 쿠폰 -> 포인트 -> 주문 생성 순서입니다.
  2. 각 서비스에 전달되는 인자와 반환값이 명확해졌습니다.
  3. 실패 케이스를 어디서 처리해야 하는지가 보였습니다. (재고 부족 예외는 ProductService에서)

다이어그램이 바꾼 것들

다이어그램을 먼저 그리고 구현에 들어가니 달라진 점이 있었습니다.

구현 속도가 빨라졌습니다. 구조가 머릿속에 있으니 코드를 작성할 때 망설임이 없었습니다.

리뷰가 쉬워졌습니다. “이 메서드가 여기 있는 게 맞나요?”라는 질문을 코드 대신 다이어그램으로 논의할 수 있었습니다.

예외 케이스가 보였습니다. 시퀀스 다이어그램을 그리다 보면 “이 단계에서 실패하면 어떻게 되지?”라는 질문이 자연스럽게 생깁니다.

정리

“구조를 시각적으로 정의하는 것이 클린 코드의 출발점”이라는 걸 이번 작업에서 체감했습니다.

요구사항은 “무엇을 만들 것인가”를 답하고, 클래스 다이어그램은 “어떤 구조로 만들 것인가”를 답하고, 시퀀스 다이어그램은 “어떤 순서로 동작할 것인가”를 답합니다. 세 가지가 모두 갖춰져야 구현이 흔들리지 않습니다.

처음부터 완벽한 다이어그램을 그릴 필요는 없습니다. 핵심 흐름만 그려도 충분합니다. 중요한 건 구현 전에 구조를 명확히 하는 습관입니다.

This post is licensed under CC BY 4.0 by the author.