내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
세션 14. 변경감지와 병합
우선 준영속 엔티티에 대하여 알아보자.
준영속 엔티티는 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 의미한다.
다음 코드를 살펴보자.
@PostMapping("/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form, @PathVariable String itemId){
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";
}
위의 새로 생성 된 book객체는 new를 통해 새로 생성된 객체이고, 아직 영속성 컨텍스트에는 등록되지 않았다.
단지 식별자를 갖고있을 뿐이다.
문제는 이 식별자는 DB상에 저장된 어떠한 엔티티의 식별자 값이라는 점이다.
비록 book이라는 객체가 new를 통해 새롭게 만들어 졌지만(JPA는 이 book이라는 객체를 감시하지 않는 상황),
DB에 저장되어 있는 엔티티의 id값인 식별자가 부여되었기 때문에 준영속 상태라고 할수있다.
(Book 객체는 이미 DB 에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준 영속 엔티티로 볼 수 있다.)
● 준영속 엔티티를 수정하는 2가지 방법
1. 변경감지 기능
2. 병합(merge) 사용하기
● 변경감지 기능
@Transactional
public void updateItem(Long itemId, Book param){ // 파라미터로 넘어온 준영속 상태의 book 엔티티
Item findItem = itemRepository.findOne(itemId); // id값으로 DB에서 조회
findItem.setPrice(param.getPrice()); // 데이터 수정
findItem.setName(param.getName());
findItem.setStockQuantity(param.getStockQuantity());
}
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법이다.
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 => 트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 이 동작해서 데이터베이스에 UPDATE SQL 문을 날려준다.
● 병합(merge) 사용
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(item);
}
준영속 상태의 Item을 전달받아 영속성 컨텍스트에 병합시켜 준다.
merge의 내부 동작 원리는 사실상 변경감지 기능과 유사하다.
- merge()의 동작 방식
1. merge() 를 실행한다.
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다.
(member 엔티티의 모든 값 을 mergeMember에 밀어 넣는다. 즉, member의 모든 값을 mergeMember에 setting해준다.)
4. 영속 상태인 mergeMember를 반환한다.
5. 이후 트랜잭션 commit 시점에 변경된 내용을 반영한 SQL UPDATE 문을 DB에 보내게 된다.
주의! : 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다.
병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
병합 기능을 사용하여 Item 엔티티를 저장하는 Repository의 코드는 다음과 같다.
@Repository
public class ItemRepository {
@PersistenceContext
EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
//...
}
이 메서드 하나로 저장과 수정(병합)을 다 처 리한다.
코드를 보면 식별자 값이 없으면 새로운 엔티티로 판단해서 persist() 로 영속화하고 만약 식별자 값이 있으면 이미 한번 영속화 되었던 엔티티로 판단해서 merge() 로 수정(병합)한다.
결국 여기서의 저장 (save)이라는 의미는 신규 데이터를 저장하는 것뿐만 아니라 변경된 데이터의 저장이라는 의미도 포함한다.
이렇게 함으로써 이 메서드를 사용하는 클라이언트는 저장과 수정을 구분하지 않아도 되므로 클라이언트의 로직이 단순해진다.
하지만 실무는 이렇게 merge로 모든 필드를 매꿔버릴수 있는 경우는 드물다.
병합은 모든 필드를 변경해버리고, 데이터 가 없으면 null 로 업데이트 해버린다.
병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데 이터를 항상 유지해야 한다.
실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
결론 : 엔티티를 변경할때는 항상 변경감지 기능을 사용하세요
- 컨트롤러에서 어설프게 엔티티를 생성하지 마세요.
- 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달하세요.(파라미터 or dto)
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하세요.
- 트랜잭션 커밋 시점에 변경 감지가 실행됩니다.
다음 컨트롤러를 살펴보자.
이글 맨위에서 본 컨트롤러 처럼 Book 이라는 엔티티를 생성하여 setter로 Book에 데이터를 전부 전달하지 않는다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
/**
* 상품 수정, 권장 코드
*/
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@ModelAttribute("form") BookForm form) {
itemService.updateItem(form.getId(), form.getName(), form.getPrice());
return "redirect:/items";
}
}
오히려 itemService에 updateItem 메서드를 호출하여 데이터만 전달하고 있다.
@Service
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
/**
* 영속성 컨텍스트가 자동 변경
*/
@Transactional
public void updateItem(Long id, String name, int price) { // id와 데이터를 전달받음
Item item = itemRepository.findOne(id); // 서비스 계층에서 엔티티 조회하고
item.setName(name); // 데이터 수정
item.setPrice(price);
}
}
서비스 계층의 트랜젝션 범위 안에서 데이터가 변경되고, commit 시점에 변경사항들이 적용된다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 지연 로딩과 조회 성능 최적화 - 2 (0) | 2022.04.20 |
---|---|
[JPA] 지연 로딩과 조회 성능 최적화 - 1 (0) | 2022.04.19 |
[JPA] 객체지향 쿼리 언어 4 (중급 문법) (0) | 2022.04.10 |
[JPA] 객체지향 쿼리 언어 3 (중급 문법) (0) | 2022.04.10 |
[JPA] 객체지향 쿼리 언어 2 (기본 문법) (0) | 2022.04.09 |
댓글