POST/Redirect/Get을 의미하는 PRG 패턴은 POST 요청을 GET으로 리다이렉션을 하는 디자인 패턴이다.
HTTP 메서드는 알고 있는데, 리다이렉션이란 뭘까?
일단 해당 번호가 의미하는 HTTP 상태코드에 대해 알아보자.
HTTP 상태코드
클라이언트가 보낸 요청의 처리 상태를 응답에서 알려주는 기능이다. 앞자리를 기준으로 아래와 같이 나누며, 이를 통해 요청 결과를 판단하고 필요시 문제를 추적할 수 있다.
1xx (Informational): 요청이 수신되어 처리 중
2xx (Successful): 요청 정상 처리
3xx (Redirection): 요청을 완료하려면 추가 행동이 필요
4xx (Client Error): 클라이언트 오류, 잘못된 문법등으로 서버가 요청을 수행할 수 없음
5xx (Server Error): 서버 오류, 서버가 정상 요청을 처리하지 못함
3xx (Redirection)
요청을 완료하기 위해 유저 에이전트의 추가 조치 필요 리다이렉트(Redirect)는 사용자가 처음 요청한 URL이 아닌, 다른 URL로 보내는 것을 의미한다.
300 Multiple Choices 301 Moved Permanently 302 Found 303 See Other 304 Not Modified 307 Temporary Redirect 308 Permanent Redirect
[302 Found 예시]
구글에 hello를 검색했을 때, 개발자도구에서 302 Found 상태를 확인할 수 있다.
응답 헤더에 Location이 있는 것을 볼 수 있다.
응답 결과에 따라 필요시 해당 위치로 자동 이동하는 것이 리다이렉션이다.
리다이렉트 흐름 이해하기 1. 클라이언트가 GET /hello로 요청을 보냄 2. 서버는 302 Found 코드와 함께 응답 헤더에 Location: /new-hello로 작성하여 변경된 위치를 알려준다. 3. 클라이언트는 다시 GET /new-hello 위치로 재 요청 4. 서버는 200OK 응답
리다이렉션은 영구 리다이렉션과 일시 다이렉션으로 나눌 수 있다.
영구 리다이렉션
특정 리소스의 URI가 영구적으로 이동. 원래의 URL은 사용하지 않음
301 Moved Permanently
리다이렉트시 요청 메서드가 GET으로 변경됨. (본문이 제거될 수도 있음)
308 Permanent Redirect
301과 동일하지만. 본문 유지! (POST 요청이면 POST 리다이렉트)
[301과 308의 리다이렉션 비교] 1. name=hello&age=20이라는 메시지를 본문에 담아 POST 요청을 보냈다.
2. 서버는 Location 헤더를 포함하여 리다이렉션 응답을 한다.
3-1. 클라이언트는 리다이렉션시에도 본문을 동일하게 유지하며 재요청한다. (308)
3-1. 301 응답의 경우. 리다이렉션은 GET으로 바뀌며 본문이 사라질 수 있다.
일시 리다이렉션
리소스의 URI가 일시적으로 변경되는 상황.
예시 상황
주문 완료 후 주문 내역 화면으로 이동. 실제로 주문 완료 페이지가 사라진 것은 아님
PRG 패턴을 적용하는 경우.
302 Found
리다이렉트시 요청이 GET으로 변하고, 본문이 제거될 수 있음
307 Temporary Redirect
요청 메서드를 반드시 유지. 본문 유지 가능성 높음
303 See Other
302와 동일하지만 반드시 GET으로 변경되어 리다이렉트
POST 요청을 GET으로 리다이렉션 하는 구조는 왜 필요할까?
멱등성 (Idempotent)
한 번 호출하든 두 번 호출하든 100번 호출하든 결과가 똑같다.
(단, 멱등은 외부 요인으로 중간에 리소스가 변경되는 것 까지는 고려하지 않는다.)
멱등 메서드
GET: 한 번 조회하든, 두 번 조회하든 같은 결과가 조회된다.
PUT: 결과를 대체한다. 따라서 같은 요청을 여러 번 해도 최종 결과는 같다.
DELETE: 결과를 삭제한다. 같은 요청을 여러번 해도 삭제된 결과는 똑같다.
POST: 멱등이 아니다! 두 번 호출하면 같은 결제가 중복해서 발생할 수 있다.
PATCH: 멱등이 아니다!
잠깐! PUT과 PATCH의 차이는?
- PUT : 기존 리소스를 대체 (덮어쓰기),
- PATCH : 요청 부분만 변경
POST의 멱등성 문제
위와 같이 멱등성이 보장되지 않는 POST 요청은 동일한 요청이 재제출되는 문제가 발생한다.
동일한 요청을 발생시키는 세 가지 방법이 있다.
1. '새로고침'으로 인한 재 요청
2. '뒤로 가기' 후 '앞으로 가기'
3. 제출 후 HTML FORM으로 돌아가서 양식을 재제출
새로고침 사례로 설명하자면, (1) 사용자가 웹 페이지에서 주문 버튼을 클릭하고, (2)새로고침을 수행하면 동일한 POST 요청이 서버로 전달되어 중복 주문이 발생할 수 있다.
결국 POST 요청의 응답을 GET으로 변경하여, GET의 멱등성 보장을 활용해 위 문제를 예방하는 것이 PRG 패턴의 목적이다.
PRG 패턴
POST 하나의 요청을 두 가지로 나누어서 진행한다.
첫 번째는 서버에 입력 데이터를 POST 하는 것. 두 번째는 클라이언트에게 출력을 GET 하는 것.
결과적으로 브라우저는 결과 페이지를 별도의 리소스처럼 불러온다.
PRG 적용 전
출처: 위키피디아
1. 클라이언트 최초 주문 요청
2. DB에 해당 주문이 저장된다.
3. 클라이언트가 주문 완료 응답 후 새로고침을 시도하여 동일한 POST 요청이 전달된다.
4. 마찬가지로 DB에 해당 주문이 저장된다. 중복 저장 발생!
PRG 적용 후
출처: 위키피디아
1. 클라이언트 최초 주문 요청 (POST 요청으로 주문 저장 시도)
2. DB에 주문이 저장되고, 11번 주문 완료 페이지를 전달한다.
3. 클라이언트는 리다이렉션 진행 (GET 요청으로 변경됨)
4. 서버 200 OK 응답! (DB 변경 사항 없음)
PRG 패턴의 활용
URL 단축 서비스, 더 이상 업데이트되지 않는 페이지, 사이트 도메인 변경 때 리다이렉트를 유용하게 사용할 수 있다.
토스에서는 결제 과정에서도 사용자가 결제를 완료하면, 결제창에서 성공 또는 실패 페이지로 이동시키는 리다이렉트 단계가 있다.
사용자가 쇼핑하는 동안에는 장바구니에 여러 개의 동일한 상품을 담아두는 것은 문제가 되지 않습니다. 장바구니의 내용과 각 상품의 수량을 표시하면 됩니다. 가장 중요한 것은 결제가 한 번만 처리되도록 하는 것입니다. 다음과 같은 형태가 될 수 있습니다.
1. 장바구니가 생성되고, 고유한 ID가 지정됨 2. 사용자가 장바구니에 상품을 추가하고 뒤로가기 하면, 최신 장바구니 상태로 갱신한 페이지를 표시하며 "이미 장바구니에 담긴 상품입니다. 추가하시겠습니까?" 메시지를 표시한다. 동일한 상품을 다시 추가하는 것은 사용자의 몫이다. 3. 장바구니를 결제하면 구매 시스템으로 전송되고 해당 장바구니 ID는 파기된다. (필요시 거래 번호를 내역 테이블에 저장한다.) 4. 사용자가 구매 후 뒤로가기를 클릭하면 브라우저는 장바구니ID를 불러오려 하지만 해당 데이터가 삭제되어 실패한다. 브라우저는 장바구니 대신 오류 메시지를 표시한다. 이를 통해 같은 장바구니를 제출하는 것은 불가능해진다. 4-2. 캐싱 브라우저 또는 프록시의 경우 뒤로가기를 클릭한 사용자는 하위 시스템에 제출된 동일한 장바구니를 볼 수 있다. 하지만 장바구니ID가 이미 삭제되었기 때문에 해당 장바구니를 제출하려고 하면 (4)번과 동일하게 실패한다.