LostCatBox

SpringMVC-CH07

Word count: 3.7kReading time: 23 min
2022/12/24 15 Share

스프링 MVC 1편 CH07

Created Time: July 1, 2022 9:25 PM
Last Edited Time: July 3, 2022 2:32 PM

프로젝트 생성

  • start.spring.io

스크린샷 2022-07-01 오후 9.30.55.png

  • 환경 설정에서 gradle검색후 test방식 실행방식 인텔리J로 바꾸기
  • 환경 설정에서 annotation을 검색후 enable하기(SF4J, Lombok위해서)

Welcome 페이지 추가

  • /resources/static/index.html
1
2
3
4
5
6
7
8
9
10
11
12
<!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>

요구사항 분석

상품 도메인 모델

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

상품 관리 기능

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

서비스 제공 흐름

  • 컨트롤러 검은색, 흰색은 뷰

스크린샷 2022-07-02 오전 3.03.58.png

상품 도메인 개발

  • Item -상품객체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package hello.itemservice.domain.item;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

//@Data //위험함 내용알고쓰기
@Getter @Setter
public class Item {
private Long id; //id 는 save시 repo에서 마지막 넣어주면됨.!
private String itemName;
private Integer price; //가격이 null로 들어갈수있음. int는 null가질수없음
private Integer quantity; //양이 null로 들어갈수있음. int는 null가질수없음

public Item(){
}

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

Lombok의 @Data는 다양한모든 기능을 넣어준다. 오히려 @Getter @Setter로 정확히 해주는것이 필요이상으로 기능을 넣지않아 안전하다.

  • ItemRepository - 상품 저장소
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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 - 상품 저장소 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class ItemRepositoryTest {
ItemRepository itemRepository = new ItemRepository();

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

@Test
void saveTest(){
//given
Item itemA = new Item("A",1,1);

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

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

@Test
void findAllTest(){
//given
Item itemA = new Item("A",1,1);
Item itemB = new Item("B",1,1);

itemRepository.save(itemA);
itemRepository.save(itemB);

//when
List<Item> result = itemRepository.findAll();
//then
Assertions.assertThat(result.size()).isEqualTo(2);
Assertions.assertThat(result).contains(itemA, itemB);

}

@Test
void updateItemTest(){
//given
Item itemA = new Item("A",1,1);
Item updateParam = new Item("B", 2, 2);
Item savedItem=itemRepository.save(itemA);
Long itemId = savedItem.getId();
//when
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());

}

상품 서비스 HTML

  • 핵심 비즈니스 로직을 개발하는 동안, 웹 퍼블리셔는 HTML 마크업을 완료했다.
  • 부트스트랩
    • 참고로 HTML을 편리하게 개발하기 위해 부트스트랩 사용했다.
    • 이동: https://getbootstrap.com/docs/5.0/getting-started/download/
      Compiled CSS and JS 항목을 다운로드하자.
      압축을 출고 bootstrap.min.css 를 복사해서 다음 폴더에 추가하자
      resources/static/css/bootstrap.min.css

HTML,css파일

  • /resources/static/css/bootstrap.min.css 부트스트랩 다운로드
  • /resources/static/html/items.html 아래 참조
  • /resources/static/html/item.html
  • /resources/static/html/addForm.html
  • /resources/static/html/editForm.html

이렇게 정적 리소스가 공개되는 /resources/static 폴더에 HTML을 넣어두면, 실제 서비스에서도 공개된다. 서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의하자.

상품 목록 - 타입 리프

  • th가 있다면 기본적으로 값을 기존것을 날리고 새로운것을 덮어쓰이는 동작함
  • @는 url링크 같은 것들의 문법, $는 참조변수의 정보로 동적치환하는법

BasicItemController

  • hello.itemservice.web.basic 위치
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor //final 붙은 애에 대해 생성자 로만들어즘
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", 1000, 1));
itemRepository.save(new Item("itemB", 2000, 2));
}

}
  • @RequiredArgsConstructor
    • final 이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
    • 또한 현재상황은 1개의 생성자 밖에없으므로 스프링이 자동으로 @Autowired주입해줌
    • 따라서 final 키워드를 빼면 안된다!, 그러면 ItemRepository 의존관계 주입이 안된다.
