이전글
2023.02.28 - [Back-End/JPA] - JPA 활용 - 홈 화면과 회원등록 화면 계층 생성
상품 수정
상품 컨트롤러 코드 추가
package jpabook.jpashop.controller;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.service.ItemService;
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.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(BookForm form) {
// 실무에서는 해당 객체를 생성하고 setter로 값을 넣기보다는
// create메서드를 생성해서 진행하는 것이 코드가 깔끔하다.
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/";
}
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
@GetMapping("items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
Book item = (Book) itemService.findOne(itemId);
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")
public String updateItem(@PathVariable String itemId, @ModelAttribute("form") BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:items";
}
}
상품 수정 폼 화면
<!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>
상품 수정 폼 이동
- 수정 버튼을 클릭하면
/items/{itemId}/edit
에 GET 방식으로 요청 - 그 결과로 updateItemForm() 메서드가 실행 되고, 해당 메서드에서는
itemService.findOne(itemId)
를 호출해서 수정할 상품을 조회하게 됩니다. - 조회 결과를 모델 객체에 담아서 뷰(
items/updateItemForm
) 에 전달해 줍니다.
상품 수정 실행
- 수정 폼에서 정보를 수정하고 Submit 버튼을 클릭
/items/{itemId}/edit
에 POST 방식으로 요청하고 updateItem() 메서드를 실행합니다.- 이때, 컨트롤러에 파라미터로 넘어온 item 엔티티 인스턴스는 현재 준영속 상태이므로, 영속성 컨텍스트의 지원을 받을 수 없고, 데이터를 수정해도 변경 감지 기능은 동작하지 않습니다.
변경 감지와 병합
변경 감지와 병합에 들어가기에 앞서, 준영속 엔티티에 대해서 알아보자
준영속 엔티티
영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다.
여기서는 itemService.saveItem(book) 에서 수정을 시도하는 Book 객체다. Book 객체는 이미 DB에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
JPA가 관리하는 영속상태 엔티티는 변경이 감지 되면 트랜잭션 커밋 시점에 값을 바꿔 준다.
하지만 현재 만든 Book은 직접 생성자를 통해서 생성한 객체이기 때문에 JPA가 관리하는 상태가 아니라는 것이다.
준영속 엔티티를 수정하는 2가지 방법
- 변경 감지 기능 사용
ItemService
@Transactional
public void updateItem(Long itemId, Book param) {
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(param.getPrice());
findItem.setName(param.getName());
findItem.setStockQuantity(param.getStockQuantity());
}
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법
트랜잭션 안에서 엔티티를 다시 조회하고, 변경할 값 선택해서 트랜잭션 커밋 시점에 변경 감지가 동작하게 하여 데이터베이스에 UPDATE SQL이 실행 되게 한다.
- 병합 사용
병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다.
ItemRepository
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(itemParam);
}
병합 동작 방식
- merge() 를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
→ 만약 1차캐시에 엔티티가없으면 데이터베이스에서 엔티티조회후 1차 캐시에 저장 - 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1” 이라는 이름이 “회원명변경”으로 바뀐다.)
- 영속 상태인 mergeMember를 반환한다.
주의점
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
→ 병합을 쓰지말고 변경감지 방법을 사용하자!
가장 좋은 해결 방법
엔티티를 변경할 때는 항상 변경 감지를 사용하자!
- 컨트롤러에서 어설프게 엔티티를 생성하지 말자!
- 트랜잭션이 있는 서비스 계층에 식별자(id)와 변경할 데이터를 명확하게 전달하기(파라미터 or dto)
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하세요
- 트랜잭션 커밋 시점에 변경 감지가 실행됩니다.
→ 위의 해결 방법을 적용한 예시입니다. 컨트롤러에서 생성하던 엔티티를 서비스 계층으로 옮겼고, 서비스 계층에서 필요한 데이터만 딱 받아서 변경 감지를 통해 변경시켯습니다.
이렇게 코드를 설계할시, 유지보수에 훨씬 좋을 것입니다.
참고
해당 글은 김영한님의 실전! 스프링 부트와 JPA 활용을 공부하며 작성한 글입니다.
https://www.inflearn.com/course/스프링부트-JPA-활용-1/dashboard