1. 요구사항 분석
먼저 상품은 "상품 ID", "상품명", "가격", "수량"에 대한 정보를 가진다. 이때 상품 관리 기능으로는 "상품 목록", "상품 상세", "상품 등록", "상품 수정" 총 4가지 기능이 있다. 지금부터 해당 웹 페이지를 만들어보자. 서비스의 흐름은 다음가 같다.
- 클라이언트는 상품 목록에 들어가서 상품들을 볼 수 있다.
- 상품 목록에서는 상품 등록 폼으로 이동할 수 있다. 상품을 등록하면 상품을 저장한다. 상품을 저장하면 내부 호출을 통해 해당 상품에 대한 상세 화면을 띄워준다.
- 상품 상세에서는 상품 수정 폼으로 이동할 수 있다. 상품을 수정하면 상품 상세로 Redirect 된다.
2. 상품 도메인 개발
우리가 사용할 상품 클래스는 다음과 같이 만들 수 있다.
@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;
}
}
해당 상품을 수정하고, 상품을 조회하는 등의 기능을 수행하는 Repository는 다음과 같다.
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>();
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 itemId, Item updateParam) {
Item item = findById(itemId);
item.setItemName(updateParam.getItemName());
item.setPrice(updateParam.getPrice());
item.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}
3. 상품 목록 - 타임리프
- 타임리프 사용 선언: <html xmlns:th="http://wwww.thymeleaf.org">
- URL 링크 표현식: @{...}
- 리터럴 대체: |...|, Ex) th:onclick="|location.href='@{/basic/items/add}'|"
- 반복 출력: th:each. Ex) <tr th:each="item : ${items}">
- 변수 표현식: ${...}, Ex) ${item.price}
- 내용 변경: th:text, Ex) <td th:text="${item.price}">10000</td>
- 조건 참조: th:if, Ex) th:if="${param.status}"
- 쿼리 파라미터 조회: param.{파라미터 이름}
타임리프는 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징으로 Natural Template이라고 부른다.
4. 컨트롤러
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "/basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
//@PostMapping("/add")
public String addItemV1(@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 addItemV2(@ModelAttribute("item") Item item) {
itemRepository.save(item);
// 자동으로 모델에 넣어주기까지 한다.
// model.addAttribute("item", item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
itemRepository.save(item);
return "basic/item";
}
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
@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}";
}
// test data
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
- redirect를 위해서 Spring에서는 "redirect:/"를 붙여주면 된다.
5. PRG Post/Redirect/Get
위 코드에서는 한 가지 문제가 있는데 이는 다음과 같다.
- 상품 목록에서 상품 등록 폼으로 이동(Get)한다.
- 상품 등록 폼에서 상품을 저장하면 저장 컨트롤러로가서(Post) 저장 컨트롤러가 상품 상세 뷰를 호출한다.
이 과정을 거쳤을 때 Url은 "상품 저장" Url이다. 이때 해당 Url에서 계속 새로 고침을 하게되면 Post 요청이 계속 들어오게 되고, 상품이 계속 추가된다. 이런 문제는 PRG(Post/Redirect/Get)을 통해 해결할 수 있다. 이 방식으로 바꾸면 다음과 같은 과정으로 진행될 것이다.
- Get요청으로 상품 등록 폼을 가져온다.
- Post요청으로 상품을 등록하고 Redirect를 통해 상품 상세 화면으로 넘어간다.
- Redirect 요청을 위해 서버로 Get요청을 보낸다.
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/"+item.getId();
}
단, "+item.getId()"와 같이 URL에 변수를 더해서 사용하면 URL 인코딩이 안되기 때문에 위험하다. 이때 RedirectAttributes를 사용할 수 있다.
9. 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}";
}
- RedirectAttribute를 사용하면 URL 인코딩과 pathVariable, 쿼리 파라미터까지 처리해준다.
- {itemId}: pathVariable 바인딩
- 나머지는 쿼리 파리미터로 처리됨. → "http://localhost:8080/basic/items/3?status=true"
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'BackEnd > Spring' 카테고리의 다른 글
[Spring MVC-2] 타임리프 - 스프링 통합과 폼 (0) | 2023.09.27 |
---|---|
[Spring MVC-2] 타임리프 - 기본 기능 (0) | 2023.09.25 |
[Spring MVC-1] 스프링 MVC - 기본 기능 (0) | 2023.09.23 |
[Spring MVC-1] 스프링 MVC - 구조 이해 (0) | 2023.09.22 |
[Spring MVC-1] MVC 프레임워크 만들기 (0) | 2023.09.19 |