스프링 MVC - 1편 - 웹 페이지 만들기 - (5) 상품 수정, PRG PostRedirectGet, RedirectAttributes
인프런 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술편을 학습하고 정리한 내용 입니다.
상품 수정
상품 수정 폼 컨트롤러
hello.itemservice.web.basic.BasicItemController
1
2
3
4
5
6
7
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable("itemId") Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
/resources/static/editForm.html → 복사 → /resources/templates/basic/editForm.html
/resources/templates/basic/editForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!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>
<form action="item.html" th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" th:value="${item.itemName}" value="상품A">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" th:value="${item.price}" value="10000">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" th:value="${item.quantity}" value="10">
</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"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
onclick="location.href='item.html'" type="button">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
상품 수정에 th:value를 사용해서 값들을 세팅했다.
이제 상품 수정을 동작하게 하는 로직을 개발하자.
상품 수정 개발
1
2
3
4
5
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable("itemId") Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
1
2
3
4
5
6
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
뭐 이런 식이다.
- GET
/items/{itemId}/edit: 상품 수정 폼 - POST
/items/{itemId}/edit: 상품 수정 처리


다음과 같이 /basic/items/{itemId}/edit 이고 저장 하면..

/basic/items/{itemId}로 리다이렉트 되었다. 물론 상품 수정도 잘 됐다.
리다이렉트
상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.
- 스프링은
redirect:/..으로 편리하게 리다이렉트를 지원한다. redirect:/basic/items/{itemId}- 컨트롤러의 매핑된
@PathVariable의 값은redirect에도 사용할 수 있다. redirect:/basic/items/{itemId}→{itemId}는@PathVariable("itemId") Long itemId값을 그대로 사용한다.
- 컨트롤러의 매핑된
참고
HTML Form 전송은 PUT, PATCH를 지원하지 않는다. GET, POST만 사용할 수 있다.
PUT, PATCH는HTTP API전송 시에 사용.
스프링에서 HTTP POST로 Form 요청할 때 히든 필드를 통해 PUT, PATCH 매핑을 사용하는 방법이 있지만, HTTP 요청 상 POST 요청이다.

내가 이전에 상품 등록 할 때 리다이렉트를 왜 안하지.. ?
했는데 이유가 있다. 중복 방지를 알려주기 위해서…
PRG Post/Redirect/Get
사실 지금까지 진행한 상품 등록 처리 컨트롤러는 심각한 문제가 있다. (addItemV1 ~ addItemV4)
상품 등록을 완료한 후에 새로고침을 해보면…


계속 생겨버린다.

POST 등록 후 새로 고침

1
2
3
4
5
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
자 상품 등록한 코드이다.
POST 요청으로 등록 → 그냥 뷰 템플릿 띄운다.
이제 브라우저에 새로 고침을 하게 되면, 내가 마지막으로 한 요청이 POST이므로
POST /add가 한번 더 나가버리게 된다.
이때 해결하는 방법이 redirect인 것이다.
1
2
3
4
5
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items" + item.getId();
}
이런 식으로 고쳐야 한다.
상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 리다이렉트 하도록 코드를 작성하자.
이런 문제 해결 방식을 PRG Post/Redirect/Get이라 한다.
주의
"redirect:/basic/items/" + item.getId() redirect에서 +item.getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다. (id는 숫자여서 괜찮은데, 한글, 영어 같은 걸 넣으면..)
다음에 설명하는 RedirectAttributes를 사용하자.
RedirectAttributes
상품을 저장하고 상품 상세 화면으로 리다이렉트 한 것 까지는 좋았다.
그런데 고객 입장에서 저장이 잘 된 것인지 아닌지 확신이 들지 않는다.
그래서 저장이 잘 되었으면 상품 상세 화면에서 저장완료! 라는 메시지를 보여달라는 요구사항이 왔다.
간단히 해결해 보자.
컨트롤러에 추가할 거다.
1
2
3
4
5
6
7
8
@PostMapping("/add")
public String addItemV6(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 를 추가해보자.
그리고 뷰 템플릿에서 이 값이 있으면, 저장 완료! 라는 메시지를 출력 해보자.

뷰단에 이렇게 추가했다.

http://localhost:8080/basic/items/3?status=true url이 다음과 같이 나왔다.
설명
RedirectAttributes를 사용하면 URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리해준다.
redirect:/basic/items/{itemId}pathVariable바인딩:{itemId}- 나머지는 쿼리 파라미터로 처리:
?status=true
이제 뷰 쪽을 보면
th:if: 해당 조건이 참이면 실행${param.status}: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능- 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 한다. 그런데 쿼리 파라미터는 자주 사용해서 타임리프 에서 직접 지원한다.
드디어 스프링 MVC 1편이 모두 끝났다.. (2024.03.09 ~ 2024.04.24 매일 아침 30분 ~ 1시간..)
스프링의 내부 구조를 배우게 된 좋은 강의였다고 생각한다.
이제 2편 봐야겠다..
댓글남기기