Spring MVC - 웹 페이지 만들기

유병익·2022년 10월 31일
0
post-thumbnail

1. 프로젝트 생성


1.1 사전 준비물


  • 프로젝트 선택
    • Project: Gradle Project
    • Spring Boot: 최신 버전 선택
    • Language: Java
  • Project Metadata
    • Group: hello
    • Artifact: item-service
    • Name: item-service
    • Package name: hello.itemservice
    • Packaging: Jar
    • Java: 11
    • Dependencies: Spring Web, Lombok
  • build.gradle 설정
    plugins {
    	id 'org.springframework.boot' version '2.7.5'
    	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    	id 'java'
    }
    
    group = 'hello'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
    	compileOnly {
    		extendsFrom annotationProcessor
    	}
    }
    
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
    tasks.named('test') {
    	useJUnitPlatform()
    }

1.2 WelcomePage


  • index.html
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <ul>
        <li>상품 관리
            <ul>
                <li><a href="/basic/items">상품 관리 - 기본</a></li>
            </ul>
        </li>
    </ul>
    </body>
    </html>

2. 요구사항 분석


2.1 개발 및 기능 요구 사항


2.1.1 상품 도메인 모델


  • 상품 ID
  • 상품명
  • 가격
  • 수량

2.1.2 상품 관리 기능


  • 상품 목록
  • 상품 상세 보기
  • 상품 등록
  • 상품 수정

2.2 서비스 제공 흐름


2.3 Item Domain


  • Item.java
    package hello.itemservice.domain.item;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @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;
        }
    }
  • ItemRepository.java
    package hello.itemservice.domain.item;
    
    import hello.itemservice.domain.dto.ItemUpdateParamDto;
    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<>();
        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, ItemUpdateParamDto updateParam) {
            Item findItem = findById(itemId);
            findItem.setItemName(updateParam.getItemName());
            findItem.setPrice(updateParam.getItemPrice());
            findItem.setQuantity(updateParam.getItemQuantity());
        }
    
        public void clearStore() {
            store.clear();
        }
    }
  • ItemRepositoryTest.java
    package hello.itemservice.domain.item;
    
    import hello.itemservice.domain.dto.ItemUpdateParamDto;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.Test;
    
    import java.util.List;
    
    import static org.assertj.core.api.Assertions.*;
    
    public 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
            ItemUpdateParamDto updateParam = new ItemUpdateParamDto("item2", 20000, 30);
            itemRepository.update(itemId,updateParam);
    
            //then
            Item findItem = itemRepository.findById(itemId);
            assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
            assertThat(findItem.getPrice()).isEqualTo(updateParam.getItemPrice());
            assertThat(findItem.getQuantity()).isEqualTo(updateParam.getItemQuantity());
        }
    }

3. HTML & Thymeleaf


3.1 HTML


3.1.1 상품 목록


  • items.html
    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link 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'" 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>
                    <td><a href="item.html">1</a></td>
                    <td><a href="item.html">테스트 상품1</a></td>
                    <td>10000</td>
                    <td>10</td>
                </tr>
                <tr>
                    <td><a href="item.html">2</a></td>
                    <td><a href="item.html">테스트 상품2</a></td>
                    <td>20000</td>
                    <td>20</td>
                </tr>
                </tbody>
            </table>
        </div>
    </div> <!-- /container -->
    </body>
    </html>

3.1.2 상품 등록


  • addFrom.html
    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link href="../css/bootstrap.min.css" rel="stylesheet">
        <style>
     .container {
     max-width: 560px;
     }
    
        </style>
    </head>
    <body>
    <div class="container">
        <div class="py-5 text-center">
            <h2>상품 등록 폼</h2>
        </div>
        <h4 class="mb-3">상품 입력</h4>
        <form action="item.html" method="post">
            <div><label for="itemName">상품명</label>
                <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
            </div>
            <div>
                <label for="price">가격</label>
                <input type="text" id="price" name="price" class="form-control"
                       placeholder="가격을 입력하세요">
            </div>
            <div>
                <label for="quantity">수량</label>
                <input type="text" id="quantity" name="quantity" class="form-control"
                       placeholder="수량을 입력하세요">
            </div>
            <hr class="my-4">
            <div class="row">
                <div class="col">
                    <button class="w-100 btn btn-primary btn-lg" type="submit">상품
                        등록
                    </button>
                </div>
                <div class="col">
                    <button class="w-100 btn btn-secondary btn-lg"
                            onclick="location.href='items.html'" type="button">취소
                    </button>
                </div>
            </div>
        </form>
    </div> <!-- /container -->
    </body>
    </html>

