[JPA] 변경 감지와 병합(merge)

sorzzzzy·2021년 11월 7일
2

TIL

목록 보기
7/36
post-thumbnail

지난번에 진행했던 hello shop 프로젝트에서 상품 수정 시 사용됐던 merge() 라는 친구를 기억하나 혹시~😎?

JPA에서 준영속 엔티티를 수정하는 방법에는 2가지가 있는데, 이것이 매우 중요하다고 강조를 하셔서 다시 한 번 정리하는 글을 작성해보려고 한다!

고 고 고 ~


준영속 엔티티란?

영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다.

임의로 만들어낸 엔티티라도 기존 식별자를 가지고 있는 경우(JPA가 식별할 수 있는 id를 가지고 있는 경우)에는 준영속 엔티티라고 말한다.

예를 들어, 내가 Food 라는 객체를 만들었다고 가정하자.
Food 객체는 이미 데이터베이스에 한 번 저장되어서 식별할 수 있는 id 가 존재한다.
이것 또한 기존 식별자를 가지고 있는 경우이므로 준영속 엔티티라고 볼 수 있다!

🤔 : 여기서 갑자기 준영속 엔티티가 왜 나왔냐~ 하면~~~?

JPA는 엔티티를 수정할 때 영속성 컨텍스트에 테이블 형태의 1차 캐시를 두고 엔티티를 저장해서,
Transaction commit 시에, 1차 캐시의 영속된 값과 현재 entity 값을 비교하여 변경이 된 것을 알아서 적용
해준다!

// 준영속 상태를 생각하지 않은 엔티티 값 수정

@PostMapping("/members/{memberId}/editName")
public String updateMemberName(@ModelAttribute("form") MemberNameEditForm form){
Member member = new Member();

member.setId(form.getId());
member.setName(form.getName());

return "redirect:/members";

위의 코드는 PostMapping 으로 "members/memberId/editName" 요청을 받아
파라미터로 넘겨 받은 MemberNameEditFormform 데이터로 item 값을 넣어줘서
formid 에 해당하는 memberNameset 했다.
➡️ 트랜잭션이 커밋되어도, 변경감지가 일어나지 않는다.즉 잘못된 코드이다.


준영속 엔티티를 수정하는 방법은 2가지가 있다.

1. 변경 감지 기능 사용
2. 병함(merger) 사용


1. 변경 감지 기능

@Transactional

void updateMember(Member memberParam)
Member findMember = em.find(Member.class, memberParam.getId());

findMember.setName(memberParam.getName());

entityManagerentity를 직접 꺼내, 값을 수정하는 방식이다.
영속성 컨텍스트에서 엔티티를 다시 조회한 후, 데이터를 수정한다.

트랜잭션 안에서 엔티티를 다시 조회하고, 변경할 값을 선택하는데,
트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 이 동작한다.


2. 병합(merge) 사용

@PostMapping("/members/{memberId}/editName")
public String updateMemberName(@ModelAttribute("form") MemberNameEditForm form){
Member member = new Member();

member.setId(form.getId());
member.setName(form.getName());

em.merge(member);

return "redirect:/members";

병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용한다.


1. merge()를 실행
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회
➡️ 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장
3. 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣음
➡️ member 엔티티의 모든 값을 mergeMember에 밀어 넣는데, 이 때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀜
4. 영속 상태인 mergeMember를 반환

✔️ 병합 시 동작 방식 정리
1️⃣ 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회
2️⃣ 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체 (=병합, 쉽게 말하면 바꿔치기!!!)
3️⃣ 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행


merge() 는 엔티티의 모든 필드를 그대로 변경한다.
만약 membername 필드만 변경하고자 수정하고 merge() 한다면
member 의 나머지 필드는 기존의 값을 잃고 null 이 대입되는 위험한 상황이 발생한다!!

이를 해결할 가장 좋은 방법은 엔티티를 변경할 때는 항상 변경 감지를 사용하는 것이다.

  • 컨트롤러에서 어설프게 엔티티를 생성하지 않기!
  • 트랜잭션이 있는 서비스 계층에 식별자와 변경할 데이터를 명확하게 전달하기!
  • 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하기
    ➡️ 이렇게 하면 트랜잭션 커밋 시점에 변경 감지가 실행됨👍🏻

📌 참고 자료

profile
Backend Developer

1개의 댓글

comment-user-thumbnail
2024년 9월 21일

좋은 글 감사합니다ㅎ.ㅎ

답글 달기