#5 - Controller에 들어가기 전에 반드시 알아야 할 것
중년개발자
@loxo
22일 전
API URL 네이밍 규칙과 리소스 중심 백엔드 설계
왜 Controller 코드를 쓰기 전에 URL부터 배워야 할까?
초보 개발자는 보통 이렇게 시작한다.
/login
/getUser
/saveOrder
/updateProfile처음엔 아무 문제 없어 보인다. 하지만 API가 10개, 50개, 200개로 늘어나는 순간 이 백엔드는 스스로 설명하지 못하는 시스템이 된다.
👉 URL은 단순한 주소가 아니라, 백엔드의 사고방식이 드러나는 설계 문서다.
Controller는 코드이기 전에 계약(Contract) 이다.
1. 업계 표준의 핵심: "행동이 아니라 리소스를 표현한다"
❌ 잘못된 사고 (동사 중심)
/getUser
/createUser
/deleteUser이 설계는 항상 질문을 만든다.
- getUser는 조회인가? 상세인가?
- createUser는 POST인가? GET인가?
- deleteUser는 여러 개면?
✅ 올바른 사고 (리소스 중심)
/users
/users/{id}무엇(What)을 다루는지가 URL에 드러난다.
행동은 URL이 아니라 HTTP Method가 표현한다.
2. HTTP Method는 "의미"를 가진다
| Method | 의미 | 예시 |
|---|---|---|
| GET | 조회 | 사용자 목록 조회 |
| POST | 생성 | 사용자 생성 |
| PUT | 전체 수정 | 사용자 정보 전체 수정 |
| PATCH | 부분 수정 | 사용자 비밀번호 변경 |
| DELETE | 삭제 | 사용자 삭제 |
👉 URL이 같아도 Method가 다르면 전혀 다른 API다.
GET /users -> 사용자 목록 조회
POST /users -> 사용자 생성
GET /users/1 -> 1번 사용자 조회
PATCH /users/1 -> 1번 사용자 일부 수정
DELETE /users/1 -> 1번 사용자 삭제이 구조만으로도 API 문서의 70%는 설명이 끝난다.
3. 리소스는 항상 "명사" + "복수형"
왜 복수형인가?
/user는 한 명인지, 개념인지 모호하다/users는 집합(Collection) 을 의미한다
/users (사용자라는 자원 전체)
/orders (주문 리소스)
/products (상품 리소스)👉 URL만 보고도 데이터 구조가 그려져야 한다.
4. 계층 구조는 "소유 관계"를 표현한다
예시: 사용자의 주문
/users/1/orders의미:
1번 사용자가 소유한 주문들
/users/1/orders/10의미:
1번 사용자의 10번 주문
❗ 이런 URL은 권한 검사, 비즈니스 규칙 설계에 직접적인 힌트를 준다.
5. URL에 절대 넣지 말아야 할 것들
❌ 동사
/create
/update
/process❌ 상태
/complete
/cancel
/approve👉 이런 것들은 대부분 비즈니스 행위이며, Controller가 아니라 Service의 책임이다.
상태 변경은 보통 이렇게 표현한다.
POST /orders/1/cancel이때 cancel은 리소스가 아니라 행위 엔드포인트이며, 정말 필요한 경우에만 예외적으로 허용한다.
✅ 정말 예외적으로 허용되는 대표적인 케이스
-
되돌릴 수 없는 상태 전이
- 주문 취소, 결제 환불, 계정 탈퇴처럼
- "한 번 실행되면 이전 상태로 돌아갈 수 없는" 행위
textPOST /orders/1/cancel POST /payments/10/refund POST /accounts/5/withdraw -
비동기 프로세스의 시작점
- 단순 CRUD가 아니라 "처리 시작"을 의미할 때
textPOST /reports/1/generate POST /videos/3/encode -
도메인 용어 자체가 행위인 경우
- 도메인에서 이미 동사로 굳어진 개념
textPOST /auth/login POST /auth/logout
👉 이런 경우에도 공통 원칙은 같다.
- 항상
POST - 단일 리소스에 귀속
- 남용하면 설계가 무너진다
6. Spring Boot 4에서 Controller URL 기본 형태
@RestController
@RequestMapping("/users")
class UserController {
@GetMapping
public List<UserResponse> list() {}
@GetMapping("/{id}")
public UserResponse detail(@PathVariable Long id) {}
@PostMapping
public void create(@RequestBody CreateUserRequest req) {}
}👉 클래스 레벨 = 리소스 👉 메서드 레벨 = HTTP Method
URL 설계가 깔끔하면 Controller 코드는 자동으로 정리된다.
7. Next.js App Router와의 URL 사고방식 연결
Next.js App Router의 핵심 철학
app/
└─ users/
├─ page.tsx -> /users (목록 화면)
├─ new/
│ └─ page.tsx -> /users/new (등록 화면)
└─ [id]/
├─ page.tsx -> /users/1 (상세 화면)
└─ edit/
└─ page.tsx -> /users/1/edit (수정 화면)Next.js 역시 리소스 중심이다.
- 폴더 = URL
- 화면은 URL의 상태 표현(View) 이다
등록 화면은 어떻게 설계할까?
프론트엔드 (화면)
/users/new의미:
사용자 리소스를 새로 생성하기 위한 화면
- 아직 ID가 없다
- 그래서
/users/{id}구조를 쓰지 않는다
백엔드 (API)
POST /users👉 화면 URL과 API URL은 역할이 다르다
- 화면: 사용자의 행동 흐름
- API: 리소스의 생성
수정 화면은 어떻게 설계할까?
프론트엔드 (화면)
/users/1/edit의미:
1번 사용자 리소스를 수정하기 위한 화면
백엔드 (API)
PATCH /users/1- 화면은
edit - API는 여전히 리소스 중심
👉 edit는 UI 개념이지, API 개념이 아니다.
초보자가 가장 헷갈리는 포인트
❌ API에도 이렇게 만들고 싶어진다
POST /users/edit
POST /users/update👉 이 순간 REST 설계는 무너진다.
✅ 올바른 분리 사고
| 구분 | URL 예시 | 책임 |
|---|---|---|
| 화면 | /users/new | 사용자 입력 |
| 화면 | /users/1/edit | 수정 UX |
| API | POST /users | 생성 |
| API | PATCH /users/1 | 변경 |
프론트의 new, edit는 화면 상태
백엔드의 POST, PATCH는 데이터 상태 변화
왜 화면 URL은 동사를 허용하는가?
결론부터 말하면 화면은 동사를 허용한다.
하지만 그 이유는 API와 완전히 다르다.
1. API URL은 "시스템에게 하는 명령"이다
API URL은 시스템에게 이렇게 말하는 것이다.
"이 리소스를 어떻게 변경해라"
그래서 API 세계에서는
- URL = 명사 (대상)
- 행동 = HTTP Method
PATCH /users/1의미:
1번 사용자 리소스를 수정하라
👉 여기서 edit 같은 동사가 들어오면
- 명령이 중복되고
- 책임이 섞이며
- REST 의미가 붕괴된다
2. 화면 URL은 "사용자의 상태를 설명"한다
화면 URL은 시스템 명령이 아니다.
/users/1/edit이 URL이 의미하는 것은 단 하나다.
"사용자는 지금 1번 사용자를 편집 중이다"
여기서 edit는
- 데이터 변경 ❌
- 화면 모드(state) ✅
3. 화면은 왜 동사를 허용해야 하는가?
이유 1. 화면에는 "중간 상태"가 존재한다
- 입력 중
- 검토 중
- 아직 저장하지 않음
/users/new
/users/1/edit이 상태들은
- 아직 어떤 데이터도 변경하지 않았고
- 그래서 API 개념이 될 수 없다
이유 2. 새로고침 · 북마크 · 공유가 가능해야 한다
/users/1/edit- 새로고침 → 여전히 수정 화면
- URL 복사 → 같은 수정 화면
👉 이것이 가능한 이유는
화면 URL이 명령이 아니라 위치(Location)이기 때문이다.
API라면 절대 허용되면 안 되는 특성이다.
이유 3. 화면은 UX 언어, API는 도메인 언어다
new,edit→ 사람이 이해하기 쉬운 UX 언어POST,PATCH→ 시스템이 이해하는 도메인 언어
이 둘을 섞으면
- 프론트는 불편해지고
- 백엔드는 혼란스러워진다
4. 정확한 역할 분리 정리
| 구분 | 동사의 의미 |
|---|---|
| 화면 URL | 상태 (지금 무엇을 하고 있는가) |
| API URL | ❌ 금지 (행위는 HTTP Method의 책임) |
한 문장으로 정리
화면은 동사를 "상태"로 쓰고,
API는 동사를 "명령"으로 쓰기 때문에 허용하지 않는다.
이 경계를 이해하면
- REST 설계가 흔들리지 않고
- Next.js App Router와 Spring Controller가
- 같은 사고 체계 위에서 자연스럽게 연결된다.
👉 프론트와 백엔드가 같은 URL 언어를 쓰되, 같은 책임을 지지는 않는다.
8. 프론트 & 백엔드 URL 일치의 중요성
잘 맞는 경우
Frontend: /users/1
Backend : GET /users/1- 설명 필요 없음
- API 문서 최소화
- 협업 비용 감소
안 맞는 경우
Frontend: /userDetail?id=1
Backend : /getUser👉 항상 추가 설명이 필요해진다.
9. 초보 개발자가 가장 많이 하는 실수 TOP 3
❌ 1. URL에 모든 의미를 담으려 한다
URL은 명사만 말한다. 판단은 Service가 한다.
❌ 2. Controller가 비즈니스 언어가 된다
Controller는 입구다. 규칙은 안쪽에서 결정된다.
❌ 3. 프론트 구조에 맞춰 URL을 설계한다
URL은 화면이 아니라 데이터와 규칙 기준이다.
10. 한 문장으로 정리
좋은 API URL은 설명이 필요 없다.
- 명사다
- 복수형이다
- 계층이 보인다
- HTTP Method만으로 행동이 드러난다
다음 단계에서는 이 URL 설계를 기준으로 실제 Controller 구현을 시작한다.
그때부터 코드는 더 이상 어렵지 않다. 이미 설계에서 절반은 끝났기 때문이다.