BackEnd Developer, Love Crossfit, Welcome to Jay's blog

AOP 개념잡기 1 - 프록시와 팩토리빈

|

AOP가 무엇인지 구체적으로 잘 와닿지가 않아서 토비의 스프링 책을 폈다. 역시 토비의 스프링이다. 그동안 궁금했던 내용들이 써있어서 좋다.

먼저 AOP가 무엇인지 알아보기에 앞서, 등장배경과 스프링이 AOP를 도입한 이유, AOP 적용을 통해 얻을 수 있는 장점이 무엇인지 이해해보자. 공부를 할때 해당 기술이 나오게 된 배경을 이해하면 확실히 오래 남는 공부를 할 수 있게 되는 것 같다.


책 토비의 스프링 3.1의 6장 AOP를 정리한 내용입니다.


# 프록시 # 프록시패턴 # 데코레이터패턴 # 리플렉션 Method # 다이내믹 프록시 # 팩토리빈 # 스프링의 프록시 팩토리빈 # 템플릿/콜백 패턴 # 서비스 추상화 # 스프링 DI # 포인트컷 #어드바이스 #어드바이저

6.1 트랜잭션 코드의 분리

요 절에서는 비즈니스 로직 전과 후에 설정되어야 하는 ‘트랜잭션 경계 설정’을 예시로 든다.

문제 상황 출발 예시 : 비즈니스 로직 안에 비즈니스 로직과 성격이 다른 트랜잭션 로직이 섞여있다. 그런데 트랜잭션은 꼭 필요한 코드라 (트랜잭션은 비즈니스 로직 전과 후에 호출되어야 한다) 제거하기가 힘들다. 어떻게 하면 트랜잭션 코드를 이 비즈니스 로직으로 부터 깔끔하게 제거할 수 있을까?

해결책 1: 메소드로 분리한다

하나의 메소드에서 분리는 하였지만 여전히 같은 서비스 안에 있다는 문제가 있다. 하나의 서비스 안에는 관련 비즈니스 로직만 모아두고 싶은데 서비스에서 완전히 코드를 제거하는건 불가능할까?

해결책 2: DI를 이용해 클래스로 분리한다

UserService에는 순수하게 유저 관련 비즈니스 로직 코드만 놔두고 트랜잭션 경계 설정을 담당하는 코드를 외부로 빼내고 싶다. 하지만 클라가 UserService 기능을 제대로 이용하려면 트랜잭션 코드가 비즈니스 로직이 선후에 적용되어야만 한다.

자 여기서 DI 기본 아이디어를 적용해보자. 실제 사용할 오브젝트의 클래스 정체는 감추고 인터페이스를 통해 간접적으로 접근하는 것이다. 구현 클래스는 얼마든지 외부에서 변경할 수 있는 것도 추가 득템.

DI를 이용해 동일한 인터페이스를 구현하는 클래스 2개를 만들어 사용해보자. UserService라는 인터페이스를 만들고 이 인터페이스를 구현한 핵심 코드를 담고 있는 비즈니스 로직 클래스와, 트랜잭션 클래스 2개로 분리시키는 것이다.

이제 핵심 코드에는 트랜잭션 관련 코드가 하나도 남지 않게 되었고, 핵심클래스에서는 부가기능을 가진 클래스의 존재 자체를 모르게 되었다. 이제 트랜잭션 경계 설정을 적용하기 위해서는 부가기능이 핵심기능을 사용하는 구조가 되었다.


6.3 다이내믹 프록시와 팩토리 빈

Client <-> UserServiceTx(트랜잭션 코드)<프록시> <-> UserServiceImpl(비즈니스 코드)<타깃>

위 내용대로 한다면, 해당 비즈니스 로직이 호출되기 위해서는 먼저 트랜잭션 코드를 거치고 트랜잭션 코드 안에서 해당 비즈니스 메소드가 호출된다(=위임). 비즈니스 로직 호출이 끝나면 다시 트랜잭션 코드로 돌아가고 client에게 응답해주는 형태가 된다.

클라이언트 입장에서는 비즈니스 로직을 호출했지만 트랜잭션 클래스가 앞단에서 마치 실제 대상인 양 요청을 받고 처리하는데 이러한 역할을 하는 객체를 프록시(Proxy)라 부른다.

프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃(Target)이라 한다.

즉, 프록시를 통해 핵심 비즈니스로직(타겟)에 접근하는 구조이다.

프록시 개념과 용도

타깃과 동일한 인터페이스를 구현하고 클라이언트와 타깃 사이에 존재하면서 기존 코드에 영향 주지 않으면서 기능의 확장(부가)과 클라이언트 접근 제어를 담당하는 오브젝트

