9. 스프링 MVC와 Thymeleaf로 웹 페이지 만들기
2025. 5. 12. 17:30ㆍSpring/MVC
1. Controller
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
/*
1. @RequiredArgsConstructor
@RequiredArgsConstructor는 final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
+ 생성자가 딱 1개만 있으면 스프링이 해당 생성자에 @Autowired로 의존관계를 주입해주기 때문에 생략 가능하다.
=> 따라서 @RequiredArgsConstructor 넣으면 아래 생성자 코드 및 @Autowired 코드 없어도 됨
@Autowired
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
*/
//2. 초기화 코드로 테스트용 데이터 추가
@PostConstruct
public void init(){
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
//3. 파라미터로 Model 받기
@GetMapping
public String items(Model model){
List<Item> items = itemRepository.findAll();
//ModelAndView mv = new ModelAndView().addObject("items", items);
model.addAttribute("items", items);
return "basic/items";
}
//4. @PathVariable사용
@GetMapping("/{itemId}")
public String itemById(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
/*
5. url 설계
addForm 화면으로 가는 요청은 GET 방식으로,
실제로 폼데이터를 넘겨주는 요청은 POST 방식으로 설정하되
url은 add로 동일하게 설정한다.
- GET /add 요청: 상품 추가 폼
- POST /add 요청: 상품 추가 처리
*/
@GetMapping("/add")
public String add(){
return "basic/addForm";
}
//6. addItem
/*
Version 1: @RequestParam 사용
*/
//@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam Integer price,
@RequestParam Integer quantity,
Model model
){
Item item = new Item(itemName, price, quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
/*
Version 2: @ModelAttribute 사용
① 객체 생성 → setter로 파라미터 값 자동 바인딩
② @ModelAttribute로 지정한 객체를 Model에 자동으로 넣어줌!
@ModelAttribute("hello") Item item
→ model.addAttribute("hello", item);
*/
//@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item){
//Item item = new Item(itemName, price, quantity);
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
/*
Version 3: @ModelAttribute의 이름을 생략 가능
클래스명의 첫 글자를 소문자로 바꿔서 (Item -> item)
모델 애트리뷰트의 이름으로 사용한다.
*/
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item){
itemRepository.save(item);
return "basic/item";
}
/*
Version 4: @ModelAttribute 자체를 생략
String, int, Integer 같은 단순 타입 = @RequestParam 붙음
나머지 객체 = @ModelAttribute 붙음
*/
//@PostMapping("/add")
public String addItemV4(Item item){
itemRepository.save(item);
return "basic/item";
}
/*
Version 5: redirect로 응답 변경
새로고침시 같은 데이터 재등록을 막기 위해서는 redirect 처리 필요
스프링은 "redirect:/..." 으로 편리하게 리다이렉트를 지원한다.
*/
//@PostMapping("/add")
public String addItemV5(Item item){
itemRepository.save(item);
Long itemId = item.getId();
return "redirect:/basic/items/"+itemId;
}
/*
Version 6: RedirectAttributes 사용
위에 메소드처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.
RedirectAttributes를 사용하면 URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리해준다.
url에 붙이지 않은 애트리뷰트는 쿼리 파라미터로 붙는다.
*/
@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}";
// 리다이렉트된 URL 예시: /basic/items/3?status=true
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
//redirect시 RedirectAttributes로 설정하지 않아도 메서드 파라미터 이름과 자동 매핑됨!
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item){
itemRepository.update(itemId,item);
return "redirect:/basic/items/{itemId}";
}
}
2. Thymeleaf 사용 HTML
<!-- 이동 URL 정리 -->
<!-- items.html -->
<button th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품 등록</button>
<a th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
th:text="${item.id}">ID</a>
<a th:href="@{|/basic/items/${item.id}|}"
th:text="${item.itemName}">상품명</a>
<!-- item.html -->
<button th:onclick= "|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
type="button">상품 수정</button>
<!-- 잘못된 리터럴 사용: "|location.href='@{/basic/items/${item.id}/edit}'|" -->
<button th:onclick="|location.href='@{/basic/items}'|"
type="button">목록으로</button>
<!-- addForm.html -->
<button th:onclick= "|location.href='@{/basic/items}'|"
type="button">취소</button>
<!-- editForm.html -->
<button th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
<!-- addForm.html -->
<!--
1. th:if : 해당 조건이 참이면 실행
2. ${param}: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 한다.
그런데 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원함
-->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
<form th:action method="post">
<!-- * action을 비워두면 현재 URL을 동일하게 사용하고, method만 명시한 post로 설정됨 * -->
<!-- th:action="/basic/items/add" -->
<div>
<label for="itemName">상품명</label>
<input type="text"
id="itemName" name="itemName"
class="form-control" placeholder="이름을 입력하세요">
</div>
...
<button class="w-100 btn btn-primary btn-lg" type="submit">상품 등록</button>
</form>
- input 태그의 id, name 속성의 차이
속성 | 역할 | for who? |
id | HTML 문서 안에서 고유한 값으로 브라우저 & JavaScript를 위한 식별자 |
화면(UI)용 |
name | 폼 데이터를 서버로 전송할 때 쓰이는 키 서버는 이 name 속성을 기준으로 값을 꺼냄 |
서버 전달용 |