본문 바로가기
테스트

스프링 컨텍스트 캐싱을 위한 테스트 환경 구축

by 코딩공장공장장 2025. 2. 20.

스프링 테스트의 경우 컨텍스트 로딩으로 많은 시간을 잡아 먹는다.

잦은 컨텍스트 로딩은 테스트 코드 실행 시간을 많이 잡아먹는 주범이기도 한다.

스프링은 각 테스트 케이스에서 사용하는 컨텍스트 설정이 같다면 이전에 로드했던 컨텍스트를 재사용하는 캐싱 기능이 존재한다.

별도의 옵션 설정이 필요한 것은 아니고 컨텍스트 설정만 같다면 자동으로 재사용한다.

 

[컨텍스트가 리로딩 되는 케이스]

@WebMvcTest( value = [ MemberFindController::class ])
class MemberFindControllerTest : BaseMockMvcTest() {
    @MockBean
    lateinit var memberFindReadCase: MemberFindReadCase


    companion object {
        const val PREFIX = "/public/member"
        const val FIND_EMAIL_URL = "$PREFIX/findEmail"
        const val FIND_PASSWD_URL = "$PREFIX/findPassword"
    }
    ...
}

 

@WebMvcTest( value = [ MemberPrivateWriteController::class ])
class MemberPrivateWriteControllerTest : BaseMockMvcTest() {

    companion object {
        const val UPDATE_PHONE_URL = "/member/phone"
    }
    ...
 }

 

위의 두 컨트롤러 테스트 코드를 보면 WebMvcTest에 value로 입력되는 컨트롤러가 다르고,

MockBean 설정 또한 다르다.

두 테스트에서 사용하는 빈 설정이 다르기에 컨텍스트 로딩이 별도로 발생된다.

컨텍스트 캐싱을 통해 1번의 로딩으로 컨텍스트를 공유하기 위해서 사소한 목 설정 하나 조차 모두 동일하게 맞춰야 한다.

 

[WebMvcTest 환경 구성] - Controller 모듈


1. 의존객체 모킹 설정

 

[ 3rd-party-lib 빈 모킹]

class SecurityWebMockBeanConfig {

    @Bean
    fun securityConfig(): SecurityConfig = SecurityConfig.newInstance()

    @Bean
    fun securityFilterChain(): SecurityFilterChain = object : SecurityFilterChain {
        override fun matches(request: HttpServletRequest?): Boolean {
            return true
        }

        override fun getFilters(): MutableList<Filter> {
            return mutableListOf()
        }
    }

    @Bean
    fun handlerMethodArgumentResolver(): HandlerMethodArgumentResolver {
        return MockUserDetailArgumentResolver()
    }

    @Bean
    fun webConfig(): WebConfig {
        return WebConfig(handlerMethodArgumentResolver())
    }
}

 

[서비스 레이어 빈 모킹]

class MathDocsMockBeanConfig {
    @Bean
    fun mathDocsPaperReadCase(): MathDocsPaperReadCase = MockMathDocsPaperReadCase()

    @Bean
    fun mathDocsPaperWriteCase(): MathDocsPaperWriteCase = MockMathDocsPaperWriteCase()

    @Bean
    fun mathDocsReadCase(): MathDocsReadCase = MockMathDocsReadCase()

    @Bean
    fun mathDocsUsageWriteCase(): MathDocsUsageWriteCase = MockMathDocsUsageWriteCase()
}

 

[모킹 빈 설정 통합]

@Import(
    SecurityWebMockBeanConfig::class,
    MathDocsMockBeanConfig::class,
    . . .
)
@TestConfiguration
class RestApiWebMvcMockBeanConfig

 

컨트롤러는 서비스 레이어의 객체들을 의존하고 있다.

스프링 시큐리티 모듈과 같은 3rd-party 의존성 또한 존재한다.

 

컨트롤러 레이어에서는 컨트롤러만 테스트할 것이므로 서비스 객체와 3rd-party-lib 객체를 모두 모킹하고, 이를 하나의 TestConfiguration으로 통합하면 된다.

 

 

2. 테스트 config 어노테이션 작성

@ContextConfiguration(classes = [RestApiWebMvcMockBeanConfig::class])
@WebMvcTest(
    value = [
        LoginFailureController::class,
        MemberReadController::class,
        MemberWriteController::class,
        ...
    ]
)
@ActiveProfiles("rest-api")
annotation class WebMvcUnitTest

 

이제 컨트롤러 테스트 환경 구성 어노테이션을 작성하면 된다.

 

WebMvcTest의 value에 테스트를 진행할 컨트롤러를 넣어주자.

해당 value에 등록이 되어야 mockMvc 테스트를 진행할 수 있다.

또한 value에 등록되는 컨트롤러의 의존객체를 모킹한 빈 설정이 정상적으로 되어있어야 컨텍스트 구동이 할 수 있다.

 

우리는 이전에 의존객체를 모킹한 빈 설정을 만들어 두었으니 해당 설정을 ContextConfiguration에 넣어주자.

컨텍스트가 로딩되면 실제 프로덕션의 빈이 아닌 모킹 빈들이 스프링 빈으로 등록될 것이다.

 

