[JPA] persist와 merge 이해(차이)
이 포스트는 스프링 스터디에서 영속성을 이야기하던 도중 “준영속 상태인 엔티티를 persist()해도 정상 동작하던데 merge() 대신에 사용해도 ㄱㅊ?”에서 시작됐다.
그래서 스터디 끝나고 새벽까지 혼자 개념 공부 다시 하면서 삽질한 결과, 문제가 발생한다!
ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
책 <자바 ORM 표준 JPA 프로그래밍> 3장을 공부한 후 대화를 나눈 거였는데 그 답이 15장에 있었다 하하.
삽질한 덕분에 영속성에 대해 깊게 이해하게 됐다.
문제가 되는 이유는 아래와 같다.
persist()를 실행하고 db 혹은 1차 캐시에 같은 entity가 존재하면 javax.persistence.EntityExistsException(언체크 예외이며 트랜잭션 롤백을 표시) 에러가 발생한다.
(참고, 언체크 예외는 예외 처리가 필수가 아니다.)
이렇게 간단한 답을 난 아래와 같이 삽질 과정을 거쳤다.
안타까운 나의 삽질 과정..
detached 상태에서 merge()가 아니라 persist()를 사용한다면 어떻게 될까?
먼저, 아래 사진은 엔티티의 생명주기이다.
참고로 persist()는 파라미터로 받은 entity를 영속성으로 바꾸어 반환하며 merge()는 준영속 상태인 파라미터가 아니라 영속 상태인 새로운 entity를 반환한다.
또, merge는 비영속, 준영속 상관없이 정상 동작한다.
식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고 조회할 수 없으면 새로 생성해서 병합한다.
detatch()로 detached된 entity A가 있다고 하자.
A를 managed 상태로 돌려 놓기 위해 원래는 merge()를 사용해야 하지만 persist()를 사용하였다고 가정하자.
원래 DB에는 id=3L, name="조예림"이라고 저장되어 있다.
Member A = em.find(Member.class, 3L); // 이때 A는 영속 상태
System.out.println("영속 상태(1차 캐시에서 가져옴) : " + A.getName());
A.setName("장원영"); // 영속 상태에서 변경
System.out.println("영속 상태 : " + A.getName()); // 아직 DB에는 반영X 1차 캐시에서 변경된 값 "장원영"으로 가져옴
em.detach(A); // 준영속 상태로 변경되면서 A 관련 1차 캐시와 관련 SQL 삭제. 즉, DB에 "장원영"으로 반영 안됨
System.out.println("준영속 상태 : " + A.getName()); // entity는 변경되어있기 때문에 "장원영"으로 출력
B = em.persist(A); // merge()를 써야 영속성이 되는데, 일부러 persist()를 써 봄.
System.out.println("영속 상태 : " + A.getName()); // A는 "장원영"으로 출력이 되지만 DB에는 아직 반영되지 않음
em.persist(A)가 아니라 원래대로 em.merge(A)이 실행된다면
준영속 entity A의 식별자로 1차 캐시에서 엔티티를 조회한다.
만약 1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회하고 1차 캐시에 저장한다.
그리고 영속 엔티티 B에 A 엔티티 값을 채워 넣으면서 B의 name이 "장원영"으로 값이 바뀐다.
마지막으로 B를 반환한다.
이후에 flush()를 하게 되면 B는 영속 상태이기 때문에 쓰기 지연을 통해 DB에 변경된 값이 반영된다.하지만 persist(A)으로 실행된다면준영속 상태인 B가 반환된다.이후 flush()를 하더라도 DB에는 반영되지 않는다.그 이유는 준영속 상태처럼 영속성 컨텍스트의 관리를 받지 못하는 엔티티는 DB에 반영되지 않기 때문.(준영속 상태는 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 어떠한 기능도 동작하지 않는다.)
이때부터 내 머릿속 논리와 위에 지워진 부분이 달라 고민에 빠졌다....
A가 준영속이더라도 persist()가 동작한다면 A, B는 동일하고 영속 상태여야 한다.
참고로 persist()는 id 유무로 비영속과 준영속을 구분하는데
준영속 상태는 항상 id을 가지고 비영속을 가질 수도 있고 가지지 않을 수도 있다.
이때부터 정말 모든 경우의 수를 생각하게 되고..
persist()의 구현 방식에 달려있다는 판단을 하게 됐다.
그래서 결국에 persist()의 코드를 열어보고 난 뒤에야 에러가 발생함을 알 수 있었다.
생각을 해보면 준영속 상태를 persist()하더라도 에러가 발생하지 않는 경우가 있긴 하다.
영속 상태였으나 flush()되지 않아 db에 저장되지 않은 경우에는, detach()된 이후에 persist()하더라도 에러가 발생하지 않는다.
detach()되면서 관련 쓰기 지연 sql도 지워지고 flush()된 적도 없기 때문에 db에는 남아있지 않기 때문이다.
저처럼 삽질하는 사람은 없을 것 같지만
허탈한 마음에 포스트 올립니다..
혹시 틀린 부분이 있다면 지적해주시면 감사하겠습니다:)