3.1.3 상품 수정


  • editForm.html
    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link href="../css/bootstrap.min.css" rel="stylesheet">
        <style>
     .container {
     max-width: 560px;
     }
    
        </style>
    </head>
    <body>
    <div class="container">
        <div class="py-5 text-center">
            <h2>상품 수정 폼</h2>
        </div>
        <form action="item.html" method="post">
            <div>
                <label for="id">상품 ID</label>
                <input type="text" id="id" name="id" class="form-control" value="1"
                       readonly>
            </div>
            <div>
                <label for="itemName">상품명</label>
                <input type="text" id="itemName" name="itemName" class="form-control"
                       value="상품A">
            </div>
            <div>
                <label for="price">가격</label>
                <input type="text" id="price" name="price" class="form-control"
                       value="10000">
            </div>
            <div>
                <label for="quantity">수량</label>
                <input type="text" id="quantity" name="quantity" class="form-control"
                       value="10">
            </div>
            <hr class="my-4">
            <div class="row">
                <div class="col">
                    <button class="w-100 btn btn-primary btn-lg" type="submit">저장
                    </button>
                </div>
                <div class="col">
                    <button class="w-100 btn btn-secondary btn-lg"
                            onclick="location.href='item.html'" type="button">취소
                    </button>
                </div>
            </div>
        </form>
    </div> <!-- /container -->
    </body>
    </html>

3.1.4 상품 상세


  • item.html
    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link href="../css/bootstrap.min.css" rel="stylesheet">
        <style> .container {
     max-width: 560px;
     }
    
        </style>
    </head>
    <body>
    <div class="container">
        <div class="py-5 text-center">
            <h2>상품 상세</h2>
        </div>
    
        <div>
            <label for="itemId">상품 ID</label>
            <input type="text" id="itemId" name="itemId" class="form-control"
                   value="1" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control"
                   value="상품A" readonly>
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000" readonly>
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control"
                   value="10" readonly>
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" onclick="location.href='editForm.html'" type="button">상품 수정
                </button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'" type="button">목록으로
                </button>
            </div>
        </div>
    </div> <!-- /container -->
    </body>
    </html>

참고

/resources/static에 HTML을 넣어두면, 실제 서비스에서도 공개된다.

서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의

3.2 Thymeleaf


3.2.1 Thymeleaf 사용법


  • Thymeleaf 사용 선언
    • <html xmlns:th="[http://www.thymeleaf.org](http://www.thymeleaf.org/)">

