이제 테스트 코드도 어느 정도 깔끔하게 정리했다. 하지만 아직 한 가지 찜찜한 부분이, 바로 애플리케이션 컨텍스트 생성 방식이다. @BeforeEach 메서드가 테스트 메서드 개수만큼 반복되기 때문에 애플리케이션 컨텍스드도 세 번 만들어진다. 지금은 설정도 간단하고 별 문제 아닌 듯하지만,
빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다. 애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다. 단순히 빈 오브젝트를 만드는 정도라면 괜찮지만, 어떤 빈은 오브젝트가 생성될 때 자체적인 초기화 작업을 진행해서 제법 많은 시간을 필요로 하기 때문이다. 또 한가지 문제는 애플리케이션 컨텍스트가 초기화될 때 어떤 빈은 독자적으로 많은 리소스를 할당하거나 독립적으로 스레드를 띄우기도 한다는 점이다. 이런 경우에는 테스트를 마칠 때마다 애플리케이션 컨텍스트내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 애플리케이션 컨텍스트가 만들어지면서 문제를 일으킬 수도 있다.
코드를 위와 같이 수정하고 실행하면 성공할 것이다.
그런데 인스턴스 변수인 context는 어디에서도 초기화해주는 코드가 없다. 따라서 setUp() 메서드에서 context를 사용하려고 하면 NullPointException이 발생한다. 하지만 위 코드는 문제가 없다. context 변수에 애플리케이션 컨텍스트가 들어 있기 때문이다. 스프링 컨텍스트 프레임워크의 JUnit 확장기능이 야간 마법을 부리는 것이다.
위 코드를 출력하면
context와 this의 오브젝트 값을 살펴보면, 동일한 context 임을 확인할 수 있다. 따라서 하나의 애플리케이션 컨텍스트가 만들어져 모든 테스트 메서드에서 사용되고 있음을 알 수 있다. 반면 UserDaoTest의 오브젝트는 매번 주소 값이 다르다. 앞에 설명한 거처럼 테스트 오브젝트를 새롭게 만들기 떄문이다.
스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유해주는 것이 전부가 아니다. 여러 개의 테스트 클래스가 있는데 모두 같은 설정 파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 애플리케이션 컨텍스트를 공유하게 해준다.
따라서 수백 개의 테스트 클래스를 만들었는데도 모두 같은 설정파일을 사용한다고 해도 테스트 전체에 걸쳐 단 한 개의 애플리케이션 컨텍스트만 만들어져 사용된다. 이 덕분에 테스트 성능이 향상된다.
@Autowireds는 스프링 DI에 사용되는 특별한 애노테이션이다.
@Autowired가 붙은 인스턴스 변수가 있으면,
일반적으로 주입을 위해서는 생성자나 수정자 메서드가 필요한데 이 경우에는 메서드가 없어도 가능하다. 또 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 갖올 수 있는데, 이런 방법을 타입에 의한 자동 와이어링이라고 한다.
하지만 이상한 점은 앞서 만든 테스트 코드는 xml 파일에 정의된 빈이 아니라, ApplicationContext라는 타입의 변수에 @Autowired를 붙였는데도 DI가 됐다. 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 셈이고 DI도 가능한 것이다.
@Autowired 를 이용해 DI를 받을 수 이다면 굳이 컨텍스트를 가져와 getBean() 하는게 아니라 직접 DI를 받을 수 있을 것이다.
@Autowired를 지정만 하면 어떤 빈이든 다 가져올 수 있다. XML에 dataSource라는 이름으로 등록한 SimpleDriverDataSource 클래스 타입은 물론이고, 인터페이스인 DataSource 타입으로 변수를 선언해도 된다.
@Autowired
SimpleDriverDataSource dataSource;
@Autowired
DataSource dataSource;
그렇다면 SimpleDriverDataSource 타입의 변수로 선언하는 방법과 DataSource 타입으로 선언하는 방법 중 어떤 것이 나을까? 그건 테스트에서 빈을 어떤 용도로 사용하느냐에 따라 다르다.
단순히 DataSource에 정의된 메스드를 테스트하고 싶으면 DataSource를 사용해도 좋다. DataSource로 선언해두면 dataSource 빈의 구현 클래스를 변경하더라도 테스트 코드를 수정할 필요가 없다.
반면에 테스트에서 SimpleDriverDataSource라는 타입의 오브젝트에 관심이 있는 경우가 있을 수도 있다.
이때는 SimpleDriverDataSource 타입으로 선언해야 한다. 예를 들어 XML에 속성으로 선언한 DB 연결정보를 확인하고 싶거나 SimpleDriverDataSource 클래스의 메서드를 직접 이용해야 한다면 사용하면 된다.
하지만 꼭 필요하지 않다면 테스트에서도 가능한 한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해두는 편이 좋다.
UserDao와 DB 커넥션 생성 클래스 사이에는 DataSource라는 인터페이스를 뒀다. 그래서 UserDao는 자신이 UserDao는 자신이 사용하는 오브젝트의 클래스가 무엇인지 알 필요가 없다. 또한 DI를 통해 외부에서 사용할 오브젝트를 주입받기 때문에 오브젝트 생성에 대한 부담을 지지 않아도 된다. 코드의 수정 없이도 얼마든지 의존 오브젝트를 바꿔가며 사용할 수 있다.
그런데 굳이 DataSource 인터페이스를 사용하고 DI를 통해 주입해주는 방식을 이용해야 하는가?
그래도 인터페이스를 두고 DI를 적용해야 한다. 그래야 하는 이유를 한번 생각해보자.
첫째, 소프트웨어 개발에서 절대 바뀌지 않는 것은 없기 때문이다. 클래스 대신 인터페이스를 사용하고, new를 이용해 생성하는 대신 DI를 통해 주입받게 하는 건 아주 단순하고 쉬운 작업이다.
둘째 , 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
세 번째 이유는 테스트 때문이다. 단지 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다. 테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 테스트 코드를 만들어야 한다. 그러기 위해서는 가능한 한 작은 단위의 대상에 국한해서 테스트해야 한다. DI는 테스트가 가장 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는 데 중요한 역할을 한다.
DI는 애플리케이션 컨텍스트 같은 스프링 컨테이너에서만 할 수 있는 작업이 아니다. 이미 오브젝트 팩토리인 DaoFactory를 이용해서 프레임워크의 도움 없이 직접 DI를 적용해봤다. 의존관계 주입에 사용하도록 수정자 메서드를 만들어뒀다. 이 수정자 메서드는 평범한 자바 메서드이므로 테스트 코드에서도 알마든지 호출해서 사용할 수 있다.
애플리케이션이 사용할 applicationContext.xml에 정의된 DataSource 빈은 서버의 DB 풀 서비스와 연결해서 운영용 DB 커넥션을 돌려주도록 만들어져 있다고 해보자. 테스트할 때 이 DataSource를 이용해도 될까?
이렇게 하면 개발자가 테스트할 때 테스트용 DB를 이용하도록 DataSource 빈을 수정했다가, 서버에 배치할때는 운영용 DB를 사용하는 DataSource 빈을 수정했다가, 서버에 배치할 때는 다시 운영용 DB를 사용하는 DataSource로 수정하는 방법도 있겠지만, 번거롭기도 하고 위험할 수도 있다.
테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들 수 있다. DataSource 구현 클래스는 스프링이 제공하는 가장 빠른 DataSource인 SingleConnectionSource 를 사용해보자. SingleConnectionSource는 DB 커넥션을 하나만 만들어주고 계속 사용하기 떄문에 매우 빠르다. 다중 사용자 환경에서는 사용할 수 없겠지만 순차적으로 진행되는 테스트에서라면 문제 없다.
이 방법은 장점은 XML 설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성할 수 있다는 것이다.
하지만 이 방식은 애플리케이션 컨텍스트에서 applicationContext.xml 파일의 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경했기 때문이다. 스프링 테스트 콘텍스트 프레임워크를 적용했다면 애플리케이션 컨텍스트는 테스트 중에 딱 한 개만 만들어지고 모든 테스트에서 공유해서 사용한다. 따라서 애플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 안흔ㄴ 것이 원칙이다. 그런데 우의 테스트 코드는 애플리케이션 컨텍스트에서 가져온 UserDao빈의 의존관계를 강제로 변경한다. 반 번 변경하면 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 계속 사용될 것이다. 이는 별로 바람직하지 못하다.
그래서 @DirtiesContext 라는 애플리케이션을 추가해줬다. 이 애노테이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다.
그렇다면 DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까?
항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다. 테스트를 위해 필요한 오브젝트의 생성과 초기화가 단순하다면 이 방법을 가장 먼저 고려해야 한다.
여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다. 이때는 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다. 테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정파일을 따로 만들어 사용하는 편이 좋다.