김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
https://www.inflearn.com/courses?s=%EA%B9%80%EC%98%81%ED%95%9C
변경감지(dirty checking)와 병합(merge)
엔티티 수정은 두가지 방법이 있습니다.
1. 변경감지 (dirty checking)
2. 병합 (merge)
그 전에 준영속 상태에 대해 알아야합니다.
준영속 엔티티: JPA가 더는 관리하지 않는 엔티티, 즉 영속성 컨텍스트에서 더 이상 관리되지 않는 엔티티를 말합니다.
(https://kingchan223.tistory.com/139?category=870827)
우리가 엔티티를 수정한다면 해당 엔티티는 준영속 상태입니다. 이렇게 준영속 상태인 엔티티를 dirty checking과 merge로 수정할 수 있습니다.
만약 dirty checking으로 book엔티티를 수정한다면 그 과정은 아래와 같습니다.
1. 트랜잭션 안에서 book을 조회해 옵니다. 이때 book은 영속성 컨텍스트에 담겨 영속상태입니다.
2. book의 name을 changeName메소드를 사용하여 바꿉니다.
3. 이때 book은 영속성 상태이므로 JPA가 이전 상태의 스냅샷과 비교하여 수정된 부분(name)을 확인하고, 반영하여 update문을 날립니다.
(https://kingchan223.tistory.com/137?category=870827/해당 포스팅의 4. 변경감지를 보시면 이해할 수 있습니다.)
병합(merge)로 수정한다면 다음과 같습니다.
( 병합은 준영속 상태의 엔티티를 영속상태로 만듭니다.)
코드를 보면 만약 item의 id가 있다면(즉 이미 DB에 있는 엔티티라면) em.merge(item)으로 item을 merge합니다. merge로 넘어온 item은 현재 준영속 상태입니다. 그런데 item을 merge하면 영속상태의 item이 반환됩니다.
참고1: save() 메서드는 식별자를 자동 생성해야 정상 동작한다. 여기서 사용한 Item 엔티티의 식별자는 자동으로 생성되도록 @GeneratedValue 를 선언했다. 따라서 식별자 없이 save() 메서드를 호출하면 persist() 가 호출되면서 식별자 값이 자동으로 할당된다. 반면에 식별자를 직접 할당하도록 @Id 만 선언 했다고 가정하자. 이 경우 식별자를 직접 할당하지 않고, save() 메서드를 호출하면 식별자가 없는 상태로 persist() 를 호출한다. 그러면 식별자가 없다는 예외가 발생한다.
참고2: 실무에서는 보통 업데이트 기능이 매우 재한적이다. 그런데 병합은 모든 필드를 변경해버리고, 데이터 가 없으면 null 로 업데이트 해버린다. 병합을 사용하면서 이 문제를 해결하려면, 변경 폼 화면에서 모든 데 이터를 항상 유지해야 한다. 실무에서는 보통 변경가능한 데이터만 노출하기 때문에, 병합을 사용하는 것이 오히려 번거롭다.
merge의 동작 방식은 아래와 같습니다.
1. merge()를실행한다.
2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 mergerMember의 '회원1'이라는 이름이 '회원명변경'으로 바뀐다.)
4. 영속 상태인 mergeMember를 반환한다.
병합시 동작 방식을 간단히 정리
- 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
- 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다.(병합한다.)
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
merger가 코드도 간단해 보여 좋아보이지만 아래와 같은 문제점이 있습니다.
주의:
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
즉 merge는 모든 값을 갉아 엎기 때문에 merge시에 어떤 값이 없다면 null로 update될 수 있어 매우 위험합니다.
그래서 엔티티 수정시에는 dirty checking을 사용하는 것을 권장합니다.
하여 최종 해결방법은 다음과 같습니다.
• 엔티티를 변경할 때는 항상 변경 감지를 사용.
• 컨트롤러에서 어설프게 엔티티를 생성하지 않는다.
• 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달.(파라미터 or dto)
• 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경.
• 트랜잭션 커밋 시점에 변경 감지가 실행.
즉 아이템 수정 컨트롤러르 아래와 같습니다.
itemService의 uodateItem메소드는 아래와 같습니다.
'JPA > JPA + SpringBoot' 카테고리의 다른 글
(X To Many ) 컬렉션 조회 최적화 (0) | 2021.09.14 |
---|---|
(X To ONE ) LAZY로딩에 의한 N+1문제 해결 (0) | 2021.09.08 |
회원 관련 기능 구현하기, 테스트 (0) | 2021.07.28 |
어플리케이션 구조, 기능 (0) | 2021.07.28 |
엔티티설계하기 (+엔티티 설계시 주의점) (0) | 2021.07.27 |