Spring

[인프런/스프링 MVC 1편] 7. 스프링 MVC - 웹 페이지 만들기 (타임리프 사용)

주니어주니 2023. 5. 11. 22:00

 

 

1. 요구사항 분석

상품을 관리할 수 있는 서비스 

 

(1) 상품 도메인 모델 

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

 

(2) 상품 관리 기능

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

 


 

2. 상품 도메인 개발 

 

1) Item - 상품 객체 

 

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;      // null이 들어올 수 있어서 int 대신 Integer
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }

}

 

 

2) ItemRepository - 상품 저장소 (DAO 역할)

 

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();
    }
}

 

(1) @Repository

  • 스프링 빈의 컴포넌트로 인식해줌
  • DB 작업

 

컨트롤러 : @Controller (프레젠테이션 레이어, 웹 요청과 응답을 처리함)
로직 처리 : @Service (서비스 레이어, 내부에서 자바 로직을 처리함)
외부I/O 처리 : @Repository (퍼시스턴스 레이어, DB나 파일같은 외부 I/O 작업을 처리함)

 

 

(2) HashMap 

 

싱글톤 객체에 멤버변수(메소드 밖) -> 여러 쓰레드에서 공유 -> 동시성 문제 -> ConcurrentHashMap 사용

지역변수(메소드 안) -> 쓰레드마다 전용 공간 할당 -> HashMap

 

(원래는 안되지만 지금은 간단 프로젝트니까 씀)

 

 

(3) static

 

static 사용 -> 모든 곳에서 공유하기 위해

store가 마치 데이터베이스처럼 사용되기 때문에 하나만 존재하고 공유

(싱글톤일 때는 static 안붙여도 o) 

 

 

 

(4) new ArrayList<>(store.values())

 

Map의 values를 그대로 반환하면 외부(itemRepository)에서 Map 내 element를 변경, 삭제할 수 있게 됨 

-> ArrayLIst로 한번 감싸면 Map의 values에 변경을 가할 수 X

 

 

(5) update 

 

원래는 변경하는 항목들만 있는 dto 객체를 따로 만드는게 나음 (지금은 간단 프로젝트니까)

 

 

 

3) ItemRepositoryTest - 상품 저장소 테스트 

 

객체가 있는 곳과 같은 위치

 

package hello.itemservice.domain.item;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

public class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    // 테스트가 끝날 때마다 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());
        // 우리가 찾은게 저장한거랑 같아야 함
        Assertions.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
        Assertions.assertThat(result.size()).isEqualTo(2);
        Assertions.assertThat(result).contains(item1, item2);
    }

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

        Long itemId = savedItem.getId();

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

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

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

 

 


 

 

3. 상품 서비스 HTML 

 

* 부트스트랩 사용

https://getbootstrap.com/docs/5.0/getting-started/download/

 

Download

Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.

getbootstrap.com

 

Compiled CSS and JS 항목 다운로드 -> 압축 풀기

-> resources/static/css/bootstrap.min.css 복사

 

item.html

items.html

addForm.html

editForm.html 생성

 


 

4. 상품 목록 (+ 타임리프 사용법)

 

1) 컨트롤러

itemRepository에서 모든 상품 조회 -> 모델에 담음 -> 뷰 템플릿 호출

 

* BasicItemController

 

package hello.itemservice.web.basic;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.util.List;

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    // 생성자가 하나일 때 -> @RequiredArgsContructor + final -> 의존성 주입
    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));
    }
}

 

(1) @RequiredArgsConstructor

  • final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어줌 -> 의존성 주입
  • 생성자가 1개만 있으면 해당 생성자에 @Autowired로 의존관계 주입
  • final 키워드를 빼면 안됨!

(2) 테스트용 데이터 추가

  • 테스트용 데이터를 itemRepository에 넣어줌
  • @PostConstruct: 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출됨

 

 

2) 뷰 템플릿

