본문 바로가기

백엔드 개발/오류 해결

@DataJpaTest를 이용해 Lazy loading 테스트할 때 주의할 점

반응형

0. 들어가며

현재 JPA에서 일대일 연관관계 매핑에 대한 글을 적고 있다.

그래서 코드를 작성해서 테스트하는데. 
내가 원하는 테스트 결과가 안 나와서 헤매다가 해법을 찾았다.

해법을 찾으면서 몰랐던 지식을 습득하기도 했다

그런데 돌이켜보니 2년전에 이미 문제를 겪어서 거기에 대한 조치를 취한 적도 있었다는 것을 알았다.

그래서 기록으로 남기려고 한다.

 

 

1. 문제 상황

아래는 이번에 문제가 생겼던 코드이다. 

일단 엔티티 Post와 PostDetails의 관계는 일대일 연관관계로 Post가 연관관계의 주인으로 외래키를 가지고 있는 구조이다.

@Entity
class Post(
        @Id @GeneratedValue
        val id: Long? = null,

        var title: String? = null,
        @Column(name = "created_at")
        val createdAt: LocalDateTime? = LocalDateTime.now(),

        @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], optional = false)
        var details: PostDetails? = null
)

@Entity
class PostDetails(
        @Id @GeneratedValue val id: Long? = null,
        @Column(nullable = false) var content: String = ""
)
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@DataJpaTest
class PostRepositoryTest(val postRepository: PostRepository) {

    @BeforeEach
    fun setUp(): Unit {

        val post = Post().apply {
            title = "JPA에서 일대일 연관 관계 정의시 고려할 점1"
            details = PostDetails(content = "안녕하세요. 여러분 게시물 내용1")
        }

        val post2 = Post().apply {
            title = "JPA에서 일대일 연관 관계 정의시 고려할 점2"
            details = PostDetails(content = "안녕하세요. 여러분 게시물 내용2")
        }

        postRepository.saveAll(listOf(post, post2))
    }

    @Test
    fun `일대일 연관관계에서 부모 엔티티가 연관 관계의 주인이고 외래키 null 허용 안하면 지연 로딩 동작함`() {
        val list = postRepository.findAll()

        list.forEach {
            Assertions.assertTrue(it.details is HibernateProxy)
        }
    }

    @Test
    internal fun testFindById() {
        val findById = postRepository.findById(1L)

        val map = findById.map { it.details }

        Assertions.assertTrue(map.get() is HibernateProxy)
        Assertions.assertEquals(map.get().content, "안녕하세요. 여러분 게시물 내용1")
    }

    @AfterEach
    fun deleteAll(): Unit {
        postRepository.deleteAll()
    }
}

 

테스트 코드에서는 Post를 불러오고 Post에서 PostDetails를 가져오면서 HibernateProxy 타입인지 검사하고 있다.

 

문제가 발생한 상황은 두가지이다.

 

1. 위 테스트 코드는 통과해야하는데. ( 일대일 단방향 연관관계이면 지연 로딩을 지원해야하는데.)

실패해서 혼란스러웠다.

 

2. @DataJpaTest를 @SpringBootTest로 변경해봤는데. 

지연 로딩은 동작하지만 이번에는 testFindById에서 LazyInitializationException이 발생했다.

 

 

2. 문제 원인

위 문제 상황의 1,2 번과 연결된 문제가 발생한 원인들이다. 

 

1. @DataJpaTest에서는 @Transactional을 포함하고 있다. @BeforeEach가 붙은 setUp() 메소드에서 Post 엔티티 두개를 영속화하고 있는데. 각 테스트 메소드가 실행되기 이전에 실행된다.

이 때, setUP()의 과정과 테스트 코드가 같은 트랜잭션(Transaction)으로 묶인 것이다.

영속화와 조회 동작이 같은 영속성 컨텍스트를 공유했다.

영속화 과정에서 Post와 PostDetails가 영속성 컨텍스트에 저장되고 조회할 때 영속성 컨텍스트에 이미 Post 엔티티가 저장되어 있어서 바로 영속성 컨텍스트에서 가져오는 것으로 보인다.

지연로딩이 필요없는 상황에서 지연로딩을 테스트하니 실패한 것이다.

 

2. @SpringBootTest는 @Transactional을 포함하지 않는다. 그러므로 setUp()과 각 테스트 메소드는 영속성 컨텍스트를 공유하지 않는다.

Post를 조회할 때 PostDetails를 프록시 객체로 가져온다. 그러나 JPA에서 지연로딩은 같은 Transaction 범위 내에서만 가능하다.

자세한 내용은 Hibernate Detached 엔티티 프록시의 초기화에 자세히 나와있다.

 

3. 문제 해결 방법

@BeforeEach로 엔티티를 저장하지 않고 직접 sql로 데이터를 저장한다.

필자는 application.properties 파일에 'spring.datasource.data' 설정을 해서 sql 파일을 실행할 것이다.

이렇게하면 테이블에 테스트를 위한 데이터를 넣을 떄 영속화되지 않으니 테스트 코드가 통과한다.

 

해결한 코드는 https://github.com/anomie7/jpa-oneToOne-example/tree/case1 에서 확인할 수 있다.

 

반응형