본문 바로가기

책/토비의 스프링 3.1(1권, 완)

6장 AOP

6.1 트랜잭션 코드의 분리

비즈니스 로직과 트랜잭션의 경계를 설정하는 코드가 서로 얽혀있다.

public void upgradeLevels() {
        TransactionStatus status = 
            this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            **List<User> users = userDao.getAll();
            for (User user : users) {
                if (canUpgradeLevel(user)) {
                    upgradeLevel(user);
                }
            }**
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }

해결책 1 : 메소드로 추출하기

비즈니스 로직을 하나의 메소드로 추출해서 이를 호출하는 방식으로 사용해도 되지만 여전히 UserService 코드에 기술적인 코드가 있다는 단점이 존재.

public void upgradeLevels() {
        TransactionStatus status = 
            this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            upgradeLevelIsInternal()
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
}

private void upgradeLevelIsInternal(){
**List<User> users = userDao.getAll();
            for (User user : users) {
                if (canUpgradeLevel(user)) {
                    upgradeLevel(user);
                }
    }
}**

해결책 2: DI 적용을 이용한 트랜잭션 분리

트랜잭션 기능이 UserService인터페이스를 구현하고 멤버 변수에 실제 비즈니스 로직를 의미하는 UserService를 넣는다. 그리고 원하는 트랜잭션 기능을 구현한다.

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;
        }
}

이때 의존관계는 다음과 같다.

Client → UserServiceTx → UserServiceImpl

위와 같이 비즈니즈 로직을 담당하는 UserServiceImpl과 기술적인 부분을 담당하는 부분을 분리할 경우 비즈니스 로직시 비즈니스 로직에만 집중할 수 있다.

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

자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시라고 부른다. 그리고 프록시를 통해서 최종적으로 요청을 위임받아 처리하는 실제 오브젝트를 타깃 또는 실제라고 부른다.

프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것이다.

프록시는

  1. 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서 사용한다. → 프록시 패턴
  2. 타깃에 부가적인 기능을 부여해주기 위해서이다. → 데코레이터 패턴

프록시와 프록시 패턴은 동일하지 않다.

데코레이터 패턴

데코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴을 말한다.

  1. 데코레이터 패턴에서는 프록시가 여러개 존재해도 괜찮다.
  2. 데코레이터 패턴에서는 프록시가 꼭 타깃을 사용하지 않아도 된다.
    1. 프록시1 → 프록시2 →프록시3 → 타깃

데코레이터의 다음 위임 대상은 인터페이스로 선언하고 생성자나 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만들어야 한다.

프록시 패턴

프록시 패턴의 프록시는 타깃의 기능을 확장하거나 추가하지 않는다. 대신 클라이언트가 타깃에 접근하는 방식을 변경해준다.

프록시 패턴에서는 타깃에 대한 레퍼런스를 제공하고 만약 실제로 필요한 경우가 생기면 그때 타깃 오브젝트를 생성해서 생성을 늦춰준다.

다이나믹 프록시

다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다.

다이내믹 프록시는 프록시 팩토리에 의해 런타임시 다이내믹하게 만들어지는 오브젝트이다.

다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다.

필요한 부가기능은 InvocationHandler를 구현한 오브젝트에 담는다.

➕InvocationHandler의 invoke메서드를 호출하면 타깃 오브젝트의 부가기능을 붙이고자 하는 메서드를 호출해준다.

public class UppercaseHandler implements InvocationHandler {
    Hello target;

    public UppercaseHandler(Hello target){
        this.target = target;
    }

    public Object invoke(Object proxy, Metho mothod, Object[] args) throws Throwable {
        String ret = (String)method.invoke(target, args); -> 타깃으로 위임
        **return ret.toUpperCase(); -> 부가기능**
    }
}
// 생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 Hello 타입으로
// 캐스팅해도 안전하다
Hello proxidHello = (Hello)Proxy.netProxyInstance(
        getClass().getClassLoader(), //동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용될 클래스 로더
        new Class[] {Hello.class}, //구현할 인터페이스
        new UppercaseHandler(new HelloTarget()) //부가기능과 위임 코드를 담은 invocationHandler
);

두번째 파라미터로 배열을 받는 것은 하나 이상의 인터페이스를 구현할수도 있기 때문이다.

ex)