📌 Thymeleaf 핵심

  • th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 값을 대체
  • th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용
  • 속성 변경 - th:href
    • th:href="@{/css/bootstrap.min.css}"
      • href="value1" 을 th:href="value2" 의 값으로 변경
      • Thymeleaf View Template → 원래 값을 th:xxx 값으로 변경
        • 만약 값이 없다면 새로 생성
      • th:href 의 값이 href 로 대체되면서 동적으로 변경할 수 있다.
      • 대부분의 HTML 속성을 th:xxx 로 변경할 수 있다.
  • URL 링크 표현식 - @{...},
    • th:href="@{/css/bootstrap.min.css}"
    • URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.
  • 속성 변경 - th:onclick
    • th:onclick="|location.href='@{/basic/items/add}'|"
  • 리터럴 대체 - |...|
    • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
      • <span th:text="'Welcome to our application, ' + ${[user.name](http://user.name/)} + '!'">
    • 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
      • <span th:text="|Welcome to our application, ${[user.name](http://user.name/)}!|">
  • 반복 출력 - th:each
    • <tr th:each="item : ${items}">
    • 반복은 th:each 를 사용한다.
    • 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있다.
  • 변수 표현식 - ${...}
    • <td th:text="${item.price}">10000</td>
    • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
    • 프로퍼티 접근법을 사용한다. ( item.getPrice() )
  • 내용 변경 - th:text
    • <td th:text="${item.price}">10000</td>
    • 내용의 값을 th:text 의 값으로 변경한다.
  • URL 링크 표현식2 - @{...},
    • th:href="@{/basic/items/{itemId}(itemId=${[item.id](http://item.id/)})}"
    • 경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
      • ex) th:href="@{/basic/items/{itemId}(itemId=${[item.id](http://item.id/)}, query='test')}"
    • 리터럴 대체 문법을 활용해서 간단히 사용할 수도 있다.
      • th:href="@{|/basic/items/${[item.id](http://item.id/)}|}"

참고

  • 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 Thymeleaf의 특징을 네츄럴 템플릿 (natural templates)이라 한다.

4. 기능 구현


  • BasicItemController
    package hello.itemservice.web.basic;
    
    import hello.itemservice.domain.dto.ItemUpdateParamDto;
    import hello.itemservice.domain.item.Item;
    import hello.itemservice.domain.item.ItemRepository;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    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 javax.annotation.PostConstruct;
    import java.util.List;
    
    @Controller
    @RequestMapping("/basic/items")
    @RequiredArgsConstructor
    @Slf4j
    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 Integer price,
                           @RequestParam Integer quantity,
                           Model model){
            Item item = new Item(itemName,price,quantity);
            itemRepository.save(item);
            model.addAttribute("item",item);
    
            return "basic/item";
        }
        /*
        @ModelAttribute의 name 속성을 설정하면,
        해당 이름으로 Model에 넣음
         */
        //@PostMapping("/add")
        public String addItemV2(@ModelAttribute("item") Item item, Model model){
    
            itemRepository.save(item);
            //model.addAttribute("item",item);              // 자동으로 추가되므로, Model 객체 생략 가능
            return "basic/item";
        }
    
        /*
        @ModelAttribute의 name 속성을 생략 -> 클래스 이름의 첫 글자를 소문자로 치환해서 Model에 넣음
         */
        //@PostMapping("/add")
        public String addItemV3(@ModelAttribute Item item){
            itemRepository.save(item);
            return "basic/item";
        }
    
        /*
        @ModelAttribute 생략 -> 클래스 이름의 첫 글자를 소문자로 치환해서 Model에 넣음
         */
        //@PostMapping("/add")
        public String addItemV4(Item item){
            itemRepository.save(item);
            return "basic/item";
        }
    
        /*
        PRG(Post, Redirect, Get) 패턴 적용
         */
        //@PostMapping("/add")
        public String addItemV5(Item item){
            itemRepository.save(item);
            return "redirect:/basic/items/" + item.getId(); // 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 인코딩 시 위험할 수 있음
        }
    
        @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 editItem(@PathVariable Long itemId, ItemUpdateParamDto itemUpdateParamDto){
    
            log.info("itemId = {}",itemId);
            log.info("itemName = {}",itemUpdateParamDto.getItemName());
            log.info("itemPrice = {}",itemUpdateParamDto.getItemPrice());
            log.info("itemQuantity = {}",itemUpdateParamDto.getItemQuantity());
    
            itemRepository.update(itemId,itemUpdateParamDto);
            return "redirect:/basic/items/{itemId}";
    
        }
        /*
        Test 데이터 추가
         */
        @PostConstruct
        public void init(){
            itemRepository.save(new Item("itemA",10000,10));
            itemRepository.save(new Item("itemB",20000,20));
        }
    }

4.1 상품 목록


	@GetMapping
    public String items(Model model){
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items",items);
        return "basic/items";
    }
  • itemRepository에서 모든 상품을 조회 → model에 담음
  • 이후 View Template 호출