데코레이터 패턴과 프록시 패턴

프록시 사용 목적에 따라 구분. 런타임시 기능의 부가로 사용하면 데코레이터 패턴이고 클라이언트 타깃 접근 방식을 제어하기 위해 사용하면 프록시 패턴이라 한다. 접근 방식 제어 방법은, 실제 타깃 객체를 만드는 대신 프록시를 클라에게 대신 넘겨주고, 실제 타깃이 필요한 시점(프록시 메소드 호출)에 실제 객체를 생성해서 요청을 위임한다. 두개 혼용해서 사용 가능

클라 -> 접근제어 프록시-> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스 코드 출력 기능<타깃>

6.3.2 다이내믹 프록시

하지만 프록시를 만드는 일은 상당히 번거롭다. 매번 새로운 프록시 클래스를 정의해야 하고, 인터페이스의 모든 메소드를 일일히 구현해 위임 코드도 넣어줘야 한다. 인터페이스 메소드가 추가되거나 변경될 때마다 함께 수정해줘야 한다는 부담감도 있다. 에휴 인터페이스 메소드 구현과 위임 기능 문제는 여러모로 간단해 보이지 않는다. 프록시 클래스를 정의하지 않고도 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 방법은 없을까?

있다. 다이내믹 프록시-

리플렉션

다이내믹 프록시는 리플렉션 기능을 이용해 프록시를 생성한다.

리플렉션이란, 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들을 접근할 수 있도록 해주는 Java API 이다. 스프링 컨테이너와 관련된 BeanFactory는 어플리케이션이 실행된 후 객체가 호출될 당시 객체를 생성하게 되는데 그 때 필요한 기술이 Reflection이다. 이게 어떻게 가능한 것일까? 자바 클래스 파일(.java)은 바이트 코드로 컴파일(.class)되어 Static 영역에 위치하게 되는데, 클래스 이름만 알고 있다면 언제든 이 영역을 뒤져서 클래스에 대한 정보를 가져올 수 있기에 가능하다. (신기하군)

리플렉션 API 중 메소드에 대한 정의를 담은 Method 인터페이스의 invoke() 이용해 메소드를 호출한다.

public Object invoke(Object obj, Object...args) // args: 파라미터 목록
/* String 클래스의 length() 메소드 정보를 가지고 온다. */
Method lengthMethod = String.class.getMethod("length")
int length = lengthMethod.invoke(name); // int length = name.length();


/* String 클래스의 charAt(int) 메소드 */
Method charAtMethod = String.class.getMethod("charAt", int.class);
charAtMethod.invoke(name, 0); // name.charAt(0);

cf.
자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 가지고 있다. ‘클래스이름.class’라고 하거나 오브젝트의 getClass() 메소드를 호출하면 클래스 정보를 담은 Class 타입의 오브젝트를 가져올 수 있는데, 이 클래스 오브젝트를 통해 클래스 코드에 대한 메타정보 조회와 오브젝트 조작이 가능하다. 예를 들어 클래스의 이름이나 어떤 클래스를 상속하고 어떤 인터페이스 구현했는지, 어떤 필드를 갖고 있고 각 타입, 어떤 메소드와 그 시그니처 정보 등. 더 나아가 오브젝트 필드의 값을 읽고 수정 할 수도 있고 원하는 파라미터 값을 이용해 메소드 호출도 가능하다.

다이내믹 프록시 적용

일반 프록시 문제점

  • 인터페이스의 모든 메소드 구현해 위임하도록 코드 만들어야함 -> 대안) 프록시 팩토리에 인터페이스 정보만 제공해주면 알아서 구현 객체를 생성해준다.
  • 부가 기능인 리턴 값을 대문자로 바꾸는 기능이 모든 메소드마다 중복되어 나타남 -> InvocationHandler 인터페이스 이용해 모든 요청을 invoke() 메소드 하나로 넘겨 부가기능 추가와 위임을 해결한다

alt text

다이내믹 프록시는 프록시 팩토리에 의해 런타임때 만들어지는 구현체이다.

Hello 인터페이스의 모든 메소드를 어떻게 invoke() 메소드 하나로 처리할까?

invoke() 메소드는 리플렉션의 Method 인터페이스 타입으로 요청을 받음으로써 가능하다.

// InvocationHandler 구현 클래스
public class UppercaseHandler implements InvocationHandler {
    Hello target;

    public UppercaseHandler(Hello target) { // 다이내믹 프록시로부터 전달받은 요청을 다시 타깃 객체에 위임해야 하기 때문에 주입받는다.
        this.target = target;
    }

    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()); // 부가기능과 위임 코드를 담은 InvocationHandler
)

6.3.4 다이내믹 프록시를 위한 팩토리 빈

