일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch9
- 스프링 mvc1 - 서블릿
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch14
- 스프링 db1 - 스프링과 문제 해결
- 타임리프 - 기본기능
- 게시글 목록 api
- jpa 활용2 - api 개발 고급
- 스프링 mvc2 - 로그인 처리
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch11
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch13
- 자바의 정석 기초편 ch7
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch8
- 스프링 입문(무료)
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch1
- jpa - 객체지향 쿼리 언어
- @Aspect
- 2024 정보처리기사 수제비 실기
- 2024 정보처리기사 시나공 필기
- Today
- Total
나구리의 개발공부기록
스프링 MVC - 웹 페이지 만들기, 상품 상세, 상품 등록 폼, 상품 등록 처리 - @ModelAttribute, 상품수정, PRG Post/Redirect/Get, RedirectAttributes 본문
스프링 MVC - 웹 페이지 만들기, 상품 상세, 상품 등록 폼, 상품 등록 처리 - @ModelAttribute, 상품수정, PRG Post/Redirect/Get, RedirectAttributes
소소한나구리 2024. 3. 9. 23:03 출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 상품 상세
1) 상품 상세 구현
(1) BasicItemController - 상품 상세 추가
- PathVariable로 넘어온 상품 ID로 상품을 조회 하고 모델에 담아둔 뒤 뷰 템플릿을 호출
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
(2) 상품 상세 뷰 - 타임리프 적용
- /resources/static/html 경로의 item.html 복사 후 수정
- 속성 변경 th:value="${item.id}": 모델에 있는 item정보를 획득하고 프로퍼티 접근법으로 출력(item.getId())하고, value속성을 th:value속성으로 변경
- 상품 수정 링크: th:onclick="|location.href='@{/basic/items/{itemId}edit(itemId=${item.id})'|"
- 상품 목록 링크: th:onclick="|location.href='@{/basic/items}'|"
<!DOCTYPE HTML>
<!-- 타임리프 선언 -->
<html xmlns:th="http://www.thymeleaf.org">
... 동일 코드 생략
<!-- CSS 적용 -->
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
... 동일 코드 생략
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control" value="1" th:value="${item.id}" readonly>
</div>
<!-- 동일하게 itemName,itemPrice, itemQuantity의 value값을 th:value="${item.id}"의 값으로 치환 -->
... 동일 코드 생략
<div class="col">
<!-- 상품 수정 링크 변경 -->
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
type="button">상품 수정</button>
... 동일 코드 생략
<!-- 상품 목록 링크 변경 -->
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">목록으로</button>
... 동일 코드 생략
</html>
2. 상품 등록 폼
1) 상품 등록 폼 구현
(1) BasicItemController - 상품 등록 폼 추가
- 상품 등록 폼과 상품 저장이 같은 URL이지만 HTTP 메서드로 구분됨
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
// 같은 URL인데 HTTP 메서드로 기능을 구분
@PostMapping("/add")
public String save() {
return "basic/addForm";
}
(2) 상품 등록 뷰 - 타임리프적용
- /resources/static/html 경로의 addForm.html 복사 후 수정
- 속성 변경 th:action : HTML form에서 action에 값이 없으면 현재 URL에 데이터를 전송함
- 하나의 URL로 HTTP메서드를 다르게 적용하여 등록 폼과 등록 처리 기능을 구분했기 때문에 깔끔하게 처리할 수 있음
- th:onclick="|location.href='@{/basic/items}'|": 취소 시 상품 목록으로 이동
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
... 동일 코드 생략
<!--th:action에 아무런 값이 없으면 현재 URL에 값을 넘김 -->
<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 class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">취소</button>
... 동일 코드 생략
</html>
3. 상품 등록 처리 - @ModelAttribute
1) 상품 등록 처리
(1) BasicItemController - addItemV1(@RequestParam)
- save()메서드를 수정하여 상품 등록을 처리
- @RequestParam으로 변수를 하나씩 받아서 Item객체를 생성해고 itemRepository를 통해 저장 후 모델에 담아 뷰로 반환
- 중요!: 상품상세에서 사용한 item.html 뷰템플릿을 그대로 재활용, return "basic/item";
- setter로 값을 입력했지만 Item객체를 생성시 생성자를 통해 값을 입력해도 됨
@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
(2) BasicItemController - addItemV2(@ModelAttribute)
- @ModelAttribute - 요청 파라미터 처리: @ModelAttribute는 Item객체를 생성하고 요청 파라미터의 값을 프로퍼티 접근법(setXxx)로 입력해줌
- @ModelAttribute - Model 추가: @ModelAttribute는 모델(Model)에 @ModelAttribute로 지정한 객체를 자동으로 넣어주기 때문에 model.addAttribute()코드를 생략해도 됨
- 모델에 담을 때는 이름이 필요한데 @ModelAttribute에 ("item") 처럼 지정한 이름을 사용하고 만약 아래처럼 아래처럼 이름을 다르게 지정하면 다른 이름이 모델에 포함됨
- @ModelAttribute("hello") Item item -> @ModelAttribute 이름을 hello으로 지정
- model.addAttribute("hello", item); -> item 객체가 Model에 hello이름으로 저장 됨
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
// @ModelAttribute("item")을 하면 model.addAttribute("item", item) 코드가 자동으로 추가가 됨ㅁ
// model.addAttribute("item", item);
return "basic/item";
}
// 즉, 파라미터의 Model model도 생략이 가능하여 아래처럼 작성해도 됨
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item) {
itemRepository.save(item)
return "basic/item";
}
(3) BasicItemController - addItemV3(@ModelAttribute의 이름 생략)
- @ModelAttribute의 이름을 생략하면 모델에 저장될 때 클래스명을 사용하며 클래스의 첫글자만 소문자로 변경해서 @ModelAttribute의 이름으로 등록됨(Item -> item)
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
// @ModelAttribute("item") 괄호도 생략이 가능함 -> V2 버전과 동일하게 동작함
// @ModelAttribute 객체 매개변수에서 객체의 첫 대문자가 소문자로 변환되어서 @ModelAttribute 이름으로 등록 됨
itemRepository.save(item);
return "basic/item";
}
(4) BasicItemController - addItemV4(@ModelAttribute자체를 생략)
- @ModelAttribute 자체를 생략하도 동작하며 V3와 동작되는 방식은 동일함
- 파라미터에 단순타입이오면 @RequestParam으로 동작하고 그 외의 타입이오면 @ModelAttribute 타입으로 동작됨
- 그러나, 이렇게까지 생략을 해야할 필요성이 있는지는 고민하면서 사용할 것
@PostMapping("/add")
public String addItemV4(Item item) {
// @ModelAttribute 자체도 생략이 가능, V3와 동작되는 방식은 동일
itemRepository.save(item);
return "basic/item";
}
4. 상품 수정
1) 상품 수정 적용
(1) BasicItemController - 상품 수정 폼 추가
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
(2) 상품 수정 폼 뷰 - 타임리프 적용
- /resources/static/html 경로의 editForm.html 복사 후 수정
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
... 동일 코드 생략
<form action="item.html" th:action method="post">
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
... 동일 코드 생략
<input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}">
... 동일 코드 생략
<input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
... 동일 코드 생략
<input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}">
... 동일 코드 생략
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
... 기존 코드 동일
</html>
(3) BasicItemController - 상품 수정 처리 추가
- 상품 등록 프로세스와 유사하게 MappingURL은 동일하게 하면서 전송 방식만 GET에서 POST로 변경하여 처리
- redirect: 스프링에서는 redirect:/... 으로 편하게 리다이렉트를 할 수 있도록 지원하며 컨트롤러에 매핑된 @PathVariable의 값을redirect에도 사용할 수 있음
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId,item);
return "redirect:/basic/items/{itemId}"; // 리다이렉트
}
** 참고
- 리다이렉트에 대한 자세한 내용은 HTTP 강의를 참고
- HTML Form 전송은 GET, POST만 사용할 수 있으나 스프링에서는 히든 필드를 통해서 PUT, PATCH 매핑을 사용할 수 있음
- 그러나 이것도 HTTP 요청상으로는 POST 요청임
5. PRG Post/Redirect/Get
1) Post, Redirect Get - 실무에서 자주 사용함
(1) 문제 상황
- 지금까지의 상품 등록 처리 컨트롤러(addItemv1 ~ addItemv4)에는 상품을 등록완료하고 웹브라우저의 새로고침을 누르면 상품이 계속 중복 등록 됨
- 그 이유는 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송하는데, 상품 등록 폼에서 데이터를 입력하고 저장하여 서버로 전송한 데이터를 새로 고침을 하게되어 해당 데이터를 다시 서버로 전송하게 되기 때문임
- 이 부분은 상품 수정에서 했던것 처럼 redirect로 반환하여 해결할 수 있음
(2) 기존 addItemv1 ~ addItemv4 상품 등록 컨트롤러의 저장 방식
- Get /add로 상품 등록 폼 컨트롤러에 요청하면 GET 으로 상품 등록 폼 뷰를 응답
- POST /add 로 상품저장 컨트롤러에 요청하면 POST로 상품 상세 뷰를 응답 (여기에서 POST /add가 최종 요청)
- 새로고침 (마지막의 요청을 재요청)을 하면 POST /add가 다시 요청되면서 상품이 저장이 되고 그 저장한 상품 상세 뷰가 반환
(3) Redirect GET을 이용하여 문제 해결
- 동일
- POST /add로 상품저장 컨트롤러에 요청하면 상품상세 컨트롤러로 Redirect /iitems/{id} 호출
- 클라이언트(웹브라우저)가 Redirect의 영향으로 GET /items/{id}로 상품상세 컨트롤러에 요청 후 상품 상세 뷰가 응답
- 여기에서 새로고침하면 Get /items{id} 가 호출되면서 상품상세 뷰만 계속 응답 됨
(3) BasicItemController - addItemV5(새로고침하면 상품이 등록되는 문제를 해결, PRG 방식 적용)
- 상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 리다이렉트
** 주의!
- return redirect: 경로 + item.getId(); 처럼 URL 변수를 더해서 사용하는 것은 URL인코딩이 안되기 때문에 위험함
- 다음에 설명하는 RedirectAttributes를 사용하여 문제 해결
// 새로고침하면 계속 상품이 등록되는 문제를 해결
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
6. RedirectAttributes
1) RedirectAttributes
(1) 요구사항 추가
- 상품을 저장하고 상품 상세 화면으로 리다이렉트하여 새로 고침에 의해 상품 등록이 중복으로 되는 것을 막은 것은 해결되었음
- 그러나 고객입장에서 저장이 잘 되었는지 확인이 불분명하여 저장이 잘 되었으면 상품상세 화면에 "저장되었습니다"라는 메시지가 출력되도록 요구사항이 추가
(2) BasicItemController - addItemV6(RedirectAttributes 사용)
- 리다이렉트를 할 때 status=true를 추가하여 뷰 템플릿에서 이 값이 있으면 '저장되었습니다'라는 메시지를 출력
- RedirectAttributes를 사용하면 URL인코딩, PathVariable, 쿼리 파라미터까지 처리해줌
- redirect:/basic/items/{itemId}, PathVariable 바인딩으로 itemId 처리
- 나머지는 쿼리 파라미터로 처리, ?status=true
// 저장이 잘 되었는지 확인하는 문구를 추가
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
// getId() -> {itemId} 치환
// 나머지는 쿼리 파라미터로 처리 (?status=true)
return "redirect:/basic/items/{itemId}";
}
(3) item.html 수정 - 뷰 템플릿에 메시지 추가
- th:if: 해당 조건이 참이면 실행
- ${param.status}: 타임리프에서 쿼리 파라미터를 조회하는 기능
원래는 컨트롤러에서 모델에 담아서 값을 꺼내야 하지만 쿼리 파라미터는 자주 사용하여 타임리프에서 직접 지원함
<html>
... 동일 코드 생략
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 저장완료 시 param.status가 ture면 저장완료라는 문구를 띄움 -->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
... 동일 코드 생략
</html>
(4) 실행 결과
- 실행해보면 최종적으로 상품을 등록하면 상품이 저장되었다는 문구가 뜨게되고, 저장하지 않고 상품 상세화면으로 돌아가면 해당 문구는 보이지 않음
** 쿼리파라미터에 정보를 남지 않게 하려면 아래처럼 작성
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
// 아래처럼 하면 쿼리파라미터에 정보가 남지 않아 보안에 적합
redirectAttributes.addFlashAttribute("status", "저장 완료");
return "redirect:/basic/items/{itemId}";
}
<!-- redirectAttributes.addFlashAttribute("status", "저장 완료"); 사용 시 -->
<h2 th:if="${status}" th:text="${status}"></h2>