/resources/static/items.html -> 정적 HTML (얘를 복사해서 동적 HTML을 만들기)

/resources/templates/basic/items.html -> 동적 HTML 뷰 템플릿 (타임리프)

 

<!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/${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>

 

 

 

 

 

📌 타임리프

 

* 타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고,

서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있음. (JSP는 JSP 코드와 HTML이 뒤죽박죽 섞여있음)

순수 HTML을 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징 -> 내츄럴 템플릿 

 

 

(1) 타임리프 사용 선언

<html xmlns:th="http://www.thymeleaf.org">

 

 

(2) th:href - 속성 변경

th:href="@{/css/bootstrap.min.css}"
    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
  • 타임리프 뷰 템플릿을 거치면 원래 값을 날리고 href를 타임리프꺼로 바꿈 (th:xxx) 없으면 새로 생성
  • HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 값으로 대체되면서 동적으로 변경
  • 대부분의 HTML 속성을 th:xxx로 변경할 수 있음

 

 

(3) th:xxx - 타임리프 핵심

  • th:xxx가 붙은 부붙은 서버사이드에서 렌더링, 기존 것 대체
    없으면 기존 html의 xxx 속성 그대로 사용
  • HTML을 파일로 직접 열었을 때, th:xxx가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 동적으로 열 수 X
  • HTML 파일 보기를 유지하면서 템플릿 기능도 가능

 

 

(4) @{...} - URL 링크 표현식 

th:href="@{/css/bootstrap.min.css}"
  • @{...} : 타임리프는 URL 링크를 사용하는 경우 @{...} 사용 -> 링크 표현식

 

(5) th:onclick - 속성 변경

// 기본 HTML
onclick="location.href='addForm.html'"

// 타임리프 적용(리터럴 대체 문법)
th:onclick="|location.href='@{/basic/items/add}'|"
<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>

 

 

(6) |...| - 리터럴 대체 문법

 

원래 이렇게 분리해서 써야되는데 

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

 

리터럴 대체 문법 -> 분리 없이 편하게 사용 

<span th:text="|Welcome to our application, ${user.name}!|">
th:onclick="|location.href='@{/basic/items/add}'|"

 

 

(7) th:each - 반복 출력

<tr th:each="item : ${items}">
  • 모델에 담긴 items 데이터가 item 변수에 하나씩 담기고, 반복 실행

 

 

(8) ${...} - 변수 표현식 

<td th:text="${item.price}">10000</td>
  • 모델에 담긴 값이나, 타임리프 변수로 선언한 값 조회
  • 프로퍼티 접근법 ( item.getPrice() -> ${item.price} )

 

 

(9) th:text - 내용 변경

<td th:text="${item.price}">10000</td>
  • 내용의 값을 th:text의 값으로 변경
    ( 10000 -> ${item.price}의 값으로 변경 )

 

 

(10) @{...} - URL 링크 표현식2

th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
  • 상품 ID를 선택하는 링크 -> 경로를 템플릿처럼 사용
  • 경로변수 ( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성
    • 예) th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
      • 생성 링크: http://localhost:8080/basic/items/1?query=test

 

 

(11) URL 링크 간단히

th:href="@{|/basic/items/${item.id}|}"
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>

 

 


 

5. 상품 상세 

 

1) 컨트롤러 

 

*BasicItemController에 추가

  • PathVariable로 넘어온 상품ID로 상품 조회 -> 모델에 담음 -> 뷰 템플릿 호출 

 

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

 

 

 

2) 뷰 템플릿

 

/resources/static/item.html -> 정적 HTML (얘를 복사해서 동적 HTML을 만들기)

/resources/templates/basic/item.html -> 동적 HTML 뷰 템플릿 (타임리프)

 

<!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>
    <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>

 

 

(1) th:value - 속성 변경 

<input type="text" id="itemId" name="itemId" class="form-control" value="1" th:value="${item.id}" readonly>
  • 모델에 있는 item 정보 획득, 프로퍼티 접근법으로 출력 

 