이제 스프링 DI를 통해 사용할 수 있도록 만들어보자. 어 그런데 문제가 생겼다. /스프링은 빈을 생성할 때 내부적으로 리플렉션 API를 이용해 빈 정의에 나오는 클래스 이름으로 빈 오브젝트를 생성한다. 하지만 다이내믹 프록시 객체는 따로 빈을 정의하지 않고 Proxy 클래스의 newProxyInstance()라는 스태틱 팩토리 메소드만을 통해서 만드는데, 클래스 자체를 내부적으로 다이내믹하게 새로 정의해서 사용하기 때문에 사전에 프록시 객체의 클래스 정보를 미리 알아내서 스프링 빈에 정의할 방법이 없다. 스프링 빈으로 등록 불가함

다이내믹 프록시를 스프링 빈으로 등록할 수 있는 다른 방법이 없을까? -> 팩토리 빈을 이용한다.

팩토리빈

팩토리 빈이란 스프링을 대신해서 객체 생성을 할 수 있도록 만들어진 특별한 빈을 말한다.

FactoryBean 인터페이스를 구현해보자.

package org.springframework.beans.factory;

public interface FactoryBean<T> {
    T getObject() throws Exception; // 빈 객체를 생성해서 돌려준다.
    Class<? extends T> getObjectType(); // 생성되는 오브잭트 타입을 알려준다
    boolean isSingleton(); // getObject()가 돌려주는 객체가 항상 같은 싱글톤 객체인지 알려준다
}
// 트랜잭션 프록시 팩토리 빈
public class TxProxyFactoryBean implements FactoryBean<Object> {
    ...
    /*
     * FactoryBean 인터페이스 구현 메소드
     * DI 받은 정보를 이용해 TransactionHandler를 사용하는 다이내믹 프록시를 생성한다.
     */
    public Object getObject() throws Exception {
        TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTarget(target);
        txHandler.setTransactionManger(transactionManager);
        txHandler.setPattern(pattern);
        return Proxy.newProxyInstance(
            getClass().getClassLoader(), new Class[] { serviceInterface },
            txHandler);
        )
    }
    ...
}
// 트랜잭션 프록시 팩토리 빈 설정
<bean id="userService" class="springbook.service.TxProxyFactoryBean">
    <property name="target" ref="userServiceImp" /> -> 한계점1) 하나의 클래스만 대상. 트랜잭션을 여러 클래스에 범용적으로 적용하진 못한다.
    <property name="transactionManager" ref="transactionManager" /> -> 2) 하나의 부가 기능만 적용 가능. 여러개 추가하고 싶다면 그만큼 같은 설정을 여러번 해줘야한다.
    <property name="pattern" value="upgradeLevels" /> -> 트랜잭션 적용할 메소드 이름
    <property name="serviceInterface" value="complex.module.CoreService" />
</bean>

프록시 팩토리 빈 방식의 장점과 한계

다이내믹 프록시와 스프링의 팩토리 빈을 함께 적용하면 프록시 클래스를 매번 작성해야하는 번거로움을 완벽하게 제거할 수 있게 되었다.

하지만, 아직도 문제가 있다. 하나의 타깃에 여러 개의 부가기능을 적용하거나 한번에 여러개의 클래스에 공통 부가 기능을 제공하려면 지금까지 살펴본 방법으로는 불가능하며 적용하기 위해서는 빈 설정이 길어지게 될 것이다.

다른 방법은 없을까? 있다. 스프링의 프록시 팩토리 빈

6.4 스프링의 프록시 팩토리 빈

지금까지 기존 코드의 수정 없이 트랜잭션 부가기능을 추가해줄 수 있는 다양한 방법을 살펴봤다. 이제 스프링은 이런 문제에 어떤 해결책을 제시하는지 살펴볼 차례이다. 스프링은 매우 세련되고 깔끔한 방식으로 이런 문제에 대한 해법을 제공해준다.

6.4.1 ProxyFactoryBean

자바 JDK에서 제공하는 다이내믹 프록시 외에도 편리하게 프록시를 만들 수 있게 해주는 다양한 기술이 존재한다. 따라서 스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다. 생성된 프록시는 스프링 빈으로 등록돼야 한다. 스프링은 프록시 객체를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.

스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리빈이다. ProxyFactoryBean은 순수하게 프록시를 생성하는 작업만 담당하고 프록시 통해 제공할 부가기능은 별도의 빈에 둘 수 있다.

사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다. InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않는다. 따라서 타깃은 InvocationHandler를 구현한 클래스가 직접 알고 있어야 했다. MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다. 그 차이 덕분에 MethodInterceptor는 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. 따라서 MethodInterceptor 오브젝트는 타깃이 다른 여러 프록시에서 공유 가능, 싱글톤 빈으로 등록 가능하다.

