비동기 외부 API 호출 로직에 대한 테스트 코드 작성하기 (Spring)

Kim Dong Kyun·2024년 6월 30일
3

개요

스프링에서 비동기로 작성 된 로직은 다른 동기 로직과 상호작용을 테스트 하기 불편하다.

불편1 - Transaction

@Transactional 어노테이션과 함께 테스트 할 시, 스레드가 달라 트랜잭션 범위가 다르기 때문에, uncommit 된 레코드를 읽을 수 없다.

설명 전, 테스트 클래스에서 @Transactional 어노테이션은

  1. 테스트 매서드는 한 스레드에서 돌게 된다.
  2. 따라서 이 스레드는 하나의 영속성 컨텍스트를 가지고 돌아간다.
  3. 그러므로 @BeforeEach 등의 셋업 작업과, 실제 테스트하는 매서드의 트랜잭션 관련 작업이 모두 끝난 후 이 데이터를 롤백하거나, 설정에 따라 커밋되게 할 수 있다.

그렇다면 비동기 매서드는 어떻게 동작할까? 스레드가 다르다면?

예를 들어

    @BeforeEach
    void setUp() {
        user = new User("test", "test", AllowedUser.JOJO.getName(), AllowedUser.JOJO.getRegNo());
        userJpaRepository.save(yoobi);
    }
...

이런 셋업 매서드가 있을 시, 비동기 스레드는 이 셋업 매서드를 알지 못한다.

위에서 얘기했듯, 테스트 매서드는 하나의 스레드가 영속성 컨텍스트를 공유하고, 이에 따라 모든 매서드가 끝난 후, 즉 모든 작업 후 커밋/롤백 된다고 얘기했다.
( 즉, 셋업 매서드가 작동할 수 있는 것은 영속성 컨텍스트 덕분이다 )

그러나 스레드가 다르고, 다른 영속성 컨텍스트에서 트랜잭션을 관리하려고 한다면

  1. 셋업 매서드 (테스트 스레드1, 영속성 컨텍스트 1)
  2. 비동기 매서드 (테스트 스레드2, 영속성 컨텍스트2)

와 같은 형태로 영속성 컨텍스트의 범위가 나뉘어지게 되고, 아래 단계를 밟게 된다.

  1. 셋업 매서드 flush (커밋이나 롤백되지 않은 상태)
  2. 비동기 매서드 start, 셋업 매서드에 대한 read 시도(DB SELECT)
  3. 셋업 매서드가 실제로 db에 커밋되지 않은 상태이므로, select 가 불가능함 (uncommited read 이므로)
  4. 따라서 이에 대한 별도 설정과 롤백 매서드가 필요함.

불편하다.


불편 2 - 외부 API 호출

만약 외부 api 호출이 오래걸린다고 생각해보자 (타임아웃 : 1분)

따라서 우리는 호출부를 비동기로 작성했고, 이 비동기 매서드는 다른 스레드에서 작성되므로 이에 대한 테스트는

    void 슈도테스트(){
    1. 서비스 매서드 호출
    2. 비동기 매서드 호출
    3. 비동기 매서드 호출 타임아웃을 기다려야함. (~1) -> CountDownLatch 등 활용
		...
	}

이런 식일 것이다.

즉, 호출과 응답이 1분 이전에 왔다고 하더라도 테스트 매서드는 이를 알 수 없으므로, Read timeout 인 1분까지 기다려야 한다는 단점이 있다.

불편하다.


테스트에서는 동기로 돌면 안되나?

@ContextConfiguration 공식문서

@ContextConfiguration defines class-level metadata that is used to determine how to load and configure an ApplicationContext for integration tests. Specifically, @ContextConfiguration declares the application context resource locations or the component classes used to load the context.

공식문서에서는 위와 같이, 통합 테스트 환경에서 어플리케이션 컨텍스트를 어떻게 load 할 것인지에 대한 설정을 @ContextConfiguration 어노테이션을 통해서 할 수 있다고 명시되어 있다.

운영 환경에서 @Asnyc 어노테이션이 이 TaskExecutor 를 통해서 동작하므로, 테스트 환경에서의 컨피규레이션은 동기 처리되는 SyncTaskExecutor 로 바꿔보자.

@ContextConfiguration
public class TextContextConfiguration {

    @Configuration
    static class TestConfig {

        @Bean
        public TaskExecutor taskExecutor() {
            return new SyncTaskExecutor();
        }
    }
}

위 설정으로 인해

  • 트랜잭션 분리, uncommited read 를 신경 쓸 필요가 없다.
  • 비동기 매서드 내에 외부 api 요청을 하고 응답을 받는 부분을 read timeout 시간까지 기다릴 필요가 없다.

추가

@ContextConfiguration, @SpringApplicationConfiguration 등은 모두 어플리케이션 컨텍스트를 등록하는 역할을 하며, 우리가 잘 알고 있듯 @SpringBootTest 라는 어노테이션으로 대체되어(간단히 설정 가능하도록 변경되어) 있다. 따라서 테스트 클래스에 @TestConfiguration 을 추가하는 방식으로 설정이 가능하다.

@SpringBootTest
public class SimpleAsyncTest {

    @TestConfiguration
    static class TestConfig {

        @Bean
        public TaskExecutor taskExecutor() {
            return new SyncTaskExecutor();
        }
    }
    
    // 실제 테스트들 ...
}

혹은 이걸 커스텀 어노테이션으로 빼도 괜찮을 것 같다. 아래와 같이.

@TestConfiguration
public class TestAsyncConfig {
    @Bean
    public TaskExecutor taskExecutor(){
        return new SyncTaskExecutor();
    }

}

// ...

@SpringBootTest
@Import({TestAsyncConfig.class})
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableAsyncTest {
}

// ...

@EnableAsyncTest
public class SimpleAsyncTest {
    // 실제 테스트들 ...
}

0개의 댓글