개요
본 글은 RFC(Request for Comments, 의견 요청서) — IETF(국제 인터넷 표준화 기구) 에서 발행하는 인터넷 기술, 프로토콜, 절차 등에 대한 기술 표준 및 정보 문서 내용을 기반으로 작성했습니다.
"PUT은 전체 수정, PATCH는 일부 수정"
흔히 이렇게 알고 계십니다. 틀린 말은 아니지만, 이 한 줄이 수많은 오해를 만듭니다.
이 글에서는 RFC 원문을 기반으로 PUT과 PATCH의 정확한 차이를 짚고, 실무에서 자주 헷갈리는 부분들을 정리합니다.
RFC 원문 정의
PUT (RFC 7231)
대상 리소스의 현재 표현을 요청 페이로드로 교체(replace) 한다.
PATCH (RFC 5789)
리소스에 부분적 수정(partial modification) 을 적용한다.
핵심 차이는 교체 vs 수정입니다.
흔히 헷갈리는 궁금증 해소
1. "전체 수정"은 테이블 전체가 아닙니다
PUT이 말하는 "전체"는 해당 리소스의 표현(representation) 전체입니다.
PUT /patients/1 이면 1번 환자 리소스 하나의 모든 필드를 보내야 한다는 뜻이지, patients 테이블 전체를 의미하는 게 아닙니다.
PUT /patients/1
{
"name": "홍길동",
"age": 30,
"phone": "010-1234-5678",
"address": "서울시 강남구"
}
여기서 phone을 빼고 보내면? → 서버는 phone을 null 또는 기본값으로 교체해야 합니다. 생략 = 삭제입니다.
2. PATCH에서 null은 별개 문제입니다
많은 분들이 이렇게 외우십니다:
"PATCH는 보낸 필드만 수정, 안 보낸 필드는 유지"
반은 맞고 반은 틀립니다.
PATCH의 RFC 정의는 "부분 수정을 적용한다"까지입니다. null을 어떻게 처리할지는 RFC가 정하지 않습니다. 그건 API 설계자의 몫입니다.
2-1. null 처리의 흔한 두 가지 전략
| 전략 | null 의미 | 필드 미포함 의미 |
| 전략 A | 값 삭제 | 기존값 유지 |
| 전략 B | 기존값 유지 | 기존값 유지 |
전략 A가 더 일반적이고, JSON Merge Patch(RFC 7396)도 이 방식을 따릅니다.
PATCH /patients/1
{
"phone": null, ← 값 삭제
"address": "부산시" ← 값 변경
← name, age: 미포함이므로 유지
}
"PATCH니까 null이면 무조건 기존값 유지"는 팩트가 아닙니다. 프로젝트 컨벤션에 따라 달라집니다.
2-2. 그런데 Spring Boot에서 전략 A 구현이 가능할까?
기본 상태에서는 구분이 불가능합니다.
Spring Boot가 사용하는 Jackson은 역직렬화 시 "phone": null(명시적 null)과 phone 필드를 아예 안 보낸 경우(미포함) 모두 Java 객체에서 null이 됩니다.
Baeldung에서도 이 동작을 명확히 설명하고 있습니다:
역직렬화 시, 누락된 필드는 해당 타입의 기본값을 갖게 된다 (예: 객체는 null, 원시 타입은 0).
Jackson GitHub Issue #229에서도 Optional<?>을 사용하면 구분이 될 것으로 기대했지만, 실제로는 null과 미포함 모두 Optional.empty()로 처리되어 구분되지 않는다는 보고가 있었습니다.
즉, 전략 A(null = 삭제, 미포함 = 유지)를 구현하려면 별도 처리가 필요합니다.
Setter 호출 추적, Map<String, Object> 직접 파싱 등의 방법으로 구현 자체는 가능하지만, 필드가 많아질수록 boilerplate 코드가 늘어납니다. 섹션 단위 PATCH처럼 "보내는 필드를 명확히 고정"하는 설계가 이런 복잡도를 원천 차단하는 방법이기도 합니다.
3. 저장/수정이 하나의 API일 때
실무에서는 "없으면 생성, 있으면 수정" 기능이 필요할 때가 있습니다.
3-1. PUT의 Upsert
RFC 7231에 따르면 PUT은 원래 이 의미를 갖고 있습니다.
- 리소스가 있으면 → 교체 (200 OK)
- 리소스가 없으면 → 생성 (201 Created)
PUT /patients/1 ← 1번이 없으면 생성, 있으면 교체
단, 이건 클라이언트가 리소스 식별자를 알고 있을 때 자연스럽습니다.
3-2. POST + 비즈니스 로직
식별자를 서버가 생성하는 구조라면 POST 하나로 처리하고, 서비스 레이어에서 분기하는 것이 현실적입니다.
// Service
public PatientResponse saveOrUpdate(PatientCommand command) {
Patient patient = patientRepository.findById(command.getId())
.map(existing -> existing.update(command))
.orElseGet(() -> Patient.create(command));
return PatientResponse.from(patientRepository.save(patient));
}
4. 섹션 단위 PATCH 패턴 (Sub-resource 패턴)
PATCH가 반드시 "필드 하나하나 null 체크"를 의미하지는 않습니다. 섹션 단위 교체도 유효한 PATCH입니다.
"섹션 단위 PATCH"라는 공식 명칭은 없지만, REST 설계에서는 이 방식을 Sub-resource 패턴이라고 부릅니다. 큰 리소스를 하위 리소스(sub-resource)로 분리하고, 각각의 엔드포인트에서 독립적으로 업데이트하는 방식입니다.
참고로 비슷한 목적의 접근 방식도 있습니다:
- Google AIP-134 FieldMask — update_mask 필드로 업데이트할 필드를 명시적으로 지정합니다.
- Intent-based update — 범용 PATCH 대신 "목적 기반 업데이트" 엔드포인트를 설계하는 방식입니다.
4-1. 이런 상황에서 쓰면 좋습니다
- 리소스의 필드가 많고, 화면(탭/섹션)별로 수정 단위가 나뉠 때
- 프론트-백엔드 간 "이 API는 이 필드들을 전부 보낸다"는 계약이 명확할 때
- 필드별 null 분기 로직을 제거하고 코드를 단순하게 유지하고 싶을 때
- 섹션별로 검증(Validation) 규칙이 다를 때
4-2. 예시
환자 정보가 기본정보 / 보험정보 / 치료정보 섹션으로 나뉠 때
PATCH /patients/1/insurance
{
"insuranceType": "건강보험",
"insuranceNumber": "12345678",
"copayRate": 30
}
insurance 섹션의 모든 필드를 전송합니다. 리소스 전체가 아닌 섹션을 교체하는 것이므로 PATCH가 맞고, 필드별 null 분기 로직이 필요 없습니다.
// 섹션 통째로 교체 — 필드별 null 분기 없음
public void updateInsurance(InsuranceCommand command) {
this.insuranceType = command.getInsuranceType();
this.insuranceNumber = command.getInsuranceNumber();
this.copayRate = command.getCopayRate();
}
섹션의 모든 필드가 항상 전송되므로 if (command.getX() != null) 같은 방어 코드가 불필요합니다.
5. 한눈에 보는 비교표
구분 PUT PATCH
| 구분 | PUT | PATCH |
| RFC | 7231 | 5789 |
| 의미 | 리소스 표현 전체 교체 | 리소스 부분 수정 |
| 필드 생략 | 생략 = 삭제(null) | 설계에 따라 다름 |
| Upsert | 스펙상 지원 | 일반적이지 않음 |
| 멱등성 | 멱등 | 멱등 아닐 수 있음 |
6. 설계할 때 같이 고려하면 좋은 것들
6-1. 멱등성이 실무에서 의미하는 것
PUT은 같은 요청을 여러 번 보내도 결과가 동일(멱등)하므로 네트워크 재시도에 안전합니다. PATCH는 멱등이 보장되지 않으므로, 프론트에서 중복 요청 방지(버튼 비활성화, 요청 ID 등)를 더 신경 써야 합니다.
6-2. 동시성 (낙관적 락)
PUT/PATCH 모두 동시 수정 충돌이 발생할 수 있습니다. 특히 PATCH는 부분 수정이라 두 사람이 다른 섹션을 동시에 고치면 한쪽이 유실될 수 있습니다. @Version 필드로 낙관적 락을 걸어두면 충돌 시 예외를 던져서 데이터 유실을 막을 수 있습니다.
6-3. 수정 후 응답 설계
변경된 리소스를 응답에 포함하면 프론트가 별도 GET 없이 바로 화면을 갱신할 수 있습니다. 실무에서는 200 + 변경된 리소스 반환이 가장 편합니다.
마무리
설계에 정답은 없습니다. 트레이드오프가 있을 뿐입니다.
PUT만 쓰면 단순합니다. 항상 리소스 전체를 보내니까 서버 코드도 직관적이고, 멱등성이 보장되니 재시도도 안전합니다. 대신 필드가 30개인 리소스에서 하나만 고치고 싶어도 30개를 다 보내야 합니다. 네트워크 비용도, 프론트의 부담도 커집니다.
PATCH만 쓰면 유연합니다. 바뀐 것만 보내니 가볍고, 화면 단위로 API를 나누기도 좋습니다. 대신 null 처리 규칙을 팀이 합의해야 하고, 멱등성을 직접 챙겨야 하고, 프론트-백엔드 간 "어디까지 보내야 하는가"의 계약이 필요합니다.
결국 중요한 건 프로젝트의 규모, 팀의 상황, 리소스의 복잡도에 맞는 선택을 하는 것입니다. 스펙이 열어둔 자유는, 우리가 상황에 맞게 판단하라는 의미이기도 합니다. 어떤 방식을 선택하든 팀 안에서 규칙을 정하고 일관되게 쓰는 것. 그게 가장 좋은 설계라고 생각합니다.
'dev.log' 카테고리의 다른 글
| 계층형 CTE 한 줄이 주석 처리된 코드 세 줄을 살린다 (0) | 2026.03.08 |
|---|---|
| 도메인 모델이 뚱뚱해지고 있다면, Read Model을 도입할 때입니다 (0) | 2026.02.25 |
| AI가 코드를 짜는 시대, 개발자로서 느낀 것들 (0) | 2026.02.17 |