Spring MVC : 간단한 웹 페이지 구현
- Spring/Spring MVC
- 2021. 12. 19.
인프런 김영한님의 강의를 듣고 정리한 내용입니다.
전체 서비스 제공
전체 서비스 제공 흐름은 위와 같이 구성이 되었다.
- 검정색 : 컨트롤러
- 하얀색 : 뷰템플릿
각 박스는 색깔별로 위와 같은 형태로 구성되어있다. 모든 페이지를 MVC 패턴으로 구현한다. 컨트롤러가 필요없어도 컨트롤러로 먼저 들어온 후, 뷰 타입으로 반환하는 형태다.
웰컴 페이지 생성
프로젝트를 생성한 후, 먼저 필요한 설정을 해주었다. Annotation Processor 및 Gradle 설정을 완료했다. 이후, 서버가 정상적으로 동작하는지 띄워보았다. 화이트 페이지가 나오며 정상동작하는 것이 확인되었다. 이후에는 편의를 위해 WelCome Page를 만들었다.
<!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>
WelCome Page는 Static의 index.html을 만들어두면, 자동으로 localhost:8080으로 접속했을 때 만들어진다. index.html의 코드는 위의 코드를 사용했다. 위의 페이지를 만든 후 서버 재가동을 해서 웰컴 페이지로 정상접속되는지를 확인했다.
필요 클래스 생성 및 테스트 (상품 클래스, 상품 저장소)
상품 클래스 생성
상품 클래스는 총 4개의 속성을 가진다.
- ID
- 상품명
- 상품 가격
- 상품 갯수
위의 속성을 만족하는 상품 클래스를 생성한다.
@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;
}
}
- Price와 Quantity는 Integer로 설정했다. Int로 설정하면 null이 들어갈 수 있으나, Integer로 설정하면 null이 들어갈 수 없기 때문이다.
- @Getter, @Setter만 사용했다. @Data를 사용하면 여러가지 편의 메서드들이 생성되지만, 이는 예상치 못하게 동작할 수 있다. 따라서 필요한 @Getter @Setter만 사용했다.
상품 저장 클래스 생성
@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 itemId){
return store.get(itemId);
}
public List<Item> findAll(){
return new ArrayList<>(store.values());
}
}
상품 저장 클래스는 위의 코드로 구현했다.
- 이 때 HashMap과 Long을 쓴 것은 멀티 쓰레드를 고려하지 않은 것이다. 멀티 쓰레드 환경에서는 HashMap과 Long에 동시에 접근 시 꼬이는 문제가 있다. 따라서 실제로는 ConcurrentHashMap, AtomicLong을 써야한다.
- @Repository 어노테이션을 추가한다. @Component가 되어 @ComponentScan의 대상이 된다. 이후 컨트롤러에 DI에 사용된다. 해당 저장소는 싱글톤으로 관리되어야 하기 때문이다.
1. 통합 컨트롤러 뼈대 구성
@RequestMapping("/basic/items")
@Controller
@RequiredArgsConstructor
public class BasicItemController {
//@RequiredArgrsConstructor를 ItemRepository 의존주입.
private final ItemRepository itemRepository;
@PostConstruct
public void init(){
Item itemA = new Item("itemA", 10000, 10);
Item itemB = new Item("itemB", 20000, 20);
itemRepository.save(itemA);
itemRepository.save(itemB);
}
통합 컨트롤러의 뼈대를 먼저 구성한다.
- URL Pattern은 "/basic/items"로 이루어져 있다.
- @Controller 어노테이션을 달아주어 Dispatcher Servlet으로 설정해준다.
- @RequiredArgsConstructor를 달아주어 private final로 선언된 itemRepository에 DI를 실행해준다.
- @PostConstructor 메서드를 만들어, 생성자 실행 후 실행되는 것을 만들어준다.
2. 상품목록 컨트롤러 구현
@GetMapping
public String items(Model model){
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
- 상품목록 컨트롤러의 URL은 /basic/items다. 클래스 레벨의 URL과 동일하기 때문에 추가 URL은 필요없다.
- itemRepository에서 저장된 모든 Item을 가지고 온다. 그리고 그 items를 Model에 담아서 전달해준다.
- String을 Return하고 @ReqeustBody, @ResponseBody가 아니기 때문에 return 값은 뷰템플릿의 논리이름이 된다.
이후 해야할 일은 뷰템플릿을 구현하고, Model에 담긴 값을 동적으로 표현해주는 것이다.
3. 상품목록 뷰템플릿 구현
<!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>
먼저 상품 목록 HTML의 정적 페이지가 구현된 코드를 가지고 왔다. 여기서 Thymeleaf 문법을 활용해서 우리가 필요한 것들을 동적으로 사용할 수 있도록 해줘야한다.
가장 먼저 위의 HTML을 resources/templates/basic 폴더에 옮겨준다.
<html xmlns:th="http://www.thymeleaf.org">
그리고 먼저 templates의 items.html 파일에 위의 코드를 명시해준다. 이 코드를 명시하면, 여기서부터는 Thymeleaf를 사용하겠다는 의미다.
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
위의 코드를 활용해서 CSS를 절대경로로 읽도록 한다. 서버를 새로 돌린 후, 정상적으로 돌아가는지를 확인한다.
정상 실행 시 위와 같은 화면이 나온다. 상품명이 @PostConstructor로 들어가는 것이 아닌 것을 알았다. 정적으로 되어있는 것이 확인되었으니 이 부분을 동적으로 고쳐주어야 한다. Items 컨트롤러에서 Items를 Model에 담아서 View에 넘겨주었다. 따라서 View에서 Items를 가지고 와서 반복문으로 출력을 해주면 된다.
<tr th:each="item : ${items}">
<td><a href="item.html" th:text="${item.id}">상품 ID</a></td>
<td><a href="item.html" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">상품가격</td>
<td th:text="${item.quantity}">상품갯수</td>
</tr>
위의 코드를 작성해준다.
- th:each는 타임리프에서 제공하는 loop문이다.
- ${...}을 사용하면 뷰 템플릿으로 넘겨진 모델에서 이름을 Key로 하는 Value를 가지고 온다.
- 자바의 Advanced For문과 마찬가지로 for(item : items) 형태로 사용할 수 있다.
- th:text는 텍스트를 출력하는 명령어다.
위 코드 적용 후, 스프링을 다시 시작해보면 위와 같은 화면이 나오게 된다. 그리고 ID, 상품명에 태그가 걸려 있는 것을 고쳐주어야 한다. 현재 상태에서는 item.html로 이동하도록 a href 태그가 걸려있는데, 이것을 동적으로 변경해주어야한다.
<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>
경로는 위의 두 가지로 바꿀 수 있다. 둘다 결과는 똑같은데 표현방식이 다른 것이다. @PostConstructor를 사용해서 넣어둔 기초 데이터가 나오도록 바뀌었고, HTML 태그가 수정되었다. 이제 상품등록 버튼을 눌렀을 때, 상품등록 컨트롤러로 이동하도록 연결을 해주어야 한다.
th:onclick="|location.href='@{/basic/items/add}'|"
onclick="location.href='addForm.html'" type="button">상품
th 명령어를 사용해서 onclick도 Thymeleaf 용으로 구현한다. 이 버튼을 클릭하게 되면 /basic/add URL로 태그되도록 구현했다.
실행 결과를 확인하면, 에러 페이지가 뜨지만 URL은 정상적으로 내부 연결이 된 것을 확인했다. 다음은 회원등록 컨트롤러를 구현해야할 차례다.
4. 상품상세 컨트롤러 구현
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
상품상세 컨트롤러는 위와 같이 구현했다.
- Get으로 넘어오는 데이터는 itemId 밖에 없다. 따라서 @ModelAttribute로 Item을 불러와도 Item 객체가 없다. 그렇기 때문에 itemId로 itemRepository에 있는 item을 찾은 후, Model에 넘겨줘야한다.
- @PathVariable 어노테이션을 활용했다. {...}에 @Pathvariable이 붙은 변수를 넣어주는 형식으로 사용이 된다. @ModelAttribute, @ReqeustParam과 유사하게 요청 파라미터에서 동일한 이름을 가진 변수를 찾아서 넣어준다.
5. 상품상세 뷰 템플릿 구현
<!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>
기본적인 HTML틀은 위와 같다. 해야할 일을 살펴보자.
- 상품 ID, 상품명, 가격, 수량의 동적 표현 필요
- 상품수정 클릭 시 경로 수정 필요
- 목록 클릭시 경로 수정 필요
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control"
th:value="${item.id}"
value="1" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control"
th:value="${item.itemName}"
value="상품A" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
th:value="${item.price}"
value="10000" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control"
th:value="${item.quantity}"
value="10" readonly>
</div>
위의 박스에 필요한 값이 동적으로 표현될 수 있도록 th:value문을 사용해서 동적으로 값이 표현될 수 있도록 했다. item.id, item.itemName, item.price, item.quantity를 동적으로 사용해서 정상적으로 출력될 수 있도록 구현했다.
<button class="w-100 btn btn-primary btn-lg"
th:onclick="|location.href='@{/basic/items/edit}'|"
onclick="location.href='editForm.html'" type="button">상품 수정</button>
상품 수정을 눌렀을 때, edit 페이지로 넘어갈 수 있도록 th:onclick을 구성했다. 이 때, |...|를 사용해서 경로가 틀어지지 않도록 했고, @{...}으로 th문의 경로를 표현해주었다.
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
th:onclick="|location.href='@{/basic/items}'|"
onclick="location.href='items.html'" type="button">목록으로</button>
</div>
목록으로 돌아가기 위해 경로 수정을 해주었다. 위와 동일하게 '/basic/items'가 불려질 수 있도록 GET Controller를 호출했고, 이후에는 해당 Controller에서 상품 상세 페이지 뷰 템플릿으로 이동하게 해두었다.
6. 상품등록 폼 컨트롤러 구현
@GetMapping("/add")
public String add(){
return "basic/addForm";
}
상품등록 폼 컨트롤러는 위와 같이 구현했다.
- 상품등록 폼 컨트롤러는 HTML FORM을 요청해서 받는 용도다. 따라서 Get으로 요청한다.
- 뷰 템플릿은 basic/addForm으로 연결한다.
5. 상품등록폭 뷰 템플릿 동적구현
<!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="formcontrol" 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="formcontrol" 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>
HTML은 위와 같다. 먼저 어떤 상태인지 확인하기 위해서 해당 HTML을 절대경로로 실행해보면 다음과 같은 것을 확인할 수 있다.
해야할 것을 살펴보자.
- 상품등록 경로
- 취소 경로
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
th:onclick="|location.href='@{/basic/items}'|"
onclick="location.href='items.html'" type="button">취소</button>
</div>
먼저 위의 th 코드를 이용해서 취소 경로를 설정했다. 취소 경로를 할 경우 상품목록으로 돌아갈 수 있도록 경로를 설정해주었다. 서버를 띄운 후 정상적으로 동작하는지 확인했다.
<form th:action="@{/basic/items/add}"
action="item.html" method="post">
<form th:action
action="item.html" method="post">
이후에는 th로 POST경로를 수정했다. 위 코드, 아래 코드 모두 가능하다. th:action이 빌 경우 현재 경로 그대로를 인식하고 POST를 한다고 한다. 따라서 편한 것으로 설정하면 addForm에서 구현해야 하는 것은 다 구현했다.
addForm.html에서는 HTML FORM 형식으로 데이터를 입력받는다. 그리고 그 데이터를 쿼리 파라미터 형식으로 서버에 POST하게 된다. 따라서 Post Mapping할 수 있는 컨트롤러의 작성이 필요하다.
7. 상품저장 컨트롤러 구현
@PostMapping("/add")
public String addV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item(itemName, price, quantity);
Item savedItem = itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
상품저장 컨트롤러 프로토타입이다.
- HTML FORM은 요청 파라미터 형식(key=value)으로 전달되기 때문에 필요한 데이터 값을 @RequestParam으로 가지고 온다.
- 뷰 템플릿으로 랜더링 할 계획이기 때문에 Model에 값을 넣어준다.
위와 같이 할 경우, 번거롭게 여러 매개변수를 가져와 하나씩 넣어야 한다. 따라서 좀 더 간단하게 구현해본다.
public String addV2(@ModelAttribute("item") Item item, Model model) {
Item savedItem = itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
두번째 타입은 @ModelAttribute를 활용한다.
- @ModelAttribute의 "name"값에 우리가 만든 객체를 넣어준다.
- 이 경우, 해당 객체의 필드와 매칭되는 요청 파라메터를 받아서 자동으로 바인딩하고, 그 객체를 만들어서 반환해준다.
- 따라서, Item을 넣어주기만 하면 된다.
public String addV3(@ModelAttribute Item item, Model model) {
Item savedItem = itemRepository.save(item);
return "basic/item";
}
세번째 타입은 Model.addAttribute()를 줄인 것이다.
- @ModelAttirbute에 name 값을 넣지 않으면, 클래스의 가장 앞부분을 소문자로 바꾼 것을 이름으로 해서 바인딩해준다.
- @ModelAttribute를 하게 되면, 자동으로 Model에 해당 변수를 addAttribute해준다.
public String addV4(Item item) {
Item savedItem = itemRepository.save(item);
return "basic/item";
}
네번째 타입은 @ModelAttribute 마저 없앴다.
- 요청 파라미터의 경우 @ModelAttribute, @RequestParam을 생략할 수 있다.
- 기본 타입 : 자동으로 @RequestParam과 매칭
- 임의 생성 객체 : 자동으로 @ModelAttibute와 매칭
위의 과정을 통해서 상품저장 컨트롤러를 완성했다. 주로 세번째 타입을 추천하는데, 네번째 타입은 명시성이 너무 부족하기 때문이라고 한다.
8. 상품수정 조회 컨트롤러 구현
@GetMapping("/edit/{itemId}")
public String edit(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
위 코드로 구현했다.
- 특정 상품을 수정하기 때문에 특정 상품 상세 페이지를 타고 들어온다. 따라서 정보를 다시 한번 표현해줄 필요가 있기 때문에 @PathVariable로 경로를 표현했다.
- Get으로 넘어올 때 ItemId만 넘어오기 때문에 Item 객체에 대한 전체 정보는 제대로 알지 못한다. 따라서, itemRepositroy에서 필요한 값을 찾아온 후 Model로 넘겨줘야한다.
9. 상품수정 조회 뷰템플릿 구현
<!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="formcontrol" 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="formcontrol" 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>
먼저 HTML 디자인 파일은 위와 같다. 해야할 일을 정리하면 다음과 같다.
- 동적으로 상품명, 가격, 수량 정보 표현하기
- POST 메서드 보내는 경로 수정하기
- 취소 시 상품상세 목록으로 돌아가도록 설정
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control"
th:value="${item.id}"
value="1"
readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol"
th:value="${item.itemName}"
value="상품A">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
th:value="${item.price}"
value="10000">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol"
th:value="${item.quantity}"
value="10">
</div>
동적으로 값이 나오게 하는 것은 th:value="${...}" 형태로 동적으로 나올 수 있게 구현해두었다.
<form th:action
action="item.html" method="post">
POST 경로는 th:action으로 설정해두었다. th:action으로 설정해두면, 뒤에 아무 경로가 없기 때문에 현재 URL 그대로 POST를 다시 보낸다는 의미다. 이를 풀어서 쓰면, th:action="/basic/items/edit/${item.id}"가 될 것이다.
<button class="w-100 btn btn-secondary btn-lg"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
onclick="location.href='item.html'" type="button">취소</button>
상품상세 목록으로 돌아가야 하기 때문에 itemId를 동적으로 표현해서 /basic/items/{itemId} 컨트롤러를 호출할 수 있도록 설정했다.
10. 상품수정 컨트롤러 구현
@PostMapping("/edit/{itemId}")
public String editForm(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items";
}
상품수정 컨트롤러를 위와 같이 구현했다.
- 동일한 URL로 들어오지만 POST 메서드로 실제 수정이 이루어지도록 POST Mapping을 했다.
- HTML FORM POST를 하면, Item정보가 넘어온다. 따라서, @ModelAttribute의 Item item으로 값을 받을 수 있따. 이를 바탕으로 update 함수를 구현해서 값을 업데이트했다.
- return 값은 스프링의 redirect: 문법을 활용해서 상품목록 페이지로 리다이렉팅 했다.
11. 상품 등록 컨트롤러의 개선(PRG 패턴 적용)
현재 상품 등록 컨트롤러의 문제점이 있다. 바로 상품 등록을 한 상태에서 새로고침을 하면 마지막 메서드인 POST /add + 쿼리파라미터가 계속 실행된다는 점이다. 즉. 새로고침을 하면 위와 같이 동일한 상품이 계속 등록되는 경우가 있다. 이 경우를 해결해주기 위해 PRG 처리를 해주고자 한다.
// @PostMapping("/add")
public String addV6(@ModelAttribute Item item) {
Item savedItem = itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
먼저 위와 같이 직접적으로 URL에 getId()로 넘기는 방법이 있다. 이렇게 URL을 더하기로 표현할 경우, 인코딩이 되지 않는다는 점과 공백이 있을 경우 제대로 된 리다이렉팅이 안될 수 있다는 것이다. 또한, 이외의 다른 값들을 View 템플릿에 넣어줄 수 없다는 것이 문제가 된다.
@PostMapping("/add")
public String addV7(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", item.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
좀 더 좋은 방법은 위처럼 RedirectAttributes라는 매개변수를 받은 후, 이 매개변수에 필요한 값들을 넣어주는 것이다. 이 경우 리디렉팅에 {...} 형식으로 값을 넣을 수 있게 되면서 인코딩이 된 URL로 리다이렉팅을 유도할 수 있다. 또한, redirectAttribute는 model처럼 값이 뷰 템플릿으로 넘어간다. 뷰 템플릿에서는 param.변수명으로 접근이 가능하다.
또한, 실제로 붙지 못한 나머지 값들은 모두 쿼리 파라메터 형식으로 뒷쪽에 붙는다. 이를 통해 클라이언트에 대략적인 성공유무를 알려줄 수도 있고, 뷰템플릿에서도 이 값을 인지하고 조건문을 활용해 여러가지들을 표현할 수 있게 된다.
12. 상품상세 뷰 템플릿 업그레이드(상품 저장 표시하기)
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
위 문구를 상품상세 뷰 템플릿에 추가를 한다. 이 경우, RedirectAttribute를 통해서 값이 넘어오는 값들을 param.변수 형태로 가져올 수 있다. 여기서 우리가 넘겨준 param.status라는 값이 true로 확인이 되면, "저장 완료"라는 것을 표현할 수 있도록 구현했다.
13. 상품삭제 뷰 템플릿 추가 + 상품삭제 컨트롤러 구현
먼저 뷰 템플릿에 상품삭제 박스를 추가하기로 했다. 기존의 상품 상세 뷰템플릿에 상품삭제 박스를 추가해서 컨트롤러로 접근할 수 있도록 구현하기로 했다.
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
th:onclick="|location.href='@{/basic/items/delete/{itemId}(itemId=${item.id})}'|"
onclick="location.href='items.html'" type="button">상품삭제</button>
</div>
박스는 바로 앞에 있는 '목록으로' 코드를 그대로 가져와서, onclick 경로를 /delete/{itemId} 형식으로 갈 수 있도록 변경했다.
@GetMapping("/delete/{itemId}")
public String delete(@PathVariable Long itemId, RedirectAttributes redirectAttributes){
itemRepository.deleteById(itemId);
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items";
}
삭제 컨트롤러는 GetMapping으로 구현했다. 현재 삭제하는데에 있어서는 GetMapping으로 넘어오는 itemId 파라미터면 충분하기 때문이다.
삭제는 HashMap의 remove 기능으로 구현했다.
삭제 이후 RedirectAttributes에 true를 반환해주었고, 상품 목록 페이지로 리디렉션 하도록 설정했다.
<h2 th:if="param.status" th:text="'상품 삭제 완료'"></h2>
상품 목록 페이지에서는 상품이 정상적으로 삭제되었을 때 "상품 삭제 완료'라는 것이 표현될 수 있도록 param.status로 구현했다.
위의 값들을 추가하고 실행해보면 다음과 같은 결과가 나오는 것을 확인할 수 있다.
'Spring > Spring MVC' 카테고리의 다른 글
Spring MVC : 가장 간단하게 시작하기 (0) | 2021.12.20 |
---|---|
Spring MVC : 가장 기초적인 부분들 정리 (0) | 2021.12.20 |
Spring MVC : 스프링 MVC 단계별 구현 (0) | 2021.12.12 |
Spring MVC : HttpServletRequest에 대한 정리 (0) | 2021.12.10 |
Spring MVC : 스프링 환경에서 서블릿 사용해보기 (0) | 2021.12.09 |