[UMC] 컨트롤 URI

Backend·2025-09-30·by 1000hyehyang

3주차 UMC 시니어 미션 두 번째는 `컨트롤 URI'

API를 설계하다 보면 단순한 CRUD만으로 설명하기 어려운 상황을 자주 만난다. 보통 REST의 기본 원칙은 “URI는 명사로 표현하고, 동작은 HTTP 메서드로 풀라”는 것이다. 그래서 /orders/123라는 주문 리소스에 PATCH를 보내면 상태를 바꾸고, DELETE를 보내면 지우는 식이다.

그런데 현실의 도메인은 그렇게 단순하지 않다. “주문 취소”라는 행위를 예로 들어 보자. 직관적으로는 DELETE /orders/123이 맞아 보일 수 있다. 하지만 실제로 주문이라는 리소스는 삭제해서는 안 되는 경우가 많다.

회계 처리, 고객 분쟁 대응, 통계 분석 등을 위해 취소된 주문도 반드시 기록으로 남아야 하기 때문이다. 그래서 취소는 삭제가 아니라 주문이라는 리소스를 그대로 두되, 상태 전이와 일련의 절차를 실행하는 행위가 된다.

즉, 취소란 단순히 status = canceled라는 값을 바꾸는 수준이 아니라, 환불 처리, 재고 복원, 쿠폰 회수, 고객 알림 발송, 감사 로그 기록까지 따라오는 복합적인 비즈니스 명령이다. 이걸 단순히

PATCH /orders/123
{ "status": "canceled" }

라고 표현한다면 여러 가지 오해의 소지가 생긴다. 클라이언트는 PATCH가 부분 업데이트라는 인식 때문에 “여러 번 호출해도 결국 status는 canceled로 수렴하니까 안전하겠네”라고 착각하기 쉽다. 하지만 실제 서버는 중복 환불이나 이중 알림 같은 비멱등적인 동작이 발생할 수 있다. API 표면이 단순히 필드 업데이트처럼 보이기 때문에, 멱등성에 대한 잘못된 가정을 하게 만드는 것이다. 이뿐 아니라 “환불은 누가 하지?”, “알림은 클라이언트에서 보내야 하나?”처럼 책임 범위가 모호해지고, 도메인 언어로서의 ‘취소(cancel)’라는 의미도 왜곡된다.

즉, 클라이언트가 서버 내부 동작까지 세부적으로 알아야 한다는 뜻은 아니다. 다만 “이 API가 단순 값 변경인지, 아니면 도메인 프로세스를 트리거하는 것인지” 정도는 분명히 알 수 있어야 한다. 그래야 클라이언트는 적절한 UI/UX를 설계하고, 재시도 정책도 올바르게 구성할 수 있다.

 

컨트롤 URI의 형태와 의미

구글 API 디자인 가이드(AIP)는 이런 상황을 풀기 위해 자원 경로 뒤에 콜론(:)을 붙여 커스텀 액션을 표현하는 방식을 권장한다.

POST /orders/123:cancel
POST /users/314:resetPassword
POST /invoices/77:sendEmail

여기서 :cancel, :resetPassword, :sendEmail은 단순 값 변경이 아니라 도메인 행위 전체를 실행한다는 의미를 드러낸다. “이 주문을 취소한다”, “이 사용자의 비밀번호를 재설정한다”, “이 청구서를 이메일로 보낸다” 같은 식이다.

콜론을 쓰는 이유는 경계를 확실히 하기 위해서다. /orders/123/cancel처럼 슬래시로 표현하면 마치 “취소라는 하위 자원”이 있는 것처럼 보인다. 반대로 /orders/123:cancel은 명확히 “주문이라는 자원에 대해 cancel이라는 액션을 실행한다”는 의도를 전달한다.

 

언제 컨트롤 URI를 사용하는가

가장 대표적인 경우는 도메인 명령이 단순 상태 수정으로 표현되기 어려울 때다. PATCH 요청으로 status 필드를 바꾸는 것만으로는 “취소”의 무게감과 부수효과를 담기 어렵다. 반대로 POST /orders/123:cancel은 “이 주문에 대해 취소 동작을 실행한다”는 의도가 곧바로 읽힌다.

또 다른 경우는 여러 자원에 걸친 워크플로를 묶어 실행해야 할 때다. 예를 들어 데이터셋을 외부 스토리지로 내보내는 동작은 단순히 데이터셋 객체의 속성을 변경하는 것이 아니라, 내보내기 작업 전체를 트리거하는 행위다. 이럴 때 POST /datasets/789:export처럼 컨트롤 URI를 두고, 서버는 202 Accepted와 함께 작업 추적용 Operation 리소스의 위치를 응답 헤더에 담아준다. 클라이언트는 그 URI를 폴링하여 완료·실패를 확인하거나, Retry-After 지시에 따라 호출 빈도를 조절한다.

또한 명사 중심 모델링이 과하게 비틀릴 때도 컨트롤 URI가 적절하다. 예를 들어 “사용자 초대 보내기”를 POST /invitations라는 독립 자원으로 설계할 수도 있지만, 도메인 관점에서는 “특정 사용자에 대한 초대 동작”으로 보는 편이 더 자연스러울 수 있다. 이런 경우 POST /users/42:invite가 더 읽기 쉽다. 다만 “초대”가 독립적인 상태 관리와 생명주기를 가진다면 별도의 자원으로 두는 편이 맞다. 결국 컨트롤 URI를 쓸지 여부는 자원이냐 행위냐를 어디에 더 무게 두느냐의 문제다.

 

표현 방식과 업계 사례

전통적으로 /orders/123/cancel처럼 슬래시로 액션을 붙이는 방식도 있었지만, 이는 하위 자원처럼 보여 자원 경계를 흐린다는 비판이 있었다. 그래서 구글 AIP는 /orders/123:cancel처럼 콜론을 붙여 액션을 명확히 구분하는 방식을 권장한다. 마이크로소프트는 또 다른 접근으로 “액션 리소스” 자체를 정의하는 방법을 제안하기도 한다. 어떤 방식을 택하든 핵심은 HTTP 표준 메서드를 그대로 활용하면서 도메인 행위를 드러내는 것이다.

실제 사례를 보면, 복구 동작은 POST /users/314:undelete, 환불은 POST /payments/99:refund, 대량 전송은 POST /invoices:bulkSend, 미디어 트랜스코딩은 POST /videos/7:transcode 같은 형태가 사용된다. 이들은 모두 “무엇을 수정한다”가 아니라 “무엇을 실행한다”는 의미를 전달한다.

 

정리

컨트롤 URI는 단순한 CRUD 자원 모델로는 담기 어려운 도메인 행위를 명확하게 표현하는 수단이다. 가능한 경우에는 명사 중심 자원 모델을 우선하되, 도메인 언어의 의미가 행위로 드러나는 편이 자연스럽다면 컨트롤 URI를 선택하는 것이 바람직하다. 이때는 POST /resource:action 형식을 따르고, 응답 규약을 명확히 한다. 즉시 완료라면 200이나 204, 삭제라면 204, 장기 실행이라면 202와 함께 작업 추적 리소스를 돌려주는 식이다.

이러한 규칙을 따르면 컨트롤 URI는 단순한 예외처리가 아니라, 도메인 언어를 보존하면서도 팀과 클라이언트 개발자가 이해하기 쉬운 API 설계 패턴으로 기능한다.

댓글 로딩 중...