웹 개발 패턴 중 자주 쓰이는 패턴으로 HTTP POST 요청
에 대한 응답이 또 다른 URL로의 GET 요청
을 위한 Redirect(응답 코드가 3XX)
여야 한다는 것을 의미한다.
즉, POST 방식
으로 온 요청에 대해서 GET 방식
의 웹페이지로 리다이렉트
시키는 패턴을 말한다.
그렇다면 여기서 이런 의문이 든다. "PRG 패턴을 사용하지 않으면 어떤 문제가 생기는데?"
전체 흐름
시작 전 예시로 사용 할 상품을 관리하는 도메인의 전체적인 흐름을 보여주겠다.
사용자는 상품 목록 컨트롤러에 요청을 해서 상품 목록을 볼 수 있고, POST 요청으로 상품을 등록하고 저장할 수 있다. 또한 상품의 상세를 볼 수 있으며, 수정 또한 가능하다.
아래는 예시로 사용 될 코드이다.
Controller
@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
private final ItemRepository itemRepository;
@Autowired
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
/** Controller */
@PostMapping("/add")
public String addItemV1(Item item) {
itemRepository.save(item);
return "/basic/item";
}
}
Item
@Data /** @Data를 사용하는 것은 좋은 습관이 아니다.
되도록이면 필요한 애노테이션을 상황에 따라서 추가하자.*/
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item(){}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
add.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
} </style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2></div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요"></div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" placeholder="수량을 입력하세요"></div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">상품
등록
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'" type="button"
th:onclick="|location.href='@{/basic/items}'|">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
웹 브라우저에서 새로 고침을 한다면 마지막에 서버에 전송한 데이터를 다시 전송한다.
이 대목이 굉장히 중요하다. 만약 상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add
+ 상품 데이터를 서버로 전송한다.
이 상태에서 새로 고침을 하게 된다면 어떤 일이 생길까?
위 사진에서 보이는 것처럼 마지막에 전송한 POST /add
+ 상품 데이터를 서버로 다시 전송한다.
중복
되어서 등록
되거나 결제
되는 문제가 발생한다.분명 심각한 문제라는 것을 알 수 있다. 이 문제를 어떻게 해결할까?
여기서 PRG 패턴
이 등장한다.
문제의 발생 원인을 생각해보면 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송하는데 그 데이터가 POST 요청이라는 것이다.
그렇다면 이 문제를 해결하려면 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 Redirect
를 호출해주면 된다. 웹 브라우저는 Redirect
의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다. 따라서 마지막에 호출한 내용이 상품 저장이 아닌 상품 상세 화면인 GET /items/{id}
가 되는 것이다.
이제 새로고침을 해도 상품 상제 화면으로 이동하게 되므로 문제가 생기지 않는다.
그럼 코드에 적용시켜보며 수정해보자.
Redicrect?
re(다시) + 지시하다(direct)를 의미한다.
클라이언트에서www.apple.com
URL을 웹 서버에 요청을 했다고 하자.
그런데 URL 주소가 변경되었거나 하는 상황에서 서버는 Redirect의 영향으로 HTTP 응답 메시지에 "www.banana.com
으로 가"라고 클라이언트에게 URL을 지시할 수 있다. 이렇게 Redirect를 이용하면 클라이언트가 서버에www.apple.com
URL 요청을 보내도www.banana.com
URL로 가게 된다.
Controller 수정
/**
* PRG - Post/Redirect/Get
*/
@PostMapping("/add") // Reditect를 적용한 PRG 패턴
public String addItemV2(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 Redirect
하도록 코드를 작성했다. 이런 문제 해결 방식을 PRG (POST / Redirect / GET)
이라한다.
"redirect:/basic/items/" + item.getId()
Redirect에서+item.getId()
처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다. 다음에 설명하자RedirectAttributes
를 사용하자.
상품을 저장하고 상품 상세 화면으로 Redirect
한 것 까지는 좋았다. 그런데 고객 입장에서 저장이 잘 된 것인지 안 된 것인지 확신이 들지 않는다. 그래서 저장이 잘 되었으면 상품 상세 화면에
"저장되었습니다"라는 메시지를 보이도록 해보자.
Controller 수정
/**
* RedirectAttributes 적용
*/
@PostMapping("/add")
public String addItemV3(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
리다이렉트 할 때 간단히 status=true
를 추가해보자.
그리고 뷰 템플릿에서 이 값이 있으면(status=true), "저장되었습니다."라는 메시지를 출력해보자.
실행해보면 다음과 같은 리다이렉트 결과가 나온다.
~url/basic/items/3?status=true
RedirectAttributes
RedirectAttributes
를 사용하면 URL 인코딩
도 해주고, pathVarible
, 쿼리 파라미터
까지 처리해준다.
redirect:/basic/items/{itemId}
PathVariable
바인딩: {itemId}
?status=true
뷰 템플릿 메시지 추가
resources/templates/basic/item.html
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
th:if
: 해당 조건이 참이면 실행${param.status}
: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능뷰 템플릿에 메시지를 추가하고 실행해보면 "저장 완료!" 라는 메시지가 나오는 것을 확인할 수 있다. 물론 상품 목록에서 상품 상세로 이동한 경우에는 해당 메시지가 출력되지 않는다.
출처
https://webstone.tistory.com/65
https://programmer93.tistory.com/76
김영한님의 Spring MVC 강의