@RequiredArgsConstructor

  • final 이 붙은 멤버변수만 사용해서 생성자를 자동 생성

@PostConstruct

해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다.

public BasicItemController(ItemRepository itemRepository) {
		 this.itemRepository = itemRepository;
}
📌 생성자가 딱 1개만 있으면 스프링이 해당 생성자에 @Autowired 로 의존관계를 주입해준다. → final 키워드 필수 !!
  • items.html - with Thymeleaf
    <!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}">
                    <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원 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}">10000</td>
                    <td th:text="${item.quantity}">10</td>
                </tr>
                </tbody>
            </table>
        </div>
    </div> <!-- /container -->
    </body>
    </html>

4.2 상품 상세


	@GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model){
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item",item);
        return "basic/item";
    }
  • item.html - with Thymeleaf
    <!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">
        <style> .container {
     max-width: 560px;
     }
        </style>
    </head>
    <body>
    <div class="container">
        <div class="py-5 text-center">
            <h2>상품 상세</h2>
        </div>
    
        <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
    
        <div>
            <label for="itemId">상품 ID</label>
            <input type="text" id="itemId" name="itemId" class="form-control"
                   value="1" th:value="${item.id}" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control"
                   value="상품A" th:value="${item.itemName}" readonly>
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000" th:value="${item.price}" readonly>
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control"
                   value="10" th:value="${item.quantity}" readonly>
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg"
                        onclick="location.href='editForm.html'"
                        th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
                        type="button">상품 수정
                </button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/basic/items}'|"
                        type="button">목록으로
                </button>
            </div>
        </div>
    </div> <!-- /container -->
    </body>
    </html>
  • PathVariable로 넘어온 상품ID로 상품을 조회 → 모델에 담음
  • 이후 View Template 호출

4.3 상품 등록 Form


	@GetMapping("/add")
    public String addForm() {
        return "basic/addForm";
    }
  • addForm.html - with Thymeleaf

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <html>
    <head>
        <meta charset="utf-8">
        <link th:href="@{/css/bootstrap.min.css}"
                href="../css/bootstrap.min.css" rel="stylesheet">
        <style>
     .container {
     max-width: 560px;
     }
    
        </style>
    </head>
    <body>
    <div class="container">
        <div class="py-5 text-center">
            <h2>상품 등록 폼</h2>
        </div>
        <h4 class="mb-3">상품 입력</h4>
        <form action="item.html" th:action method="post">
            <div><label for="itemName">상품명</label>
                <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
            </div>
            <div>
                <label for="price">가격</label>
                <input type="text" id="price" name="price" class="form-control"
                       placeholder="가격을 입력하세요">
            </div>
            <div>
                <label for="quantity">수량</label>
                <input type="text" id="quantity" name="quantity" class="form-control"
                       placeholder="수량을 입력하세요">
            </div>
            <hr class="my-4">
            <div class="row">
                <div class="col">
                    <button class="w-100 btn btn-primary btn-lg" type="submit">상품
                        등록
                    </button>
                </div>
                <div class="col">
                    <button class="w-100 btn btn-secondary btn-lg"
                            onclick="location.href='items.html'"
                            th:onclick="|location.href='@{/basic/items}'|"
                            type="button">취소
                    </button>
                </div>
            </div>
        </form>
    </div> <!-- /container -->
    </body>
    </html>
  • 상품 등록 Form은 단순히 View Template만 호출

  • 속성 변경 - th:action

    • HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송
    • 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 같게 설정
      • HTTP 메서드로 두 기능을 구분
    • 하나의 URL로 등록 폼과, 등록 처리를 쉽게 처리 가능

4.4 상품 등록 처리


  • @RequestParam 또는 @ModelAttribute 를 사용해 요청 파라미터 데이터를 받는다.
  • Item 객체를 생성하고 itemRepository를 통해서 저장
  • 저장된 item 을 모델에 담아서 뷰에 전달

