일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 모두의네트워크
- 백준
- 문자열
- 모두를위한딥러닝
- 팀플회고
- React Native
- 정리
- 데베
- 리액트 네이티브 프로젝트 생성
- 깃허브 토큰 인증
- 데이터베이스
- 깃허브 로그인
- 깃 터미널 연동
- 백준 4358 자바
- 머신러닝
- 백준 4949번
- 스터디
- 깃 연동
- 네트워크
- 딥러닝
- 모두의 네트워크
- 자바
- HTTP
- 리액트 네이티브 시작하기
- 백준 5525번
- 지네릭스
- 백준 4358번
- SQL
- 리액트 네이티브
- 모두를 위한 딥러닝
- Today
- Total
솜이의 데브로그
[Spring Boot] 웹 계층 개발(2) 본문
Reference : Inflearn 실전 스프링 부트와 JPA 활용1 (김영한님 강의)
상품 등록
상품 등록 컨트롤러
/controller/ItemController
package jpabook.jpashop.controller;
import jpabook.jpashop.domain.item.Book;
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.PostMapping;
@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){
Book book = new Book();
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";
}
}
멤버 컨트롤러와 동일하게 작동한다. Model을 통해 새로운 폼을 등록하고, 실제로 post 를 받았을 때 create 함수가 작동하여 생성한다.
그리고 작업이 끝나면 itemService를 통해 book 아이템을 저장하고, 아이템 목록을 보여주는 화면으로 redirect 한다.
위의 코드처럼 다 Set 으로 하는것보다, createBook() 같은 함수를 통해 한번에 parameter를 넘겨 생성해서 setter를 다 제거하는 것이 좋은 설계이다.
상품 등록 뷰 코드를 작성한다.
상품 목록
상품 목록 컨트롤러
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
위의 코드에서 하단에 추가
items를 모델에 담아 폼 형태로 넘겨주고, 상품 목록 뷰 html 코드에서는 해당 아이템을 쭉 돌면서 내용을 출력한다.
상품 목록은 다음과 같이 확인 가능하다.
여기서 상품 수정할 수 있는 기능을 추가해보자.
상품 수정
상품 수정과 관련된 컨트롤러
@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(@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";
}
- @ModelAttribute("form") 어노테이션 : updateItemForm 의 object 이름을 넘겨주는 것으로 사용
- itemId를 누군가가 조작해서 넘겨서, 마음대로 수정할 수 있으므로 조심해야한다. (보안상으로 위험)
- 유저가 아이템에 대해서 권한이 있는지 체크하는 로직을 서버에 넣어야한다.
- saveItem을 호출하면 transactional 이 걸린 상태로 itemRepository.save를 다시 호출한다.
- 이 save는 item Id가 null 이면 새로운 object이므로 persist를 하고, 아니면 수정 목적으로 em.merge를 호출하게 된다.
변경 감지와 병합 (merge)
준영속 엔티티란?
- JPA 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 말한다.
- JPA에 한번 들어갔다 나와서 (DB에 한번 저장된) 식별자가 있는 것을 준영속 상태의 객체라고 한다.
- 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
- 준영속 엔티티의 문제점 -> JPA가 관리를 하지 않는다. 따라서 값을 변경해도 DB에 업데이트가 자동으로 일어나지 않는다.
그렇다면 준영속 상태의 엔티티를 어떻게 변경할 수 있을까? -> 2가지 방법
- 변경 감지 기능 사용
- 병합 (merge) 사용
변경 감지 기능
ItemService.java
@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());
}
findItem으로 찾아온 영속 상태.
이런 경우, 값을 다 setting 한 다음에 @Transactional 에 의해 커밋되고, 커밋된 후에는 flush를 날린다. 즉, 영속성 컨텍스트에서 변경된 것이 무엇인지를 찾아낸다. 그리고 바뀐 값을 update해서 DB에 반영한다.
즉, 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법.
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 -> 트랜잭션 커밋 시점에 변경 감지 (Dirty Checking)
이 동작을 한 뒤 데이터베이스에 UPDATE SQL 실행
병합 사용
merge는 직접 하나하나 입력해서 수정하던것이 한번에 바뀌는 방식.
병합 동작 방식
- merge() 를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시 (영속성 컨텍스트 entityManager)에서 엔티티를 조회한다.
- 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다. (db에서 가져옴)
- 조회한 영속 엔티티 (위 그림에선 mergeMember)에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 기본적인 필드를 다 바꿔치기 함. 이 때 mergeMember의 "회원1" 이라는 이름이 "회원명 변경"으로 바뀐다.)
- 영속 상태인 mergeMember를 반환한다.
즉, 아래 코드와 같은 내용이 된다.
@Transactional
public Item updateItem(Long itemId, Book param){
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(param.getPrice());
findItem.setName(param.getName());
findItem.setStockQuantity(param.getStockQuantity());
return findItem;
}
기존의 parameter로 넘어온 것은 영속성 컨텍스트로 변경되지 않고, 반환된 내용만 영속성 컨텍스트에서 관리되는 객체이다.
다시 한번 병합 시 동작 방식을 간단히 정리하자면
1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다. (병합한다.)
3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
주의 할 점
- 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
- 실무에서는 보통 업데이트 기능이 매우 제한적이다. 그런데 병합은 모든 필드를 변경해버리고, 데이터가 없는 경우 null로 업데이트해버린다. 병합을 사용해서 문제를 해결하기 위해서는 변경 폼 화면에서 모든 데이터를 항상 유지해야한다.
- 실무에서는 보통 변경 가능한 데이터만 노출하기 때문에, merge 를 사용해서 데이터를 다 갈아치우는 방법보다는, 귀찮더라도 변경감지를 하는 방법을 권장한다.
좋은 해결 방법
- 컨트롤러에서 어설프게 엔티티를 생성하지 말자.
- 트랜잭션이 있는 서비스 계층에서 식별자(id)와 변경할 데이터를 명확하게 전달한다. (파라미터 or dto)
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경한다.
- 트랜잭션 커밋 시점에 변경감지가 실행된다.
ex ) ItemController
@PostMapping("items/{itemId}/edit")
public String updateItem(@PathVariable Long 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);
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
entity를 파라미터로 쓰지 말고 정확하게 필요한 데이터만 받아서 사용한다.
(다 전달하지 않고 UpdateItemDto 등을 사용해서 dto를 넘겨줘도 된다.)
+setter 없이 엔티티안에서 바로 추적할 수 있도록 메소드를 만드는 것이 좋다.
'dev > Spring Boot' 카테고리의 다른 글
Spring Data JPA 에서 get~ 메서드와 find~ 메서드의 차이점을 알아보자 (0) | 2024.02.04 |
---|---|
List<>를 변수로 가지고 있을 때 엔티티 업데이트 (0) | 2023.02.04 |
[SpringBoot] 웹 계층 개발(1) (0) | 2022.03.25 |
[Spring Boot] 주문 도메인 개발(2) (0) | 2021.12.03 |
[Spring Boot] 주문 도메인 개발(1) (0) | 2021.11.17 |