이제 테스트 코드도 어느 정도 깔끔하게 정리를 마쳤다. 하지만 아직 부족한 부분이 있다. 바로 애플리케이션 컨텍스트 생성 방식이다. @Before 메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 그만큼 만들어진다. 지금은 크게 문제는 안되지만 빈이 복잡해지고 많아지면 생성에 적지 않은 시간이 걸릴 수 있다. 따라서 애플리케이션 컨텍스트는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다.
스프링은 JUnit을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.
UserDaoTest에 스프링의 텍스트 컨텍스트 프레임워크를 적용해보자.
- 테스트를 위한 애플리케이션 컨텍스트 관리
스프링 테스트 컨텍스트 프레임 적용
먼저 @Before 메소드에서 애플리케이션 컨텍스트를 생성하는 코드를 제거한다.
그리고 이 ApplicationContext 타입의 인스턴스 변수를 선언하고 스프링이 제공하는 @Autowired 어노테이션을 붙여준다.
마지막으로 클래스 레벨에 @RunWith와 @ContextConfiguration 어노테이션을 아래와 같이 추가해준다.
수정한 테스트를 실행해보면 문제없이 성공할 것이다. 그런데 인스턴스 변수인 context는 어디에서도 초기화해주는 코드가 없다. 따라서 setUp() 메소드에서 context를 사용하려고 하면 NullPointerException이 발생해야 한다. 발생하지 않는 이유는 context 변수에 애플리케이션 컨텍스트가 들어 있기 때문이다.
@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit가 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정 파일 위치를 지정한 것이다.
테스트 메소드의 컨텍스트 공유
아래와 같이 setUp() 메소드에 다음 두 줄을 추가하고 테스트를 다시 실행해보자.
setUp() 메소드는 @Before가 붙어 있으니 매 테스트 메소드가 실행되기 전에 한 번씩 실행된다. 실행 결과는 아래와 같다.
결과를 보면 context는 세 번 모두 동일한 값을 보여준다. 반면에 UserDaoTest의 값은 매번 다른 것을 알 수 있다.
그 말은 앞에서 설명한 것처럼 JUnit는 테스트 메소드를 실행할 때마다 새로운 테스트 오브젝트를 만들기 때문이다.
그렇다면 context 변수에 어떻게 애플리케이션 컨텍스트가 들어 있는 것인가??
스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.
일종의 DI라고 볼 수 있지만 애플리케이션 오브젝트 사이의 관계를 관리하기 위한 DI와는 조금 성격이 다르다.
이렇게 해서 하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있음을 확인했다.
테스트 클래스의 컨텍스트 공유
스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유해주는 것이 전부가 아니다. 여러 개의 테스트 클래스가 있는데 모두 같은 설정 파일을 가진 애플리케이션을 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해 준다. 그림으로 보는 것이 이해가 쉬우니 그림으로 보고 이해하자.
따라서 수백 개의 테스트 클래스를 만들었는데 모두 같은 설정 파일을 사용한다고 해도 테스트 전체에 걸쳐 단 한 개의 애플리케이션 컨텍스트만 만들어져 사용된다. 이 덕분에 테스트 성능이 대폭 향상됨은 더 설명할 필요도 없다.
@Autowired
스프링의 DI에 사용되는 특별한 어노테이션이다. 뒤에서 자세히 설명할 것이므로 일단 이렇게 알아두자.
@Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다.
타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 일반적으로 생성자나 수정자 메소드가 필요하지만 이 경우에는 메소드가 없어도 주입이 가능하다. 또 별도의 DI 설정 없이 필드의 타입 정보를 이용해 빈을 자동으로 가져오는데, 이를 타입에 의한 자동와이어링이라고 한다.
스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 그래서 위에서 applicationContext.xml을 따로 빈 처리를 안 했음에도 @Autowired에 의해 DI가 된 것이다.
@Autowired를 이용해 애플리케이션 컨텍스트가 갖고 있는 빈을 DI 받을 수 있다면 굳이 컨텍스트를 가져와 getBean()을 사용하는 것이 아니라 아예 UserDao 빈을 직접 DI 받을 수 있다.
코드가 더 깔끔해진 것을 알 수 있다. @Autowired를 지정해주기만 하면 어떤 빈이든 다 가져올 수 있다. XML에 dataSource라는 이름으로 등록한 SimpleDriverDataSource 타입의 빈을 가져오고 싶다면, 다음과 같이 추가하면 된다.
@Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다.
타입으로 가져올 빈이 하나를 선택할 수 없다면 변수의 이름과 같은 이름의 빈이 있는지 확인한다.
DataSource타입의 빈이 하나는 dataSource이고 다른 하나는 dataSource2라면 첫 번째 빈이 주입될 것이다.
- DI와 테스트
UserDao와 DB 커넥션 생성 클래스 사이에는 DataSource라는 인터페이스가 있다. 그래서 UserDao는 자신이 사용하는 오브젝트의 클래스가 무엇인지 알 필요가 없다. DI를 통해 외부에서 사용할 오브젝트를 주입받기 때문에 오브젝트 생성에 대한 부담을 지지 않고 코드 수정 없이 얼마든지 의존 오브젝트를 바꿔가며 사용할 수 있다.
인터페이스를 두고 DI를 적용해야 하는 이유에 대해 한번 생각해보자.
첫째, 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다. 인터페이스를 사용하고, DI를 통해 주입받게 하는 건 아주 단순하고 쉬운 작업이다. 변경이 필요한 시점에 수정에 들어가는 시간과 비용을 줄여준다면 DI를 적용 안 할 이유가 없다.
둘째, 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해 두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다. 우리가 DB 커넥션의 개수를 카운팅 하는 부가기능을 추가한 것이 그 예다.
셋째, 테스트 때문이다. 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는 데 중요한 역할을 한다.
테스트에 DI를 이용하는 방법을 몇 가지 살펴보자.
테스트 코드에 의한 DI
DI는 애플리케이션 컨텍스트 같은 스프링 컨테이너에서만 할 수 있는 작업이 아니다. DaoFactory를 이용해서 프레임워크의 도움 없이 직접 DI를 적용해보기도 했다. UserDao에는 DI 컨테이너가 의존관계 주입에 사용하도록 수정자 메소드를 만들어뒀다. 이 수정자 메소드는 평범한 자바 메소드이므로 테스트 코드에서도 얼마든지 호출해서 사용할 수 있다. 따라서 테스트 코드 내에서 이를 이용해서 직접 DI해도 된다. 즉, UserDao가 사용할 DataSource 오브젝트를 테스트 코드에서 변경할 수 있다는 뜻이다.
테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들 수 있다. DataSource 구현 클래스는 스프링이 제공하는 가장 빠른 DataSource인 SingleConnectionDataSource를 사용해보자.
SingleConnectionDataSource는 DB 커넥션을 하나만 만들어두고 계속 사용하기 때문에 매우 빠르다. 다중 사용자 환경에서는 사용할 수 없겠지만 순차적으로 진행되는 테스트에서라면 문제없다.
연결한 DB 이름도 testdb로 변경한다. @Before 메소드에서 이렇게 준비된 테스트용 DataSource 오브젝트를 생성하고 애플리케이션 컨텍스트에서 가져온 dao 오브젝트의 setDataSource() 메소드를 통해 DI 해줄 수 있다. 이렇게 해두면 테스트가 진행되는 동안 UserDao가 테스트용 DataSource를 사용해서 동작하게 된다.
이 방법의 장점은 XML 설정 파일을 수정하지 않고 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있다는 것이다.
@DirtiesContext 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 메소드가 끝나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해 준다. 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서다.
하지만 이 때문에 애플리케이션 컨텍스트를 매번 만드는 것은 조금 찜찜하다.
테스트를 위한 별도의 DI 설정
테스트 코드에서 빈 오브젝트를 수동으로 DI 하는 방법은 장점보다 단점이 많다.
- 코드가 많아져 번거롭다.
- 애플리케이션 컨텍스트를 매번 만들어야 하는 부담이 있다.
이 방법 말고 DI의 장점을 살려서 DAO가 테스트에서만 다른 DataSource를 사용하게 하는 방법은 아래와 같다.
먼저 기존의 applicationContext.xml을 복사해서 test-applicationContext.xml이라고 만들고 dataSource 빈의 설정을 테스트용으로 바꿔주자.
그리고 UserDaoTest의 @ContextConfiguration 어노테이션에 있는 locations 엘리먼트의 값을 새로 만든 설정 파일로 변경해준다.
애플리케이션 컨텍스트도 한 개만 만들어서 모든 테스트에서 공유할 수 있다.
컨테이너 없는 DI 테스트
이번에는 컨테이너를 사용하지 않고 수동 DI만을 이용해 만들어진 테스트 코드를 살펴보자.
이 방법은 @RunWith나 @Autowired를 사용하지 않고 @Before 메소드에 직접 UserDao의 오브젝트를 생성하고, 테스트용 DataSource 오브젝트를 만들어 직접 DI 해줬다.
매번 새로운 오브젝트가 만들어진다는 부담이 있지만 코드가 단순해지고 이해하기 편해졌다. 애플리케이션 컨텍스트가 만들어지는 번거로움이 없으니 그만큼 테스트 시간까지 절약할 수 있다.
'Spring > 토비의 스프링 정리' 카테고리의 다른 글
3.1 다시 보는 초난감 DAO (0) | 2022.04.07 |
---|---|
2.5 학습 테스트로 배우는 스프링 (0) | 2022.04.06 |
2.3 개발자를 위한 테스팅 프레임워크 JUnit (2) | 2022.04.04 |
2.2 UserDaoTest 개선 (0) | 2022.04.01 |
2-1 UserDaoTest 다시보기 (0) | 2022.03.31 |