토비의 스프링 정리 프로젝트 #2.4 스프링 테스트 적용

Jake Seo·2021년 7월 15일
0

토비의 스프링

목록 보기
14/29

@BeforeEach의 문제

public class UserDaoTest {
    UserDao userDao;

    @BeforeEach
    public void setUp() {
        ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
        this.userDao = applicationContext.getBean(UserDao.class);

이전 2.3에서 위와 같이 UserDaoTest를 구성했다. 코드는 매우 깔끔하지만, JUnit의 특성상 매번 새 오브젝트를 만들게 되는데, ApplicationContext도 메소드 개수만큼 만들어진다는 것이 문제이다.

현재 사용하는 수준의 ApplicationContext에는 그다지 많은 빈이 들어가지 않고 복잡한 빈이 들어가지도 않는다. 그래서 모든 싱글톤 빈 오브젝트를 초기화한다고 해도 큰 시간이 걸리지 않는다.

그러나 다음과 같은 빈들을 사용할 때는 문제가 생길 수 있다.

  • 시간을 많이 잡아먹는 자체적인 초기화 작업을 진행하는 빈
  • 독자적으로 많은 리소스를 할당하는 빈
  • 독자적인 스레드를 띄우는 빈

독자적인 리소스나 스레드를 사용하는 빈을 사용하는 경우 문제는 사용한 리소스를 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 애플리케이션 컨텍스트를 만들며 문제가 생길 수 있다는 점이다.

테스트는 일관성있는 실행 결과를 보장해야 하고, 테스트의 실행 순서가 결과에 영향을 미치지 않아야 한다. 다행히도 애플리케이션 컨텍스트는 초기화되고 나면 내부의 상태가 바뀌는 일은 거의 없다. 왜냐하면 빈이 싱글톤으로 만들어졌기 때문에 상태를 갖지 않기 때문이다.

UserDao 빈을 가져다가 add(), get()을 사용한다고 해서 UserDao 빈의 상태가 바뀌진 않는다. DB의 상태는 각 테스트에서 알아서 관리할 것이므로 문제가 되지 않는다.

애플리케이션 컨텍스트는 특별한 경우가 아니면 여러 테스트가 공유해서 사용해도 된다.

@BeforeAll이라는 애노테이션을 이용하여 해당 테스트 클래스가 시작하기 전에 static한 공간에 애플리케이션 컨텍스트를 저장해놓을 수도 있지만, JUnit을 이용하면 더 좋은 방법이 있다.

테스트에서 애플리케이션 컨텍스트 관리

스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

스프링 테스트 컨텍스트 프레임워크 적용

// @SpringBootTest (SpringBoot)
// @Runwith(SpringJUnit4ClassRunner.class) (JUnit4)
@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class UserDaoTest {
    @Autowired ApplicationContext applicationContext;
    UserDao userDao;

    @BeforeEach
    public void setUp() {
        System.out.println("applicationContext = " + applicationContext);
        System.out.println("this = " + this);
        this.userDao = this.applicationContext.getBean("userDao", UserDao.class);

위와 같이 코드를 작성하면, spring-test 의존성이 테스트에서 사용할 ApplicationContext 하나를 만들고, 공유하도록 지정할 수 있다.

  • @ExtendWith는 JUnit5에서 테스트 클래스를 확장할 때 쓰이는 애노테이션이다.
  • @ContextConfigurationlocations라는 엘리먼트를 통해 ApplicationContext에 사용될 xml파일의 위치를 지정해줄 수 있다.
  • @Autowired는 테스트용 ApplicationContext 내부에 있는 정의된 타입의 빈(위 경우 ApplicationContext)을 찾아서 주입해준다.

테스트 메소드의 컨텍스트 공유

setUp() 메소드에 프린트문이 있는데, 출력 결과를 보면 다음과 같다.

applicationContext = org.springframework.context.support.GenericApplicationContext@2a377f99, started on Thu Jul 15 11:53:09 KST 2021
this = toby_spring.UserDaoTest@42a241e
applicationContext = org.springframework.context.support.GenericApplicationContext@2a377f99, started on Thu Jul 15 11:53:09 KST 2021
this = toby_spring.UserDaoTest@233e0f3e
applicationContext = org.springframework.context.support.GenericApplicationContext@2a377f99, started on Thu Jul 15 11:53:09 KST 2021
this = toby_spring.UserDaoTest@6de25b37

결과를 보면 매번 같은 주소에 있는 ApplicationContext 즉, 동일한(identical) ApplicationContext를 출력해주는 것을 볼 수 있다.

그런데 this의 주소는 매번 다르다. JUnit이 매번 오브젝트를 새로 만들고 있지만 ApplicationContext는 처음에 만든 것을 재활용하고 있는 것을 알 수 있다.

이러한 컨텍스트 공유에 의해 테스트 수행 속도가 매우 빨라진다. 처음에만 ApplicationContext를 생성하는데 시간이 걸리고, 나중에는 생성된 것을 이용하기 때문에 매우 빠르다.

테스트 클래스의 컨텍스트 공유

@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")

다른 테스트 클래스라 하더라도, 같은 스프링 설정파일에서 나온 ApplicationContext는 계속 공유될 수 있다.

this = toby_spring.ApplicationContextTest@52950ef1
applicationContext = org.springframework.context.support.GenericApplicationContext@6ae4b7b3, started on Thu Jul 15 11:58:22 KST 2021
applicationContext = org.springframework.context.support.GenericApplicationContext@6ae4b7b3, started on Thu Jul 15 11:58:22 KST 2021
this = toby_spring.UserDaoTest@72f05b15
applicationContext = org.springframework.context.support.GenericApplicationContext@6ae4b7b3, started on Thu Jul 15 11:58:22 KST 2021
this = toby_spring.UserDaoTest@76a4c6c
applicationContext = org.springframework.context.support.GenericApplicationContext@6ae4b7b3, started on Thu Jul 15 11:58:22 KST 2021
this = toby_spring.UserDaoTest@71f71a4d

위는 ApplicationContextTest라는 클래스를 만들어서 독립된 해당 테스트 클래스에서 사용하는 ApplicationContext도 살펴보았는데, 동일한 ApplicationContext를 쓰는 것을 확인했다.

물론 테스트 클래스마다 다른 설정파일을 사용하도록 해도 되고, 몇개의 테스트에서만 다른 설정파일을 사용하는 것도 가능하다. 스프링은 설정파일의 종류만큼 애플리케이션 컨텍스트를 만들고, 같은 설정 파일을 지정한 테스트에서는 이를 공유하도록 한다.

@Autowired

기본적으로 타입이 일치하는 빈을 인스턴스 변수에 주입해주는 역할이다. 별도의 생성자, 수정자 등이 필요 없다. 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있다. 이러한 방법을 타입에 의한 자동 와이어링이라고 한다.

ApplicationContext 타입이 @Autowired 로 불러와졌던 이유는 스프링 ApplicationContext는 초기화할 때 자기 자신도 빈으로 등록하기 때문이다. 따라서 ApplicationContext 타입의 빈이 존재하는 것과 같고, DI도 가능하다.

@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class UserDaoTest {
    @Autowired UserDao userDao;

위와 같이, 그냥 바로 UserDao 타입을 @Autowired 해도 애플리케이션 컨텍스트에서 빈을 잘 가져온다.

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
    </bean>

위와 같이 설정 파일에 등록되어있던 3개의 빈 중 아무거나 원하는 것은 다 @Autowired로 가져올 수 있다.

단, 같은 타입의 빈이 2개이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 테스트 시에 용도에 따라 단순히 인터페이스 말고 더 구체적인 클래스로 @Autowired를 수행해야 할 필요가 있을 때도 있다.

테스트는 필요하다면, 얼마든지 애플리케이션 클래스와 밀접한 관계를 맺고 있어도 상관 없다. 하지만 필요하지 않다면 가능한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해두는 편이 좋다.

DI와 테스트

사실 특정한 구현체 하나만 쓸 때는 인터페이스를 통한 DI를 해야할까 고민하는 경우가 있다. 하지만 그런 경우에도 인터페이스를 통해 DI하는 것이 좋다. 그 이유는 다음과 같다.

  • 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
    • 인터페이스를 사용하고 DI를 적용하는 것은 많은 비용이 들지 않지만, 적용하지 않았을 경우 변경이 필요할 때 수정에 들어갈 시간과 비용의 부담은 크다.
  • 다른 차원의 서비스 기능을 도입하는데 도움이 된다.
    • 이전에 DB 커넥션의 개수를 카운팅하는 부가기능을 만든적이 있다. 이 경우에도 인터페이스를 통한 느슨한 결합 덕에 쉽게 이전의 코드를 고치지 않고도 새로운 기능을 가진 구현 클래스를 만들고 적용할 수 있었다. 참조 링크
    • 스프링은 위와 같은 기법을 일반화해서 AOP라는 기술로 만들어준다.
  • 효율적인 테스트를 만들기 위해 도움이 된다.
    • 테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 코드를 만들어야 한다. 그러기 위해서는 가능한 작은 단위의 대상에 국한해서 테스트해야 한다.

테스트 코드에 의한 DI

DI는 스프링 컨테이너에 의해서만 수행될 수 있는 작업은 아니다. 직접 DI를 사용해도 무관하다. UserDao는 스프링 도입 이전 버전의 DaoFactory에서와 같이 new UserDao(myDataSource)와 같은 방식을 사용해서도 사용할 수 있다.

    @BeforeEach
    public void setUp() {
        DataSource dataSource = new SingleConnectionDataSource(
                "jdbc:postgresql://localhost/toby_spring",
                "postgres",
                "password",
                true
        );
        userDao.setDataSource(dataSource);

위와 같이 DataSource를 내가 사용하고 싶은 DataSource로 바꾸어도 무관하다. 그러나 위와 같은 방식은 ApplicationContext에 올라가 있는 UserDao를 오염시킨다. 다른 클래스에서 테스트할 때도 위와 같이 바뀐 DataSource가 적용된 UserDao를 사용하기 때문에 이 방법은 좋지 않다.

그럼에도 불구하고 ApplicationContext를 오염시켜서 테스트를 하고 싶다면, @DirtiesContext라는 애노테이션을 클래스에 추가해서 진행할 수 있다. 클래스에 @DirtiesContext를 추가하면, 이 애노테이션이 붙은 클래스에서는 ApplicationContext를 공유하지 않는다.

매번 ApplicationContext를 새로 만들어준다. (ApplicationContext를 오염시켜도 영향이 없는 반면 속도는 이전처럼 느려질 것이다.)

테스트를 위한 별도의 DI 설정

보통은 위와 같이 ApplicationContext를 오염시키는 방법보다는 테스트를 위한 설정파일을 하나 더 만드는 방식을 선호한다. test-applicationCotnext.xml이라는 파일을 생성하고, 바라보는 DB만 테스트용으로 바꾸면 아래와 같다.

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/test" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
    </bean>

test-applicationContext.xml파일을 작성하면

@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/test-applicationContext.xml")
public class UserDaoTest {

위와 같이 locations에 파일 위치만 바꿔주면 테스트 환경에 적합한 환경을 가진 설정파일을 이용해서 테스트를 진행할 수 있다. ApplicationContext도 하나만 만들어서 모든 테스트에서 공유할 수 있다. 설정 파일을 하나 더 작성하고 테스트에 맞게 수정해주는 수고만으로 테스트에 적합한 오브젝틔 의존관계를 만들어 사용할 수 있다.

컨테이너 없는 DI 테스트

사실 UserDao는 아예 스프링 컨테이너를 사용하지 않고 테스트를 만들 수 있다. UserDaoDataSource 구현 클래스 어디에도 스프링의 API를 직접 사용하거나 ApplicationContext를 이용하지 않는다.

public class UserDaoTest {
  UserDao dao;
  
  ...
  
  @BeforeEach
  public void setUp() {
    ...
    dao = new UserDao();
    DataSource = new SingleConnectionDataSource(
      "jdbc:postgresql://localhost/test", "postgres", "password", true
    );
    dao.setDataSource(dataSource);
  }
}

위와 같이 작성하면 테스트를 위한 DataSource를 만드는 번거로움은 있지만, ApplicationContext를 아예 사용하지 않으니 코드는 더 단순해지고 이해하기 편해진다. 또 ApplicationContext를 세팅하는데 필요한 시간도 절약할 수 있다.

DI는 객체지향 프로그래밍 스타일이다. 따라서 DI를 위해 컨테이너가 반드시 필요한 것은 아니다. DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.

DI를 어디에 적용할지 고민되면, 효과적인 테스트를 만들기 위해 어떤 필요가 있을지를 생각해보면 도움이 된다. 일반적으로 테스트하기 좋은 코드가 좋은 코드일 가능성이 높다. 테스트하기 나쁜 코드가 나쁜 코드일 가능성이 높다.

침투적 기술과 비침투적 기술

침투적(invasive) 기술은 애플리케이션 코드가 특정 기술 관련 API나 인터페이스, 클래스 등에 종속되는 것을 말한다. 비침투적(non-invasive) 기술은 애플리케이션 로직이 특정 기술 관련 API나 인터페이스, 클래스 등에 종속되지 않는다

비침투적 기술은 기술에 종속되지 않은 순수한 코드를 유지할 수 있게 해준다. 스프링은 이러한 비침투적 기술의 대표적인 예다. 그래서 스프링 컨테이너 없는 DI 테스트도 가능한 것이다.

스프링을 사용하는데 있어서 UserDao 내부에 어떤 스프링 API도 포함될 필요는 없었다.

DI를 이용한 테스트 방법 선택

DI를 테스트에 이용하는데 있어서 세가지 방법을 알아보았다.

  • 테스트 코드에 의한 DI
  • 테스트를 위한 별도의 DI 설정
  • 컨테이너 없는 DI 테스트

항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하는 것이 좋다. 테스트 수행 속도가 가장 빠르고 테스트가 간결하다. 테스트를 위해 필요한 오브젝트 생성과 초기화가 단순하다면 이 방법을 가장 먼저 고려하는 것이 좋다.

여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우, 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다. 테스트 전용 설정 파일을 따로 만들어 관리해주는 것이 선호된다.보통 개발환경, 테스트환경, 운영환경 3가지 각기 다른 설정 파일을 만들어 사용하는 경우가 일반적이다.

테스트 설정을 이미 따로 만들었는데도 예외적인 의존관계를 강제로 구성하고 싶을 때는 DI받은 오브젝트에 다시 테스트코드로 수동 DI해서 테스트하는 방법을 사용하면 된다. 이 경우에는 @DirtiesContext 애노테이션을 붙이는 것을 잊지 말자.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글