엔티티의 동등성 비교
영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다. 이 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다. 영속성 컨텍스트를 통해 데이터를 저장하거나 조회하면 1차 캐시에 엔티티가 저장된다.
이 1차 캐시 덕분에 변경 감지 기능도 동작하고, 이름 그대로 1차 캐시로 사용되서어 데이터베이스를 통하지 않고 데이터를 바로 조회할 수도 있다.
영속성 컨텍스트를 더 정확히 이해하기 위해서는 1차 캐시의 가장 큰 장점인 애플리케이션 수준의 반복 가능한 읽기를 이해해야 한다. 같은 영속성 컨텍스트에서 엔티티를 조회하면 다음 코드와 같이 항상 같은 엔티티 인스턴스를 반환한다.
단순이 동등성 비교 수준이 아니라 정말 주소값이 같은 인스턴스를 반환한다.
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);
assertTrue(member1 == member2); // 둘은 같은 인스턴스
영속성 컨텍스트가 같을 때 엔티티 비교
JPA 트랜잭션 기본 전략은 먼저 시작된 트랜잭션이 있으면 그 트랜잭션을 그대로 이어받아 사용하고 없으면 새로 시작한다. 만약 다른 전략을 사용하고 싶으면 propagation 속성을 변경하면 된다.
@Transactional(propagation = Propagation.REQUIRED) // 기본값
트랜잭션 안에서 테스트 코드를 진행해보았다.
@RunWith(SpringJUnit4ClassRunner.class)
@Transactional // 트랜잭션 안에서 테스트를 실행한다.
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() throws Exception {
// Given
Member member = new Member(1L, "kim");
// When
Long saveId = memberService.join(member);
// Then
Member findMember = memberRepository.fineOne(saveId);
assertTrue(member == findMember); // 참조값 비교
}
}
트랜잭션 안에서 테스트 코드가 시작하므로 테스트의 범위와 트랜잭션의 범위가 같다.
테스트 전체에서 같은 영속성 컨텍스트에 접근하기 때문에 저장하기 전에 member 객체와 저장 후 조회한 member 객체가 동일하다.
영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.
- 동일성(identical) : == 비교가 같다.
- 동등성(equivalent) : equals() 비교가 같다.
- 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.
테스트 클래스에 @Transactional을 적용하면 테스트가 끝날 때 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백한다. 그래야 데이터베이스에 영향을 주지 않고 테스트를 반복해서 할 수 있기 때문이다.
문제는 롤백시에는 영속성 컨텍스트를 플러시 하지 않아 어떤 SQL이 실행되는지 콘솔 로그에 남지 않는다.
만약 어떤 SQL이 실행되는지 알고싶을땐 마지막에 em.flush()를 강제로 호출하면 된다.
영속성 컨텍스트가 다를 때 엔티티 비교
이번에는 다른 영속성 컨텍스트에서 테스트 코드를 진행해보자.
@RunWith(SpringJUnit4ClassRunner.class)
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() throws Exception {
// Given
Member member = new Member(1L, "kim");
// When
Long saveId = memberService.join(member);
// Then
Member findMember = memberRepository.fineOne(saveId);
assertTrue(member == findMember); // 참조값 비교
}
}
@Service
@Transactional
public class MemberService {
....
}
@Repository
@Transactional
public class MemberRepository {
....
}
Test 클래스가 아닌 Service에서 트랜잭션이 시작한다.
위 예제의 테스트는 실패한다. 왜 실패하는지 이해했다면 영속성 컨텍스트의 생존 범위를 대부분 이해했다고 볼 수 있다.
memberService.join이 끝난 후 member 객체는 준영속 상태가 되고 memberService와 memberRepository가 다른 영속성 컨텍스트에 있기 때문에 같은 객체라고 볼 수 없는 것이다.
영속성 컨텍스트가 다르면 엔티티 비교는 다음과 같다
- 동일성(identical) : == 비교가 실패한다.
- 동등성(equivalent) : equals() 비교가 만족한다. 단 equals()를 구현해야 한다. 보통 비즈니스 키로 구현한다.
- 데이터베이스 동등성 : @Id인 데이터베이스 식별자가 같다.
정리하자면 동일성 비교는 같은 영속성 컨텍스트의 관리를 받는 영속 상태의 엔티티에만 적용할 수 있다.
그렇지 않을 때는 비즈니스 키를 사용한 동등성 비교를 해야 한다.
'JPA' 카테고리의 다른 글
JPA 배치 처리 (0) | 2022.01.25 |
---|---|
JPA 읽기 전용 쿼리의 성능 최적화 (0) | 2022.01.25 |
JPA 예외 처리 (0) | 2022.01.24 |
엔티티 그래프로 연관관계 조회 하기 (0) | 2022.01.21 |
JPA 리스너를 이용한 공통 이벤트 처리 (0) | 2022.01.21 |