[Spring with Docs] Spring 통합 테스트 시 데이터베이스 충돌이 발생했던 이유를 공식 문서를 통해 알아보자

Jihoon Oh·2022년 4월 26일
1

Spring with Docs

목록 보기
2/2
post-thumbnail

우아한테크코스 레벨 2 스프링 체스 미션을 진행하는 도중, 각각의 테스트에서는 문제가 없었으나 통합 테스트를 진행하는 과정에서 문제가 생겼다. 컨트롤러, 서비스, DAO에 전부 DB를 달아놓고 테스트하고 있었는데, 서비스 테스트에 @Transactional이나@AfterEach로 삽입한 데이터를 지워주는 과정을 빼먹었더니 서비스 이외의 다른 테스트에서 데이터를 삽입하는 과정에서 Primary Key Violation이 발생했다. 그런데 이전 문서에서 알아봤듯이 테스트 실행 시 마다 schema.sql을 실행하고, schema.sql 파일 내에 DROP TABLE 명령이 들어가 있어서 매 번 테이블이 삭제되니 데이터도 날아가서 문제가 발생하지 않나 하는 의문이 들었다. 문제 해결 자체는 @Transactional 어노테이션을 붙여주는 것 만으로 해결하긴 했지만, 명쾌한 이유를 알고 싶었고, 우테코 코치 제이슨의 도움을 받아 그 이유를 알 수 있었다. (JSON! JSON! JSON! JSON!)

명확한 레퍼런스를 확보하기 위해, 공식 문서를 읽어보면서 다시 정리해보도록 하자.

알아보기에 앞서: ApplicationContext

이유를 알아보기에 앞서 먼저 스프링의 ApplicationContext를 알고 넘어가야 명확한 이해를 할 수 있기 때문에 ApplicationContext에 대해 먼저 공부해보았다. 공식문서에는 ApplicationContext에 대해 이렇게 설명되어 있다.

Central interface to provide configuration for an application. This is read-only while the application is running, but may be reloaded if the implementation supports this.
An ApplicationContext provides:

  • Bean factory methods for accessing application components. Inherited from ListableBeanFactory.
  • The ability to load file resources in a generic fashion. Inherited from the ResourceLoader interface.
  • The ability to publish events to registered listeners. Inherited from the ApplicationEventPublisher interface.
  • The ability to resolve messages, supporting internationalization. Inherited from the MessageSource interface.
  • Inheritance from a parent context. Definitions in a descendant context will always take priority. This means, for example, that a single parent context can be used by an entire web application, while each servlet has its own child context that is independent of that of any other servlet.

In addition to standard BeanFactory lifecycle capabilities, ApplicationContext implementations detect and invoke ApplicationContextAware beans as well as ResourceLoaderAware, ApplicationEventPublisherAware and MessageSourceAware beans.

(ApplicationContext는) 응용 프로그램의 설정을 제공하는 중앙 인터페이스다. 프로그램이 실행되는 중에는 read-only지만, 다시 로딩되도록 구현할 수도 있다. ApplicationContext는 다음의 기능을 제공한다.

  • 프로그램의 구성 요소에 접근할 수 있는 Bean factory 메서드들. ListableBeanFactory로부터 상속.
  • 일반적인 방식으로 파일 리소스를 불러오는 기능. ResourceLoader로부터 상속.
  • 등록된 listener들에 이벤트를 게시하는 기능. ApplicationEventPublisher로부터 상속.
  • 국제화를 지원하는 메시지 resolve 기능. MessageSource로부터 상속.
  • 상위 컨텍스트로부터 상속받고, 하위 컨텍스트에 정의된 내용이 항상 우선권을 가짐. 예를 들어 단일 부모 컨텍스트는 전체 웹 어플리케이션에서 사용될 수 있지만, 각 서블릿은 다른 서블릿과는 독립적인 본인만의 자식 컨텍스트를 가짐.

표준 BeanFactory 라이프사이클 기능 외에도 ApplicationContext 구현은 ResourceLoaderAware, ApplicationContextAware, ApplicationEventPublisherAware, MessageSourceAware 빈들을 탐지하고 호출한다.

The org.springframework.context.ApplicationContext interface represents the Spring IoC container and is responsible for instantiating, configuring, and assembling the beans. The container gets its instructions on what objects to instantiate, configure, and assemble by reading configuration metadata. The configuration metadata is represented in XML, Java annotations, or Java code. It lets you express the objects that compose your application and the rich interdependencies between those objects.

org.spring framework.context.ApplicationContext 인터페이스는 SpringIoC 컨테이너를 나타내며 빈의 인스턴스화, 구성 및 조립을 담당한다. 컨테이너는 설정 메타데이터를 읽음으로써 인스턴스화, 구성 및 모을 객체 대한 지침을 가져온다. 메타데이터는 XML, Java 주석 또는 Java 코드로 표시된다. 응용 프로그램을 구성하는 객체와 이러한 객체 간의 풍부한 상호 의존성을 표현할 수 있다.

