(2) 엔티티 비교
영속성 컨텍스트 내부엔 엔티티 인스턴스를 보관하기 위한 1차 캐시
가 있고, 이는 영속성 컨텍스트와 생명주기가 같다.
영속성 컨텍스트를 통해 데이터 저장/조회 시 1차 캐시
에 엔티티가 저장. 이를 통해 변경 감지
기능도 동작하고, DB를 통하지 않고 데이터를 바로 조회 가능
1차 캐시의 가장 큰 장점은 애플리케이션 수준의 반복 가능한 읽기 이다.
같은 영속성 컨텍스트에서 엔티티를 조회하면 다음 코드와 같이 항상 같은 엔티티 인스턴스를 반환 (동등 비교(equals)
가 아닌 주소값이 같은 인스턴스를 반환)
Member member1 = em.find(Member.class, 1L);Member member2 = em.find(Member.class, 1L);assertTrue(member1 == member2); // 둘이 같은 인스턴스다.
15.2.1 영속성 컨텍스트가 같을 때 엔티티 비교
영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 만족한다.
동일성 :
==
비교동등성 :
equals()
비교데이터베이스 동등성 :
@Id
인 데이터베이스 식별자가 같다.
Member findMember = memberRepository.findOne(saveId);assertTrue(member == findMember); // 참조값 비교 - true
기본 전략은 먼저 시작된 트랜잭션이 있으면 그 트랜잭션을 그대로 이어 받아 사용하고, 없으면 새로 시작한다.
테스트 클래스에 @Transactional 적용 시 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백한다.
하지만 롤백 시 영속성 컨텍스트를 플러시하지 않아 어떤 SQL이 실행되는지 콘솔 로그에 남지 않는다.
15.2.2 영속성 컨텍스트가 다를 때 엔티티 비교
테스트 클래스에 @Transactional
이 없고, 서비스에만 @Transactional
이 있으면 다음과 같은 트랜잭션 범위와 영속성 컨텍스트 범위를 갖는다.
Member member = new Member("kim");Long saveId = memberService.join(member);Member findMember = memberRepository.findOne(saveId); // 리포지토리의 트랜잭션에서 조회한 엔티티를 반환assertTrue(member == findMember); // 참조값 비교 - false : 테스트 클래스에서 조회한 member와 리포지토리에서 조회한 findMember는 서로 다른 인스턴스
테스트 코드에서 회원가입하여 만든 Member는 서비스 계층의 트랜잭션, 이후 레포지토리에서 조회한 Member는 레포지토리 계층의 트랜잭션을 사용함
=> 서로 다른 영속성 컨텍스트를 갖는다.
member
와 findMember
는 인스턴스는 다르지만 같은 데이터베이스 로우를 가르키고 있어서 사실상 같은 엔티티다.
동일성 비교
영속성 컨텍스트에 따라서 동일성 비교(==) 로 엔티티를 비교할 수 있을 수도 있고 아닐 수도 있다.
(OSIV 는 동일성 비교로 가능, 하지만 영속성 컨텍스트가 달라진다면 동일성 비교로는 불가능)
데이터베이스 동등성 비교
엔티티를 영속화해야 식별자를 얻을 수 있다는 문제가 있다. (엔티티 영속화 전에는 식별자 값이 null)
직접 부여하는 방식에는 데이터베이스 식별자 비교도 가능하다.
하지만 항상 식별자를 먼저 부여하는 것을 보장하기는 쉽지 않다.
동등성 비교
엔티티 비교 시 비즈니스 키를 활용한 동등성 비교를 권장. (
equals()
)equals()
오버라이딩 시비즈니스 키
가 되는 필드를 선택. -> 보통 중복되지 않고 거의 변하지 않는 데이터베이스 기본 키 후보들이 좋은 대상.유일성만 보장되면 가끔 있는 변경 정도는 허용
ex) 회원 엔티티에 이름과 연락처가 같은 회원이 없다면 (회원 이름 + 연락처) 정도만 조합해서 사용 가능
정리
동일성 비교 : 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용 가능
동등성 비교 : 다른 영속성 컨텍스트의 관리를 받는다면, 유일한 비즈니스 키를 사용해 엔티티 비교