어드바이스: 타깃이 필요 없는 순수한 부가기능

MethodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다. ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다. 마치 SQL 쿼리 정보에 종속되지 않는 JdbcTemplate이기 때문에 수많은 DAO 메소드가 하나의 JdbcTemplate 객체를 공유할 수 있는 것과 마찬가지이다.

MethodInterceptor처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스(advice)라고 부른다.

ProxyFactoryBean은 기본적으로 JDK가 제공하는 다이내믹 프록시를 만들어준다. 경우에 따라서는 CGLib이라고 하는 오픈소스 바이트코드 생성 프레임워크를 이용해 프록시를 만들기도 한다.

어드바이스는 타깃 오브젝트에 종속되지 않는 순수한 부가기능을 담은 오브젝트라는 사실을 잘 기억해두자.

포인트컷: 부가기능 적용할 메소드 선정 방법

MethodInterceptor 오브젝트는 여러 프록시가 공유해서 사용할 수 있다. 그러기 위해서 MethodInterceptor 오브젝트는 타깃 정보를 갖고 있지 않도록 만들었다. 그 덕분에 MethodInterceptor를 스프링의 싱글톤 빈으로 등록할 수 있었다. 그런데 여기에다 트랜잭션 적용 대상 메소드 이름 패턴을 넣어주는 것은 곤란하다. 트랜잭션 적용 메소드 패턴은 프록시마다 다를 수 있기 때문에 여러 프록시가 공유하는 MethodInterceptor에 특정 프록시에만 적용되는 패턴을 넣으면 문제가 된다.

InvocationHandler는 타깃과 메소드 선정 알고리즘 코드에 의존하고 있지만, 스프링의 ProxyFactoryBean 방식은 두 가지 확장 기능인 부가기능(Advice)과 메소드 선정 알고리즘(Pointcut)을 활용하는 유연한 구조를 제공한다.

스프링은 부가기능을 제공하는 오브젝트를 어드바이스라고 부르고, 메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다. 어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다. 두 가지 모두 여러 프록시에서 공유가 가능하도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록이 가능하다.

alt text

alt text

프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지를 확인해달라고 요청한다. 포인트컷은 Pointcut 인터페이스를 구현해서 만들면 된다. 프록시는 포인트컷으로부터 부가기능을 적용할 대상 메소드인지 확인받으면, MethodInterceptor 타입의 어드바이스를 호출한다. 어드바이스는 JDK의 다이내믹 프록시의 InvocationHandler와 달리 직접 타깃을 호출하지 않는다.

어드바이스가 일종의 템플릿이 되고 타깃을 호출하는 기능을 갖고 있는 MethodInvocation 오브젝트가 콜백이 되는 것이다. 템플릿은 한 번 만들면 재사용이 가능하고 여러 빈이 공유해서 사용할 수 있듯이, 어드바이스도 독립적인 싱글톤 빈으로 등록하고 DI를 주입해서 여러 프록시가 사용하도록 만들 수 있다.

프록시로부터 어드바이스와 포인트컷을 독립시키고 DI를 사용하게 한 것은 전형적인 전략 패턴 구조다.

// 포인트컷까지 적용한 ProxyFactoryBean
@Test
public void pointcutAdvisor() {
    ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(new HelloTarget());

    // 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    // 이름 비교조건 설정. sayH로 시작하는 모든 메소드를 선택하게 한다.
    pointcut.setMappedName("syaH*");

    // 포인트컷과 어드바이스를 advisor로 묶어서 한 번에 추가
    pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

    Hello proxiedHello = (Hello) pfBean.getObject();

    assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
    assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
	assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
}

ProxyFactoryBean에는 여러 개의 어드바이스와 포인트컷이 추가될 수 있다. 포인트컷과 어드바이스를 따로 등록하면 어떤 어드바이스(부가 기능)에 대해 어떤 포인트컷(메소드 선정)을 적용할지 애매해지기 때문이다. 그래서 이 둘을 Advisor 타입의 오브젝트에 담아서 조합을 만들어 등록하는 것이다. 여러 개의 어드바이스가 등록되더라도 각각 다른 포인트컷과 조합될 수 있기 때문에 각기 다른 메소드 선정 방식을 적용할 수 있다.

어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)

어드바이스와 포인트컷의 재사용

리플렉션을 통한 타깃 메소드 호출 작업의 번거로움은 MethodInvocation 타입의 콜백을 이용한 덕분에 대부분 제거할 수 있었다. ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다. 그 덕분에 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있었다.

alt text

References

리플렉션
https://brunch.co.kr/@kd4/8