ApplicationContext스프링 컨테이너라고 하는데, BeanFactory를 포함하고 있기 때문에 스프링 빈을 조회하고 관리하는 역할을 할 수 있다. BeanFactory는 가장 최상위의 스프링 컨테이너이며 ApplicationContextBeanFactory에 여러 부가 기능들을 추가해놓은 컨테이너다. ApplicationContext역시 구현체이기 때문에 여러 형태로 구현해서 사용할 수 있는데, 구현체에 따라 XML, 자바 코드 등으로 스프링 컨테이너를 만들 수 있다. 아래는 스프링 공식 문서에서 제공하는 스프링 컨테이너의 시스템 구성 과정 모식도다.

ApplicationContext의 구성 등에 대해서 다루기에는 다른 길로 새는 것 같으니 ApplicationContext에 대해서는 "스프링 구성 요소를 설정하는 컨테이너" 정도로 정리하고 넘어가도록 하자.

통합 테스트 시 ApplicationContext를 매 번 읽어오는가?

정답은 No다. 한 번의 테스트에 여러 스프링 테스트를 실행하면, ApplicationContext는 캐싱된다.

The Spring TestContext Framework provides consistent loading of Spring ApplicationContext instances and WebApplicationContext instances as well as caching of those contexts. Support for the caching of loaded contexts is important, because startup time can become an issue — not because of the overhead of Spring itself, but because the objects instantiated by the Spring container take time to instantiate. For example, a project with 50 to 100 Hibernate mapping files might take 10 to 20 seconds to load the mapping files, and incurring that cost before running every test in every test fixture leads to slower overall test runs that reduce developer productivity.

Test classes typically declare either an array of resource locations for XML or Groovy configuration metadata — often in the classpath — or an array of component classes that is used to configure the application. These locations or classes are the same as or similar to those specified in web.xml or other configuration files for production deployments.

By default, once loaded, the configured ApplicationContext is reused for each test. Thus, the setup cost is incurred only once per test suite, and subsequent test execution is much faster. In this context, the term “test suite” means all tests run in the same JVM — for example, all tests run from an Ant, Maven, or Gradle build for a given project or module. In the unlikely case that a test corrupts the application context and requires reloading (for example, by modifying a bean definition or the state of an application object) the TestContext framework can be configured to reload the configuration and rebuild the application context before executing the next test.

Spring Test Context Framework는 Spring ApplicationContext 인스턴스와 WebApplicationContext 인스턴스의 일관된 로딩과 이러한 컨텍스트의 캐싱을 제공한다. 시작 시간이 문제가 될 수 있기 때문에 로드된 컨텍스트의 캐싱 지원은 중요하다. 시작 시간은 Spring 자체의 오버헤드 때문이 아니라 Spring 컨테이너에 의해 인스턴스화된 개체가 인스턴스화하는 데 시간이 걸리기 때문이다. 예를 들어, 50~100개의 최대 절전 모드 매핑 파일이 있는 프로젝트에서 매핑 파일을 로드하는 데 10~20초가 걸릴 수 있으며, 모든 테스트 픽스쳐에서 모든 테스트를 실행하기 전에 이러한 비용이 발생하여 전체 테스트 실행 속도가 느려져 개발자 생산성이 저하된다.

테스트 클래스는 일반적으로 XML 또는 Groovy 구성 메타데이터에 대한 리소스 위치 배열(일반적으로 클래스 경로에 있음) 또는 응용 프로그램을 구성하는 데 사용되는 구성 요소 클래스 배열 중 하나를 선언한다. 이러한 위치 또는 클래스는 프로덕션 배포에 대한 web.xml 또는 기타 구성 파일에 지정된 위치 또는 클래스와 유사하다.

기본적으로 로드되면 구성된 ApplicationContext가 각 테스트에 재사용된다. 따라서 설정 비용은 "test suite" 당 한 번만 발생하며 후속 테스트 실행이 훨씬 더 빠르다. 여기서 "test suit"이란 모든 테스트를 동일한 JVM에서 실행하는 것을 - 예를 들면, 모든 테스트는 특정 프로젝트나 모듈에 대해 Ant, Maven 또는 Gradle 빌드에서 된다 - 의미한다. 테스트에서 응용 프로그램 컨텍스트가 손상되어 새로고침이 필요한 경우(예: 빈 정의 또는 응용 프로그램 개체의 상태를 수정함) 다음 테스트를 실행하기 전에 구성을 다시 로드하고 응용 프로그램 컨텍스트를 재구성하도록 TestContext 프레임워크를 구성할 수 있다.

즉, 매 테스트마다 Spring을 띄우고 빈을 새로 만들고 하는 것은 굉장한 시간적, 자원적 낭비이기 때문에, 최초 실행 시에 ApplicationContext를 구성하고 이후의 테스트는 기존에 로드된 ApplicationContext를 재사용 한다. 컨텍스트가 뜰 때 schema.sql이 실행되는 것은 맞으나, 컨텍스트가 교체되지 않기 때문에 기존의 데이터베이스를 다음 테스트에서도 재사용하여 문제가 발생했던 것이다. 실제로 테스트를 돌리고 로그를 확인해보면,

ChessControllerTest 시에는 스프링 로고가 뜨지만,

GameDaoTest 시에는 스프링 로고가 뜨지 않은 것을 볼 수 있다. (컨텍스트가 로드되어야 터미널에 스프링 로고가 뜬다.)

참고자료

Spring.io - Core Technologies
Spring.io - Testing
킹갓제너럴엠퍼러 제이슨

profile
Backend Developeer

1개의 댓글

comment-user-thumbnail
2022년 4월 26일

👍

답글 달기