자 이제, 아래와 같이 우리가 만든 어노테이션을 여러 테스트 코드에 붙여 테스트 실행을 확인해보자.

컨텍스트 캐싱을 통해 구동이 1번만 일어남을 로그를 통해 확인할 수 있을 것이다.

@WebMvcUnitTest
class MemberFindControllerTest : BaseMockMvcTest() {
    companion object {
        const val PREFIX = "/public/member"
        const val FIND_EMAIL_URL = "$PREFIX/findEmail"
        const val FIND_PASSWD_URL = "$PREFIX/findPassword"
    }

    @Test
    fun `이메일 찾기 - 성공`() {
        // given
        val reqBody = mapOf(
            "userName" to "이름",
            "phoneNumber" to "01012341234"
        )

        //when
        val resultAction = getRequest(FIND_EMAIL_URL, reqBody)

        // then
        assert2xx(resultAction)
    }

 

 

[DataJpaTest 환경 구성] - Repository 모듈


1. TestContainer 설정
2. DataSource 설정
3. 의존객체 모킹
4. 테스트 config 어노테이션 설정

 

1. TestContainer 설정

TestContainer 또한 전역적으로 공유하고자 static 블록에 컨테이너 설정을 하였다.

class MysqlTCExtension : Extension {
    companion object {
        var mysqlContainer: MySQLContainer<Nothing> =
            MySQLContainer<Nothing>("mysql:8.3.0")
                .apply {
                    this.withDatabaseName("numberbox_tc")
                    this.withUsername("root")
                    this.withPassword("1111")
                    this.withUrlParam("characterEncoding", "UTF-8")
                }

        init {
            mysqlContainer.start()
        }
    }
}

 

2. DataSource 설정

TestContainer에서 설정한 DB로 커넥션이 연결되도록 설정하였다.

@TestConfiguration
class TCDataSourceConfig {
    @Bean
    fun dataSource(): HikariDataSource {
        return DataSourceBuilder.create()
            .type(HikariDataSource::class.java)
            .url(MysqlTCExtension.mysqlContainer.jdbcUrl)
            .username(MysqlTCExtension.mysqlContainer.username)
            .password(MysqlTCExtension.mysqlContainer.password)
            .build()
    }

}

 

3. 외부 의존성 모킹

리포지토리는 mvc 레이어의 마지막 레이어에 위치하여 의존객체가 없는 경우가 많아 모킹 처리할 대상이 많지 않다.

필요한 대상만 이전에 컨트롤러에서 모킹 처리했던 것과 같이 진행하면 된다.

@TestConfiguration
class MockOrmBeanConfig {
    @Bean
    fun ipAddressService(): IPAddressService = object : IPAddressService {
        override fun getIPAddress(): String {
            return "127.0.0.1"
        }

        override fun getPublicIPAddress(): String {
            return "127.0.0.1"
        }
    }
}

 

4. 테스트 config 설정

@DataJpaTest
@ExtendWith(value = [MysqlTCExtension::class])
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = [QueryDslConfiguration::class, MockOrmBeanConfig::class, TCDataSourceConfig::class])
@ActiveProfiles("orm-jpa-adapter-tc-test")
annotation class TcDBJpaTest

 

이전 1~3번에 설정했던 config를 한군데에 모아놓았다.

DB 연동 테스트이므로 DB설정과 커넥션 설정이 추가됬을 뿐, 의존객체를 모킹하고 테스트 config를 설정하는 절차는 컨트롤러 모듈 설정과 크게 다르지 않다.

 

마찬가지로 사용하는 곳에서 테스트용 Config 어노테이션만 붙여준다면 해당 어노테이션을 사용하는 모든 곳에서 컨텍스트 캐싱을 통해 공유할 수 있다.

@TcDBJpaTest
class MathContentsGrammarWriteRepositoryTest(
    @Autowired
    private val em: EntityManager,
    @Autowired
    private val mathContentsGrammarWriteRepository: MathContentsGrammarWriteRepository
) {

    @Test
    fun `수학문제 문법 저장`() {
        // given
        val contentsId = 4907L
        val grammar = ""

        // when
        mathContentsGrammarWriteRepository.createGrammar(contentsId, grammar)
        em.flush()
        em.clear()
    }

 

 

정리


WebMvcTest와 DataJpaTest를 사용하는 테스트에서는 환경 구성을 하는 내용을 소개하였지만,

SpringBootTest는 소개하지 않았다.

소개할 수 없었던 이유는 SpringBootTest는 작성된 케이스가 하나도 존재하지 않았다.

웹서버와 DB와 같은 외부 소프트웨어 연동이 필요로 한 경우에는 스프링 프레임워크를 사용하였지만

그 외의 테스트에서는 스프링 프레임워크 없이 순수 프로그래밍 언어로 작성하였다.

 

컨텍스트 캐싱은 매우 유용한 기능이다.

컨텍스트 캐싱을 적극적으로 이용하는 것도 중요하지만, 이것보다 우선 고려해야하는 것은 컨텍스트를 로딩하지 않는 것이라고 생각한다. 전체 프로젝트에 스프링 컨텍스트를 로딩하는 테스트가 몇개 존재하였지만 이것들도 모두 제거를 하고 결국에는 WebMvcTest와 DataJpaTest만 남게 되었다.

반응형