[Spring Web MVC] Spring MVC와 Thymeleaf 예시
스프링 MVC를 사용해 아주 간단한 쇼핑몰 프로젝트를 진행해보자.
여기서 검은색은 컨트롤러고, 컨트롤러를 통해 뷰를 호출한다.
이렇게 요구사항이 정리되면 디자이너 / 웹 퍼블리셔 / 백엔드 / 프론트엔드 개발자가 업무를 분담해서 프로젝트를 진행한다.
디자이너 : 디자인하고 결과물을 웹 퍼블리셔 혹은 프론트엔드 개발자에게 넘겨준다.
웹 퍼블리셔 : 디자인을 바탕으로 HTML CSS를 만들어 개발자에게 넘겨준다.
백엔드 : HTML CSS 가 나오기 전까지는 시스템을 설계하고 비즈니스 모델을 개발한다. 이후 화면이 나오면 뷰 템플릿으로 변환해 동적으로 화면을 구성하고 흐름을 제어한다.
프론트엔드 : 디자인을 바탕으로 HTML CSS를 만드는 역할도 하고, HTTP API를 통해 웹 클라이언트가 요구하는 데이터와 기능을 제공한다. 이 때 백엔드 개발자는 뷰 템플릿을 사용하지 않아도 된다.
//@Data
@Getter @Setter
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item(){
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
먼저 상품을 설계하자. 아이디 / 가격 / 수량을 가진다.
여기서 롬복의 @Data 애너테이션을 사용하면 Getter / Setter / RequiredArgConstructor / ToString / EqualsAndHash.. 등등 여러 가지가 자동으로 생성된다.
필요하지 않은 요소들을 추가하게 되면 의도하지 않은 동작이 발생할 수 있으니 조심하자. 필요한 요소만 가져다 쓰는게 합리적이다.
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); // 실무에서는 ConcurrentHashMap
private static long sequence = 0L;
public Item save(Item item){
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id){
return store.get(id);
}
public List<Item> findAll(){
return new ArrayList<>(store.values());
}
public void update(Long id, Item updateParam){
Item findItem = findById(id);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
// 이런 작업은 DTO 를 통해 작업하는 편이 좋음.
}
public void clearStore(){
store.clear();
}
}
Repository 에는 몇 가지 비즈니스 로직과 데이터베이스에 접근하는 작업을 처리한다. (여기서는 Map으로 저장.)
@Controller
@RequestMapping("/basic/items")
//@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@Autowired // 생성자 하나면 생략 가능
public BasicItemController(ItemRepository itemRepository){
this.itemRepository = itemRepository;
}
@GetMapping
public String items(Model model){
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
// for test
@PostConstruct
public void init(){
itemRepository.save(new Item("zasdf", 1,2));
itemRepository.save(new Item("z423asdf", 121,23));
}
}
생성자가 하나 뿐이면 @Autowired 애너테이션을 생략할 수 있고, 롬복의 @RequiredArgsConstructor 애너테이션을 사용하면 final 이 붙은 멤버변수에 대해 생성자도 자동으로 만들어준다.
@Controller 애너테이션이 붙어있으니 GetMapping 으로 해당 URL 요청이 들어올 때 basic/items 이름의 뷰를 찾는다.
<!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">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품
등록</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}"> <!-- 모델에 있는 items를 꺼내온다. 프로퍼티 접근법을 사용-->
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId = ${item.id})}" th:text="${item.id}">회원 아이디</a></td>
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId = ${item.id})}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">가격</td>
<td th:text="${item.quantity}">수량</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
동적 웹 페이지를 보여주기 위해 뷰 템플릿으로 타임리프를 사용하자.
<html xmlns: th="http://www.thymeleaf.org"> 로 타임리프 사용을 선언한다.
th:href 를 사용하면 뷰가 렌더링 될 때 기존의 href 값을 지정한 값으로 갈아치운다. (값이 없으면 새로 생성한다.)
대부분의 HTML 속성을 th: ~~~ 를 통해 변경할 수 있다.
HTML 페이지를 그대로 열어보면 th 가 적용되지 않고, 뷰 템플릿을 통해 열어보면 th: ~~~ 의 값이 ~~~ 로 대체되면서 HTML 값을 동적으로 변경할 수 있다.
순수 HTML 속성을 그대로 유지하면서 뷰 템플릿도 사용할 수 있어 타임리프를 네츄럴 템플릿 이라고도 한다.
@{...} 를 통해 URL 링크를 표현할 수 있다. URL 링크 표현식이라고 부른다.
"@[/basic/items/{itemId}(itemId=${item.id})}" 링크 표현식을 사용해 경로를 템플릿처럼 편하게 사용할 수 있고, 쿼리 파라미터도 생성할 수 있다. (간단할 경우 리터럴 문법을 사용하자.)
타임리프에서는 문자와 표현식은 분리돼있어 더해서 사용해야 한다.
'abc' + ${alpha.d} + '!' 원래는 이런식으로 사용해야 된다. 하지만 리터럴 대체 문법인 | | 를 사용하면?
"|abc${alpha.d}|" 이런식으로 더하기 없이 편하게 사용할 수 있다.
th:each 를 통해 반복 출력을 구현할 수 있다.
자바의 forEach 문법과 비슷하게 사용한다.
${...} 를 통해 모델에서 값을 조회해서 사용할 수 있다.
이 때 프로퍼티 접근법을 사용해 getter, setter를 자동으로 호출한다.
th:action HTML Form 을 통해 데이터를 전송할 때 action에 값이 없으면 현재 URL에 데이터를 전송한다.
상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 같게 하고 HTTP 메서드를 통해 두 요청을 구분하는 방식을 사용해 하나의 URL로 등록 폼과 등록 처리를 깔끔하게 처리할 수 있다.
@PostMapping("/add")
public String save(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model){
Item item = new Item(itemName, price, quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
상품을 추가하고 보여줄 때도 모델에 정보를 넣어주고 뷰를 재사용하면 된다.
@PostMapping("/add")
public String save(@ModelAttribute("item") Item item){
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute 애너테이션을 사용하면 좀 더 간단하게 처리할 수 있다.
해당 객체를 만들어서 값을 넣어주고, 모델에 데이터를 넣어주는 역할을 처리해준다.
@ModelAttribute("item") 에서 item이 모델에서의 키 값으로 사용한다. (지정하지 않으면 클래스명의 첫글자를 소문자로 한 이름을 사용)
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
극한으로 줄이면 @ModelAttribute 애너테이션도 생략할 수 있는데... 항상 적절하게 줄이도록 하자.
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
상품 수정도 처리하는 URL을 같게 하고 요청 메서드 타입으로 구분했다.
Post 방식으로 해당 URL이 요청되면 @ModelAttribute 애너테이션으로 상품 정보를 받아오고 업데이트 후 상품 상세 정보 페이지로 리다이렉트된다.
새로고침은 마지막에 서버에 전송한 데이터를 다시 전송하는 작업이다.
상품 등록 폼에서 데이터를 입력하고 저장하면 POST / add 를 통해 서버에게 데이터를 전송한다.
여기서 새로고침을 하면? 다시 POST / add 를 통해 서버에게 데이터를 전송한다.
POST 로 상품을 등록하는 작업 뒤에 리다이렉트를 추가해서 문제를 해결할 수 있다.
이러면 새로고침을 한다고 해도 마지막으로 전송한 데이터가 리다이렉션이라 상품이 중복돼서 저장되지 않는다.
이런 방식을 PRG (Post Redirection Get) 라고 부른다.
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}";
}
URL 인코딩 / PathVariable 처리 / 쿼리 파라미터까지 처리해준다.
PathVariable 로 바인딩되는 값은 그대로 치환해서 사용하고, 나머지는 쿼리 파라미터로 사용한다. (?status = true)
타임리프를 사용하면 클라이언트의 요청 메세지의 쿼리 파라미터를 가져올 수 있고, 쿼리 파라미터의 값에 따라 HTML을 동적으로 변형할 수 있다. (th:if / param.~~~ 사용)
'Spring > Spring Web MVC' 카테고리의 다른 글
[Spring Web MVC] Thymeleaf (2) (0) | 2022.08.20 |
---|---|
[Spring Web MVC] Thymeleaf (1) (0) | 2022.08.19 |
[Spring Web MVC] 응답 정보 다루기 (0) | 2022.08.17 |
[Spring Web MVC] 요청 정보 다루기 (0) | 2022.08.17 |
[Spring Web MVC] Spring MVC (0) | 2022.08.16 |
댓글
이 글 공유하기
다른 글
-
[Spring Web MVC] Thymeleaf (2)
[Spring Web MVC] Thymeleaf (2)
2022.08.20 -
[Spring Web MVC] Thymeleaf (1)
[Spring Web MVC] Thymeleaf (1)
2022.08.19 -
[Spring Web MVC] 응답 정보 다루기
[Spring Web MVC] 응답 정보 다루기
2022.08.17 -
[Spring Web MVC] 요청 정보 다루기
[Spring Web MVC] 요청 정보 다루기
2022.08.17