[토비의 스프링] 다이내믹 프록시
1) 프록시의 문제점과 다이내믹 프록시
- 프록시는 기존 코드에 영향을 주지 않으면서 타깃의 기능을 확장하거나
접근 방법을 제어할 수 있는 유용한 방법이다.
그럼에도 불구하고 많은 개발자는 타깃 코드를 직접 고치고 말지 번거롭게 프록시를 만들지는 않겠다고 생각한다.
왜냐하면 프록시를 만드는 일이 상당히 번거롭게 느껴지기 때문이다.
- 매번 새로운 클래스를 정의해야 하고 ,
인터페이스의 구현해야 할 메소드는 많으면 모든 메소드를 일일히 구현해서
위임하는 코드를 넣어야 하기 때문이다.
- 단위 테스트를 위해 목이나 스텁을 일일이 클래스로 정의하고
인터페이스의 모의 메소드를 구현하는 일이 불편했던 것과 마찬가지다.
- 그렇다면 목 오브젝트를 만드는 불편함을 목 프레임워크를 사용해 편리하게 바꿨던 것처럼
프록시도 일일이 모든 인터페이스를 구현해서 클래스를 새로 정의하지 않고도
편리하게 만들어서 사용할 방법은 없을까?
- 물론 있다. 자바에는 java.lang.reflect 패키지 안에 프록시를 손쉽게 만들 수 있도록
지원해주는 클래스들이 있다.
기본적인 아이디어는 목 프레임워크와 비슷하다.
일일이 프록시 클래스를 정의하지 않고도 몇 가지 API를 이용해
프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것이다.
2) 프록시의 구성과 프록시 작성의 문제점
- 프록시는 다음의 두 가지 기능으로 구성된다.
(1) 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
(2) 지정된 요청에 대해서는 부가기능을 수행한다.
- 트랜잭션 부가기능을 위해 만든 UserServiceTx는 기능 부가를 위한 프록시다.
아래의 UserServiceTx 코드에서 이 두 가지 기능을 구분해보자.
public class UserServiceTx implements UserService {
UserService userService; -- 타깃 오브젝트
...
public void add(User user){
this.userService.add(user); -- 메소드 구현과 위임
}
public void upgradeLevels(){ -- 메소드 구현
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition()); -- 부가기능 수행
try {
userService.upgradeLevels(); -- 위임
this.transactionManager.commit(status); -- 부가 기능 수행
}catch(RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
- UserServiceTx 코드는 UserService 인터페이스를 구현하고
타깃으로 요청을 위임하는 트랜잭션 부가기능을 수행하는 코드로 구분할 수 있다.
이렇게 프록시의 역할은 위임과 부가작업이라는 두 가지로 구분할 수 있다.
그렇다면 프록시를 만들기가 번거로운 이유는 무엇일까?
역시 두 가지 이유를 찾아볼 수 있다.
(1) 타깃의 인터페이스를 구현하고 위임하는 코드를 작성하기가 번거롭다는 점이다.
부가기능이 필요 없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 한다.
복잡하진 않지만 인터페이스의 메소드가 많아지고 다양해지면 상당히 부담스러운 작업이 될 것이다.
또, 타깃 인터페이스의 메소드가 추가되거나 변경될 때마다 함께 수정해줘야 한다는 부담도 있다.
(2) 부가기능 코드가 중복될 가능성이 많다는 점이다.
트랜잭션은 DB를 사용하는 대부분의 로직에 적용될 필요가 있다.
아직까지 add() 메소드에는 트랜잭션 부가기능을 적용하지 않았지만,
사용자를 추가하는 과정에서 다른 작업이 함께 진행돼야 한다면,
add() 메소드에도 트랜잭션 경계설정 부가기능이 적용돼야 한다.
메소드가 많아지고 트랜잭션 적용의 비율이 높아지면 트랜잭션 기능을 제공하는 유사한 코드가
여러 메소드에 중복돼서 나타날 것이다.
- 사용자 관리 로직 외에도 다양한 비즈니스 로직을 담은 클래스가 만들어질 것이다.
그때마다 메소드에 트랜잭션 기능을 부여하는 코드가 중복돼야 할지 모른다.
트랜잭션 외의 프록시를 활용할 만한 부가기능, 접근제어 기능은 일반적인 성격을 띤 것들이 많다.
따라서 다양한 타깃 클래스와 메소드에 중복돼서 나타날 가능성이 높다.
- 두 번째 문제인 부가기능의 중복 문제는 중복되는 코드를 분리해서 어떻게든 해결해보면 될 것 같지만,
첫 번째 문제인 인터페이스 메소드의 구현과 위임 기능 무제는 간단해 보이지 않는다.
바로 이런 문제를 해결하는데 유용한 것이 바로 JDK 다이내믹 프록시다.