(2) 상품수정 링크

th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
  • ${item.id}에 있는 값이 -> {itemId}로 들어감

 

 

 

 


 

6. 상품 등록 폼

 

1) 컨트롤러 

  • 상품 등록 폼으로 이동 -> 뷰 템플릿만 호출 
// 상품 등록 폼
@GetMapping("/add")
public String addForm() {
    return "basic/addForm";
}

 

 

2) 뷰 템플릿 

 

/resources/static/addForm.html -> 정적 HTML (얘를 복사해서 동적 HTML을 만들기)

/resources/templates/basic/addForm.html -> 동적 HTML 뷰 템플릿 (타임리프)

 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link href="../css/bootstrap.min.css"
          th: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>

 

(1) th:action - 속성 변경 

 

<form action="item.html" th:action method="post">

 

원래는 

<form action="item.html" th:action="/basic/items/add" method="post">

 

 

 

이렇게 적는데 

  • th:action 으로만 적으면 현재 URL에 데이터를 전송함
  • 상품 등록 폼의 URL과 실제 등록 처리를 하는 URL을 똑같이 맞추고 HTTP 메소드로 두 기능 구분
    • 상품 등록 폼 :     GET      /basic/items/add
    • 상품 등록 처리 : POST   /basic/items/add
  • -> 하나의 URL로 등록 폼, 등록 처리를 처리 

 

 

 

 


 

7. 상품 등록 처리 

 

 

1) @RequestParam 사용

 

 

(1) 컨트롤러

 

// 상품 등록 처리
@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";
}

 

-> 반환값 잘못됐음. 10)에서 수정

 

  • @RequestParam String itemName : 상품 등록 폼의 form에서 작성한 itemName 요청 파라미터 데이터를 해당 변수에 받음
  • Item 객체 생성해서 데이터 담음 -> itemRepository에 저장 -> item을 모델에 담아서 뷰로 전달 

 

 

(2) 뷰 템플릿 (상품 상세의 item.html을 그대로 사용) 

 

모델에서 담은 item의 값을 보여줌

 

 

 

 

2) @ModelAttribute 사용

 

@RequestParam 으로 변수를 하나하나 받아서 Item 생성 -> 불편 -> @ModelAttribute 사용

 

 

// 상품 등록 처리
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item) {

    itemRepository.save(item);
    //model.addAttribute("item", item);	// 자동추가 -> 생략 가능

    return "basic/item";
}

 

* @ModelAttribute

  1. 요청 파라미터 처리
    • Item 객체 생성 -> 요청 파라미터 값을 프로퍼티 접근법으로 입력해줌
  2. Model에 추가 
    • Model에 @ModelAttribute로 지정한 객체를 자동으로 넣어줌 
    • @ModelAttribute("item") -> Model에 담는 이름 -> 뷰 템플릿에서 꺼내는 이름 ( ${item.id} )

 

 

 

3) @ModelAttribute 이름 생략 

 

// 상품 등록 처리
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {

    itemRepository.save(item);
    //model.addAttribute("item", item);     // 자동추가 -> 생략 가능

    return "basic/item";
}

 

  • @ModelAttribute의 이름 생략
    • 모델에 저장될 때 클래스의 첫글자만 소문자로 변경해서 등록
    • 예) @ModelAttribute Item item 
      • Item -> item 으로 모델에 추가됨
      • HelloWorld -> helloWorld 로 모델에 추가됨

 

 

4) @ModelAttribute 자체도 생략 

 

// 상품 등록 처리
@PostMapping("/add")
public String addItemV4(Item item) {

    itemRepository.save(item);

    return "basic/item";
}

 

  • @ModelAttribute 자체 생략
    • 기본형 -> @RequestParam 자동 적용 
    • 참조형 -> @ModelAttribute 자동 적용
    • 위와 마찬가지로 첫글자만 소문자로 변경해서 model에 등록 

 


 

