9. 스프링 MVC와 Thymeleaf로 웹 페이지 만들기

2025. 5. 12. 17:30Spring/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 속성을 기준으로 값을 꺼냄
서버 전달용