Trouble Shooting

테스트 코드에서 @Transactional이 적용되지 않는 문제

olsohee 2024. 5. 14. 16:15

상황 설명

ScheduleService에 대한 테스트 코드를 작성하던 중에 다음 에러가 발생했다. 

  • Exception in thread "pool-5-thread-6" org.springframework.dao.InvalidDataAccessApiUsageException: Query requires transaction be in progress, but no transaction is known to be in progress
  • Caused by: jakarta.persistence.TransactionRequiredException: Query requires transaction be in progress, but no transaction is known to be in progress

찾아보니 트랜잭션이 적용되지 않아서 발생하는 문제라고 한다. 그런데 나는 다음과 같이 서비스 계층에 @Transactional을 적용했다. 

@Service
@Transactional
@RequiredArgsConstructor
public class ScheduleService {
	...
}

 

따라서 에러 발생 원인을 아무리 생각해도 알 수 없었다. 그런데 문득 테스트 코드에서 사용하는 ScheduleService 객체가 문제일 거 같아서 테스트 코드를 다시 한 번 살펴보니 그 원인을 알 수 있었다. 

문제 원인

결론적으로 테스트 코드의 ScheduleService 객체가 원인이었다. ScheduleService는 UserService를 참조한다. 그리고 UserService는 Redis를 사용한다. 따라서 ScheduleService에 대한 테스트가 Redis에 의존하지 않기 위해 UserService를 상속받아 새롭게 정의된, Redis를 사용하지 않는 UserServiceStub을 ScheduleService에 주입해주었다.

@SpringBootTest
@Transactional
class ScheduleServiceTest {

    @Autowired private ScheduleRepository scheduleRepository;
    @Autowired private RelationshipRepository relationshipRepository;
    private ScheduleService scheduleService;
    private UserServiceStub userService = new UserServiceStub();

    @BeforeEach
    public void setup() {
        // 각 테스트가 실행될 때마다 ScheduleService 객체를 새로 생성 (UserServiceStub을 주입)
        scheduleService = new ScheduleService(scheduleRepository, relationshipRepository, userService);
    }

    private class UserServiceStub extends UserService {

        Map<String, User> userStore = new HashMap<>();

        public UserServiceStub() {
            super(null, null);
            userStore.put("member@naver.com", new User(1, "member@naver.com", "member", Role.MEMBER, "01011112222", null));
            userStore.put("trainer@naver.com", new User(2, "trainer@naver.com", "trainer", Role.TRAINER, "01011112222", "userCode"));
        }

        @Override
        public User getUser(String email) {
            return userStore.get(email);
        }
    }
    
    ...
}

 

그런데 이 부분이 문제였다. 위 코드를 보면 테스트 코드에서 사용하는 ScheduleService 객체는 스프링 빈으로 등록된 객체가 아니다. 그저 매번 new로 새로 생성해주는 객체이다. 따라서 이 객체에 대해서는 트랜잭션이 적용되지 않는다. @Transactional은 스프링 빈으로 등록되는 객체에 대해, 그 객체를 상속받으며 트랜잭션을 담당하는 CGLIB 객체를 생성하여 스프링 빈으로 등록한다(참고: https://olsohee.tistory.com/83). 따라서 위 코드에서 매번 새로 생성한 ScheduleService 객체에 트랜잭션이 적용되지 않았던 것이다.

해결

ScheduleService에 운영 환경과는 다른 테스트용 의존성을 주입해야 한다. 따라서 UserServiceStub을 주입받는 ScheduleService를 매번 새로 생성하던 기존의 방법에서 @TestConfiguration을 사용하는 방법으로 변경했다. @TestConfiguration을 통해 실제 애플리케이션 컨텍스트가 아닌 테스트용 configuration을 사용할 수 있다.

@SpringBootTest
@Transactional
class ScheduleServiceTest {

    @Autowired private ScheduleRepository scheduleRepository;
    @Autowired private RelationshipRepository relationshipRepository;
    @Autowired private ScheduleService scheduleService;

    @TestConfiguration
    static class TestConfig {

        @Autowired private ScheduleRepository scheduleRepository;
        @Autowired private RelationshipRepository relationshipRepository;

        @Bean
        public ScheduleService scheduleService() {
            return new ScheduleService(scheduleRepository, relationshipRepository, userServiceStub());
        }

        @Bean
        public UserServiceStub userServiceStub() {
            return new UserServiceStub();
        }
    }

    static class UserServiceStub extends UserService {

        Map<String, User> userStore = new HashMap<>();

        public UserServiceStub() {
            super(null, null);
            userStore.put("member@naver.com", new User(1, "member@naver.com", "member", Role.MEMBER, "01011112222", null));
            userStore.put("trainer@naver.com", new User(2, "trainer@naver.com", "trainer", Role.TRAINER, "01011112222", "userCode"));
        }

        @Override
        public User getUser(String email) {
            return userStore.get(email);
        }
    }
    
    ...
}

Reference