리다이렉트 심화 — 301·302·303·307·308 정확히 구분하기

301 영구 vs 308, 302 vs 307, 그리고 303 See Other까지. 메서드·본문 보존 여부와 캐시 가능성을 기준으로 다섯 개의 3xx 리다이렉트를 구분하고, PRG 패턴으로 중복 제출을 막는 방법까지 정리합니다.

· 10 min read · PALDYN Team

지난 글에서 3xx와 Location 헤더로 리다이렉트의 기본 골격을 잡았다면, 이번에는 실무에서 가장 많이 헷갈리는 부분으로 들어간다. 301과 308은 둘 다 “영구”인데 왜 둘 다 존재하고, 302와 307은 뭐가 다르며, 303 See Other는 언제 쓰는가. 결론부터 말하면 이 다섯 개를 가르는 축은 단 두 가지다. 요청 메서드와 본문을 보존하는가, 그리고 응답을 캐시해도 되는가. 이 두 축만 잡으면 나머지는 표 한 장으로 정리된다.

두 개의 축으로 보는 전체 그림

리다이렉트 상태코드 매트릭스

다섯 개의 상태코드를 의미·메서드 보존·캐시 세 칼럼으로 펼치면 위와 같다. 핵심은 가로줄이 아니라 세로 칼럼이다.

  • 영구 vs 임시: 301·308은 영구(Permanent), 302·307·303은 임시(Temporary). 영구는 클라이언트가 결과를 캐시하고 다음부터 원래 URL을 건너뛰어도 된다.
  • 메서드 보존 vs 변환 허용: 307·308은 원래 메서드와 본문을 반드시 그대로 유지한다. 301·302는 역사적 관행상 POST → GET 변환이 허용된다. 303은 무조건 GET으로 바꾼다.

이 두 축의 조합이 곧 “언제 무엇을 쓰는가”의 답이다.

301 Moved Permanently vs 308 Permanent Redirect

둘 다 “이 리소스는 영구히 이사했다”는 뜻이고, 둘 다 캐시 가능하다. 차이는 메서드 보존 여부 하나뿐이다.

POST /old-api HTTP/1.1
Host: example.com
Content-Type: application/json

{"name":"paldyn"}

이 POST 요청에 서버가 301로 응답하면, 다수의 클라이언트(브라우저 포함)는 다음 요청을 GET /new-api로 메서드를 바꿔 보낸다. 본문도 사라진다. RFC 명세는 “메서드 변경을 권장하지 않는다”고 하지만, 301이 정의되던 시절의 브라우저 구현이 POST를 GET으로 바꿔버렸고 그 관행이 사실상 표준처럼 굳었다. 그래서 301은 GET·HEAD 리소스의 영구 이전에만 안전하게 쓴다.

반면 308은 메서드와 본문을 보존한다고 명세가 못 박았다. POST는 POST 그대로, 본문도 그대로 새 URL로 다시 간다. API 엔드포인트를 영구 이전하면서 POST/PUT/DELETE까지 동작을 유지해야 한다면 308이 정답이다.

HTTP/1.1 308 Permanent Redirect
Location: https://example.com/new-api

정리하면 사용자가 보는 웹페이지 URL의 영구 이전은 301, 메서드를 보존해야 하는 API의 영구 이전은 308이다.

302 Found vs 307 Temporary Redirect

이 쌍은 301/308과 정확히 같은 관계를 “임시” 버전으로 반복한다.

  • 302 Found: 임시 이전. 301과 마찬가지로 역사적으로 POST → GET 변환이 허용된다. 캐시는 기본적으로 안 된다(별도 헤더로 명시하지 않는 한).
  • 307 Temporary Redirect: 임시 이전이되, 메서드와 본문을 반드시 보존한다. 302의 메서드 변환 모호함을 없애려고 도입된 코드다.
HTTP/1.1 307 Temporary Redirect
Location: https://example.com/maintenance-target

점검 중 우회, 부하 분산, 일시적 미러처럼 “잠깐 다른 곳으로 보내지만 원래 동작(POST 포함)은 그대로여야 한다”면 307을 쓴다. 302는 메서드가 바뀔 수 있으므로 GET 흐름에만 안심하고 쓴다.

303 See Other — 항상 GET으로

303은 다른 둘과 성격이 다르다. “이 요청의 결과는 다른 URL에서 GET으로 확인하라”는 의미다. 원래 메서드가 무엇이었든 클라이언트는 무조건 GET으로 Location을 따라간다. 본문은 당연히 버린다.

이게 왜 유용한가. 사용자가 폼을 POST로 제출했을 때, 서버는 처리를 끝낸 뒤 “처리 결과 페이지를 GET으로 보라”고 안내하고 싶다. 307처럼 POST를 보존하면 결과 페이지에 또 POST가 날아가 버린다. 바로 이 시나리오가 다음에 나올 PRG 패턴이다.

