1. 상품 등록, 상품 목록 조회
BookForm
@Getter @Setter
public class BookForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
private String author;
private String isbn;
}
ItemController
@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 finishedForm(BookForm bookForm) {
Book book = new Book();
book.setIsbn(bookForm.getIsbn());
book.setName(bookForm.getName());
book.setAuthor(bookForm.getAuthor());
book.setPrice(bookForm.getPrice());
book.setStockQuantity(bookForm.getStockQuantity());
itemService.saveItem(book);
return "redirect:/";
}
// 상품 목록
@GetMapping("/items")
public String findItems(Model model) {
model.addAttribute("items", itemService.findItems());
return "/items/itemList";
}
// 상품 수정
@GetMapping("/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
Book item = (Book) itemService.findItem(itemId);
BookForm bookForm = new BookForm();
bookForm.setId(itemId);
bookForm.setName(item.getName());
bookForm.setPrice(item.getPrice());
bookForm.setIsbn(item.getIsbn());
bookForm.setAuthor(item.getAuthor());
bookForm.setStockQuantity(item.getStockQuantity());
model.addAttribute("form", bookForm);
return "/items/updateItemForm";
}
@PostMapping("/items/{itemId}/edit")
public String updateItem(BookForm form){
// Book item = (Book) itemService.findItem(form.getId());
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setIsbn(form.getIsbn());
book.setAuthor(form.getAuthor());
book.setStockQuantity(form.getStockQuantity());
itemService.saveItem(book);
return "redirect:/items";
}
}
(설명생략)
2. 상품 수정
- 처음 상품수정 화면을 열었을 땐 @GetMapping방식으로, updateItemForm()에 들어오게 된다.
@PathVariable을 통하여 URL에서 전달된 itemId로 수정할 Item을 호출하고 이 아이템의 기존 값들로 BookForm으로 만들어서 model의 addAttribute를 통해 데이터를 뷰에 전달했다. - 수정화면이 등장하여, 수정을 진행하고 submit을 누르면 다시 Controller의 @PostMapping을 받은 updateItem()으로 들어오게되어 매개변수 form을 통해 데이터를 수정시켜주면된다.
(html에서 이렇게 action이 생략되었을 경우, 현재 URL을 그대로 다시 사용한다. 같은 URL이라도 대신 전송방식이 POST로 변경되었음으로 updateItem()메소드로 들어올 수 있는 것이다.)
매개변수 form을 통해 데이터를 수정시켜줄 때는 변경감지와 Merge로 2가지 선택지가 있다.
3. 변경감지 VS Merge
Merge
위의 ItemController의 updateItem()을 그대로 실행한 경우, 뷰에서 전달받은 BookForm 객체 form의 데이터를 이용하여 Book 객체를 만들고 saveItem을 진행하는데, 이 코드는 레포지터리의 save 함수를 호출해서 결국 em.merge(item)을 실행한다.
ItemRepository
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
// 상품 등록
public Long save(Item item) {
if(item.getId() == null){
em.persist(item);
} else {
em.merge(item);
}
return item.getId();
}
위의 ItemController에서 upodateItem()의 새로 생성한 book 객체는 준영속성 상태이다. Id는 실제 DB에 저장된 객체의 것과 같지만 영속성컨텍스트 안에 들어가 있지 않아서 JPA에 의해 관리되고 있지 않은 상태를 말한다. 이 준영속성 상태의 객체를 DB에 반영하기 위해 사용하는 것이 merge()함수 이다.
merge함수는 말그대로 병합인데 이 함수를 호출하면, DB에서 해당 Id가 일치하는 데이터(실제 Data)를 가져와서 영속성 컨텍스트에 놓고, mege할때 매개변수로 전달했던 book의 모~~든 속성값으로 업데이트 시켜주는 것이다.
em.merge()의 결과 값이 실제 업데이팅된 데이터이고 이는 영속성컨텍스트에서 관리되니, 추가적으로 변경할 것이 있다면 메소드가 반환한 객체로 사용해야 한다. 트랜잰션 커밋 시점에 변경감지를 통해 모든 변경사항이 DB에 반영된다.
주의!! 실제 실무에서는 merge()의 사용을 꺼려한다. 왜냐면 의도하지 않은 속성값도 다 변경될 수 있기 때문이다 !! em.merge(book)할 때 전달한 데이터의 특정 속성값이 비어 있다면, 병합할 때 실제 데이터의 특정값도 null값으로 변경 시킬 수 있기 때문에 의도치않은 데이터 손실이 발생할 수도 있는 것이다.
따라서, 그냥 변경감지(Dirty Checking)을 사용하자.
변경감지
변경감지에 맞게 수정해보자
ItemController - updateItem()
@PostMapping("/items/{itemId}/edit")
public String updateItem(BookForm form) {
itemService.updateItem(form.getId(),
new BookDTO(form.getName(), form.getPrice(),
form.getStockQuantity(), form.getAuthor(), form.getIsbn()));
return "redirect:/items";
}
ItemService - updateItem()
@Transactional
public void updateItem(Long itemId, BookDTO bookDTO){
Book book = (Book) itemRepository.findOne(itemId);
book.setName(bookDTO.getName());
book.setPrice(bookDTO.getPrice());
book.setStockQuantity(bookDTO.getStockQuantity());
book.setAuthor(bookDTO.getAuthor());
book.setIsbn(bookDTO.getIsbn());
}
service의 updateItem()을 보면 Id로 DB에서 해당 데이터를 찾아서 영속성 컨텍스트에 담고 데이터를 변경하여, 오롯이 변경감지로만 데이터를 변경할 수 있도록 코드를 수정했다.
이처럼 DTO를 만들어서 넘기는 것이 가장 좋지만, DTO를 만들지 않는다면, updateItem()의 파라미터에 필요한 것들만 담아서 활용해도 된다. 따로 Book객체를 만들지 않는것이 관건이다.
Redirect
redirect:에 대한 이해가 필요합니다.
redirect:를 해버리면 뷰 템플릿을 요청하지 않습니다. 대신에 HTTP 302를 보냅니다.
이것을 리다이렉트라고 하는데요. 이렇게 되면 웹 브라우저는 /items라는 URL로 완전히 다시 요청하게 됩니다. 실제 URL창이 변하는 것을 확인할 수 있습니다.
쉽게 이야기해서 redirect:를 사용하면 뷰 템플릿 자체를 요청하지 않습니다.
따라서 @ModelAttribute에서 model.addAttribute("form") 기능이 수행은 되지만, 뷰 템플릿을 사용하지 않으니 모델은 전혀 사용되지 않습니다.
by. 김영한 선생님
이 게시물은 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 수강하고 정리한 내용임을 밝힙니다.
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본
www.inflearn.com