이 글에서 얻어갈 것들
- HTTP 메서드(GET, POST, PUT, PATCH, DELETE) 의 역할과 차이를 명확히 이해할 수 있다.
- 멱등성(Idempotency)이 무엇인지, 왜 실무에서 중요한지 체감할 수 있다.
- 결제 시스템 등에서 Idempotency Key 전략이 왜 필요한지 이해할 수 있다.
- REST API 설계에서 흔히 발생하는 실수 TOP 7 을 점검할 수 있다.
- Spring 기반 Java 백엔드에서 HTTP 메서드를 어떻게 적용하는지 코드로 확인할 수 있다.
왜 HTTP 메서드를 제대로 알아야 할까?
REST API를 설계할 때 단순히 CRUD 에 맞춰 HTTP 메서드를 선택하는 것만으로는 충분하지 않습니다.
실제 서비스에서는 언제든 다음과 같은 상황이 발생할 수 있습니다.
- 네트워크 타임아웃으로 인한 재시도
- 클라이언트 버그로 인한 중복 요청
- 로드밸런서의 자동 재전송
이때 멱등성을 고려하지 않은 API는 데이터 정합성 문제로 직결됩니다. 특히 결제, 주문, 포인트 적립처럼 금전이 오가는 도메인에서는 멱등성 개념이 서비스 품질을 가르는 핵심 설계 기준이 됩니다.
네트워크는 항상 불안정하다고 가정하고 설계해야 합니다. "요청이 한 번만 도달할 것" 이라는 전제는 개발 환경에서만 유효합니다.
실제 서비스에서 멱등성을 고려하지 않은 API는 장애가 났을 때 비로소 그 빈틈이 드러난다. 멱등성은 좋은 습관이 아리나 실제 장애를 막는 방어선이다.
1.HTTP 메서드란?
HTTP 메서드는 클라이언트가 서버에 요청을 보낼 때 "이 요청이 어떤 행위를 수행하는지" 를 명시하는 수단입니다.
HTTP는 이미지, 영상, HTML, JSON, 텍스트 등 모든 데이터를 바이너리로 전송할 수 있는 프로토콜이며,
메서드는 요청이 조회인지, 생성인지, 수정인지, 삭제인지를 서버에 알려주는 역할을 합니다.
HTTP 메시지 구조
요청 메시지 (Request)
GET /users/1 HTTP/1.1
Host: api.example.com
Content-Type: application/json
(바디가 없으면 생략)
응답 메시지 (Response)
HTTP/1.1 200 OK
Content-Type: application/json
{"id":1,"name":"홍길동"}
공백라인은 단순 구분자가 아니라 HTTP 파서가 헤더와 바디를 분리하는 기준입니다.
REST API에서 메서드 역할
REST 설계의 핵심 원칙은 단순하다.
URI 는 리소스(명사) 만 표현하고, 행위(동사) 는 HTTP Method 로 표현합니다.
- 잘못된 설계: /getUsers, /createUser, /deleteUser
- 올바른 설계:
GET /users
POST /users
DELETE /users/1
URI 에 동사를 넣으면 메서드와 역할이 중복됩니다. POST /createUser 는 "POST 로 생성한다" 는 의미가 이미 담겨 있느데 URL 에 또 create를 넣는 건 같은 말을 두 번 하는 것과 같습니다. 코드는 읽히는 것입니다. 중복은 혼란을 만듭니다.
2. 주요 HTTP 메서드
GET - 조회
GET /users
- 서버 상태를 변경하지 않는다. (Safe)
- 멱등성을 가진다.
- 캐싱이 가능하다.
GET은 멱등성을 보장하므로 캐싱 전략을 활용하면 트래픽 폭주 상황에서도 서버 부하를 크게 줄일 수 있습니다.
대표적인 캐싱 헤더: Cache-Control, ETag, Last-Modified
POST - 생성/처리
POST /users
- 새로운 리소스를 생성하거나 서버 측 특정 처리를 수행
- 멱등성이 없다 -> 호출할 때마다 상태가 바뀔 수 있음
- 생성된 리소스 ID는 서버가 결정
POST 는 본질적으로 멱등하지 않으므로, 결제나 주문 같은 핵심 도메인에서는 Idempotency Key 전략을 반드시 도입해야 합니다.
이를 통해 중복 요청 시 안전하게 기존 결과를 반환할 수 있습니다.
PUT - 전체 교체
PUT /users/1
- 클라이언트가 지정한 URI의 리소스를 전체 교체
- 리소스 존재 시 -> 전체 교체/ 없으면-> 생성 (Upsert)
- 멱등성을 가진다.
PUT upsert는 실무에서 거의 사용하지 않습니다.
클라이언트가 ID를 직접 결정해야 하기 때문에, 중복 ID 충돌이나 보안/정합성 문제를 야기할 수 있습니다.
PATCH - 부분 수정
PATCH /users/1
{
"email": "new@email.com"
}
- 지정한 필드만 변경하고 나머지는 유지
- 멱등성은 설계에 따라 달라진다.
실무에서는 수정 API 는 PATCH를 기본으로 사용하고, PUT은 "리소스 전체를 명시적으로 교체할 떄만" 사용하는 것이 안전합니다.
DELETE - 삭제
DELETE /users/1
- 리소스를 삭제
- 멱등성을 가진다. -> 이미 없는 리소스를 삭제해도 서버 상태 동일
DELETE는 Safe 하지 않지만 멱등입니다.
삭제된 리소스가 없으면 상태 그대로 라는 특징 덕분에 여러 번 호출해도 안전합니다.
3. PUT vs PATCH - 실무에서 PATCH를 선호하는 이유
기존 유저 데이터
{
"name": "Hwang",
"email": "hwang@email.com",
"age": 30,
"phone": "010-1111-2222"
}
PUT 요청:
PUT /users/1
{
"name": "Hwang",
"email": "hwang@email.com"
}
결과(서버 구현에 따라):
{
"name": "Hwang",
"email": "hwang@email.com",
"age": null, // 데이터 손실
"phone": null // 데이터 손실
}
-> 일부 데이터 손실 위험
PATCH 요청:
PATCH /users/1
{
"email": "new@email.com"
}
결과:
{
"name": "Hwang",
"email": "new@email.com", // 변경됨
"age": 30, // 유지
"phone": "010-1111-2222" // 유지
}
기존 데이터 유지, 필요한 부분만 안전하게 수정
PUT을 쓰다가 실수로 일부 필드만 전송하면 기존 데이터가 null로 덮어씌어지는 사고가 발생합니다. 실제 운영 환경에서 이런 실수는 생각보다 자주 일어납니다. 수정 API는 기본적으로 PATCH를 선택하고. PUT은 "리소스 전체를 명시적으로 교체 하는 경우" 에만 의도적으로 사용하는 것을 권장합니다.
4. 멱등성(Idempotency)이란?
동일한 요청을 여러 번 보내도 서버의 최종 상태가 항상 동일한 성질입니다.
수학적으로 표현하면
f(f(x)) = f(x)
한 번 호출하든 100번 호출하든 결과가 같아야 합니다.
| 메서드 | 멱등성 | Safe | 캐싱 |
| GET | O | O | O |
| POST | X | X | X |
| PUT | O | X | X |
| PATCH | 설계에 따라 | X | X |
| DELETE | O | X | X |
Safe 와 멱등성 - 헷갈리기 쉬운 개념
Safe는 서버 상태를 아예 변경하지 않는 것이고(GET, HEAD만 해당), 멱등은 여러 번 호출해도 최종 상태가 같은 것입니다. DELETE는 서버의 리소스를 삭제하므로 Safe 하지 않습니다.
하지만 몇 번을 호출해도 "해당 리소스가 없음" 상태가 유지되므로 멱등합니다.
PATCH의 멱등성은 설계에 따라 달라집니다.
// 멱등한 PATCH — 몇 번 호출해도 결과 동일
PATCH /users/1 { "status": "active" }
// 비멱등 PATCH — 호출할 때마다 값이 달라짐
PATCH /counter { "op": "increment" }
PATCH 는 어떤 로직을 담느냐에 따라 멱등할 수도, 아닐 수도 있습니다.
PATCH 는 비멱등이다 라고 단정 짓는 것 자체가 틀렸습니다. 내가 설계하는 PATCH가 멱등인지 아닌지를 의식적으로 판단하는 것이 핵심입니다.
5. 멱등성과 결제 시스템 - Idempotency Key 전략
멱등성 개념이 실제로 어떻게 쓰이는지는 결제 도메인을 보면 가장 잘 이해됩니다.
문제 상황
1. 사용자가 결제 버튼 클릭
2. POST /payments 요청
3. 서버 결제 처리 완료
4. 응답이 클라이언트에 도달하지 않음
5. 재시도 → POST /payments 재전송
6. 결제 두 번 발생 ⚠️
서버 입장에서는 두 요청 모두 유효한 새 요청입니다. 멱등성을 보장하는 장치가 없다면 결제는 두 번 일어납니다.
해결책 - Idempotency Key:
POST /payments
Idempotency-Key: payment-unique-key-abc123
{
"amount": 10000,
"method": "card"
}
서버는 이 키를 기준으로 요청을 판단합니다.
- 처음 요청 -> 결제 처리 후 키와 결과를 저장
- 같은 키 재요청 -> 새로 처리하지 않고 기존 결과 반환
POST 는 본질적으로 멱등하지 않기 때문에, 안전한 결제 처리를 위해 반드시 API 레벨에서 구현해야 합니다.
6. REST API 설계 실수 TOP 7
- URI에 동사를 사용합니다.
- 잘못된 설계
- /getUsers /createUser /deleteUser
- 올바른 설계
- GET /users POST /users DELETE /users/1
- URL은 자원을 표현하고, 동사는 메서드가 담당합니다. 두 곳에 동사를 넣는 건 역할 중복입니다.
- 잘못된 설계
- POST 를 모든 곳에 사용합니다.
- 잘못된 설계
- POST /users/update
- POST /users/getList
- 올바른 설계
- POST /users/delete
- GET /users
- PATCH /users/1
- DELETE /users/1
- HTTP 메서드 자체가 행위를 표현하는 수단인데 이를 무시하고 POST로 통일하면 API를 읽는 사람이 의도를 파악하기 어렵습니다,
- 잘못된 설계
- 리소스를 단수로 표현합니다.
- 컬렉션을 나타낼 때는 복수 명사를 사용하는 것이 관례다. 팀 내에서 통일하는 것이 중요합니다.
- 리소스의 소유 관계를 URI 로 표현하지 않습니다.
- 쿼리 파라미터로 조건을 거는 방식
- GET /orders?userId=1
- 소유 관계를 URI 계층 구조로 표현하는 방식
- GET /users/1/orders
- 두 설계 모두 틀리지 않습니다 차이는 의도에 있습니다.
- GET /orders?userId=1은 "전체 주문 중 userId=1인 것을 필터링"하는 느낌입니다.
- 반면 GET /users/1/orders는 URI 계층 구조 자체가 "이 주문들은 유저 1에 종속된 것"이라는 소유 관계를 명확히 드러냅니다. 읽는 사람이 URI만 봐도 관계를 바로 파악할 수 있습니다.
- 실무에서는 이렇게 구분하는 경우가 많다.
- GET /users/1/orders → 유저 1의 주문 목록 (소유 관계)
GET /orders?status=pending → 전체 주문 중 대기 중인 것 (필터링/검색)
- GET /users/1/orders → 유저 1의 주문 목록 (소유 관계)
- 쿼리 파라미터로 조건을 거는 방식
- 상태 코드를 판단 기준 없이 사용합니다
- 상황에 맞는 코드 선택
- 멱등성 고려 안함 -> 중복 요청 시 Idempotency Key전략 필요
- API 버전 관리하지 않습니다
- 버전 없음 - 스펙 변경 시 하위 호환이 불가합니다.
- users
- URI 버전 관리
- /v1/users, /v2/users 등 명시
- 서비스가 커지면 반드시 API 버전 관리가 필요해집니다. 처음부터 /v1을 붙이는 습관을 들이십다.
- 나중에 추가하려면 이미 배포된 클라이언트가 있어 되돌리기 어렵습니다.
- 버전 없음 - 스펙 변경 시 하위 호환이 불가합니다.
실무에서는 사소한 설계 실수도 장애, 버그. UX 문제로 이어집니다.
미리 규칙과 기준을 잡는 것이 유지보수와 서비스 안정성에 큰 차이를 만듭니다.
7. Spring에서 HTTP 메서드 적용
@RestController
@RequestMapping("/v1/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<List<UserResponse>> getUsers() {
return ResponseEntity.ok(userService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> createUser(@RequestBody UserCreateRequest request) {
UserResponse response = userService.create(request);
URI location = URI.create("/v1/users/" + response.getId());
return ResponseEntity.created(location).body(response);
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> replaceUser(@PathVariable Long id, @RequestBody UserReplaceRequest request) {
return ResponseEntity.ok(userService.replace(id, request));
}
@PatchMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(@PathVariable Long id, @RequestBody UserUpdateRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
최종 정리
| 메서드 | 역할 | 멱등 | Safe | 캐싱 | 실무 포인트 |
| GET | 조회 | ✅ | ✅ | ✅ | 서버 상태 변경 금지 |
| POST | 생성/처리 | ❌ | ❌ | ❌ | ID는 서버 결정, 중복 주의, Idempotency Key 고려 |
| PUT | 전체 교체 | ✅ | ❌ | ❌ | 일부 필드만 보내면 null 위험, Upsert 거의 안 씀 |
| PATCH | 부분 수정 | ⚠️ | ❌ | ❌ | 설계에 따라 멱등 여부 달라짐, 수정 API 기본 선택 |
| DELETE | 삭제 | ✅ | ❌ | ❌ | 200 vs 204 팀 기준 통일 필요 |
마치며
HTTP 메서드는 CRUD 표현 도구가 아니라, 서비스 안정성과 데이터 정합성을 지키는 설계 기반입니다.
항상 스스로 물어야 합니다.
이 요청이 여러 번 호출되면 어떤 일이 발생하는가?
이 사고 방식은 HTTP 에만 국한되지 않습니다.
Kafka Consumer 중복 처리, 분산 트랜잭션, 이벤트 소싱 등 안정적 시스템 설계 전반에서 반복적으로 등장합니다.
HTTP 메서드와 멱등성 개념을 이해한 개발자는, 그 사고방식을 더 큰 시스템 설계에도 자연스럽게 적용할 수 있습니다.
'Network' 카테고리의 다른 글
| 네트워크 예외 - 종료 (0) | 2025.05.12 |
|---|---|
| 버퍼(Buffer)와 I/O 최적화 (0) | 2025.04.21 |
| HTTP 메서드란? 멱등성 (0) | 2025.04.01 |
| HTTP/ HTTPS , SSL 가속기 (0) | 2025.03.19 |
| L4 , L7 로드밸런서 (0) | 2025.02.12 |