흰 스타렉스에서 내가 내리지

테스트 환경에서 엔티티 비교 본문

JPA

테스트 환경에서 엔티티 비교

주씨. 2024. 4. 28. 01:26
728x90

# 엔티티 비교

같은 영속성 컨텍스트에서 엔티티를 조회하면 다음 코드와 같이 항상 같은 엔티티 인스턴스를 반환한다.

Member member1 = em.find(Member.class, "1L");
Member member2 = em.find(Member.class, "2L");

assertTrue(member1 == member2);

 

 

# 영속성 컨텍스트가 같을 때 엔티티 비교

@Transactional
public class MemberServiceTest{
    ...
    
    @Test
    public void join(){
        //Given
        Member member = new Member("member1");
        
        //When
        Long saveId = memberService.join(member);
        
        //Then
        Member findMember = memberRepository.findOne(saveId);
        assertTrue(member == findMember);	// 참조값 비교
    }
}

@Transactional
public class MemberService{
    ...
    
    public Long join(Member member){
        ...
        memberRepository.save(member);
        return member.getId();
    }
}

 

  • 테스트 클래스에 @Transactional 이 선언되어 있으면 트랜잭션을 먼저 시작하고 테스트 메소드를 실행한다.
    • 테스트 클래스의 join() 메소드는 이미 트랜잭션 범위에 들어 있고, 이 메소드가 끝나면 트랜잭션이 종료된다.
    • 따라서, join() 메서드에서는 항상 같은 트랜잭션과 같은 영속성 컨텍스트에 접근한다.
    • 참고)  1트랜잭션 1PC
  • 주목해야 할 것은 저장한 회원과 회원 리포지토리에서 찾아온 엔티티가 완전히 같은 인스턴스라는 점이다.
    • 이것은 같은 트랜잭션 범위에 있으므로 같은 영속성 컨텍스트를 사용하기 때문이다.
  • 영속성 컨텍스트가 같으면 엔티티를 비교할 때 다음 3가지 조건을 모두 만족한다.
    • 동일성 (identical) : ==
    • 동등성 (equivalent) : equals()
    • 데이터베이스 동등성 : @Id 인 데이터베이스 식별자가 같다

 

테스트에도 @Transactional 이 있고 서비스에도 @Transactional 이 있다.
기본 전략은 먼저 시작된 트랜잭션이 있으면 그 트랜잭션을 그대로 이어 받아 사용하고 없으면 새로 시작한다.

 

테스트 클래스에 @Transactional 을 적용하면 테스트가 끝날 떄 트랜잭션을 커밋하지 않고 트랜잭션을 강제로 롤백하낟.
그래야 데이터베이스에 영향을 주지 않고 테스트를 반복해서 할 수 있기 때문이다.

문제는 롤백시에는 영속성 컨텍스트를 플러시하지 않기 때문에 실행된 SQL 을 콘솔에서 확인할 수 없다.
어떤 SQL 이 실행되는지 콘솔을 통해 보고 싶으면 테스트 마지막에 em.flush() 를 강제로 호출하면 된다.

 


# 영속성 컨텍스트가 다를 때 엔티티 비교

//@Transactional  // 테스트에서 트랜잭션을 사용하지 않는다
public class MemberServiceTest{
    ...
    
    @Test
    public void join(){
        //Given
        Member member = new Member("member1");
        
        //When
        Long saveId = memberService.join(member);
        
        //Then
        Member findMember = memberRepository.findOne(saveId);
        assertTrue(member == findMember);	// 실패한다.
    }
}

@Transactional
public class MemberService{
    ...
    
    public Long join(Member member){
        ...
        memberRepository.save(member);
        return member.getId();
    }
}

@Transactional
public class MemberRepository{
    ...
}

 

  • 테스트 클래스에 @Transactional 이 없고 서비스에만 @Transactional 이 있다.
  1. 테스트 코드에서 memberService.join(member) 를 호출해서 회원가입을 시도하면 서비스 계층에서 트랜잭션이 시작되고 PC1 이 만들어진다.
  2. 서비스 계층이 끝날 떄 트랜잭션이 커밋되면서 영속성 컨텍스트가 플러시된다. 이때 트랜잭션과 영속성 컨텍스트가 종료된다. 따라서 member 엔티티 인스턴스는 준영속 상태가 된다.
  3. 테스트 코드에서 memberRepository.findOne(saveId) 를 호출해서 저장한 엔티티를 조회하면 리포지토리 계층에서 새로운 트랜잭션이 시작되면서 새로운 PC2 가 생성된다.
  4. DB 에서 조회된 엔티티를 PC2 에 저장하고 반환한다.
  5. memberRepository.findOne() 메소드가 끝나면서 트랜잭션이 종료되고 PC2 도 종료된다.
  • member 와 findMember 는 각각 다른 영속성 컨텍스트에서 관리되었기 때문에 둘은 다른 인스턴스다.
    • 따라서 동일성(==) 검사가 실패한다.

 

 

-→ 엔티티를 비교할 때는 비즈니스 키를 활용한 동등성(equals()) 비교를 권장한다.