LostCatBox

SpringMVC-CH07

Word count: 3.7kReading time: 23 min
2022/12/24 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. 프로젝트 생성
    1. 2.1. Welcome 페이지 추가
    2. 2.2. 요구사항 분석
      1. 2.2.1. 상품 도메인 모델
      2. 2.2.2. 상품 관리 기능
      3. 2.2.3. 서비스 제공 흐름
    3. 2.3. 상품 도메인 개발
  3. 3. 상품 서비스 HTML
    1. 3.1. HTML,css파일
  4. 4. 상품 목록 - 타입 리프
    1. 4.1. BasicItemController
    2. 4.2. 타입 리프 써보기
      1. 4.2.1. 타입리프 사용 선언
      2. 4.2.2. 타입리프 핵심
      3. 4.2.3. URL 링크 표현식 - @{…}
      4. 4.2.4. 리터럴 대체 - |…|
      5. 4.2.5. 반복 출력 - th:each
      6. 4.2.6. 변수 표현식 - ${…}
      7. 4.2.7. 내용 변경 - th:text
      8. 4.2.8. URL 링크 표현식1,2
  5. 5. 상품 상세
  6. 6. 상품 등록폼
  7. 7. 상품 등록 처리 - @ModelAttribute
    1. 7.1. 상품 수정 개발
  8. 8. PRG Post/Redirect/Get
    1. 8.1. 해결 구현
    2. 8.2. RedirectAttributes