[토비의 스프링 3.1] 6장 고립된 단위 테스트
단위 테스트와 통합 테스트
‘테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시켜서 테스트하는 것’이 단위테스트이고, ‘두 개 이상의, 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나, 외부의DB파일, 서비스 등의 리소스가 참여하는 테스트’가 통합 테스트입니다.
단위 테스트와 통합 테스트 중에서 어떤 방법을 쓸 지는 아래 몇 가지 가이드라인을 통해 알 수 있습니다.
- 항상 단위 테스트를 먼저 고려한다
- 하나의 클래스나 성격과 목적이 같은 긴밀한 클래스 몇 개를 모아서 외부와의 의존관계를 모두 차단하고 필요에 따라 스텁이나 목 오브젝트 등의 테스트 대역을 이용하도록 테스트를만든다. 단위 테스트는 테스트 작성도 간단하고 실행 속도도 빠르며 테스트 대상 외의 코드나 환경으로부터 테스트 결과에 영향을 받지도 않기 때문에 가장 빠른 시간에 효과적인 테스트를 작성하기에 유리하다.
- 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만든다.
- 단위 테스트로 만들기가 어려운 코드도 있다. 대표적인 게 DAO다. DAO는 그 자체로 로직을 담고 있기보다는 DB를 통해 로직을 수행하는 인터페이스와 같은 역할을 한다. SQL을 JDBC를 통해 실행하는 코드만으로는 고립된 테스트를 작성하기가 힘들다. 작성한다고 해도 가치가 없는 경우가 대부분이다. 따라서 DAO는 DB까지 연동하는 테스트로 만드는 편이 효과적이다. DB를 사용하는 테스트는 DB에 테스트 데이터를 준비하고. DB에 직접 확인을 하는 등의 부가적인 작업이 필요하다.
- DAO 테스트는 DB라는 외부 리소스를 사용하기 때문에 통합 테스트로 분류된다. 하지만코드에서 보자면 하나의 기능 단위를 테스트하는 것이기도 하다. DAO를 테스트를 통해 충분히 검증해두면, DAO를 이용하는 코드는 DAO 역할을 스텁이나 목 오브젝트로 대체해서 테스트 할 수 있다. 이후에 실제 DAO와 연동했을 때도 바르게 동작하리라고 확신할 수 있다. 물론 각각의 단위 테스트가 성공했더라도 여러 개의 단위를 연결해서 테스트하면 오류가 발생할 수도 있다. 하지만 충분한 단위 테스트를 거친다면 통합 테스트에서 오류가 발생 할 확률도 줄어들고 발생한다고 하더라도 쉽게 처리할 수 있다.
- 여러 개의 단위가 의존관계를 가지고 동작할 때를 위한 통합 테스트는 필요하다. 다만, 단위 테스트를 충분히 거쳤다면 통합 테스트의 부담은 상대적으로 줄어든다.
- 단위 테스트를 만들기가 너무 복잡하다고 판단되는 표드는 처음부터 통합 테스트를 고려해본다. 이때도 통합 테스트에 참여하는 코드 중에서 가능한 한 많은 부분을 미리 단위 테스트로 검증해두는 게 유리하다.
- 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트다. 가능하면 스프링의 지원 없이 직접 코드 레벨의 DI를 사용하면서 단위 테스트를 하는 게 좋겠지만 스프링의 설정 자체도 테스트 대상이고, 스프링을 이용해 좀 더 추상적인 레벨에서 테스트해야할 경우도 종종 있다. 이럴 땐 스프링 테스트 컨텍스트 프레임워크를 이용해 통합 테스트를 작성한다.
프록시란
트랜잭션을 비즈니스 로직 코드와 완전히 분리했을 때 위와 같은 구조가 됩니다. (어떻게 분리했는 지는 이전 글 참고) 이때 핵심기능은 부가기능을 가진 클래스의 존재 자체를 모릅니다. 따라서 부가기능이 핵심기능을 사용하는 구조가 되는 것 입니다.
하지만 이 때 클라이언트가 핵심 기능을 가진 클래스를 직접 사용해버리면, 부가기능이 적용될 기회가 없게 됩니다. 그래서 부가기능은 자신이 핵심 기능을 가진 클래스 인 것 처럼 꾸며서, 클라이언트가 자신을 거쳐 핵심 기능을 사용하도록 만들어야 합니다.
클라이언트를 속이기 위해서는 위와 같이 클라이언트는 인터페이스를 통해서만 핵심기능을 사용할 수 있도록 하고, 부가기능도 같은 인터페이스를 구현하여 클라이언트와 핵심기능 사이에 끼어들면 됩니다.
이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것 처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시(proxy)라고 부릅니다. 그리고 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃이나 실체라고 부릅니다.
프록시의 특정은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것입니다. 또한 사용 목적에 따라 두 가지로 구분되는데, 첫째는 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서고, 두 번째는 타깃에 부가적인 기능을 부여해주기 위해서 사용합니다. 두 가지 모두 대리 오브젝트라는 개념의 프록시를 두고 사용한다는 점은 동일 하지만, 목적에 따라서 디자인 패턴에서는 다른 패턴으로 구분합니다.
데코레이터 패턴
데코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴을 말합니다. 다이내믹하게 기능을 부가한다는 의미는 컴파일 시 점, 즉 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않다는 뜻입니다. 따라서 데코레이터 패턴에서는 프록시가 꼭 하나로 제한되지 않고, 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없습니다.
위 그림과 같이 여러 개의 프록시의 순서를 정해서 단계적으로 위임하는 구조로 만들면 됩니다.
프록시 패턴
프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우를 말합니다. 프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않습니다. 대신 클라이언트가 타깃에 접근하는 방식을 변경해줍니다. 타깃 오브젝트에 대한 레퍼런스가 미리 필요한 경우 프록시 패턴을 적용하면 됩니다. 또는 클라이언트로 하여금 원격 오브젝트에 대한 접근 방법을 제공해주는 것도 프록시 패턴을 사용하는 경우입니다.
리플렉션
리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것 입니다.
자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 갖고 있습니다. ‘클래스이름.class’라고 하거나 오브젝트의 getClass( ) 메소드를 호출하면 클래스 정보를 담은 Class 타입의 오브젝트를 가져올 수 있습니다. 클래스 오브젝트를 이용하면 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있습니다.
예를 들어 클래스의 이름이 무엇이고 어떤 클래스를 상속하고 어떤 인터페이스를 구현했는지, 어떤 필드를 갖고 있고, 각각의 타입은 무엇인지, 메소드는 어떤 것을 정의했고, 메소드의 파라미터와 리턴 타입은 무엇인지 알아낼 수 있습니다. 더 나아가서 오브젝트 필드의 값을 읽고 수정할 수도 있고, 원하는 파라미터 값을 이용해 메소드를 호출할 수도 있습니다. 리플렉션에 대한 자세한 내용은 java.lang.reflect 패키지의 자바문서를 읽어보면 됩니다.
다이내믹 프록시는 리플랙션 기능을 이용하여 프록시를 만들어줍니다.
다이내믹 프록시
일일히 프록시 클래스를 정의하지 않고도 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것 입니다.
프록시의 역할은 위임과 부가작업이라는 두 가지 기능으로 나뉘는데, 이 때 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거로운 문제와, 부가기능 코드가 중복될 가능성이 많다는 점이 있습니다. 이러한 문제들을 해결하는 데 유용한 것이 JDK의 다이내믹 프록시 입니다.
다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어집니다. 이때 만들어진 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어지며 클라이언트는 이 오브젝트를 타깃 인터페이스를 통해 사용할 수 있습니다. 이 덕분에 프록시를 만들 때 인터페이스를 모두 구현하면서 클래스를 정의하는 수고를 덜 수 있습니다.
다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것입니다. 타깃 인터페이스의 모든 메소드 요청이 하나의 메소 드로 집중되기 때문에 중복되는 기능을 효과적으로 제공할 수 있습니다. 리플렉션으로 메소드와 파라미터 정보를 모두 가지고 있으므로 타깃 오브젝트의 메소드를 호출하거나, 위의 InvocationHandler 구현 오브젝트가 타깃 오브젝트 레퍼런스를 가지고 있으면 라플렉션을 이용해 간단히 위임 코드를 만들면 됩니다.