IT/디버깅

[디버깅] test 코드에서 em.remove() delete 쿼리 안나가는 이슈 해결

happy_life 2023. 3. 30. 18:34

JPA로 em.remove()를 하였지만 delete 쿼리가 나가지 않는 에러가 발생하여 이를 해결하는 과정을 포스팅하려고 합니다.

문제상황

 

 

 

1. delete 쿼리 안나가는 이슈 상황

1. test 코드입니다. 코드는 간단하게 아래와 같습니다.

package dev.devpool.service;

import dev.devpool.domain.Child;
import dev.devpool.domain.Member;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

@SpringBootTest
@Transactional
public class MemberServiceTest {
    @Autowired
    MemberService memberService;

    @Autowired
    EntityManager em;

    @Test
    public void ss() {

        Member member = new Member();
        member.setName("kim");
        member.setEmail("rere25@naver.com");
        member.setPassword("dasda");
        member.setNickName("귀요미");

        Child child = new Child();
        child.setMember(member);

        member.getChildren().add(child);

        memberService.join(member);


        System.out.println("================");
        memberService.delete(member.getId());
        System.out.println("================");

    }
}

 

2. Transactional 어노테이션이 @Service 계층, Test 코드 두 개 모두에 있습니다.

 

물론 em.flush()를 통하면 delete 쿼리는 나갑니다. 하지만 @Transactional 어노테이션이 있는 경우 하나의 Transaction안에서 commit() 등을 모두 관리하기 때문에 delete flush()가 없더라도 delete 쿼리가 나가야된다고 생각했습니다. 

 

 

 

2. delete 쿼리 안나가는 이슈 해결

결론적으로 Test 코드에서 @Transactional을 사용하면 큰 문제가 발생할 수 있습니다. 따라서 @AfterEach 메서드 등을 사용해 delete을 따로 해주는 것이 좋습니다. 사용하지 말아야하는 것은 아니지만 특히 Service 계층을 Test할 때는 사용 시 주의할 점들이 많습니다.

 

** 추가 @Transactional을 사용하지 않는다면 Test 코드에서 EntityManager를 사용하지 못해 flush() 등을 사용할 수 없습니다. 그렇다면 어떤 방법으로 쿼리를 확인하고 Test를 할 수 있을까요?

스프링이 제공하는 TransactionTemplate를 사용하면 됩니다.

 

수정한 코드는 아래와 같습니다.

@SpringBootTest
public class MemberServiceTest {
   @Test
    public void ss() {
        transactionTemplate.execute(status -> {
            Member member = new Member();
            member.setName("kim");
            member.setEmail("rere25@naver.com");
            member.setPassword("dasda");
            member.setNickName("귀요미");

            Child child = new Child();
            child.setMember(member);

            member.getChildren().add(child);

            memberService.join(member);

            em.flush();
            em.clear();


            System.out.println("================");
            memberService.delete(member.getId());
            System.out.println("================");

            return null;
        });
    }
    }

문제해결

 

 

 

3. delete 쿼리 안나가는 이슈 원인

Test 코드에서 데이터를 롤백시키기 위해 @Transactional을 붙이는 경우 몇가지 유의사항이 있습니다. 

 

1. 메서드 단위로 트랜잭션이 적용되고 Commit()이 테스트 메서드 종료 시점에 수행되기 때문에 발생하는 이슈

Test 코드에서 @Transactional을 붙이면 메서드 단위로 트랜잭션이 적용됩니다.  테스트 코드가 있는 클래스에 @Transactional이 붙어있다면 마지막의 MemberService의 delete()에서 만들어져 쓰기지연 SQL 저장소에 적재된 delete 쿼리는 테스트에서의 @Transactional 때문에 날아가지 않습니다. @SpringBootTest를 붙인 통합 테스트에서 @Transactional을 붙이면 테스트를 종료하면서 테스트에서 했던 사항을 다 롤백합니다. 이 기능 + JPA 쓰기 지연 SQL 저장소가 더해져서 위와 같이 delete 쿼리가 나가지 않는 등의 이슈가 발생합니다.

 

 

2. LazyInitailizationException 이슈

서비스 계층에서 실수로 @Transactional을 붙여주지 않으면 Test코드에서는 @Transactional이 있어 영속성 컨텍스트를 통해 LazyLoading이 동작하지만 실제 운영시에 Lay Loading이 불가능해 LazyInitailizationException 에러가 발생할 수 있습니다. 물론 @DataJpaTest에는 @Transactional 어노테이션이 붙어있어서 의문이 들 수 있습니다. 하지만 이는 서비스 계층을 위한 테스트 어노테이션이 아닌 단순히 JPA 구성요소를 위한 Test임을 명심해야 합니다. 

 

Spring @DataJpaTest docs 일부

Annotation for a JPA test that focuses only on JPA components.