본문 바로가기
공부/Spring

[Spring MVC](8) 웹 페이지 만들기

by 다음에바꿔야지 2024. 1. 22.

상품을 관리할 수 있는 서비스를 만들어보자.

 

상품 도메인 모델

* 상품 ID
* 상품명

* 가격

* 수량
상품 관리 기능 

* 상품 목록

* 상품 상세

* 상품 등록

* 상품 수정

 

상품 도메인 개발

package hello.itemservice.domain.item;

import lombok.Data;

@Data
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;
    }
}

 

 

ItemRepository - 상품 저장소

package hello.itemservice.domain.item;

import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class ItemRepository {
    private static final Map<Long, Item> store = new HashMap<>(); // static
    private static long sequence = 0L; // static

    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 findItem = findById(itemId);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
    }

    public void clearStore() {
        store.clear();
    }
}

 

 

ItemRepositoryTest - 상품 저장소 테스트

package hello.itemservice.domain.item;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

class ItemRepositoryTest {
    ItemRepository itemRepository = new ItemRepository();

    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save() {
        //given
        Item item = new Item("itemA", 10000, 10);

        //when
        Item savedItem = itemRepository.save(item);

        //then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(savedItem);
    }

    @Test
    void findAll() {
        //given
        Item item1 = new Item("item1", 10000, 10);
        Item item2 = new Item("item2", 20000, 20);

        itemRepository.save(item1);
        itemRepository.save(item2);

        //when
        List<Item> result = itemRepository.findAll();

        //then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2);
    }

    @Test
    void updateItem() {
        //given
        Item item = new Item("item1", 10000, 10);
        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();

        //when
        Item updateParam = new Item("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);

        //then
        Item findItem = itemRepository.findById(itemId);

        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
    }
}

 

 

상품 상세, 등록 폼, 수정 폼 HTML도 각각 추가한다.(HTML은 생략)

 

BasicItemController

package hello.itemservice.web.basic;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;

@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";
    }
    
    /**
     * 테스트용 데이터 추가
     */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}

@RequiredArgsConstructor

: final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.(final을 빼면 의존관계 주입이 안된다)

 

 

상품 상세 - BasicItemController에 추가

    @GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/item";
    }

@PathVariable로 넘오온 상품ID로 상품을 조회하고, 모델에 담아둔다. 그리고 뷰 템플릿을 호출한다.

 

상품 등록 - BasicItemController에 추가

    @GetMapping("/add")
    public String addForm() {
        return "basic/addForm";
    }

 

상품 등록 처리(addItemV1) - BasicItemController에 추가

 

    @PostMapping("/add")
    public String addItemV1(@RequestParam String itemName,
                       @RequestParam Integer price,
                       @RequestParam Integer quantity,
                       Model model) {
        Item item = new Item();
        item.setItemName(itemName);
        item.setPrice(price);
        item.setQuantity(quantity);

        itemRepository.save(item);

        model.addAttribute("item", item);

        return "basic/item";
    }

요청 파라미터 형식을 처리해야 하므로 @RequestParam을 사용했다.

하나하나 setter로 넣어주는 것은 불편하다. @ModelAttribute를 사용해보자.

 

상품 등록 처리(addItemV2) - BasicItemController에 추가

    @PostMapping("/add")
    public String addItemV2(@ModelAttribute("item") Item item) {
        itemRepository.save(item);
//        model.addAttribute("item", item); // 자동 추가되기 때문에 생략 가능

        return "basic/item";
    }

ModelAttribute의 이름이 파라미터 이름과 같으면 생략 가능하다. 생략해보자.

 

상품 등록 처리(addItemV3) - BasicItemController에 추가

    @PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item) {
        itemRepository.save(item);
        // 클래스명: Item의 첫글자를 소문자로 바꿔서 name으로 사용(Item -> item)

        return "basic/item";
    }

@ModelAttribute 자체도 생략 가능하다. 생략해보자.

 

상품 등록 처리(addItemV4) - BasicItemController에 추가

    @PostMapping("/add")
    public String addItemV4(Item item) {
        itemRepository.save(item);
        return "basic/item";
    }

 

지금까지 진행한 상품 등록 처리 컨트롤러는 상품 등록을 완료 후, 새로 고침 버튼을 클릭해보면 상품이 계속 등록되는 것을 확인할 수 있다.

HTTP 섹션에서 공부했듯이, PRG(Post, Redirect, Get)로 해결해야한다.

 

상품 등록 처리(addItemV5) - BasicItemController에 추가

    @PostMapping("/add")
    public String addItemV5(Item item) {
        itemRepository.save(item);
        return "redirect:/basic/items/" + item.getId();

이 코드 중 redirect에서 + item.getId()처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.

그때 RedirectAttributes를 사용해보자.

 

상품 등록 처리(addItemV6) - BasicItemController에 추가

    @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}";
    }

'RedirectAttributes' 를 사용하면 URL 인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리해준다.

뷰 템플리셍서 status가 true이면 '저장 완료' 메시지를 나타나도록 했다.

 

상품 수정 폼 컨트롤러 - BasicItemController에 추가

* GET /items/{itemId}/edit : 상품 수정 폼

* POST /items/{itemId}/edit : 상품 수정 처리

    @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}";
    }

마지막에 뷰 템플릿을 호출하는 대신 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.

 

타임리프를 이용한 뷰 템플릿은 나중에 다시 다뤄보도록 하겠고, 일단 상품 관리 프로그램 작성은 완료되었다!