사전 POST
인터페이스

POST

gabury1고친 사람 github-actions[bot]

POST 는 서버에 데이터를 보내 처리해 달라고 요청하는 방법입니다. 주문을 넣거나 글을 올리거나 파일을 올릴 때 씁니다. 서버는 받은 데이터로 무언가를 새로 만들거나 바꿀 수 있습니다. 그래서 같은 요청을 두 번 보내면 결과도 두 번 생길 수 있습니다.

쉽고 빠른 이해

POST 는 서버에 "이 데이터를 받아서 처리해 달라"고 보내는 요청입니다. 예를 들어 쇼핑몰의 주문 버튼을 누르면 주문 내용이 POST 로 서버에 갑니다.

받아 오기만 하는 요청과 무언가를 바꾸는 요청을 갈라 두어야 합니다. 그래야 캐시는 어떤 응답을 저장해도 되는지 압니다. 브라우저와 클라이언트는 어떤 요청을 마음 놓고 다시 보내도 되는지 압니다. POST 는 "바꿀 수 있는 요청"이라는 표시입니다.

어떻게 도나:

  1. 클라이언트가 주소와 함께 보낼 데이터를 요청 본문에 담습니다
  2. 서버가 그 데이터로 주문을 만들거나 글을 저장합니다
  3. 서버가 결과를 상태 코드로 알려 줍니다. 새로 만들었으면 만든 것의 주소도 같이 줍니다

대가가 있습니다. 같은 POST 를 다시 보내면 주문이 두 건 생길 수 있습니다. 그래서 브라우저는 새로고침 때 경고를 띄웁니다. 응답이 끊겼을 때도 함부로 다시 보내기 어렵습니다.

상세

POST 는 HTTP(HyperText Transfer Protocol, 웹의 요청·응답 규약) 의 요청 메서드 가운데 하나입니다. 요청 메서드는 요청 첫 줄 맨 앞에 붙는 낱말로, 이 요청으로 무엇을 하려는지 서버에 알립니다. 받아 오기만 하는 GET 과 달리 POST 는 보낸 데이터를 서버가 처리하게 합니다.

이 절은 요청 모양, POST 가 약속하지 않는 성질과 그 결과, 캐시와 브라우저 규칙, 이웃 메서드를 차례로 봅니다.

요청 한 벌

POST 요청은 세 부분으로 이뤄집니다. 첫 줄에는 메서드와 대상 주소가 옵니다. 그 아래에 헤더가 오고, 빈 줄 하나 뒤에 요청 본문이 옵니다.

요청 본문은 서버에 넘길 데이터 덩어리입니다. GET 은 보통 본문 없이 주소만 보냅니다. POST 는 이 본문이 요청의 본론입니다.

아래는 주문 하나를 넣는 요청입니다. 본문이 JSON(JavaScript Object Notation, 중괄호로 값을 적는 텍스트 형식) 이라는 것을 Content-Type 헤더가 알립니다.

http
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
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 가 캐시에 미치는 영향

HTTP 캐시 · 무효화 · 캐시 가능성 · 프록시

POST 를 둘러싼 브라우저 보안 규칙

CORS · 프리플라이트 요청 · 동일 출처 정책 · CSRF · CSRF 토큰

POST 를 주로 쓰는 설계 방식

REST · GraphQL · PRG 패턴 · RPC · 웹훅

POST 가 속하는 상위 규격

HTTP · RFC 9110 · RFC 9111

다른 이름: POST 메서드 · 포스트