본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

Use Case: 시스템과 액터의 상호작용으로 요구사항을 기술하는 방법

유스케이스(Use Case)는 액터(Actor)가 시스템을 사용해 달성하려는 목표를 시나리오 단위로 기술한 요구사항 명세 기법이다. 기능 목록 대신 사용자 목표를 중심으로 정상 흐름과 예외 흐름을 구조화한다. 이 글에서는 유스케이스의 구성 요소, 작성 원리, 코드 매핑, 실무 활용과 주의점을 다룬다.

By 송민성

1. 개념

유스케이스(Use Case)는 액터(Actor)가 특정 목표를 달성하기 위해 시스템과 주고받는 상호작용을 하나의 시나리오로 기술한 것이다. 이 개념은 Ivar Jacobson이 제안했고, 이후 UML(Unified Modeling Language)의 표준 모델링 요소가 되었다.

핵심 구성 요소는 다음과 같다.

요소설명
액터(Actor)시스템 외부에서 시스템과 상호작용하는 사람, 외부 시스템, 시간 이벤트
주 액터(Primary Actor)목표를 가지고 유스케이스를 시작하는 액터
사전 조건(Precondition)유스케이스 시작 전에 참이어야 하는 조건
사후 조건(Postcondition)성공 종료 후 보장되는 상태
기본 흐름(Main Success Scenario)모든 것이 순조로울 때의 단계
대안/예외 흐름(Extensions)기본 흐름의 특정 단계에서 갈라지는 분기

유스케이스는 "시스템이 무엇을 하는가"를 다루며, "어떻게 구현하는가"는 포함하지 않는다. 예를 들어 "주문 결제" 유스케이스에는 "PG사 API를 호출한다"는 내부 구현이 아니라 "시스템이 결제를 승인받는다"까지만 적는다.

2. 왜 사용하는가

  • 목표 중심 요구사항: "로그인 기능", "결제 기능" 같은 기능 나열은 누가 왜 쓰는지 드러나지 않는다. 유스케이스는 사용자 목표 단위로 요구사항을 묶는다.
  • 이해관계자 간 공통 언어: 기획자, 개발자, QA가 같은 시나리오 문서를 보고 합의할 수 있다.
  • 예외 흐름의 조기 발견: 기본 흐름만 구현하고 실패 경로를 놓치는 문제를 설계 단계에서 줄인다.
  • 테스트 케이스의 원천: 기본 흐름과 각 확장 흐름이 곧 인수 테스트(Acceptance Test) 시나리오가 된다.
  • 구조 설계의 출발점: 클린 아키텍처 등에서 유스케이스는 애플리케이션 계층의 단위로 직접 매핑된다.

3. 동작 원리

3.1 작성 절차

  1. 시스템 경계(System Boundary) 정하기: 무엇이 시스템 안이고 밖인지 정한다.
  2. 액터 식별: 시스템을 사용하거나 시스템이 호출하는 외부 주체를 찾는다.
  3. 목표 도출: 각 액터가 시스템으로 이루려는 목표를 동사구로 적는다. 예: "주문을 취소한다".
  4. 기본 흐름 작성: 액터와 시스템이 번갈아 수행하는 단계를 번호로 적는다.
  5. 확장 흐름 작성: 기본 흐름의 단계 번호를 참조해 실패·분기 조건을 적는다.
  6. 관계 정리: 필요하면 포함(include)과 확장(extend) 관계를 사용한다.

3.2 유스케이스 간 관계

  • 포함(include): 여러 유스케이스에서 공통으로 항상 수행되는 부분을 분리한다. 예: "주문 결제"와 "정기결제 등록"이 모두 "본인 인증"을 포함.
  • 확장(extend): 특정 조건에서만 기본 유스케이스에 끼어드는 선택적 동작이다. 예: "주문 결제"에서 쿠폰이 있을 때만 "쿠폰 적용"이 확장.
  • 일반화(generalization): 액터나 유스케이스 사이의 상속 관계이다.

3.3 작성 예시 (텍스트)

text
유스케이스: 주문을 취소한다
주 액터: 구매자
사전 조건: 구매자가 로그인했고, 주문 상태가 '결제완료'이다
사후 조건: 주문 상태가 '취소'이고 결제 금액이 환불 요청된다

기본 흐름
1. 구매자가 주문 상세에서 취소를 요청한다.
2. 시스템이 취소 가능 상태인지 검증한다.
3. 시스템이 주문 상태를 '취소'로 변경한다.
4. 시스템이 환불을 요청한다.
5. 시스템이 취소 완료를 구매자에게 알린다.

확장 흐름
2a. 주문이 이미 '배송중'이다.
    1. 시스템이 취소 불가 사유를 알린다.
    2. 유스케이스를 실패로 종료한다.
4a. 환불 요청이 실패한다.
    1. 시스템이 환불 재시도 대기열에 등록한다.
    2. 기본 흐름 5단계로 이어진다.

4. 코드 예제

유스케이스 하나를 애플리케이션 서비스의 클래스 하나(또는 함수 하나)로 매핑하는 방식을 TypeScript로 보인다. 기본 흐름은 메서드 본문의 순서로, 확장 흐름은 분기와 오류로 드러난다.