8. 상품 수정 폼 

 

1) 컨트롤러 

 

// 상품 수정 폼
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {

    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);

    return "basic/editForm";
}

 

 

 

2) 뷰

 

정적 HTML -> 동적 HTML 

 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <link href="../css/bootstrap.min.css"
        th: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" th:action method="post">
    <div>
      <label for="id">상품 ID</label>
      <input type="text" id="id" name="id" 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}">
    </div>
    <div>
      <label for="price">가격</label>
      <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
    </div>
    <div>
      <label for="quantity">수량</label>
      <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}">
    </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'"
                th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
                type="button">취소
        </button>
      </div>
    </div>
  </form>
</div> <!-- /container -->
</body>
</html>

 

 

 

 


 

9. 상품 수정 처리

 

1) 컨트롤러 

 

// 상품 수정 처리
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {

    itemRepository.update(itemId, item);

    // 상품 상세 화면 경로로 리다이렉트
    return "redirect:/basic/items/{itemId}";
}

 

  • @PathVariable를 이용해서 경로로 온 요청파라미터 받고
    @ModelAttribute를 이용해서 수정 폼에서 온 Item 객체 받음

  • 리다이렉트
    • "redirect:/basic/items/{itemId}" 
    • 컨트롤러에 매핑된 @PathVariable의 값을 redirect에도 사용 가능

 

 


 

10. PRG (POST / REDIRECT / GET)

 

 

10-1. 상품 등록 문제점

 

* 현재 상품 등록 처리 컨트롤러 

 

// 상품 등록 처리
@PostMapping("/add")
public String addItemV4(Item item) {

    itemRepository.save(item);

    return "basic/item";
}

 

상품 저장 -> 상품 상세 페이지를 반환(호출) 

-> 이 상태로 새로고침 하면 -> 같은 내용으로 id만 변하면서 상품이 계속 등록됨

 

 

 

 

 

 

POST 등록 후 새로고침 

 

* 웹 브라우저의 새로고침은 마지막에 서버에 전송한 데이터를 다시 전송함

  • 상품 등록 -> 상품 상세 반환 : 이 과정에서 마지막으로 서버에 전송한 데이터는 상품 등록 (POST) !
    -> 새로고침 -> 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시 전송 -> 중복 데이터

 

 

10-2. 해결 방법 -> Redirect 

 

 

* 상품 상세화면으로 Redirect 

  • 상품 상세화면으로 리다이렉트 호출 -> GET 방식으로 상세화면으로 이동 -> 마지막에 호출한 내용이 GET / items/{id} 
    -> 새로고침 해도 GET 방식을 통한 상세화면 이동 !

 

* 수정 코드 

 

// 상품 등록 처리
@PostMapping("/add")
public String addItemV5(Item item) {

    itemRepository.save(item);

    return "redirect:/basic/items/" + item.getId();
}

 

 


 

11. RedirectAttributes

 

저장이 잘 되었으면 상품 상세 화면에서 "저장 완료" 메시지 띄우기 

 

 

1) 상품 등록 처리 컨트롤러

 

// 상품 등록 처리
@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 + 쿼리 파라미터 처리 해줌
    • redirect:/basic/items/{itemId}
      • pathVariable 바인딩(경로 상에 pathVariable이 있으면 치환해줌) : {itemId}
      • 나머지는 쿼리 파라미터로 처리 : ?status=true
      • 리다이렉트 결과 : 
        http://localhost:8080/basic/items/3?status=true

 

 

2) 상품 상세 뷰 템플릿 

 

<div class="container">
    <div class="py-5 text-center">
        <h2>상품 상세</h2>
    </div>

    <!-- 리다이렉트 메시지 추가 -->
    <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>

 

  • th:if : 해당 조건이 참이면 실행
  • ${param.status} : 쿼리파라미터 조회