BackEnd/Spring

[Spring MVC-1] 스프링 MVC - 웹 페이지 만들기

코딩마루 2023. 9. 24. 03:38

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

위 코드에서는 한 가지 문제가 있는데 이는 다음과 같다.

  1. 상품 목록에서 상품 등록 폼으로 이동(Get)한다.
  2. 상품 등록 폼에서 상품을 저장하면 저장 컨트롤러로가서(Post) 저장 컨트롤러가 상품 상세 뷰를 호출한다.

이 과정을 거쳤을 때 Url은 "상품 저장" Url이다. 이때 해당 Url에서 계속 새로 고침을 하게되면 Post 요청이 계속 들어오게 되고, 상품이 계속 추가된다. 이런 문제는 PRG(Post/Redirect/Get)을 통해 해결할 수 있다. 이 방식으로 바꾸면 다음과 같은 과정으로 진행될 것이다.

  1. Get요청으로 상품 등록 폼을 가져온다.
  2. Post요청으로 상품을 등록하고 Redirect를 통해 상품 상세 화면으로 넘어간다.
  3. 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