@Configurable 왜이리 안 되지;

http://forum.springframework.org/showthread.php?t=43690
위 링크에 있는 것과 똑같은 현상이 벌어지고 있습니다.

1. @Confiurable 애노테이션을 도메인 객체위에 붙이고…
2. XML 설정에서 해당 도메인 객체를 등록하고 이 때 scope을 prototype으로…
3. <context:spring-configured /> 추가해주고.. (@Configurable 붙은 녀석이 애플리케이션에서 생성될 때 스프링이 관리하도록…)
4. <context:load-time-weaver/> 추가해주고.. (LTW 사용해야 3번일을 할 수 있으니까..)
끝..

이렇게 하면

    @Test
    public void testDI() throws Exception {
        Member member = new Member();
        assertNotNull(member.getRepository());
    }

이 테스트가 통과해야 하는데..

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.context.weaving.AspectJWeavingEnabler#0’: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘loadTimeWeaver’: Initialization of bean failed; nested exception is java.lang.IllegalStateException: ClassLoader [sun.misc.Launcher$AppClassLoader] does NOT provide an ‘addTransformer(ClassFileTransformer)’ method. Specify a custom LoadTimeWeaver or start your Java virtual machine with Spring’s agent: -javaagent:spring-agent.jar

-javaagent:spring-agent.jar 옵션을 추가해서 실행시키라는 에러가 발생합니다.

그래서 이클립스의 Run -> Open Reun Dialog 클리갛고, JUnit 에서 우클릭 한다음 new로 새로 하나 만들고 아규먼트에 인자를 다음과 같이 줬습니다.

-javaagent:sd:\eclipse\workspace\spring2.5\lib\spring-agent.jar

하지만 에러 메시지는 동일합니다. OTL…
레퍼런스에서 Table 6.1. DefaultContextLoadTimeWeaver  LoadTimeWeavers 표를 보고 위버 설정 파일을 다음과 같이 수정했습니다.

<context:load-time-weaver weaver-class=”org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver”/>

자… 그랬더니 이번에는 레퍼런스를 보라는 에러가…ㄷㄷㄷ

java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.

@Configurable 사용해야 하는 이유

DTO를 공부하다가 들었던 의문.. 그동안 내가 만들었던 DO들의 빈약함(Anemic Domain Model)에 대해 명쾌하고 깔끔하게 정리를 해주신 글을 발견했습니다. 그리고 그 대안으로 Rich 또는 Smart DO 그리고 DDD로 이어지는 흐름도 좋은 글입니다. 머리가 맑아지는 동시에 공부할 것들로 머리가 꽉차게 되는 그런 글이기도 합니다.

스프링프레임워크와 DDD(Driven Driven Design)

기존에는 도메인 객체를 모든 레이어에 걸쳐서 사용했었습니다. 그런데 DDD를 하면 도메인이 서비스 계층과 리파지토리 계층 사이에 끼이게 됩니다. 이제 하나의 계층으로 자리를 잡게 되는 것이죠.
사용자 삽입 이미지출처: https://www.dbguide.net/know/know102001.jsp?mode=view&divcateno=9&divcateno_=9&pg=1&idx=3229

이렇게 되면 도메인 객체에 무언가(DAO나 Repository) 주입시켜주어야 합니다. 그런데.. 이 주입시키는 일 Dependency Injection을 사용하려면 스프링이 관리하는 bean이어야 합니다. 즉 스프링 설정 파일에 도메인 객체가 bean으로 등록되어 있어야 한다는 것이죠. 하지만 문제는 도메인 객체들은 bean으로 설정하여 종속성을 관리할 객체로는 적당하지 않습니다.

이 객체들의 생성을 스프링 컨테이너가 관리할 수 없습니다. 이 객체들은 애플리케이션 동작 중에 생성되기 때문에, 스프링이 생명주기를 관리할 bean으로 등록하여 사용한다는 것은 말이 안됩니다.

이럴 때 등장하는 녀석이 바로 저 @Configurable 애노테이션 입니다. 이녀석이 붙어있는 클래스를 스프링에 등록해두면, 스프링은 그 객체가 생성될 때 그 객체가 필요로 하는 bean들을 주입해 줍니다. 엄청나죠. 이런 일이 어떻게 가능할까요. 바로 AOP 때문에 가능하죠. method 호출 Joinpoint만을 지원하는 스프링 AOP로는 이런 일을 할 수 없습니다. 생성자 실행 Joinpoint를 지원하는 AspectJ가 할 수 있는 일이죠.

그래서 @Configurable을 사용하려면 AsepctJ Load Time Weaver가 필요합니다.

결국 스토리는 이렇게 됩니다.

  1. DDD를 하고싶어. (도메인 객체가 DAO를 필요로 하고 있어.)
  2. @Configurable을 사용해야 겠다. (애플리케이션 실행 도중 생기는 객체도 스프링에서 관리할꺼야.)
  3. LTW가 필요하구나.

아니면 간단하게 다음과 같이 해도 되죠.

  1. DDD를 하고싶어. (도메인 객체가 DAO를 필요로 하고 있어.)
  2. 도메인 생성자에서 new 써주지뭐.

아래의 스토리가 더 짧고 간단합니다. 그런데도 저는 첫 번째 스토리대로 하고 싶습니다. 왜냐면, 스프링이 도중에 생겨난 객체들 까지 관리하게 되면, 그 객체들(풍성해진 도메인)에 스프링의 DI와 AOP를 적용할 수 있기 때문이죠. 아래 스토리대로 하면, 도메인 객체가 참조하는 DAO가 바뀌면 소스코드를 매번 바꿔줘야 되겠죠. 그리고 다른 코드는 전부 DI랑 AOP 사용해놓고 도메인 계층만 왕따 시키는 것도 아니고.. 좀 그렇차나요.ㅎㅎ;

그래서.. 좀전까지 @Configurable과 LTW랑 팀을 맺고 저랑 2:1로 씨름을 하고 있었습니다. 제가 졌습니다. 오늘은..-_-;; 잘 안 되더라구요. Eclipse에서 JVM 아규먼트 설정해 주는게 틀렸나.. 왜 그러징;;

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 장 팔았다.
감사합니다. 공공의적을 구매 하셨습니다.
쌩큐  공공의적잘봐!