typescript
type OrderStatus = "PAID" | "SHIPPING" | "CANCELED"; interface Order { id: string; buyerId: string; status: OrderStatus; amount: number; } interface OrderRepository { findById(id: string): Promise<Order | null>; save(order: Order): Promise<void>; } interface RefundGateway { requestRefund(orderId: string, amount: number): Promise<void>; } interface RefundRetryQueue { enqueue(orderId: string, amount: number): Promise<void>; } class CancelOrderError extends Error {} // 유스케이스: 주문을 취소한다 class CancelOrderUseCase { constructor( private readonly orders: OrderRepository, private readonly refunds: RefundGateway, private readonly retryQueue: RefundRetryQueue, ) {} async execute(input: { orderId: string; buyerId: string }): Promise<void> { const order = await this.orders.findById(input.orderId); if (!order || order.buyerId !== input.buyerId) { throw new CancelOrderError("주문을 찾을 수 없습니다."); } // 확장 흐름 2a: 취소 불가 상태 if (order.status !== "PAID") { throw new CancelOrderError(`취소할 수 없는 상태입니다: ${order.status}`); } // 기본 흐름 3: 상태 변경 order.status = "CANCELED"; await this.orders.save(order); // 기본 흐름 4 / 확장 흐름 4a: 환불 요청과 실패 시 재시도 등록 try { await this.refunds.requestRefund(order.id, order.amount); } catch { await this.retryQueue.enqueue(order.id, order.amount); } } } // 인메모리 구현으로 실행 확인 async function main() { const store = new Map<string, Order>([ ["o1", { id: "o1", buyerId: "u1", status: "PAID", amount: 15000 }], ["o2", { id: "o2", buyerId: "u1", status: "SHIPPING", amount: 9000 }], ]); const orders: OrderRepository = { findById: async (id) => store.get(id) ?? null, save: async (o) => void store.set(o.id, o), }; const failingGateway: RefundGateway = { requestRefund: async () => { throw new Error("PG timeout"); }, }; const queued: string[] = []; const queue: RefundRetryQueue = { enqueue: async (id) => void queued.push(id), }; const useCase = new CancelOrderUseCase(orders, failingGateway, queue); await useCase.execute({ orderId: "o1", buyerId: "u1" }); console.log(store.get("o1")?.status, queued); // CANCELED [ 'o1' ] try { await useCase.execute({ orderId: "o2", buyerId: "u1" }); } catch (e) { console.log((e as Error).message); // 취소할 수 없는 상태입니다: SHIPPING } } main();

테스트도 시나리오와 1:1로 대응시킬 수 있다. 아래는 같은 흐름을 검증하는 Shell 실행 예이다(ts-node 설치 가정).

shell
npx ts-node cancel-order.ts

6. 실무 사용 사례

  • 요구사항 문서화: 기획 문서의 "기능 정의서"를 유스케이스 형태로 쓰면 예외 처리 누락이 줄어든다.
  • 애플리케이션 계층 설계: 클린 아키텍처나 헥사고날 아키텍처에서 CancelOrderUseCase처럼 유스케이스 하나가 클래스 하나가 된다. 컨트롤러는 입력 변환만 하고 비즈니스 흐름은 유스케이스가 담당한다.
  • 인수 테스트 작성: 기본 흐름과 확장 흐름 각각을 BDD(Behavior-Driven Development)의 Given-When-Then 시나리오로 옮긴다.
  • API 설계 검토: 유스케이스 한 개가 API 호출 한 번인지 여러 번인지 살펴 엔드포인트 경계를 결정한다.
  • 애자일 환경: 사용자 스토리(User Story)가 짧은 약속이라면 유스케이스는 그 스토리를 상세화한 명세로 쓰인다. 둘은 대체 관계가 아니라 상세 수준이 다르다.

7. 주의할 점

  • UI·구현 세부사항 혼입: "버튼을 클릭한다", "DB에 INSERT한다" 같은 서술은 변경에 취약하다. 의도 수준으로 적는다.
  • 너무 잘게 쪼개기: "입력창에 이메일을 입력한다" 수준으로 쪼개면 유스케이스가 아니라 단순 단계 목록이 된다. 액터가 의미 있는 목표를 달성하는 단위여야 한다.
  • include/extend 남용: 관계를 과도하게 쓰면 다이어그램은 복잡해지고 가독성은 떨어진다. 중복 제거가 실제로 필요할 때만 쓴다.
  • 다이어그램만 그리고 끝내기: 유스케이스 다이어그램은 목록과 관계만 보여준다. 실제 가치는 각 유스케이스의 텍스트 명세에 있다.
  • 비기능 요구사항 누락: 성능, 보안, 가용성은 유스케이스 흐름에 잘 드러나지 않으므로 별도로 관리해야 한다.
  • 코드와의 1:1 강제 금지: 유스케이스와 클래스를 매핑하는 것은 유용한 관례이지 규칙이 아니다. 팀의 아키텍처에 맞게 조정한다.

8. 핵심 정리

  • 유스케이스(Use Case)는 액터의 목표를 기준으로 시스템과의 상호작용을 시나리오로 기술한 요구사항 명세이다.
  • 구성은 액터, 사전·사후 조건, 기본 흐름, 확장 흐름이다. 실패 경로를 단계 번호에 연결해 적는 것이 핵심이다.
  • 포함(include)은 항상 수행되는 공통 부분, 확장(extend)은 조건부 선택 동작에 쓴다.
  • 코드에서는 유스케이스 하나를 애플리케이션 서비스 하나로 매핑하고, 확장 흐름은 분기·예외 처리로 구현한다.
  • 구현 세부사항을 배제하고, 목표 단위로 적절한 크기를 유지하며, 텍스트 명세를 중심에 두어야 효과가 크다.