PRG 패턴 — 303으로 중복 제출 막기

PRG 패턴 흐름

POST로 폼을 제출한 직후 결과 페이지를 그대로 200으로 렌더링하면, 사용자가 브라우저에서 새로고침(F5)할 때 브라우저가 “이 POST를 다시 보낼까요?”를 묻고, 무심코 확인하면 같은 주문이 두 번 들어간다. 이걸 막는 표준 해법이 PRG(Post / Redirect / Get)다.

  1. 클라이언트가 POST /orders로 주문을 제출한다.
  2. 서버는 주문을 저장한 뒤 303 See OtherLocation: /orders/1234를 응답한다.
  3. 브라우저는 자동으로 GET /orders/1234를 요청해 결과 페이지를 받는다.

이제 브라우저의 마지막 요청 기록은 POST가 아니라 GET이다. 사용자가 새로고침해도 안전한 GET만 반복되고, POST 재전송 경고도 뜨지 않는다.

POST /orders HTTP/1.1
Host: shop.example.com
Content-Type: application/x-www-form-urlencoded

item=keyboard&qty=1

HTTP/1.1 303 See Other
Location: /orders/1234

PRG에서 굳이 303을 쓰는 이유가 여기서 분명해진다. 302를 써도 많은 브라우저가 GET으로 바꾸지만 그건 “관행”일 뿐 보장이 아니다. 303은 명세가 GET 전환을 보장하므로 PRG의 의도를 정확히 표현한다.

캐시 가능성 — 영구 코드만 캐시된다

리다이렉트도 응답이므로 캐시 규칙을 따른다. 기본 캐시 가능 여부는 다음과 같다.

  • 301·308 (영구): 기본적으로 캐시 가능. 브라우저가 이를 캐시하면 다음부터 원래 URL 요청을 서버에 보내지도 않고 바로 새 URL로 간다.
  • 302·307·303 (임시): 기본적으로 캐시되지 않는다. 매번 원래 URL을 다시 물어본다.
# 응답의 상태코드와 Location, 캐시 관련 헤더 확인
curl -sI https://example.com/old-path

# 리다이렉트를 끝까지 따라가며 각 홉을 출력
curl -sIL https://example.com/old-path | grep -iE 'HTTP/|location|cache-control'

여기엔 운영상 함정이 하나 있다. 301을 잘못 걸면 브라우저가 영구 캐시해서, 나중에 설정을 고쳐도 사용자 브라우저는 한동안 옛 리다이렉트를 따라간다. 그래서 “확실히 영구”가 아니면 302/307로 시작하고, 검증이 끝난 뒤 영구로 승격하는 것이 안전하다.

곁가지 — 304, 그리고 deprecated된 305·306

  • 304 Not Modified: 형식상 3xx지만 리다이렉트가 아니다. 조건부 요청(If-None-Match/If-Modified-Since)에 대해 “캐시된 것 그대로 써도 된다”고 알리는 응답으로, Location을 동반하지 않는다. 캐시 검증의 영역이다.
  • 305 Use Proxy: 보안 문제로 사실상 폐기. 클라이언트가 무시하도록 권고된다.
  • 306 (unused): 과거에 예약됐다가 더 이상 쓰이지 않는다.

실무에서 다루는 리다이렉트는 결국 301·302·303·307·308 다섯 개와, 캐시 검증용 304가 전부다.

의사결정 정리

상황                                  | 코드
────────────────────────────────────────────────
페이지 URL 영구 이전 (GET)            | 301
API/메서드 보존 영구 이전             | 308
임시 우회 (GET 흐름)                  | 302
임시 우회 + 메서드 보존 (POST 등)     | 307
폼 제출 후 결과 페이지로 (PRG)        | 303
조건부 요청 캐시 검증                 | 304
  • 영구냐 임시냐를 먼저 정하고(캐시 영향), 그다음 메서드를 보존해야 하는지를 정하면 코드가 자동으로 결정된다.
  • 메서드 보존이 필요하면 무조건 307/308. 모호함이 없다.
  • 폼 제출 후 결과 페이지로 넘기는 PRG에는 303이 정석이며, 새로고침 중복 제출을 깔끔하게 막아준다.

다음 글에서는 이런 요청/응답이 오가는 토대인 프로토콜 자체로 내려가, HTTP/1.0과 1.1이 무엇이 어떻게 달라졌는지를 정리한다.


지난 글: HTTP 리다이렉트 완전 정복 — 3xx와 Location

다음 글: HTTP/1.0 vs HTTP/1.1 — 무엇이 달라졌나


읽어주셔서 감사합니다. 😊