1
2
3
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}

타입 리프 써보기

  • 뷰 템플릿 영역으로 items.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!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>

타입리프 사용 선언

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

타입리프 핵심

  • 핵심은 th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체한다. th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용된다.
    HTML을 파일로 직접 열었을 때, th:xxx 가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다. 따라서 HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있다.
  • 예시로 href=”value1” th:href=”value2” 존재 한다면, th:href값으로 href 속성값이 변경된다. 속성이 없다면 새로 생성한다.

URL 링크 표현식 - @{…}

  • th:href="@{/css/bootstrap.min.css}"
  • @{…} : 타임리프는 URL 링크를 사용하는 경우 @{…} 를 사용한다. 이것을 URL 링크 표현식이라 한다.
  • URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함한다.

리터럴 대체 - |…|

  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 한다.
    <span th:text="'Welcome to our application, ' + ${user.name} + '!'">
  • 다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
    <span th:text="|Welcome to our application, ${user.name}!|">
  • 예시로 location.href='/basic/items/add'
    • th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''”
    • th:onclick="|location.href='@{/basic/items/add}'|”

반복 출력 - 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 의 값으로 변경한다.
  • 여기서는 10000을 ${item.price} 의 값으로 변경한다.

URL 링크 표현식1,2

  • th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
  • 상품 ID를 선택하는 링크를 확인해보자.
  • URL 링크 표현식을 사용하면 경로를 템플릿처럼 편리하게 사용할 수 있다.
    경로 변수( {itemId} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
  • 리터럴 대체문법도 활용하여 URL 링크 표현식 가능하다
    • th:href="@{|/basic/items/${[item.id](http://item.id/)}|}"

타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다. JSP를 생각해보면, JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다. 오직 서버를 통해서 JSP를 열어야
한다. 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)이라 한다.

상품 상세

  • BasicItemController에 추가
1
2
3
4
5
6
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
  • PathVariable 로 넘어온 상품ID로 상품을 조회하고, 모델에 담아둔다. 그리고 뷰 템플릿을 호출한다.
  • 해당 html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<!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>
  • 속성 변경 - th:value
    • th:value=”${item.id}”
      모델에 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. ( item.getId() ) value 속성을 th:value 속성으로 변경한다.
  • 상품수정 링크
  • 목록으로 링크

상품 등록폼

  • controller에 추가
1
2
3
4
@GetMapping("/add")
public String addForm(){
return "basic/addForm";
}
  • html
    • 추후에 같은 링크에 Post맵핑하여 save처리할예정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!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>

<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>
  • 속성 변경 - th:action
    • th:action
    • HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
      상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고 HTTP 메서드로 두 기능을 구분한다.
      • 상품 등록 폼: GET /basic/items/add
      • 상품 등록 처리: POST /basic/items/add

상품 등록 처리 - @ModelAttribute

  • 상품 등록 처리시 전달된 데이터는 POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10
    • 예) 회원 가입, 상품 주문, HTML Form 사용
  • controller
    • 아래 방법 모두 사용가능
    • @RequestParam가능
    • @ModelAttribute도 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//    @PostMapping("/add")
public String save(@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";
}

// @PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item){
itemRepository.save(item);
// model.addAttribute("item",item); // 자동추가되므로, 생략 가능
return "basic/item";
}
@PostMapping("/add")
public String addItemV3(Item item){ //이렇게 작성시 @ModelAttribute가 되고, Model에 등록은 "item"으로 자동으로됨(규칙은 타입 맨앞 대문자 소문자로변경)
itemRepository.save(item);
return "basic/item";
}
  • @ModelAttribute에 대해 알아야할점
    • @ModelAttribute 는 Item 객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.
    • @ModelAttribute 는 중요한 한가지 기능이 더 있는데, 바로 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다. 지금 코드를 보면 model.addAttribute(“item”, item) 가 주석처리 되어 있어도 잘 동작하는 것을 확인할 수 있다.
      • @ModelAttribute(“hello”) Item item 이름을 hello 로 지정
      • model.addAttribute(“hello”, item); 모델에 hello 이름으로 저장
    • @ModelAttribute 는 모델에 등록될 이름을 생략하면 클래스의 첫글자만 소문자로 변경해서 등록한다 (Item→item)
    • @ModelAttribute 자체도 생략가능하다
  • controller에 추가
    • Get, Post 나눠서 개발 redirect부분 잘보기