📌 중요

  • 상품 상세에서 사용한 item.html View Template을 재활용한다.

4.4.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";
    }

4.4.2 @ModelAttribute


		/*
    @ModelAttribute의 name 속성을 설정하면,
    해당 이름으로 Model에 넣음
     */
    @PostMapping("/add")
    public String addItemV2(@ModelAttribute("item") Item item, Model model){

        itemRepository.save(item);
        //model.addAttribute("item",item);       // 자동으로 추가되므로, Model 객체 생략 가능
        return "basic/item";
    }
  • @ModelAttribute의 name 속성 설정 → 해당 이름으로 Model에 넣음

4.4.3 @ModelAttribute name 생략


		/*
    @ModelAttribute의 name 속성을 생략 -> 클래스 이름의 첫 글자를 소문자로 치환해서 Model에 넣음
     */
    @PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item){
        itemRepository.save(item);
        return "basic/item";
    }
  • @ModelAttribute name의 속성 생략 → 클래스 이름의 첫 글자를 소문자로 치환해 Model에 넣음

4.4.2 @ModelAttribute 전체 생략


		/*
    @ModelAttribute 생략 -> 클래스 이름의 첫 글자를 소문자로 치환해서 Model에 넣음
     */
    @PostMapping("/add")
    public String addItemV4(Item item){
        itemRepository.save(item);
        return "basic/item";
    }
  • @ModelAttribute 생략 → 클래스 이름의 첫 글자를 소문자로 치환해서 Model에 넣음

5. 상품 수정


5.1 상품 수정 Form


	@GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable long itemId, Model model){
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item",item);
        return "basic/editForm";
    }
  • 수정에 필요한 정보 조회
  • 수정 Form View 호출

5.2 상품 수정 처리


	@PostMapping("/{itemId}/edit")
    public String editItem(@PathVariable Long itemId, ItemUpdateParamDto itemUpdateParamDto){

        log.info("itemId = {}",itemId);
        log.info("itemName = {}",itemUpdateParamDto.getItemName());
        log.info("itemPrice = {}",itemUpdateParamDto.getItemPrice());
        log.info("itemQuantity = {}",itemUpdateParamDto.getItemQuantity());
        
        itemRepository.update(itemId,itemUpdateParamDto);
        return "redirect:/basic/items/{itemId}";

    }
  • 상품 등록과 전체 프로세스가 유사함
    • GET → /items/{itemId}/edit : 상품 수정 폼
    • POST → /items/{itemId}/edit : 상품 수정 처리

📌 Spring은 redirect: 으로 redirect를 지원

  • @PathVariable 의 값은 redirect 에도 사용 할 수 있다.
    - {itemId} → @PathVariable Long itemId 의 값을 그대로 사용

6. PRG Pattern


6.1 지금까지 개발한 Controller의 문제점


  • 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
  • 상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송
  • 이 상태에서 새로 고침을 하면 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시 전송
    • 내용은 같고, ID만 다른 상품 데이터가 계속 쌓이게 된다.

6.2 해결 방안


  • 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 redirect 호출
  • 웹 브라우저는 redirect의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다.
    • 따라서 마지막에 호출한 내용 → 상품 상세 화면인 GET /items/{id}
    • 새로 고침을 해도 상품 상세 화면으로 이동 → 새로 고침 문제 해결!!

6.3 적용


6.3.1 PRG Pattern 적용


	/*
    PRG(Post, Redirect, Get) 패턴 적용
    */
    @PostMapping("/add")
    public String addItemV5(Item item){
        itemRepository.save(item);
        return "redirect:/basic/items/" + item.getId(); // URL 인코딩 시 위험할 수 있음
    }
  • URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.
    • "redirect:/basic/items/" + item.getId()

6.3.2 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}";
    }
  • RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리
  • redirect:/basic/items/{itemId}
    • pathVariable 바인딩: {itemId}
    • 나머지는 쿼리 파라미터로 처리: ?status=true
profile
Backend 개발자가 되고 싶은

0개의 댓글