POST
고친 사람 github-actions[bot]
POST 는 서버에 데이터를 보내 처리해 달라고 요청하는 방법입니다. 주문을 넣거나 글을 올리거나 파일을 올릴 때 씁니다. 서버는 받은 데이터로 무언가를 새로 만들거나 바꿀 수 있습니다. 그래서 같은 요청을 두 번 보내면 결과도 두 번 생길 수 있습니다.
쉽고 빠른 이해
POST 는 서버에 "이 데이터를 받아서 처리해 달라"고 보내는 요청입니다. 예를 들어 쇼핑몰의 주문 버튼을 누르면 주문 내용이 POST 로 서버에 갑니다.
받아 오기만 하는 요청과 무언가를 바꾸는 요청을 갈라 두어야 합니다. 그래야 캐시는 어떤 응답을 저장해도 되는지 압니다. 브라우저와 클라이언트는 어떤 요청을 마음 놓고 다시 보내도 되는지 압니다. POST 는 "바꿀 수 있는 요청"이라는 표시입니다.
어떻게 도나:
- 클라이언트가 주소와 함께 보낼 데이터를 요청 본문에 담습니다
- 서버가 그 데이터로 주문을 만들거나 글을 저장합니다
- 서버가 결과를 상태 코드로 알려 줍니다. 새로 만들었으면 만든 것의 주소도 같이 줍니다
대가가 있습니다. 같은 POST 를 다시 보내면 주문이 두 건 생길 수 있습니다. 그래서 브라우저는 새로고침 때 경고를 띄웁니다. 응답이 끊겼을 때도 함부로 다시 보내기 어렵습니다.
상세
POST 는 HTTP(HyperText Transfer Protocol, 웹의 요청·응답 규약) 의 요청 메서드 가운데 하나입니다. 요청 메서드는 요청 첫 줄 맨 앞에 붙는 낱말로, 이 요청으로 무엇을 하려는지 서버에 알립니다. 받아 오기만 하는 GET 과 달리 POST 는 보낸 데이터를 서버가 처리하게 합니다.
이 절은 요청 모양, POST 가 약속하지 않는 성질과 그 결과, 캐시와 브라우저 규칙, 이웃 메서드를 차례로 봅니다.
요청 한 벌
POST 요청은 세 부분으로 이뤄집니다. 첫 줄에는 메서드와 대상 주소가 옵니다. 그 아래에 헤더가 오고, 빈 줄 하나 뒤에 요청 본문이 옵니다.
요청 본문은 서버에 넘길 데이터 덩어리입니다. GET 은 보통 본문 없이 주소만 보냅니다. POST 는 이 본문이 요청의 본론입니다.
아래는 주문 하나를 넣는 요청입니다. 본문이 JSON(JavaScript Object Notation, 중괄호로 값을 적는 텍스트 형식) 이라는 것을 Content-Type 헤더가 알립니다.
POST /orders HTTP/1.1
Host: shop.example
Content-Type: application/json
{"item": "book", "qty": 1}
Content-Type 은 본문이 어떤 형식인지 적는 헤더입니다. 서버는 이 값을 보고 본문을 어떻게 읽을지 정합니다. 같은 데이터라도 이 값이 틀리면 서버가 본문을 못 읽습니다.
본문에 담는 형식
POST 본문의 형식은 정해져 있지 않습니다. 보내는 쪽이 고르고 Content-Type 에 적습니다. 실무에서 자주 만나는 것은 셋입니다. 둘은 웹 페이지의 입력 양식인 HTML(HyperText Markup Language) 폼이 씁니다. 나머지 하나는 프로그램끼리 부르는 API(Application Programming Interface) 가 주로 씁니다.
Content-Type |
담기는 모양 | 주로 쓰는 곳 |
|---|---|---|
application/x-www-form-urlencoded |
item=book&qty=1 |
HTML 폼 기본값 |
multipart/form-data |
칸마다 경계선으로 나눈 조각 | 파일 올리기 |
application/json |
JSON 한 덩이 | API 호출 |
첫째와 둘째는 HTML 폼이 보내는 형식입니다. 폼의 method 를 post 로 두면 입력값이 이 형식으로 본문에 담깁니다. 파일이 섞이면 둘째를 씁니다. 파일의 바이트를 글자로 바꾸지 않고 그대로 실을 수 있기 때문입니다.
응답과 상태 코드
서버는 처리 결과를 상태 코드로 알립니다. 상태 코드는 응답 첫 줄에 오는 세 자리 숫자입니다. 앞자리가 2 면 성공, 4 면 요청 쪽 잘못, 5 면 서버 쪽 잘못입니다.
POST 가 성공했을 때 자주 쓰는 코드는 셋입니다.
| 코드 | 뜻 | 함께 오는 것 |
|---|---|---|
201 Created |
새 자원을 만들었다 | 만든 자원의 주소를 담은 Location 헤더 |
200 OK |
처리했고 결과를 본문에 담았다 | 처리 결과 |
204 No Content |
처리했고 돌려줄 본문은 없다 | 없음 |
201 은 POST 로 새 것을 만들 때 가장 잘 맞습니다. Location 헤더에 새 주문의 주소가 오면 클라이언트는 그 주소로 GET 을 보내 주문을 다시 읽을 수 있습니다.
HTTP/1.1 201 Created
Location: /orders/42
위 응답은 주문이 만들어졌고 그 주문의 주소가 /orders/42 라는 뜻입니다. 주문 번호는 서버가 정했습니다. 클라이언트는 보낼 때 이 번호를 몰랐습니다.
서버가 정하는 새 주소
POST 는 보통 모음을 가리키는 주소로 보냅니다. /orders 는 주문 하나가 아니라 주문 목록입니다. 거기에 POST 를 보내면 서버가 새 주문을 만들고 번호를 붙입니다.
이 점이 PUT 과 갈리는 곳입니다. PUT 은 클라이언트가 주소를 직접 대고 "이 주소의 내용을 이것으로 바꿔 달라"고 합니다. 주소를 누가 정하나로 둘을 가르면 헷갈리지 않습니다.
안전성과 멱등성
HTTP 는 메서드마다 두 가지 성질이 있는지 없는지를 정해 둡니다. 이 성질이 캐시와 재시도의 규칙을 정하므로 먼저 뜻을 봅니다.
안전하다는 것은 요청이 서버 상태를 바꾸지 않는다는 뜻입니다. GET 이 안전합니다. 검색 엔진의 수집 프로그램이 링크를 마구 따라가도 괜찮은 까닭이 이것입니다.
멱등하다는 것은 같은 요청을 여러 번 보내도 한 번 보낸 것과 결과가 같다는 뜻입니다. PUT 은 멱등합니다. 같은 내용으로 두 번 덮어써도 결과는 한 번 덮어쓴 것과 같습니다. DELETE 도 같은 까닭으로 멱등합니다.
POST 는 둘 다 약속하지 않습니다. 보낼 때마다 새 주문이 생길 수 있으니 멱등하지 않습니다. 상태를 바꿀 수 있으니 안전하지도 않습니다. 이 한 가지 사실에서 아래 문제들이 나옵니다.
| 메서드 | 안전 | 멱등 |
|---|---|---|
| GET | ✓ | ✓ |
| PUT | ✗ | ✓ |
| DELETE | ✗ | ✓ |
| POST | ✗ | ✗ |
다시 보내면 생기는 중복
네트워크는 응답을 잃어버릴 수 있습니다. 클라이언트가 POST 를 보냈는데 응답이 안 오면, 서버가 처리했는지 못 했는지 알 길이 없습니다. 요청이 가다가 사라졌을 수도 있습니다. 응답만 돌아오다 사라졌을 수도 있습니다.
GET 이나 PUT 이면 그냥 다시 보내면 됩니다. 멱등하니 두 번 처리돼도 결과가 같습니다. POST 는 다릅니다. 서버가 이미 처리했다면 다시 보낸 요청이 주문을 하나 더 만듭니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: POST /orders
Note over 서버: 주문 42 를 만든다
서버--x클라이언트: 201 응답이 중간에 사라진다
Note over 클라이언트: 시간 초과. 처리됐는지 모른다
클라이언트->>서버: 같은 POST 를 다시 보낸다
Note over 서버: 주문 43 을 또 만든다
서버-->>클라이언트: 201 Created
그림에서 사용자는 주문을 한 번 넣었는데 서버에는 두 건이 남았습니다. 그래서 HTTP 클라이언트와 프록시는 보통 POST 를 자동으로 재시도하지 않습니다. 프록시는 클라이언트와 서버 사이에서 요청을 대신 전달하는 중간 서버입니다.
이 문제를 푸는 흔한 방법은 요청마다 고유한 값을 붙이는 것입니다. 클라이언트가 주문마다 한 번 만든 값을 헤더에 실어 보냅니다. 서버는 이미 처리한 값이 다시 오면 새로 만들지 않고 처음 결과를 돌려줍니다. 이 값을 멱등성 키라고 부릅니다. POST 자체를 바꾸지 않고 서버가 멱등하게 처리해 주는 방식입니다.
새로고침과 다시 제출
브라우저에서도 같은 문제가 생깁니다. 폼을 POST 로 보낸 뒤 결과 화면에서 새로고침을 누르면 브라우저는 마지막 요청을 다시 보내려 합니다. 마지막 요청이 POST 이니 주문이 또 들어갈 수 있습니다. 그래서 브라우저는 "양식을 다시 제출할까요" 같은 경고를 띄웁니다.
이를 피하려면 먼저 리다이렉트를 알아야 합니다. 리다이렉트는 "다른 주소로 다시 요청하라"는 응답입니다. 브라우저는 이 응답을 받으면 알려 준 주소로 알아서 새 요청을 보냅니다.
이 응답을 쓰는 관례가 PRG(Post/Redirect/Get) 패턴입니다. PRG 패턴에서는 서버가 POST 를 받으면 결과 화면 대신 리다이렉트 응답으로 브라우저를 다른 주소로 보냅니다. 브라우저는 거기서 GET 으로 결과를 읽습니다. 아래 그림의 303 See Other 는 원래 메서드와 상관없이 GET 으로 다시 요청하라는 리다이렉트 코드입니다.
sequenceDiagram
participant 브라우저
participant 서버
브라우저->>서버: POST /orders
Note over 서버: 주문 42 를 만든다
서버-->>브라우저: 303 See Other · Location /orders/42
브라우저->>서버: GET /orders/42
서버-->>브라우저: 200 OK · 주문 화면
이제 브라우저의 마지막 요청은 GET 입니다. 새로고침을 눌러도 주문 화면을 다시 읽을 뿐 주문이 새로 생기지 않습니다.
POST 응답과 캐시
캐시는 응답을 저장해 두었다가 같은 요청이 오면 서버를 거치지 않고 돌려줍니다. GET 응답이 주된 대상입니다. 같은 주소를 다시 읽으면 같은 내용이 오리라고 기대할 수 있기 때문입니다.
POST 응답은 사정이 다릅니다. 같은 주소로 보내도 본문이 다르면 결과가 다릅니다. 보낼 때마다 서버 상태도 바뀝니다. 그래서 브라우저와 프록시 캐시는 대개 POST 응답을 저장하지 않습니다.
POST 는 캐시를 지우는 쪽으로 작용합니다. 캐시는 POST 같은 안전하지 않은 요청이 성공하면 그 주소에 저장해 둔 응답을 무효화합니다. 주문을 넣은 뒤에도 옛 주문 목록이 캐시에서 나오면 곤란하기 때문입니다.
조회에 POST 를 쓰는 경우
POST 는 상태를 바꿀 때 쓰는 것이 기본입니다. 그런데 읽기만 하는 요청에 POST 를 쓰는 경우도 흔합니다. GET 은 보낼 값을 주소 뒤 쿼리 문자열에 붙입니다. 주소 길이에는 서버와 프록시마다 상한이 있습니다. 조건이 길고 복잡하면 본문에 담을 수 있는 POST 가 편합니다.
GraphQL 이 대표적입니다. GraphQL 은 조회 질의도 보통 POST 본문에 JSON 으로 담아 한 주소로 보냅니다. 질의 문서가 길고 구조가 깊어 주소에 넣기 어렵기 때문입니다.
대가도 있습니다. 조회를 POST 로 보내면 HTTP 는 그 요청이 안전한지 알 수 없습니다. 캐시는 응답을 저장하지 않습니다. 클라이언트도 마음 놓고 재시도하지 못합니다. 읽기라는 사실은 서버와 클라이언트만 압니다. 프록시 같은 중간 장비는 모릅니다.
브라우저가 따로 확인하는 POST
출처는 주소의 스킴·호스트·포트를 묶은 것입니다. https://a.com 페이지에서 https://api.b.com 을 부르면 호스트가 달라 다른 출처로 보내는 요청이 됩니다.
브라우저는 이런 요청에 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 규칙을 겁니다. 서버가 허락해야 브라우저가 응답을 스크립트에 넘깁니다. 이 규칙은 보내는 것 자체를 막지 않고 결과 읽기를 막습니다.
POST 가운데 본문이 JSON 인 요청은 보내기 전에 확인 요청을 먼저 거칩니다. 브라우저가 OPTIONS 메서드로 "이 POST 를 보내도 되나"를 먼저 묻습니다. 서버가 허락해야 진짜 POST 가 갑니다. 이 확인을 프리플라이트 요청이라고 부릅니다.
HTML 폼의 두 형식인 application/x-www-form-urlencoded 와 multipart/form-data 는 이 확인을 건너뜁니다. 폼은 CORS 가 생기기 전부터 다른 출처로 POST 를 보낼 수 있었습니다. 그래서 이런 POST 는 서버의 허락을 묻지 않고 곧장 서버에 도착합니다. CORS 가 막는 것은 응답 읽기뿐이라 서버는 요청을 그대로 처리합니다.
이 틈을 노리는 공격이 CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)입니다. 공격자의 사이트가 사용자 몰래 다른 사이트로 폼을 제출하게 만듭니다. 이때 브라우저는 그 사이트의 쿠키에 담긴 로그인 정보를 요청에 함께 실어 보낼 수 있습니다. 서버는 로그인한 사용자가 직접 보낸 요청으로 여기고 처리합니다.
이웃 메서드와 가르기
POST 는 쓰임이 가장 넓은 메서드입니다. 다른 메서드가 딱 맞지 않을 때 POST 로 보내는 경우가 많습니다. 헷갈리는 이웃은 넷입니다.
| 메서드 | 하는 일 | POST 와 가르는 질문 |
|---|---|---|
| GET | 읽기 | 서버 상태를 바꾸나 |
| PUT | 주소를 대고 통째로 바꾸기 | 주소를 클라이언트가 정하나 |
| PATCH | 일부만 바꾸기 | 이미 있는 것의 일부를 고치나 |
| DELETE | 지우기 | 없애는 것이 목적인가 |
REST(Representational State Transfer, 주소마다 자원을 두고 메서드로 다루는 설계 방식) 방식의 API 는 대개 목록 주소에 POST 를 보내 새 항목을 만듭니다. 이미 있는 항목을 고칠 때는 PUT 이나 PATCH 를 씁니다. 이 관례가 강제는 아닙니다. 어느 메서드로도 서버는 무엇이든 할 수 있습니다. 다만 메서드를 성질에 맞게 고르면 캐시와 재시도가 알아서 올바르게 움직입니다.
관련 항목
POST 와 나란히 서는 HTTP 메서드
요청 메서드 · GET · PUT · PATCH · DELETE · HEAD · OPTIONS
POST 가 약속하지 않는 성질
안전한 메서드 · 안전하지 않은 메서드 · 멱등성 · 멱등성 키 · 재시도 · 중복 요청
POST 요청과 응답을 이루는 구성 요소
요청 본문 · HTTP 헤더 · Content-Type · 상태 코드 · 201 Created · 303 See Other · Location 헤더 · 리다이렉트
POST 본문에 담기는 형식
JSON · application/x-www-form-urlencoded · multipart/form-data · HTML 폼 · 파일 업로드 · 쿼리 문자열
POST 가 캐시에 미치는 영향
POST 를 둘러싼 브라우저 보안 규칙
CORS · 프리플라이트 요청 · 동일 출처 정책 · CSRF · CSRF 토큰
POST 를 주로 쓰는 설계 방식
REST · GraphQL · PRG 패턴 · RPC · 웹훅
POST 가 속하는 상위 규격
다른 이름: POST 메서드 · 포스트