6.6. Proxying mechanisms

Spring AOP는 JDK의 Dynamic Proxy 또는 CGLib을 사용하여 프록시를 만듭니다. 타겟이 되는 객체가 인터페이스를 하나라도 구현했다면 JDK의 Proxy를 사용할 것이고 구현한 인터페이스가 하나도 없을 경우에는 CGLib을 사용하게 됩니다. 원할 때는 명시적으로 CGLib을 사용하도록 할 수도 있지만 다음의 제약사항들이 있습니다.

1. final 메소드는 어드바이스가 적용되지 않습니다. – 오버라이딩 못하기 때문이죠.
2. CGLIB 라이러리가 추가로 필요합니다. – spring이 알려줄 겁니다.
3. 생성자가 두 번 호출 됩니다. – 상속해서 만든거니깐 그렇겠죠.

명시적으로 CGLIB을 사용하고 싶을 땐 다음 처럼 설정해 줍니다.

@AspectJ 기반
<aop:aspectj-autoproxy proxy-target-class=”true”/>

Schema 기반
<aop:config proxy-target-class=”true”>  
</aop:config>

6.6.1. Understanding AOP proxies

사용자 삽입 이미지일반적인 객체에 대한 호출을 그림으로 표현한 것입니다.

사용자 삽입 이미지이건 프록시 객체를 호출했을 떄를 그림으로 표현한 것입니다. 프록시에 있는 foo()를 먼저 하고 그 다음에 타겟 객체의 foo()를 호출하고 있습니다.

이때 self-invocation issue가 발생합니다. 소스 코드로 확인 하는게 좋겠습니다.

public interface Pojo {
    public void foo();
    public void bar();
}

public class SimplePojo implements Pojo {
    public void foo(){
        System.out.println(“this is foo and call bar()”);
        bar();
    }
    public void bar() {
        System.out.println(“this is bar”);
    }
}

인터페이스와 타겟이 될 클래스가 있으며 이것을 프록시팩토리를 사용하여 apsect를 만들고 간단한 어드바이스를 추가합니다.

    @Test
    public void name() {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());

        Pojo pojo = (Pojo) factory.getProxy();
        pojo.foo();
    }

추가한 어드바이스는 다음과 같습니다.

    public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
        System.out.println(“Before Adivce”);
    }

테스트를 실행하면 결과는 다음과 같습니다.

Before Adivce
this is foo and call bar()
this is bar

원하던 결과는 이게 아니죠. 원래는 다음 처럼 나와야 합니다.

Before Adivce
this is foo and call bar()
Before Adivce
this is bar

어드바이스가 타겟에 적용되었고 별다른 포인트컷 표현식을 쓰지 않았기 떄문에 모든 메소드에 적용이 되어야 합니다. foo()를 호출 했고 foo()에서 bar()를 호출 했기 때문에 bar()를 호출 할 때도 적용이 됐어야 하는데 그렇치 않았습니다.

프록시 객체를 지나서 타겟 객체에 다다른 뒤에 타겟 객체에서 자기 자신의 메소드를 호출 할 때는 프록시를 거치지 않기 때문에 이렇게 됩니다. 이걸 self-invocation issue 라고 합니다.

이 이슈의 해결 방법은.. 다소 invasive 하지만 어쩔 수 없이 다음 처럼 프록시를 통해서 호출하도록 코드를 수정해야 합니다.

public class SimplePojo implements Pojo {
    public void foo(){
        System.out.println(“this is foo and call bar()”);
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        System.out.println(“this is bar”);
    }
}

레퍼런스에는 조금 메소드가 다르더군요. 레퍼런스에 오타도 있고 메소드 이름도 잘못 되어 있었지만 이클립스가 도와주고 있기 때문에 쉽게 고쳐 쓸 수 있었습니다.

    @Test
    public void name() {
        ProxyFactory factory = new ProxyFactory(new SimplePojo());
        factory.addInterface(Pojo.class);
        factory.addAdvice(new RetryAdvice());
        factory.setExposeProxy(true);
        Pojo pojo = (Pojo) factory.getProxy();
        pojo.foo();
    }

6.4. Choosing which AOP declaration style to use

6.4.1. Spring AOP or full AspectJ?

Spring AOP는 별도의 컴파일러나 위버가 필요 없으며 AspectJ 보다 단순합니다. 하지만 컨테이너에 의해 관리되는 bean에만 advice를 적용할 수 있고 적용되는 joinpoint가 메소드 실행 시점 뿐 입니다.

도메인 객체 또는 컨테이너에 의해 관리되지 않는 객체들에 Advice를 적용해야 하거나 더 다양한 joinpoint가 필요하다면 AspectJ를 사용하는 것이 좋겠습니다.

6.4.2. @AspectJ or XML for Spring AOP?

XML 기반의 Spring AOP는 JDK 버젼이랑 관계 없이 사용할 수 있다는 장점이 있으며 설정 파일을 보고 aspect가 적용된 상태를 파악할 수 있습니다. 하지만 필요한 정보를 두 곳에 나눠 놓는 다는 점에서 DRY(Don’t Repeat Youeself) 원칙을 어긴다고 볼 수 있습니다. @AspectJ 에서는 && || ! 를 사용하여 포인트컷 끼리의 연산을 할 수가 있었는데 그걸 못합니다.

5.0 미만의 JDK를 사용해야 한다면 스키마 기반 Spring AOP를 사용하시고(유일한 선택 사항이죠.) 5.0 이상일 때 단순한 설정(예를 들어 선언적 트랙잭션 관리 같은 것) 이외에 애스팩트가 필요하다면 @AspectJ 를 사용하는게 좋겠습니다.

