본문 바로가기

🌱 우아한테크코스 6기

[TroubleShooting] 에러 해결 과정에서 알게 된 @DirtiesContext 동작 방식

나의 상황

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
class ThemeDaoTest {

    private final JdbcTemplate jdbcTemplate;
    private final ThemeDao themeDao;
    private final SimpleJdbcInsert themeInsertActor;

    @Autowired
    public ThemeDaoTest(JdbcTemplate jdbcTemplate, ThemeDao themeDao) {
        this.jdbcTemplate = jdbcTemplate;
        this.themeDao = themeDao;
        this.themeInsertActor = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("theme")
                .usingGeneratedKeyColumns("id");
    }

    // ...

}

1. DirtiesContext의 classMode를 BEFORE_EACH_TEST_METHOD로 설정한 경우

  • 테스트 코드 내에서 jdbcTemplate를 사용하려고 하면 아래의 두 예외가 발생했다. 
    • JDBC connection을 가져올 수 없다.
    • HikariPool-1이 닫혀있다. (HikariPool은 JDBC connection pool의 한 종류이다.)

 

2. DirtiesContext의 classMode를 AFTER_EACH_TEST_METHOD로 설정한 경우

  • 테스트 코드가 성공적으로 실행되었다.


왜지...?

 

@DirtiesContext

@DirtiesContext는 dirty한 ApplicationContext를 초기화하는 어노테이션이다.
클래스와 메서드 레벨에 붙을 수 있으며 언제 context를 재생성할지 지정할 수 있다.
 
Context 재생성 시기를 지정하는 여러 옵션 중 BEFORE_EACH_TEST_METHOD와 AFTER_EACH_TEST_METHOD의 경우 테스트가 어떤 순서로 동작되는지 확인해보았다.

 

1. BEFORE_EACH_TEST_METHOD

테스트 코드를 실행하기 전 스프링 서버를 한 번 열었다 닫는다.
로그의 14번째 라인까지 서버를 여는 로그이며 마지막 두 라인을 통해 서버를 닫음을 확인할 수 있다.

그 후 테스트 실행을 위해 스프링 서버를 한 번 더 띄운다. 이전(HikariPool-1)과는 다른 풀(HikariPool-2)이 할당됨을 확인할 수 있다.
결과적으로 BEFORE_EACH_TEST_METHOD 모드로 실행되는 로직은 아래와 같다.

[ 서버 ON - 서버 OFF ] - [ 서버 ON - 테스트 - 서버 OFF ] - [ 서버 ON - 테스트 - 서버 OFF ] - …

 

2. AFTER_EACH_TEST_METHOD

AFTER_EACH_TEST_METHOD 모드로 실행되는 로직은 아래와 같다.

[ 서버 ON - 테스트 - 서버 OFF ] - [ 서버 ON - 테스트 - 서버 OFF ] - …

 

HikariDataSource

다음으로는 두번째로 출력 되었던 아래의 에러 메시지를 분석해보자.

 
HikariDataSource 클래스를 살펴보았다.
HikariDataSource는 HikariPool을 가지고 있다.

 
스프링 서버를 올리고 내릴 때 콘솔에 출력된 로그 메시지를 참고하여 코드를 찬찬히 읽어보았다.

HikariDataSource HikariDataSource (HikariPool-1) has been closed. 예외 메시지가 이곳에서 출력됨을 알 수 있다.

스프링을 띄울 때 콘솔에 출력되던 위의 로그 또한 이곳에서 출력됨을 확인할 수 있다. 위 코드를 통해 스프링 서버를 띄우면서 HikariDataSource가 HikariPool을 할당받는다.
 

스프링 서버가 닫힐 때 출력되던 로그는 이곳에서 출력된다. 스프링 서버를 내릴 때 HikariPool을 shut down 함을 알 수 있다.


스프링 서버를 띄울 때 출력된 로그를 다시 확인해보자.
 
테스트 메서드를 실행할 때 띄워진 HikariPool의 이름은 HikariPool-2이었고, 예외의 원인이었던 HikariPool의 이름은 HikariPool-1이었다. HikariPool-1은 닫혀있고 현재 스프링 서버에서 열려있는 풀은 HikariPool-2인데, JdbcTemplate에 할당된 DataSource는 HikariPool-1에 존재한다. 즉 jdbcTemplate이, 닫혀 있는 HikariPool-1에 접근을 시도했으므로 예외가 발생한 것이다.

결론: @DirtiesContext 어노테이션을 통해 매번 스프링 서버가 재실행된다. 스프링 서버가 새로 시작될 땐 새로운 데이터베이스 풀이 할당되며, 스프링 서버를 종료할 때 사용한 풀을 닫는다.

 

번외) 생성자 주입 vs 필드 주입

의존성을 주입하는 방식에 따라서도 테스트 실행 결과가 달라졌다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
class ThemeDaoTest {

    private final JdbcTemplate jdbcTemplate;
    private final ThemeDao themeDao;
    private final SimpleJdbcInsert themeInsertActor;

    @Autowired
    public ThemeDaoTest(JdbcTemplate jdbcTemplate, ThemeDao themeDao) {
        this.jdbcTemplate = jdbcTemplate;
        this.themeDao = themeDao;
        this.themeInsertActor = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("theme")
                .usingGeneratedKeyColumns("id");
    }

    // ...

}

생성자 주입 방식의 경우 단 한 번의 JdbcTemplate만을 할당받을 수 있기 때문에 처음 할당받은 객체를 그대로 사용해야한다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
class ThemeDaoTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private ThemeDao themeDao;
    private SimpleJdbcInsert themeInsertActor;

    @Autowired
    public ThemeDaoTest(JdbcTemplate jdbcTemplate, ThemeDao themeDao) {
        this.jdbcTemplate = jdbcTemplate;
        this.themeDao = themeDao;
        this.themeInsertActor = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("theme")
                .usingGeneratedKeyColumns("id");
    }

    // ...

}

필드 주입 방식의 경우 여러 번의 JdbcTemplate을 할당받을 수 있기 때문에 열려 있는 풀의 데이터 소스를 할당받을 것이다.
 
테스트 클래스가 생성될 때 생성자로 단 한 번의 의존성을 주입하는 것이 아닌, 필드를 통해 한 번 이상의 주입이 가능하면 예외 없이 성공적으로 동작한다. 필드 주입의 경우 매 테스트마다 새로 의존성을 주입받을 수 있기 때문이다.

 

테스트 결과

BEFORE_EACH_TEST_METHOD 실패 | 불변인 JdbcTemplate에 할당된 풀이 닫혀 있다. 성공 | 열려 있는 풀을 JdbcTemplate에 할당한다.
AFTER_EACH_TEST_METHOD 성공 | 불변인 JdbcTemplate에 할당된 풀이 열려 있다. 성공 | 열려 있는 풀을 JdbcTemplate에 할당한다.

 
참고

 

근거는 있지만 저의 뇌피셜이므로 오류 보고 환영합니다!