웹 개발에서 폼(form) 제출 후 새로고침이나 뒤로 가기 등으로 인해 중복 데이터가 저장되는 문제는 매우 흔하게 발생합니다. 특히 결제, 회원가입, 게시글 등록 등 중요한 작업에서 이 문제가 발생하면 심각한 데이터 오류로 이어질 수 있습니다.
이런 문제를 효과적으로 해결하는 대표적인 방법이 바로 PRG(Post-Redirect-GET) 패턴입니다.
결제, 회원가입, 게시글 등록 등 중복 저장이 치명적인 모든 폼 처리에 PRG 패턴을 적용해야 합니다.
PRG는 POST → Redirect → GET의 3단계로 동작합니다.
POST
사용자가 폼을 작성해 서버로 POST 요청을 보냅니다. 서버는 이 요청을 받아 데이터(예: 주문, 결제 등)를 처리합니다.
Redirect (302/303 응답)
서버는 결과 페이지를 바로 반환하지 않고, 클라이언트에게 "다른 URL로 이동하라"는 리다이렉트(3xx 응답)를 보냅니다.
GET
클라이언트(브라우저)는 리다이렉트된 URL로 GET 요청을 보냅니다. 서버는 이 GET 요청에 대해 결과(예: 주문 완료 페이지)를 반환합니다.
sendRedirect()는 클라이언트에게 3xx 응답을 보내기 때문에 브라우저는 기본적으로 GET 요청을 사용합니다.
즉, POST 요청의 결과를 바로 응답하지 않고 GET 요청으로 전환함으로써, 데이터 중복 처리 문제를 방지하는 방식입니다.
| 효과/장점 | 설명 |
|---|---|
| 중복 제출 방지 | 새로고침, 뒤로 가기 등으로 인한 중복 POST 실행을 원천적으로 차단합니다. |
| 멱등성 확보 | POST 이후에는 GET만 반복되므로, 멱등한 상태를 유지할 수 있습니다. |
| 사용자 경험 개선 | 브라우저 경고창 없이 결과 페이지를 자연스럽게 보여주어 사용자 경험을 개선합니다. |
| URL 공유/북마크 가능 | 결과 페이지가 GET 요청이므로, URL을 공유하거나 북마크할 수 있습니다. |
| 데이터 일관성/신뢰성 보장 | 중복 데이터 저장, 중복 결제 등 치명적인 오류를 예방할 수 있습니다. |
// PRG 패턴 미적용 (중복 처리 위험)
@PostMapping("/pay")
public String pay() {
// 결제 로직
return "success"; // forward로 결과 페이지 반환
}
// PRG 패턴 적용 (중복 처리 방지)
@PostMapping("/pay")
public String pay() {
// 결제 로직
return "redirect:/success"; // 리다이렉트로 GET 요청 유도
}
@GetMapping("/success")
public String success() {
return "success";
}
src/
└─ main/
└─ webapp/
├─ WEB-INF/
│ └─ views/
│ └─ success.jsp
├─ payForm.jsp
└─ web.xml
<form action="/pay" method="post">
결제 금액: <input type="text" name="amount">
<input type="submit" value="결제하기">
</form>
@WebServlet("/pay")
public class PayServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String amount = req.getParameter("amount");
System.out.println("결제 처리 완료: " + amount);
// 여기서 JSP로 바로 forward하지 않음
// 대신 리다이렉트 (GET 요청 유도)
resp.sendRedirect(req.getContextPath() + "/success");
}
}
@WebServlet("/success")
public class SuccessServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 결과 페이지로 내부 forward (URL 유지)
RequestDispatcher rd = req.getRequestDispatcher("/WEB-INF/views/success.jsp");
rd.forward(req, resp);
}
}
<h2>결제가 성공적으로 완료되었습니다</h2>
/pay 서블릿이 처리 후 sendRedirect("/success")GET /success 요청/success 서블릿은 JSP로 forward → 사용자에게 응답 화면 보여줌위처럼 PRG 패턴을 적용하면, POST 후 새로고침해도 GET만 반복되어 결제 로직이 중복 실행되지 않습니다.
가장 중요한 것은, POST 요청 처리 후에는 반드시 redirect를 통해 GET 요청으로 결과 페이지를 응답하도록 설계하는 것입니다.