예제 만들면서 써보니까 전 @AspectJ가 훨씬 편하더군요.

Schema 기반 Introduction

Introduction 예제 와 동일한 예제입니다. <aop:declare-parents> 를 사용하였다는 것만 다르죠. 흐흣;;

새로 추가할 메소드를 가진 인터페이스와 그것을 구현한 클래스르 만듭니다.

public interface TicketTracked {
    void incrementTicketCount();
}

public class TicketTrackedImpl implements TicketTracked {
    static int count = 0;
    public void incrementTicketCount() {
        System.out.println(“표 ” + (++count) + ” 장 팔았다.”);
    }
}

그리고 this()를 사용하여 포인트컷을 만들어 줍니다.

<aop:pointcut id=”countTicket”
            expression=”execution(* sell*(..)) and this(ticketTracked)”/>

다음 이 포인트 컷을 사용할 introduction을 설정해 줍니다. 이 설정은 <aop:aspect> 바로 아래에 있어야 합니다. 안그럼 에러나 나더군요~

<aop:declare-parents
                types-matching=”aop.newStyle.domain.KeesunCinema”
                implement-interface=”aop.newStyle.aspect.TicketTracked”
                default-impl=”aop.newStyle.aspect.TicketTrackedImpl” />
<aop:after method=”ticketTtrack” pointcut-ref=”countTicket”/>

그리고 테스트를 해보면 원하는 결과를 확인할 수 있습니다.

    @Test
    public void sellTicket() {
        cinema.sellTicket(movie, new Date());
    }

어서 오세요. 무엇을 도와드릴까요?
어서 오세요. 무엇을 도와드릴까요?
왔어? 영화 뭐 볼껀데?
하이 공공의적보려고?
표 1 장 팔았다.
감사합니다. 공공의적을 구매 하셨습니다.
쌩큐  공공의적잘봐!

Schema 기반 Advice parameters

Advice parameters 여기서 살펴봤던 것과 거의 동일합니다. args() 표현식을 사용하여 포인트컷을 정의합니다.

<aop:pointcut id=”sellTicket2″ expression=”execution(* sell*(..)) and args(movie,..)”/>

movie라는 이름의 파라미터를 받는 메소드의 조인포인트를 가리키게 됩니다. 이걸 받아서 처리할 어드바이스를 만듭니다.

    public void veryWelcome(Movie movie){
        System.out.println(“하이 ” + movie.getName() + “보려고?”);
    }

xml 파일에 설정해 줍니다.

<aop:aspect id=”cinema” ref=”aspect”>
            <aop:before method=”welcome” pointcut-ref=”sellTicket” />
            <aop:before method=”welcome” pointcut-ref=”sellTicket” />
            <aop:after-returning method=”afterSellTicket” pointcut-ref=”sellTicket” returning=”ticket”/>
            <aop:around method=”aroundSellTicket” pointcut-ref=”sellTicket”/>
            <aop:before method=”veryWelcome” pointcut-ref=”sellTicket2″ arg-names=”movie”/>
</aop:aspect>

args-names에 넣어준 값 movie는 어드바이스 역할을 하는 메소드에서 받는 메개변수 이름입니다. 테스트를 실행하면 원하던 결과를 확인할 수 있습니다.

    @Test
    public void sellTicket() {
        cinema.sellTicket(movie, new Date());
    }

어서 오세요. 무엇을 도와드릴까요?
어서 오세요. 무엇을 도와드릴까요?
왔어? 영화 뭐 볼껀데?
하이 공공의적보려고?
감사합니다. 공공의적을 구매 하셨습니다.
쌩큐  공공의적잘봐!

before중에서 가장 마지막이고 around에서 pjp.proceed 이전에 수행하는 작업을 before로 보면 예상할 수 있는 위치에 수행된 걸 확인할 수 있습니다.

어드바이스 예제

around 어드바이스 역할을 메소드를 만듭니다. @Around 어드바이스 예제 의 예제와 거의 동일합니다.

메소드의 첫번째 인자로 ProceedingJoinPoint가 와야 하며 throws Throwable을 붙여줍니다.

    public Object aroundSellTicket(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println(“왔어? 영화 뭐 볼껀데?”);
        Ticket ticket = (Ticket) pjp.proceed();
        System.out.println(“쌩큐  ” + ticket.getMovie().getName() + “잘봐!”);
        return ticket;
    }

xml 설정 파일에 aound 어드바이스를 설정해 줍니다.

<aop:aspect id=”cinema” ref=”aspect”>
            <aop:before method=”welcome” pointcut-ref=”sellTicket” />
            <aop:before method=”welcome” pointcut-ref=”checkTicket” />
            <aop:after-returning method=”afterSellTicket” pointcut-ref=”sellTicket” returning=”ticket”/>
            <aop:around method=”aroundSellTicket” pointcut-ref=”sellTicket”/>
</aop:aspect>

테스트를 실행합니다.

    @Test
    public void sellTicket() {
        cinema.sellTicket(movie, new Date());
    }

어서 오세요. 무엇을 도와드릴까요?
왔어? 영화 뭐 볼껀데?
감사합니다. 공공의적을 구매 하셨습니다.
쌩큐  공공의적잘봐!

이 전에 사용했던 예제의 어드바이스가 같이 적용이 됐기 때문에 결과가 저러헤게 나왔습니다. 어드바이스가 적용되는 순서는 Advice ordering 여기서도 살펴봤지만 간단하게 aspect에 등록되어 있는 순이라고 생각해도 될 것 같습니다.