1
2
3
4
5
6
7
8
9
10
11
@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}";
}
  • editForm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!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>

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

상품 수정 개발

  • controller
1
2
3
4
5
@PostMapping ("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item){
itemRepository.update(itemId,item);
return "redirect:/basic/items/{itemId}";
}
  • 상품 수정은 상품 등록과 전체 프로세스가 유사하다.
    • GET /items/{itemId}/edit:상품수정폼
    • POST /items/{itemId}/edit : 상품 수정 처리
  • 리다이렉트
    • 상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.
    • 스프링은 redirect:/… 으로 편리하게 리다이렉트를 지원한다.
    • redirect:/basic/items/{itemId}
      • 컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에도 사용 할 수 있다.
      • redirect:/basic/items/{itemId} {itemId} 는 @PathVariable Long itemId 의 값을 그대로 사용한다.

HTML Form 전송은 PUT, PATCH를 지원하지 않는다. GET, POST만 사용할 수 있다. PUT, PATCH는 HTTP API 전송시에 사용
스프링에서 HTTP POST로 Form 요청할 때 히든 필드를 통해서 PUT, PATCH 매핑을 사용하는 방법이 있지만, HTTP 요청상 POST 요청이다.

PRG Post/Redirect/Get

  • post로 add하고 새로고침…하면 계속 제품 추가됨..문제점

  • 스크린샷 2022-07-02 오후 6.43.04.png

    스크린샷 2022-07-02 오후 6.43.16.png

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

    스크린샷 2022-07-02 오후 6.44.30.png

    • 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
      새로 고침 문제를 해결하려면 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트를 호출해주면 된다.
      웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동한다. 따라서마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 되는 것이다.
      이후 새로고침을 해도 상품 상세 화면으로 이동하게 되므로 새로 고침 문제를 해결할 수 있다.

해결 구현

  • controller에서 변경

    1
    2
    3
    4
    5
    6
    @PostMapping("/add")
    public String addItemV3(Item item){ //이렇게 작성시 @ModelAttribute가 되고, Model에 등록은 "item"으로 자동으로됨(규칙은 타입 맨앞 대문자 소문자로변경)
    itemRepository.save(item);
    // model.addAttribute("item",item); // 자동추가되므로, 생략 가능
    return "redirect:/basic/items/"+item.getId();
    }
  • 상품 등록 처리 이후에 뷰 템플릿이 아니라 상품 상세 화면으로 리다이렉트 하도록 코드를 작성해보자. 이런 문제 해결 방식을 PRG Post/Redirect/Get 라 한다.

"redirect:/basic/items/" + item.getId() redirect에서 +item.getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다. 다음에 설명하는 RedirectAttributes 를 사용하자.

RedirectAttributes

  • 상품을 저장하고 상품 상세 화면으로 리다이렉트 한 것 까지는 좋았다. 그런데 고객 입장에서 저장이 잘 된 것인지 안 된 것인지 확신이 들지 않는다. 그래서 저장이 잘 되었으면 상품 상세 화면에 “저장되었습니다”라는 메시지를 보여달라는 요구사항이 왔다. 간단하게 해결해보자.
  • RedirectAttributes 를 사용하면 URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다.
  • redirect:/basic/items/{itemId}
    • pathVariable 바인딩: {itemId}
    • 나머지는 쿼리 파라미터로 처리: ?status=true
  • controller에 추가
1
2
3
4
5
6
7
@PostMapping("/add")
public String addItemV5(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
  • 리다이렉트 할 때 간단히 status=true 를 추가해보자. 그리고 뷰 템플릿에서 이 값이 있으면, 저장되었습니다. 라는 메시지를 출력해보자.
1
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
CATALOG
  1. 1. 스프링 MVC 1편 CH07
  2. 2. 프로젝트 생성
  3. 3. 상품 서비스 HTML
  4. 4. 상품 목록 - 타입 리프
  5. 5. 상품 상세
  6. 6. 상품 등록폼
  7. 7. 상품 등록 처리 - @ModelAttribute
  8. 8. PRG Post/Redirect/Get