테스트 환경을 만들어주기 위해 , 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록 사용하는 이런 오브젝트를 테스트 대역 이라고한다. (DummyMailSender)
대표적인 테스트 대역은 테스트 스텁이다.
테스트 스업은 데스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것을 말한다.
테스트 스텁은 일반적으로 메서드를 통해 전달하는 파라미터와 달리, 테스트 코드 내부에서 간접적으로 사용되기 때문에, Di 등을 통해 미리 의존 오브젝트를 테스트 스텁으로 변경해야한다.
테스트 대상 오브젝트의 메소드가 돌려주는 결과 뿐 아니라 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶다면 테스트 대상의 간접적인 출력을 검증하고, 테스트 대상 오브젝트와 의존 오브젝트 사이에서 일어나는 일을 검즘항 수 있도록 특별히 설계된 목 오브젝트를 사용해야한다.
Mocktio 와 같은 목 프레임워크는 목 클래스를 일일이 준비해 둘 필요가 없다.
간단히 메소드 호출만으로 다이나믹하게 특정 인터페이스를 구현한 테스트용 ㅇ목 오브젝트를 만들 수 있다.
Mocktio 목 오브젝트는 다음의 네 단계를 거쳐 사용한다.
1. 인터페이스를 이용해 목 오브젝트를 만든다.
2. 목 오브젝트가 리턴할 값이 있으면 이를 저장한다, 메소드가 호출되면 예외를 강제로 던지게 할 수 있따.
3. 테스트 대상 오브젝트에 DI해서 목 오브젝트가 테스트 중 사용되도록 만든다.
4. 테스트 대상 오브젝트를 사용 후 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇번 호출됐는지 검증하다.
기존 UserService 클래스에는 서비스 기능과 트랜젝션 기능이 한 곳에 모여있었다. 서로 다른 관심임에도 불구하고,
그래서 점진적으로 UserService 를 인터페이스로 두고 UserSerivecImpl 과 UserServiceTx 가 인터페이스를 정의하도록 새로운 구현 클래스를 만들었다
관심이 다른 기능을 한 두 클래스로 나눠서 , UserService를 사용하려는 클라이언트는 UserServiceTx 클래스를 사용하고, UserServiceTx 실제 기능은 UserSerivecImpl에게 위임하고 트랜잭션 기능만을 수행한다.
유저의 목적은 UserSerivce 의 실제 구현체인 UserSerivecImpl를 사용하고 싶어하지만 그 사이에 UserServiceTx가 끼어든 셈이다.
그래서 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자 , 즉 프록시라고 부른다, 여기서는 UserServiceTx가 프록시라고 볼 수 있다.
그리고 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타겟 또는 실체 라고 부른다.
프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있따는 것이다.
예를 들어 소스코드를 출력하는 기능을 가진 핵심기능이 있다.
이 클래스에 데코레이터 패턴 개념을 부여해 타깃과 같은 인터페이스를 구현하는 프록시를 만들 수 있다.
(소스코드에 라인넘버 추가, 문법에 따른 색 변경, 특정 폭으로 소스 자르기, 페이지 표시 등..)
일반적으로 사용하는 프록시와 디자인 패턴에서 말하는 프록시 패턴은 다르다.
후자는 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우다.
데코레이터 패턴과 달리 프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않는다.
타깃 오브젝트를 생성하기 복잡하거나 당장 필요하지 않는 경우에는 꼭 필요한 시점까지 오브젝트를 생성하지 않는 편이 좋다. 그런데 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있다. 이럴 때 프록시 패턴을 적용하면 된다.
실제 타깃 오브젝트 대신 프록시를 넘겨주는 것이다, 그 후 프록시의 메서드를 통해 프록시가 타깃 오브젝트를 생성하고 요청을 위임해주는 식이다.
프록시의 기능 치고는 인터페이스를 구현한 클래스를 매번 만들어 사용하는 것은 번거로운 일이다.
일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도 편리하게 만드는 방법이 존재한다.
java.lang.reflect 패키지 않에 프록시를 손쉽게 만들 수 있도록 하는 클래스들이 있다. ( 목 프레임워크와 비슷하다 )
밑에 설명
첫번째 구현 인터페이스를 만드는 것이 귀찮다.
두번째 기능 메서드의 중복이 일어날 수 있다.
이 두가지의 문제를 해결하는 데 유용한 것이 JDK 의 다이나믹 프록시다.
다이나믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다.
리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만드는 것이다.
String name = "Spring";
의 오브젝트가 있고 이 스트링의 길이를 알고 싶다.
리플렉션 API 중에서 메소드에 대한 정의를 담은 Method 라는 인터페이스를 이용해 메서드 호출 방법을 살펴보자.
String 클래스의 정보를 담은 Class 타입의 정보는 String.class 라고 하면 가져올 수 있다, 또는 오브젝트.getClass()로 가져올 수 있다.
그리고 이 클래스 정보에서 특정 이름을 가진 메소드 정보를 가져올 수 있다.
Method lengthMethd = String.class.getMethod("length");
// 실행시킬 메서드의 파라미터가 있을 경우 두번 쨰 파라미터에 실행시킬 메서드의 타입을 적어줄 수 있따.
Method 인터페이스는 메소드에 대한 자세한 정보를 담고 잇고, 이를 이용해 특정 오브젝트의 메서드를 실행 시킬 수 있다.
Method interface 의 invoke() 메서드를 사용하면 된다.
// 실행시킬 대상 오브젝트와 , 파라미터 목록.
public Object invoke(Object obj, Object ...args)
int length = lengthMethd.invoke(name)
다이나믹 프록시는 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어진 오브젝트,
다이나믹 프록시 오브젝트는 타깃의 인터페이스와 같ㅇ른 타입으로 만들어진다.
프록시 팩토리에게 인터페이스 정보만 제공하면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어준다.
다이나믹 프록시가 인터페이스 구현 클래스의 오브젝트를 만들어주지만, 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야한다. 부가기능은 InvocationHandler를 구현한 오브젝트에 담는다.
//InvocationHandler 의 유일한 메서드
public Object invoke(Object proxy, Method method , Object[] args)
441
public class UppercaseHandler implements InvocationHandler {
Hello target;
public UppercaseHandler(Hello target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String ret = (String) method.invoke(target,args);
return ret.toUpperCase();
}
}
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] { Hello.class},
new UppercaseHandler(new HelloTarget())
);
핸들러 invoke 의 method.invoke() 의 리턴값을 스트링으로 단정짓고 있다.
다른 리턴 타입을 갖는 메소드가 추가되면 이는 캐스팅 오류가 난다.
Method 를 이요한 타깃 오브젝트의 메소드 호출 후 리턴 타입을 확인해서 타 캐스팅일 진행해주어야한다.
InvocationHandler 방식의 또 한가지 장접은 타깃의 종류에 상관없이 작동한다는 것이다.
public class UppercaseHandler implements InvocationHandler {
Object target;
public UppercaseHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object ret = method.invoke(target,args);
if(ret instanceof String)
return ((String)ret).toUpperCase();
else {
return ret;
}
}
reference : toby spring