public interface AInterface {
    String call();
}
@Slf4j
public class AImpl implements AInterface{
    @Override
    public String call() {
        log.info("A 호출");
        return "a";
    }
}
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {

    private final Object target;

    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    //target에 있는 메서드를 호출하면 무조건 invoke가 호출됨
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        Object result = method.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);
        return result;
    }
}
void dynamicA(){
        AInterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }

스프링 빈에는 팩토리 빈과 UserServiceImpl만 빈으로 등록한다.

팩토리 빈은 다이내믹 프록시가 위임할 타깃 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI 받아둬야 한다.

단점

  1. 여러개의 클래스에 동시에 공통 관심사를 제공하지 못한다.
  2. 하나의 타깃에 여러 개의 부가기능을 적용하려고 할 때 동일한 부가기능을 타깃에 속한 메소드에 전부 추가해줘야 한다.

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

프록시 팩토리 빈이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor인터페이스를 구현해서 만든다.

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

//        Object result = method.invoke(target, args);
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime={}", resultTime);
        return result;
    }
}
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
  • MethodInvocation은 일종의 콜백 오브젝트로, proceed()메소드를 실행하면 타깃 오브젝트의 메소드를 일종의 공유 가능한 템플릿처럼 동작하는 것이다.
  • 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스라고 부른다.

➕만약 인터페이스가 아닌 구체클래스에 대해 동적 프록시를 적용하면 CGLIB를 이용해 동적 프록시를 사용한다.

포인트 컷

메소드 선정하는 알고리즘을 담은 오브젝트

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

➕프록시 하나에 하나 이상의 어드바이저를 적용할 수 있다.

6.5 스프링 AOP

여전히 남은 문제점이 존재한다.

→ 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 프록시 팩토리 빈 설정 정보를 추가해줘야 한다.

즉 한번에 여러개의 빈에 프록시를 적용하지 못하였다.

빈 후처리기는 빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고 오브젝트 자체를 바꿔치기 할 수도 있다.

@Slf4j
    static class AToBPostProcessor implements BeanPostProcessor{
        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            log.info("beanName={} bean={}", beanName, bean);
            if (bean instanceof A){
                return new B();
            }

            return bean;
        }
    }

빈 후처리기 동작 방식

  1. 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다.
  2. 빈 후처리기의 로직을 수행한다.

포인트 컷은

  1. 오브젝트를 선택할 수 있고
  2. 오브젝트 내의 메소드를 선택할 수 있다.

포인트 컷

정규식이나 JSP의 EL과 비슷한 일종의 표현식 언어를 사용해서 포인트 컷을 작성할 수 있도록 하는 방법이다.

execution([접근제한자 패턴] 타입패턴[타입패턴.] 이름패턴(타입패턴 | "..", ...) [throws 예외패턴])

  1. [접근제한자 패턴] : public, private 와 같은 접근제한자, 생략가능
  2. 타입패턴[타입패턴.] : 리턴값의 타입 패턴 + 패키지와 클래스 이름에 대한 패턴 생략 가능하다.
  3. 이름패턴(타입패턴 | "..", ...) : 메서드 이름 타입패턴, 파라미터의 타입 패턴을 순서대로 넣을 수 있다.
    1. 와일드카드를 이용해 파라미터 개수에 상관없는 패턴을 만들 수 있다.
  4. throws 예외패턴 : 예외이름패턴

➕ pointCut에는 execution, within, args, @target, @within, @annotation, @args, bean, this, target등 다양한 문법을 지원한다.

AOP

  1. 관점 지향 프로그래밍이다.
  2. 애스펙트는 부가될 기능을 정의한 코드인 어드바이스와 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있다.

용어

타깃 : 부가 기능을 부여할 대상

어드바이스 : 부가 기능을 담은 모듈

조인 포인트 : 특정 작업이 수행되는 지점을 의미한다. (메서드, 클래스, 패키지등 다양할 수 있다.)

포인트컷 : 조인포인트를 결정하는 역할을 하는 모듈

프록시 : 부가기능을 제공하는 실제 객체

어드바이저 : 포인트 컷 + 어드바이스

다음과 같이 사용한다.

다음과 같이 사용한다.

6.6 트랜잭션 속성

' > 토비의 스프링 3.1(1권, 완)' 카테고리의 다른 글

5장 서비스 추상화  (1) 2023.11.23
4장 예외  (1) 2023.11.02
3장 템플릿  (0) 2023.10.12
2장 테스트  (1) 2023.10.05
1장 오브젝트와 의존관계  (0) 2023.09.27