[Spring Boot2][2] 7. 웹 계층 개발(2)
🏷 상품 수정
상품 등록, 조회와 달리 수정은 까다로운 부분이 많으니 집중해서 듣고 확실히 이해하도록 하자😉
✔️ 상품 수정 컨트롤러
// itemId는 변경될 수 있으므로 @PathVariable 사용
@GetMapping("items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
// 캐스팅을 하는 것이 좋은 방법은 아니나, 여기서는 예제 단순화를 위해 사용
Book item = (Book) itemService.findOne(itemId);
// 폼을 업데이트 할 건데, 엔티티가 아닌 BookForm 을 보냄
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
model.addAttribute("form", form);
return "items/updateItemForm";
}
@PostMapping("items/{itemId}/edit")
// @PathVariable Long itemId : 폼에서 그대로 넘어오기 때문에 생략해도 됨
// @ModelAttribute("form") : 폼에서 오브젝트 이름으로 설정한 부분이 그대로 넘어옴
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
- 수정 버튼을 선택하면
/items/{itemId}/edit
URL을 GET 방식으로 요청 - 그 결과로
updateItemForm()
메서드를 실행하는데 이 메서드는itemService.findOne(itemId)
를 호출해서 수정할 상품을 조회 - 조회 결과를 모델 객체에 담아서 뷰(
items/updateItemForm
)에 전달
✔️ 상품 수정 폼
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form th:object="${form}" method="post">
<!-- id -->
<input type="hidden" th:field="*{id}" />
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" />
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요" />
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요" />
</div>
<div class="form-group">
<label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control" placeholder="저자를 입력하세요" />
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control" placeholder="ISBN을 입력하세요" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
🏷 변경 감지와 병합(merge)
💡 즈엉~말 중요한 내용이니 꼭❗️ 완벽하게 이해하고 넘어갑시다^-^
준영속 엔티티란 ❓
- 영속성 컨텍스트가 더는 관리하지 않는 엔티티!
- 여기서는
itemService.saveItem(book)
에서 수정을 시도하는Book
객체다. Book
객체는 이미 DB 에 한번 저장되어서 식별자가 존재한다!- 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
준영속 엔티티를 수정하는 2가지 방법❗️
- 변경 감지 기능 사용
- 병합(
merge
) 사용
1️⃣ 변경 감지 기능 사용
@Transactional
// itemParam : 파리미터로 넘어온 준영속 상태의 엔티티
void update(Item itemParam) {
Item findItem = em.find(Item.class, itemParam.getId()); // 같은 엔티티를 조회한다.
findItem.setPrice(itemParam.getPrice()); // 데이터를 수정한다.
}
✔️ 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법
- 트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택
- 트랜잭션 커밋 시점에 변경 감지(
Dirty Checking
) 이 동작해서 데이터베이스에 UPDATE SQL 실행!
2️⃣ 병합(merge
) 사용
병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다❗️
@Transactional
// itemParam : 파리미터로 넘어온 준영속 상태의 엔티티
void update(Item itemParam) {
Item mergeItem = em.merge(item);
}
1️⃣ merge()
를 실행
2️⃣ 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회함
➡️ 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장
3️⃣ 조회한 영속 엔티티(mergeMember
)에 member
엔티티의 값을 채워 넣는다.
➡️ member
엔티티의 모든 값을 mergeMember
에 밀어 넣는데, 이 때 mergeMember
의 “회원1”이라는 이름이 “회원명변경”으로 바뀜
4️⃣ 영속 상태인 mergeMember
를 반환한다.
✔️ 병합 시 동작 방식 정리
1️⃣ 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회
2️⃣ 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체 (=병합, 쉽게 말하면 바꿔치기!!!)
3️⃣ 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
🤚🏻 주의
- 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다(=모든 필드를 교체해버림!)
- 병합시 값이 없으면
null
로 모든 필드를 업데이트 할 위험이 생긴다!
📌 상품 리포지토리의 저장 메서드 분석 - ItemRepository
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
// 처음 저장할 때는 아이템의 id가 존재하지 않으므로 이 부분을 고려해야 함
if (item.getId() == null) {
// 아이템 그냥 저장
em.persist(item);
} else {
// 아이템 업데이트(?) - 나중에 자세히 배움
em.merge(item);
}
}
save()
메서드는 식별자 값이 없으면(=null
) 새로운 엔티티로 판단해서 영속화(persist
)하고 식별자가 있 으면 병합(merge)- 지금처럼 준영속 상태인 상품 엔티티를 수정할 때는 id 값이 있으므로 병합 수행!
💡 가장 좋은 해결 방법 - 엔티티를 변경할 때는 항상 변경 감지를 사용하자!
- 컨트롤러에서 어설프게 엔티티를 생성하지 않기!
- 트랜잭션이 있는 서비스 계층에 식별자와 변경할 데이터를 명확하게 전달하기!
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하기
➡️ 이렇게 하면 트랜잭션 커밋 시점에 변경 감지가 실행됨👍🏻
🏷 상품 주문
✔️ 상품 주문 컨트롤러
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping("/order")
public String createForm(Model model) {
// 내가 가진 모든 멤버들과 아이템들을 다 가져옴
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
// 가져온 후 모델에 담아서 폼으로 넘김
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping("/order")
public String order(@RequestParam("memberId") Long memberId,
@RequestParam("itemId") Long itemId,
@RequestParam("count") int count) {
// 파라미터로 주문 생성
orderService.order(memberId, itemId, count);
// 주문 내역 목록으로 리다이렉트
return "redirect:/orders";
}
}
- 메인 화면에서 상품 주문을 선택하면
/order
를 GET방식으로 호출 OrderController
의createForm()
메서드- 주문 화면에는 주문할 고객정보와 상품 정보가 필요하므로
model
객체에 담아서 뷰에 넘겨줌
✔️ 상품 주문 폼
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<form role="form" action="/order" method="post">
<div class="form-group">
<label for="member">주문회원</label>
<select name="memberId" id="member" class="form-control">
<option value="">회원선택</option>
<option th:each="member : ${members}"
th:value="${member.id}"
th:text="${member.name}" />
</select>
</div>
<div class="form-group">
<label for="item">상품명</label>
<select name="itemId" id="item" class="form-control">
<option value="">상품선택</option>
<option th:each="item : ${items}"
th:value="${item.id}"
th:text="${item.name}" />
</select>
</div>
<div class="form-group">
<label for="count">주문수량</label>
<input type="number" name="count" class="form-control" id="count" placeholder="주문 수량을 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br/>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
⬆️ 실행 결과 👍🏻
🏷 주문 목록 검색, 취소
✔️ 주문 목록 검색 컨트롤러
@GetMapping("/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
✔️ 주문 목록 검색 화면
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div>
<div>
<form th:object="${orderSearch}" class="form-inline">
<div class="form-group mb-2">
<input type="text" th:field="*{memberName}" class="form-control" placeholder="회원명"/>
</div>
<div class="form-group mx-sm-1 mb-2">
<select th:field="*{orderStatus}" class="form-control">
<option value="">주문상태</option>
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
th:value="${status}"
th:text="${status}">option
</option>
</select>
</div>
<button type="submit" class="btn btn-primary mb-2">검색</button>
</form>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>회원명</th>
<th>대표상품 이름</th>
<th>대표상품 주문가격</th>
<th>대표상품 주문수량</th>
<th>상태</th>
<th>일시</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${orders}">
<td th:text="${item.id}"></td>
<td th:text="${item.member.name}"></td>
<td th:text="${item.orderItems[0].item.name}"></td>
<td th:text="${item.orderItems[0].orderPrice}"></td>
<td th:text="${item.orderItems[0].count}"></td>
<td th:text="${item.status}"></td>
<td th:text="${item.orderDate}"></td>
<td>
<a th:if="${item.status.name() == 'ORDER'}" href="#" th:href="'javascript:cancel('+${item.id}+')'"
class="btn btn-danger">CANCEL</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
<script>
function cancel(id) {
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", "/orders/" + id + "/cancel");
document.body.appendChild(form);
form.submit();
}
</script>
</html>
✔️ 주문 취소 컨트롤러
@PostMapping("/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
다음에 할 일.....🤔
이제까지 전체적으로 한 사이클을 돌아봤다!!!!
다음에 할 일은 API를 만들어 보는 것이다😮
과거에는 이렇게 화면 만드는 정도로 끝나는 정도가 많았지만,
요즘 개발은 주로 싱글 페이지 애플리케이션도 많고, 거의 API로 통신해야 하는 상황도 상당히 많다!
그래서 우리는❗️
➡️ JPA를 가지고 어떻게 API를 잘 만들어 내는지,
➡️ 그리고 나아가서 성능 최적화까지 다뤄볼 예정이다!
API도 할 수 이따! 이따!
Author And Source
이 문제에 관하여([Spring Boot2][2] 7. 웹 계층 개발(2)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@sorzzzzy/Spring-Boot22-7.-웹-계층-개발2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)