지난 편에서는 상품 목록, 상세, 등록까지 구현했다.
이번에는 상품 수정 기능을 추가하고, 등록 후 새로고침 시 발생하는 문제를 어떻게 해결하는지도 살펴본다.
1. 상품 수정 폼
컨트롤러
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
- @PathVariable로 전달된 상품 ID를 이용해 상품을 조회
- 모델에 담아 editForm.html에 전달
뷰 템플릿 (editForm.html)
<form th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" 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}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" th:value="${item.price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" th:value="${item.quantity}">
</div>
<button class="btn btn-primary" type="submit">저장</button>
<button class="btn btn-secondary"
th:onclick="|location.href='@{/basic/items/{id}(id=${item.id})}'|"
type="button">취소</button>
</form>
- th:value: 수정할 기존 데이터를 미리 채워 넣는다.
- 취소 버튼은 해당 상품 상세 화면으로 이동한다.
2. 상품 수정 처리
컨트롤러
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
- 상품 수정 후 다시 상품 상세 화면으로 이동하도록 redirect:를 사용한다.
- @ModelAttribute를 사용하면 파라미터 바인딩과 모델 등록이 자동 처리된다.
3. 새로고침 문제와 PRG 패턴
문제 상황
상품 등록 컨트롤러(addItemV1~V4)는 상품 등록 후 바로 뷰 템플릿을 반환했다.
이 상태에서 브라우저에서 새로고침(F5) 을 누르면, 직전의 POST 요청이 그대로 다시 실행되며 상품이 중복 등록되는 문제가 발생한다.
해결 방법: PRG (Post/Redirect/Get)
- 상품 등록 후에는 뷰 템플릿으로 바로 이동하지 않고 리다이렉트를 수행한다.
- 이렇게 하면 브라우저의 마지막 요청이 GET으로 바뀌어, 새로고침해도 POST가 반복 실행되지 않는다.
코드 예시
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
- 등록 후 /basic/items/{id} 로 리다이렉트
- 단, 문자열 결합 방식은 URL 인코딩 문제가 생길 수 있으므로 권장하지 않는다.
4. RedirectAttributes
상품 등록 후 “저장되었습니다” 같은 메시지를 표시하려면 어떻게 할까?
리다이렉트 시 쿼리 파라미터를 함께 전달하면 된다. 이때 RedirectAttributes를 활용할 수 있다.
코드 예시
@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}";
}
- RedirectAttributes는 URL 인코딩도 처리해주고, PathVariable과 쿼리 파라미터도 함께 지원한다.
- 결과적으로 아래와 같은 리다이렉트가 발생한다.
http://localhost:8080/basic/items/3?status=true
뷰 템플릿 (item.html) 메시지 추가
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
- ${param.status} : 타임리프에서 쿼리 파라미터를 직접 조회할 수 있다.
- th:if : 조건이 참일 때만 해당 태그 렌더링
5. 회고
이번 장에서는 상품 수정 기능과 더불어 PRG(Post/Redirect/Get) 패턴을 학습했다.
특히, 새로고침 시 중복 등록 문제가 왜 발생하는지, 그리고 이를 리다이렉트로 어떻게 방지할 수 있는지 명확히 이해할 수 있었다.
과거 프로젝트에서 회원 가입 후 새로고침으로 계정이 여러 개 생기는 문제가 있었는데, 그때 단순히 클라이언트 쪽에서만 막으려 했었다.
이제 보니 서버 차원에서 PRG 패턴과 RedirectAttributes를 적용하는 것이 훨씬 근본적인 해결책이었다.
앞으로는 이런 플로우 제어까지 고려해서 API와 화면을 설계해야겠다고 느꼈다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 MVC 1편 완강!!!!!!! - 전체 정리 (1) | 2025.08.18 |
---|---|
[Spring] MVC 22편 - 웹 페이지 만들기 (4, 최종 정리) (0) | 2025.08.18 |
[Spring] MVC 20편 - 웹 페이지 만들기 (2) (0) | 2025.08.18 |
[Spring] MVC 19편 - 웹 페이지 만들기 (1) (1) | 2025.08.18 |
[Spring] MVC 18편 - HTTP 응답과 메시지 컨버터 (0) | 2025.08.18 |