8.4 사용할 AOP 선언방식 선택하기
일 단 주어진 요구사항을 구현하는데 관점이 가장 좋은 접근이라고 결정했다면 스프링 AOP와 AspectJ중에서 어느 것을 사용할 것인지, Aspect 언어(code) 방식, @AspectJ 어노테이션 방식, 스프링 XML 방식중에서 어느 것을 사용한 것인지 어떻게 결정하는다. 이러한 결정은 어플리케이션의 요구사항, 개발 도구, AOP에 대한 팀의 익숙도같은 많은 요소에 영향을 받는다.
8.4.1 Spring AOP냐? 완전한 AspectJ냐?
작 업할 수 있는 가장 간단한 것을 사용해라. 스프링 AOP는 개발이나 빌드 프로세스에 AspectJ 컴파일러/ 위버(weaver)를 도입해야 하는 요구사항이 없으므로 완전한 AspectJ를 사용하는 것보다 간단하다. 스프링 빈에서 작업의 실행을 어드바이즈 하는 것이 필요한 것의 전부라면 스프링 AOP가 좋은 선택이다. 스프링 컨테이너가 관리하지 않는 객체(보통 도메인 객체같은)를 어드바이즈 해야한다면 AspectJ를 사용해야 할 것이다. 간단한 메서들 실행외에 조인포인트를 어드바이즈해야 한다면 마찬가지로 AspectJ를 사용해야 할 것이다.(예를 들면 필드를 가져오거나 조인포인트를 설정하는 등)
AspectJ를 사용하는 경우 AspectJ 언어 문법("code style"이라고도 알려진)과 @AspectJ 어노테이션 방식의 선택권이 있다. 알기 쉽게 자바 5이상을 사용하지 않는다면 code style을 사용해야 한다. 설계상 관점이 커다란 역할을 한고 AspectJ Development Tools (AJDT) 이클립스 플러그인을 사용할 수 있다면 AspectJ 언어의 문법을 사용하는 것이 더 바람직하다. AspectJ 언어가 관점을 작성하기 위한 목적으로 설계된 언어이기 때문에 더 깔끔하고 간단하다. 이클립스를 사용하지 않거나 어플리케이션에서 주요 역할을 하지 않는 약간의 관점만 가지고 있다면 IDE에서 일반적인 자바 컴파일과 @AspectJ 방식을 사용하고 빌드스크립트에 관점을 위빙하는 단계를 추가하는 것을 고려해라.
8.4.2 스프링 AOP에서 @AspectJ냐? XML이냐?
스프링 AOP를 사용하기로 했다면 @AspectJ방식인지 XML방식인지를 선택해야 한다. 알기 쉽게 자바 5이상을 사용하지 않으면 XML 방식이 적절한 선택이다. 자바 5 프로젝트에서는 고려해야 할 여러가지 트레이드오프가 있다.
기 존 스프링 사용자들에게는 XML 방식이 가장 친숙할 것이다. XML 방식은 어떤 JDK 버전과도 사용할 수 있고(하지만 포인트컷 표현식내에서 이름이 붙은 포인트컷을 참조하는 것은 여전히 자바 5이상이 필요하다.) 순수한 POJO가 기반이 될 것이다. 엔터프라이스 서비스를 설정하는 도구로 AOP를 사용하는 경우 XML은 좋은 선택이 될 수 있다. (독립적으로 변경하기를 원하는 설정의 일부로 포인트컷 표현식을 고려하는 지가 좋은 검사방법이다.) XML방식을 사용하면 시스템에 어떤 관점이 있는지가 확실히 더 명확할 것이다.
XML방식에는 두가지 단점이 있다. 첫째로 한 곳에서 요구사항의 구현부를 완전히 감추지(encapsulate) 못한다. DRY 원리에서는 시스템에는 어떤 정보(knowledge)라도 단 하나의 신뢰할 수 있고 명확한 정보만 있어야 한다고 말한다. XML 방식을 사용할 때 어떻게 요구사항이 구현되었느가 하는 정보는 기반이되는 빈 클래스의 선언과 설정파일의 XML에 나누어져 있다. @AspectJ 방식을 사용하는 경우에는 이러한 정보가 감추어진(encapsulated) 하나의 모듈(관점)이 있다. 두번째로 XML 방식은 @AspectJ 방식보다 표현할 수 있는 것이 약간 더 제한적이다. XML 방식에서는 "싱글톤" 관점 인스턴스화 모델만 지원하고 XML에서 선언한 이름이 붙은 포인트컷을 결합할 수 없다. 예를 들어 @AspectJ 방식에서는 다음과 같은 것을 작성할 수 있다.
@Pointcut(execution(* get*()))
public void propertyAccess() {}
@Pointcut(execution(org.xyz.Account+ *(..))
public void operationReturningAnAccount() {}
@Pointcut(propertyAccess() && operationReturningAnAccount())
public void accountPropertyAccess() {}
XML 방식으로 처음 두 포인트컷을 선언할 수 있다.
<aop:pointcut id="propertyAccess"
expression="execution(* get*())"/>
<aop:pointcut id="operationReturningAnAccount"
expression="execution(org.xyz.Account+ *(..))"/>
XML 접근의 단점은 이러한 정의와 결합하는 'accountPropertyAccess' 포인트컷을 정의할 수 없다는 것이다.
@AspectJ 방식은 추가적인 인스턴스화 모델과 더 풍부한 포인트컷 조합을 지원한다. 모듈단위로 관점을 유지할 수 있는 것도 @AspectJ 방식의 장점이다. 또한 스프링 AOP와 AspectJ가 모두 @AspectJ 관점을 이해할 수 있다는 점도(그러므로 소비할 수 있다) 장점이다. 그러므로 나중에 추가적인 요구사항을 구현하는데 AspectJ의 기능이 필요하다고 결정했다면 아주 쉽게 AspectJ기반의 접근으로 마이그레이션 할 수 있다. 결국 스프링팀은 엔터프라이즈 서비스의 간단한 "설정"이상의 관점이 있다면 언제나 @AspectJ 방식을 더 선호한다.
8.5 관점 타입 섞기
오 토프록시 지원을 사용한 @AspectJ방식의 관점, 스키마로 정의한 <aop:aspect> 관점, 어드바이저를 선언한 <aop:advisor>, 심지어 스프링 1.2방식을 사용해서 정의한 프록시와 인터셉터를 같은 설정파일에서 섞어서 사용하는 것이 가능하다. 이 모두는 동일한 지원 메카니즘에 기반해서 구현되었고 어떤 어려움도 없이 같이 둘 수 있다.
8.6 프록싱 메카니즘
스프링 AOP는 주어진 대상객체에 대한 프록시를 생성하는데 JDK 동적 프록시와 CGLIB를 모두 사용한다. (선택할 수 있을 때는 JDK 동적 프록시를 더 선호한다.)
프 록시되는 대상 객체가 최소 하나의 인터페이스를 구현했다면 JDK 동적 프록시를 사용할 것이다. 대상 타입이 구현한 모든 인터페이스는 프록시될 것이다. 대상 객체가 어떤 인터페이스도 구현하지 않았다면 CGLIB 프록시가 생성될 것이다.
CGLIB 프록시를 사용하도록 강제하려면(예를 들어 인터페이스를 구현한 객체가 아니라 대상객체가 정의한 모든 메서드를 프록시하는 경우) 할 수 있지만 몇가지 이슈를 고려해봐야 한다.
- final 메서드는 오버라이드 할 수 없으므로 어드바이즈할 수 없다.
- JDK에서 동적 프록시를 사용할 수 있더라도 클래스 패스에 CGLIB 2 바이너리가 필요할 것이다. CGLIB이 필요한데 CGLIB 라이브러리 클래스가 클래스패스에 존재하지 않는다면 스프링은 자동으로 경고할 것이다.
- 프 록시된 객체의 생성자는 두번 호출될 것이다. 이는 CGLIB 프록시 모델의 자연스러운 결과로 각 프록시된 객체마다 하위클래스가 생성된다. 각각의 프록시된 인스턴스마다 실제 프록시된 객체와 어드바이스를 구현한 하위클래스의 인스턴스 두 개의 객체가 생성된다. JDK 프록시를 사용할 때는 이러한 동작이 없다. 생성자는 보통 할당을 하는 곳이고 실제 로직은 구현되어 있지 않기 때문에 보통 프록시된 타입의 생성자를 두번 호출하는 것은 이슈가 아니다.
<aop:config proxy-target-class="true">
<!-- 다른 빈은 여기에 정의한다... -->
</aop:config>
@AspectJ 오토프록시를 사용할 때 CGLIB 프록시를 강제하려면 <aop:aspectj-autoproxy> 요소의 'proxy-target-class' 속성을 true로 설정해라.
<aop:aspectj-autoproxy proxy-target-class="true"/>
Note
여 러 <aop:config/> 부분은 런타임시에 <aop:config/> 부분에서(보통 여러 XML 빈 정의 파일에서) 지정한 설정 중 가장 강력한 프록시 설정을 적용해서 하나의 통일된 오토프록시 창조자(creator)로 합쳐진다. 이는 <tx:annotation-driven/>와 <aop:aspectj-autoproxy/>에도 적용된다.
분 명히 말하자면 <tx:annotation-driven/>, <aop:aspectj-autoproxy/>, <aop:config/> 요소의 'proxy-target-class="true"'를 사용하면 이 세가지 모두에 CGLIB 프록시를 사용하도록 강제할 것이다.
여 러 <aop:config/> 부분은 런타임시에 <aop:config/> 부분에서(보통 여러 XML 빈 정의 파일에서) 지정한 설정 중 가장 강력한 프록시 설정을 적용해서 하나의 통일된 오토프록시 창조자(creator)로 합쳐진다. 이는 <tx:annotation-driven/>와 <aop:aspectj-autoproxy/>에도 적용된다.
분 명히 말하자면 <tx:annotation-driven/>, <aop:aspectj-autoproxy/>, <aop:config/> 요소의 'proxy-target-class="true"'를 사용하면 이 세가지 모두에 CGLIB 프록시를 사용하도록 강제할 것이다.
8.6.1 AOP 프록시 이해하기
스프링 AOP는 프록시 기반이다. 자신만의 관점을 작성하거나 스프링 프레임워크가 제공하는 스프링 AOP기반의 관점을 사용하기 전에 스프링 AOP가 프록시 기반이라는 말이 실제 의미하는 바를 이해하는 것은 무척 중요하다.
다음 코드가 설명하는 일반적이고, 프록시되지 않고 특별한 것도 없으면서 직관적인 객체 참조를 가진 첫 시나리오를 생각해보자.
public class SimplePojo implements Pojo {
public void foo() {
// 다음의 메서드 호출은 'this' 참조상에서 직접 호출한다
this.bar();
}
public void bar() {
// 다른 로직...
}
}
객체 참조상에서 메서드를 호출하면 다음에서 보듯이 해당 객체참조 상에서 메서드가 직접 호출된다.
public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// 이는 'pojo' 참조에서의 직접 메서드 호출이다
pojo.foo();
}
}
클라이언트 코드가 가진 참조가 프록시일 경우에는 약간 달라진다. 다음 다이어그램과 코드를 보자.
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
Pojo pojo = (Pojo) factory.getProxy();
// 이는 프록시상에서의 메서드 호출이다!
pojo.foo();
}
}
여 기서 이해해야할 핵심은 Main 클래스의 main(..) 내부의 클라이언트 코드가 프록시에 대한 참조를 가지고 있다는 것이다. 이는 해당 메서드 참조에서의 메서드 호출은 프록시상에서 호출된고 이러한 프록시는 해당 메서드 호출과 관계된 모든 인터셉터(어드바이스)에 위임할 수 있다는 것을 의미한다. 하지만 일단 호출되면 결국 대상 객체에 도달한다.(이 경우에는 SimplePojo 참조) this.bar()나 this.foo()처럼 그 자체에서 호출된 모든 메서드는 프록시가 아니라 this 참조에 대해서 호출될 것이다. 이는 중요한 의미를 가지는데 자기호출(self-invocation)이 실행될 기회를 가진 메서드 호출과 연관된 어드바이스가 되지 않을 것이라는 것을 의미한다.
그래서 이와 관련해서 무엇이 이뤄지는가? 가장 좋은 접근(여기서 좋은 이라는 말은 느슨하다(loosely)는 의미로 사용한다.)은 자기호출(self-invocation)이 일어나지 않도록 코드를 리팩토링 하는 것이다. 이는 확실히 개발자가 약간의 작업을 해야하지만 그대로 가장 좋고 덜 침략적인(least-invasive) 접근이다. 다음 접근은 정말로 터무니 없는 것이기 때문에 이 접근을 명확하게 설명하는 것이 조심스럽다. 다음과 같이 해서 클래스내의 로직을 스프링 AOP에 완전히 밀접하게 연결할 수 있다.(숨이 막힌다!)
public class SimplePojo implements Pojo {
public void foo() {
// 이는 동작하기는 하지만...
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// 다른 로직...
}
}
이는 코드가 스프링 AOP에 완전히 커플링되었고 클래스 자체가 AOP를 무시하고 AOP 컨텍스트에서 사용된다는 것은 인지하게 만든다. 프록시를 생성했을 때는 추가적인 설정도 필요하다.
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.adddInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// 이는 프록시상에서의 메서드 호출이다!
pojo.foo();
}
}
마지막으로 AspectJ는 프록시기반의 AOP 프레임워크가 아니므로 이러한 자기호출(self-invocation) 이슈가 없다는 것을 알아야 한다.
8.7 @AspectJ 프록시의 프로그래밍적인 생성
<aop:config> 나 <aop:aspectj-autoproxy>를 사용하는 설정에서 관점을 선언하는데 추가적으로 프로그래밍적으로 대상객체를 어드바이즈하는 프록시를 생선하는 것도 가능하다. 스프링 AOP API의 전체 내용은 다은 장에서 본다. 여기서는 @AspectJ 관점을 사용하는 프록시를 자동으로 생성하는 능력에 집중하려고 한다.
하나 이상의 @AspectJ 관점이 어드바이즈하는 대상객체에 대한 프록시를 생성하는데 org.springframework.aop.aspectj.annotation.AspectJProxyFactory 클래스를 사용할 수 있다. 다음에서 설명하듯이 이 클래스의 기본적인 사용방법은 아주 간단하다. 전체 내용은 Javadoc을 봐라.
// 주어진 대상객체에 대한 프록시를 만들 수 있는 팩토리를 생성한다
AspectJProxyFactory factory = new AspectJProxyFactory(targetObject);
// 관점을 추가한다, 클래스는 @AspectJ 관점이어야 한다
// 다른 관점과 필요한만큼 여러번 이 메서드를 호출할 수 있다
factory.addAspect(SecurityManager.class);
// 이미 존재하는 관점 인스턴스도 추가할 수 있다. 제공된 객체의 타입은 @AspectJ 관점이어야 한다
factory.addAspect(usageTracker);
// 여기서 관점 객체를 획득한다..
MyInterfaceType proxy = factory.getProxy();
8.8 스프링 어플리케이션에서 AspectJ 사용하기
이 번 장에서 지금까지 본 내용은 전부 순수 스프링 AOP다. 이번 섹션에서는 스프링 AOP가 단독으로 제공하는 기능이상이 필요한 경우 어떻게 스프링 AOP 대신(또는 추가적으로) AspectJ 컴파일러/위버를 사용하는지 볼 것이다.
스프링은 spring-aspects.jar에서 단독으로 사용할 수 있는 작은 AspectJ 관점 라이브러리를 같이 제공한다. 이 라이브러리의 관점을 사용하려면 라이브러리를 클래스 패스에 추가해야 한다. Section 8.8.1, “스프링에 도메인 객체를 의존성 주입시 AspectJ 사용하기”와 Section 8.8.2, “AspectJ에 대한 스프링의 다른 관점들(aspects)”에서 이 라이브러리의 내요와 어떻게 사용하는 지를 설명하고 있다. Section 8.8.3, “스프링 IoC를 사용해서 AspectJ 관점 설정하기”에서는 AspectJ 컴파일러로 위빙된 AspectJ 관점을 어떻게 의존성 주입하는 지를 설명한다. 마지막으로 Section 8.8.4, “스프링 프레임워크에서 AspectJ를 사용한 로드타임 위빙(Load-time weaving)”에서는 AspectJ를 사용하는 스프링 어플리케이션의 로드타임 위빙(load-time weaving)에 대한 인트로덕션을 설명한다.
8.8.1 스프링에 도메인 객체를 의존성 주입시 AspectJ 사용하기
스 프링 컨테이너는 어플리케이션 컨텍스트에 정의된 빈들을 인스턴스화하고 설정한다. 스프링 컨테이너는 주어진 이름의 빈 정의가 담고 있는 설정을 적용하기 위해 미리 존재하는 객체를 설정하는 빈 팩토리를 요청할 수도 있다. spring-aspects.jar에는 어떠한 빈에도 의존성 주입을 할 수 있는 기능을 이용하는 어노테이션 주도(annotation-driven) 관점이 포함되어 있다. 이 기능은 컨테이너의 제어범위 밖에서 생성된 객체에 사용하기 위함이다. 도메인 객체는 때로 new 오퍼레이터나 데이터베이스 쿼리를 위한 ORM 도구를 사용해서 프로그래밍적으로 생성하기 때문에 이 범주에 포함되곤 한다.
@Configurable 어노테이션은 클래스가 스프링주도(Spring-driven) 설정에 적합함을 나타낸다. 가장 간단한 경우에 단순히 마커(marker) 어노테이션으로 사용할 수 있다.
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable
public class Account {
// ...
}
이 방법으로 마커 인터페이스로 사용할 때 스프링은 정규화된 타입 이름 (com.xyz.myapp.domain.Account)과 같은 이름인 프로토타입 범위의 빈 정의를 사용해서 어노테이션이 붙은 타입(이 경우에는 Account)의 새로운 인스턴스를 설정할 것이다. 빈의 기본 이름이 해당 타입의 정규화된 이름이므로 프로토타입 정의를 선언하는 편한 방법은 id 속성은 그냥 생략하는 것이다.
<bean class="com.xyz.myapp.domain.Account" scope="prototype">
<property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
사용할 프로토타입 빈 정의의 이름을 명시적으로 지정하고 싶다면 어노테이션내에서 직접 지정할 수 있다.
package com.xyz.myapp.domain;
import org.springframework.beans.factory.annotation.Configurable;
@Configurable("account")
public class Account {
// ...
}
위와 같이 지정하면 스프링은 "account"라는 이름의 빈 정의를 찾아서 새로운 Account 인스턴스를 설정하는 정의로 사용할 것이다.
프 로토타입 범위의 빈 정의를 전혀 지정하지 않고 자동연결을 사용할 수도 있다. 스프링이 자동연결을 하도록 하려면 @Configurable 어노테이션의 'autowire' 프로퍼티를 사용해라. 이름이나 타입으로 자동연결하도록 각각 @Configurable(autowire=Autowire.BY_TYPE)나 @Configurable(autowire=Autowire.BY_NAME를 지정해라. 대안으로는 스프링 2.5에서처럼 필드나 메서드 레벨에서 @Autowired나 @Inject를 사용해서 @Configurable 빈에 명시적이고 어노테이션 주도적인 의존성 주입을 지정하는 것이 바람직하다. (더 자세한 내용은 Section 4.9, “어노테이션기반의 컨테이너 설정”를 봐라.)
마 지막으로 dependencyCheck 속성을 사용해서 새롭게 생성되고 설정된 객체내의 객체 참조들에 대해 스프링이 의존성 검사를 하도록 할 수 있다. (예시: @Configurable(autowire=Autowire.BY_NAME,dependencyCheck=true)) dependencyCheck를 true로 설정하면 모든 프로퍼티(프리미티브 타입이나 컬렉션은 아니다.)를 설정한 후에 스프링이 유효성검사를 할 것이다.
어노테이션을 자기자신에게 사용하는 것은 당연히 아무런 의미가 없다. 어노테이션이 있는 곳에서 동작하는 spring-aspects.jar의 AnnotationBeanConfigurerAspect이다. 본질적으로 관점은 "@Configurable 어노테이션이 붙은 타입의 새로운 객체의 초기화과정이 반환된 후 어노테이션의 프로퍼티에 따라 스프링을 사용해서 새롭게 생성된 객체를 설정한다."고 말한다. 이 맥락에서 Serializable 객체가 역직렬화를 겪는 것처럼 초기화과정은 새롭게 인스턴스화된 객체를 의미한다. (예시: readResolve()를 통해서)
Note
위 의 문단에서 핵심 중 하나는 '본질적으로'이다. 대부분의 경우에서 '새로운 객체의 초기화과정이 반환된 후'의 정확한 의미는 충분할 것이다. 이 문맥에서 '초기화 과정 이후'는 객체가 생성된 이후에 의존성이 주입될 것이라는 의미이다. 즉, 클래스의 생성자 바디에서는 의존성을 사용할 수 없다는 의미이다. 생성자 바디를 실행하기 이전에 의존성을 주입해서 생성자의 바디안에서 사용할 수 있게 하려면 @Configurable 선언에 다음과 같이 정의해야 한다.
AspectJ의 다양한 포인트컷 타입의 언어 시맨틱에 대한 자세한 내용은 AspectJ Programming Guide의 부록을 참고해라.
위 의 문단에서 핵심 중 하나는 '본질적으로'이다. 대부분의 경우에서 '새로운 객체의 초기화과정이 반환된 후'의 정확한 의미는 충분할 것이다. 이 문맥에서 '초기화 과정 이후'는 객체가 생성된 이후에 의존성이 주입될 것이라는 의미이다. 즉, 클래스의 생성자 바디에서는 의존성을 사용할 수 없다는 의미이다. 생성자 바디를 실행하기 이전에 의존성을 주입해서 생성자의 바디안에서 사용할 수 있게 하려면 @Configurable 선언에 다음과 같이 정의해야 한다.
@Configurable(preConstruction=true)
AspectJ의 다양한 포인트컷 타입의 언어 시맨틱에 대한 자세한 내용은 AspectJ Programming Guide의 부록을 참고해라.
이것이 동작하려면 어노테이션이 붙은 타입이 AspectJ 위버로 위빙되어야 한다. - 이 작업을 할 빌드시점의 Ant나 Maven 태스크(AspectJ Development Environment Guide의 예제를 봐라.)를 사용하거나 로딩시점의 위빙(Section 8.8.4, “스프링 프레임워크에서 AspectJ를 사용한 로드타임 위빙(Load-time weaving)”를 봐라.)을 사용할 수 있다. AnnotationBeanConfigurerAspect 자체가 스프링으로 설정되어야 한다.(새로운 객체를 설정하는데 사용할 빈 팩토리에 대한 참조를 획득하기 위해서) 스프링 context 네임스페이스는 이 작업을 하는 편리한 태그를 정의한다. 어플리케이션 컨텍스트 설정에 다음을 포함시켜라.
<context:spring-configured/>
스키마 대신에 DTD를 사용하면 다음 정의가 똑같은 역할을 한다.
<bean
class="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"
factory-method="aspectOf"/>
관 점이 설정되기 이전에 생성된 @Configurable 객체의 인스턴스는 로깅을 하는데 이슈가 될 수 있고 생성되는 객체의 설정이 없다는 경고가 나올 것이다. 예제는 스프링이 초기화할 때 도메인 객체들을 생성하는 스프링 설정의 빈이 될 것이다. 이 경우에 수동으로 설정 관점에 의존하는 빈을 지정하는 "depends-on" 빈 속성을 사용할 수 있다.
<bean id="myService"
class="com.xzy.myapp.service.MyService"
depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">
<!-- ... -->
</bean>
Note
@Configurable 실제로 런타임시에 해당 시맨틱에 의존하려는 의도가 없다면 빈 설정자 관점(bean configurer aspect)을 통해서 @Configurable 처리를 활성화하지 말아라. 특히 일반적인 스프링 빈처럼 컨테이너에 등록된 빈 클래스에 @Configurable를 사용하지 말아라. 그렇지 않으면 초기화가 컨테이너에서 한번, 관점에서 한번, 이렇게 두번 발생한다.
@Configurable 실제로 런타임시에 해당 시맨틱에 의존하려는 의도가 없다면 빈 설정자 관점(bean configurer aspect)을 통해서 @Configurable 처리를 활성화하지 말아라. 특히 일반적인 스프링 빈처럼 컨테이너에 등록된 빈 클래스에 @Configurable를 사용하지 말아라. 그렇지 않으면 초기화가 컨테이너에서 한번, 관점에서 한번, 이렇게 두번 발생한다.
8.8.1.1 @Configurable 객체의 유닛테스트
@Configurable 지원의 목표 중 하나는 하드 코딩된 검색과 연관되는 어려움없이 도메인 객체의 독립적인 유닛 테스팅을 가능하게 하는 것이다. @Configurable 타입이 AspectJ로 위빙되지 않았다면 어노테이션은 유닛테스트에는 영향을 미치지 않으므로 테스트 객체에서 목(mock)이나 스텁(stub) 프로퍼티 참조를 설정해서 평소처럼 처리할 수 있다. @Configurable 타입이 AspectJ로 위빙된 경우에도 평소처럼 컨테이너밖에서 유닛테스트를 할 수 있지만 @Configurable 객체를 생성할 때마다 해당 객체는 스프링이 설정하지 않았다는 경고메시지를 볼 것이다.
8.8.1.2 여러 어플리케이션 컨텍스트에서 동작하기
@Configurable 지원을 구현하는데 사용한 AnnotationBeanConfigurerAspect는 AspectJ 싱글톤 관점이다. 싱글톤 관점의 범위는 static 멤버의 범위와 같다. 즉, 타입을 정의하는 클래스로더마다 하나의 관점 인스턴스가 존재한다. 이는 같은 클래스로더 계층내에서 여러 어플리케이션 컨텍스트를 정의한 경우 어디에 <context:spring-configured/> 빈을 정의하고 클래스패스 어디에 spring-aspects.jar 파일을 둘 것인지를 고려해봐야 한다.
공통의 비즈니스 서비스들과 이 서비스가 필요로하는 모든 것들을 정의하는 부모 어플리케이션 컨텍스트를 공유하고 해당 서블릿마다 개별적인 정의를 담고 있는 하나의 자식 어플리케이션 컨텍스트를 가진 전형적인 스프링 웹앱 설정을 생각해 보자. 이러한 컨텍스트들은 모두 같은 클래스로더 계층내에서 같이 존재할 것이므로 AnnotationBeanConfigurerAspect는 컨텍스트 중 하나에 대한 참조만 가질 수 있다. 이 경우에는 공유된 (부모) 어플리케이션 컨텍스트에 <context:spring-configured/> 빈을 정의하기를 권장한다. 이는 도메인 객체에 주입할 서비스를 정의한다. 그 결과 @Configurable 메카니즘을 사용해서 자식(서블릿에 한정된) 컨텍스트에서 정의한 빈에 대한 참조를 가진 도메인 객체를 설정할 수 없다.(어쨌든 당신이 하려고 하는 것은 아닐 것이다.)
같은 컨테이너안에 여러 웹 어플리케이션을 배포한다면 각 웹 어플리케이션이 각자의 클래스로더를 사용해서 spring-aspects.jar의 타입을 로드하는 것을 보장해야 한다.(예를 들면 'WEB-INF/lib'에 spring-aspects.jar 를 둠으로써) spring-aspects.jar 만을 컨테이너 범위의 클래스패스(container wide classpath)에 추가했다면 아마도 원하지 않는 동일한 관점 인스턴스를 모든 웹 어플리케이션이 공유할 것이다.
8.8.2 AspectJ에 대한 스프링의 다른 관점들(aspects)
@Configurable 관점에 추가적으로 spring-aspects.jar는 @Transactional 어노테이션이 붙은 타입과 메서드의 스프링의 트랜잭션 관리에 사용할 수 있는 AspectJ 관점을 담고 있다. 이는 스프링 컨테이너 외부에서 스프링 프레임워크의 트랜잭션 지원을 사용하려는 사용자를 위한 것이 주목적이다.
@Transactional 어노테이션은 해석하는 관점은 AnnotationTransactionAspect이다. 이 AnnotationTransactionAspect 관점을 사용하는 경우에는 반드시 구현한 인터페이스(존재한다면)가 아니라 구현(implementation) 클래스에 어노테이션을 붙혀야 한다.(또는 해당 클래스의 메서드에) AspectJ는 인터페이스에 붙은 어노테이션은 상속되지 않는다는 자바의 규칙을 따른다.
클래스에 붙은 @Transactional 어노테이션은 해당 클래스의 모든 퍼블릭 작업의 실행에 기본 트랜잭션 의미(semantics)를 지정한다.
클 래스내의 메서드에 붙은 @Transactional 어노테이션은 클래스 어노테이션(존재한다면)의 기본 트랜잭션 의미를 덮어쓴다. public, protected, 기본 가시성의 메서드는 모두 어노테이션이 붙을 것이다. protected와 기본 가시성을 가진 메서드에 직접 어노테이션을 붙히는 것이 이러한 메서드의 실행에 대한 트랜잭션 경계를 얻는 유일한 방법이다.
스프링 설정과 트랜잭션 관리는 사용하고 싶지만 어노테이션은 사용하고 싶지 않은(또는 사용할 수 없는) AspectJ 프로그래머들을 위해서 자신만의 포인트컷 정의를 제공하려고 확장할 수 있는 abstract 관점도 spring-aspects.jar에 포함되어 있다. 더 자세한 내용은 AbstractBeanConfigurerAspect와 AbstractTransactionAspect 관점의 소스를 봐라. 예제로써 정규화된 클래스 이름을 매칭하는 프로토타입 빈 정의를 사용한 도메인 모델에서 정의한 객체의 모든 인스턴스를 설정하는 관점을 어떻게 작성할 수 있는지 다음의 발췌한 소스에서 보여준다.
public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {
public DomainObjectConfiguration() {
setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());
}
// 새로운 빈의 생성 (도메인 모델의 어떤 객체)
protected pointcut beanCreation(Object beanInstance) :
initialization(new(..)) &&
SystemArchitecture.inDomainModel() &&
this(beanInstance);
}
8.8.3 스프링 IoC를 사용해서 AspectJ 관점 설정하기
스 프링 어플리케이션에서 AspectJ 관점을 사용할 때 스프링을 사용해서 이러한 관점을 설정할 수 있기를 원하고 기대하는 것은 자연스럽다. AspectJ 런타임 스스로가 관점을 생성하는 책임이 있고 관점이 사용하는 AspectJ 인스턴스화 모델('per-xxx' 절)에 의존하는 스프링으로 관점을 생성하는 AspectJ를 설정하는 방법이다.
대부분의 AspectJ 관점은 싱글톤 관점이다. 이러한 관점의 설정은 아주 쉽다. 그냥 평소처럼 관점 타입을 참조하는 빈 정의를 생성하고 'factory-method="aspectOf"' 빈 속성을 포함한다. 이는 스프링이 스스로 인스턴스를 생성하려고 시도하지 않고 AspectJ에 관점 인스턴스를 요청해서 획득하도록 한다. 예를 들면 다음과 같다.
<bean id="profiler" class="com.xyz.profiler.Profiler"
factory-method="aspectOf">
<property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
싱 글톤이 아닌 관점을 설정하기가 약간 더 어렵다. 하지만 프로토타입 빈 정의를 생성하고 AspectJ 런타임이 일단 생성한 관점 인스턴스를 설정하는데 spring-aspects.jar의 @Configurable 지원을 사용해서 설정할 수 있다.
AspectJ 로 위빙하려는 몇몇 @AspectJ 관점들(예를 들어 도메인 모델 타입에 로드타임(load-time) 위빙을 사용하는 경우)과 스프링 AOP로 사용하려는 다른 @AspectJ 관점들(이러한 관점들은 모두 스르링을 사용해서 설정한다.)이 있다면 설정에서 정의한 @AspectJ 관점들의 정확한 서브셋인 스프링 AOP @AspectJ 오토프록싱을 오토프록싱에 사용하도록 지정해야 한다. 이는 <aop:aspectj-autoproxy/> 선언내부에서 하나 이상의 <include/> 요소를 사용해서 지정할 수 있다. 각 <include/> 요소는 이름패턴을 지정해서 최소 하나이상의 패턴과 매칭된 이름을 가진 빈에만 스프링 AOP 오토프록시 설정을 사용한다.
<aop:aspectj-autoproxy>
<aop:include name="thisBean"/>
<aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
Note
<aop:aspectj-autoproxy/> 요소의 이름때문에 오해하지 말아라. <aop:aspectj-autoproxy/>를 사용하면 스프링 AOP 프록시를 생성할 것이다. @AspectJ 방식의 관점 선언을 여기서 사용하지만 AspectJ 런타임은 포함되지 않는다.
<aop:aspectj-autoproxy/> 요소의 이름때문에 오해하지 말아라. <aop:aspectj-autoproxy/>를 사용하면 스프링 AOP 프록시를 생성할 것이다. @AspectJ 방식의 관점 선언을 여기서 사용하지만 AspectJ 런타임은 포함되지 않는다.
8.8.4 스프링 프레임워크에서 AspectJ를 사용한 로드타임 위빙(Load-time weaving)
로 드타임 위빙(Load-time weaving, LTW)은 AspectJ 관점들이 자바 버츄얼 머신 (JVM)에 로딩되듯이 어플리케이션의 클래스 파일에 위빙되는 과정을 말한다. 이번 섹션은 스프링 프레임워크의 특정 컨텍스트에서 LTW를 설정하고 사용하는데 집중한다. 이번 섹션은 LTW를 소개하는 섹션이 아니다. LTW의 구체적인 내용과 AspectJ로 LTW를 설정하는 자세한 내용(스프링은 전혀 사용하지 않고)은 AspectJ 개발환경 가이드의 LTW 부분을 봐라.
스 프링 프레임워크에 AspectJ LTW를 적용해서 얻는 이득은 위빙과정보다 훨씬 세밀한 제어를 할 수 있다는 것이다. '평범한' AspectJ LTW는 JVM을 기동할 때 VM 아규먼트를 지정해서 켜지는 자바 (5+) 에이전트의 영향을 받는다. 그러므로 이는 JVM의 설정이고 몇몇 경우에는 잘 동작할 것이지만 때로는 너무 거친 설정이다. 스프링에 적용된 LTW는 명확히 훨씬 세밀하고 '하나의 JVM에서 여러 어플리케이션을 사용하는' 환경(일반적인 어플리케이션 서버환경처럼)에 더 적합한 ClassLoader마다 LTW를 켤 수 있다.
더군다나, 어떤 환경에서는 이 지원으로 -javaagent:path/to/aspectjweaver.jar (이 섹션 후반에서 설명한다)나 -javaagent:path/to/org.springframework.instrument-{version}.jar를 (앞에서 spring-agent.jar라고 얘기한) 추가해야 하는 어플리케이션 서버의 실행 스크립트를 수정하지 않고도 로드타입 위빙을 적용할 수 있다. 개발자들은 보통 실행 스크립트 같은 배포설정을 담당하는 관리자들에게 의존하지 않고도 어플리케이션 컨텍스트를 구성하는 하나 이상의 파일에 로드타임 위빙을 사용하도록 수정할 수 있다.
이제 설득은 그만하고 스프링을 사용하는 AspectJ LTW의 간단한 예제를 보자. 다음 에제에서 사용한 요소에 대한 제사한 내용은 이어서 설명한다. 전체 예제는 Petclinic 샘플 러플리케이션을 보길 바란다.
8.8.4.1 첫 예제
시 스템의 성능 문제의 원인을 찾는 어플리케이션 개발자라고 가정해 보자. 프로파일링 도구를 사용하는 대신에 우리가 할 작업은 성능 매트릭스를 아주 빠르게 얻을 수 있는 간단한 프로파일링 관점을 적용해서 즉시 특정영역에 세밀한 프로파일링 도구를 적용할 수 있도록 한다.
다음은 프로파일링 관점이다. 아주 고급스러운 것은 없고 단순히 @AspectJ 방식의 관점 선언을 사용해서 빠르고 거친(quick-and-dirty) 시간에 기반한 프로파일러다.
package foo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.util.StopWatch;
import org.springframework.core.annotation.Order;
@Aspect
public class ProfilingAspect {
@Around("methodsToBeProfiled()")
public Object profile(ProceedingJoinPoint pjp) throws Throwable {
StopWatch sw = new StopWatch(getClass().getSimpleName());
try {
sw.start(pjp.getSignature().getName());
return pjp.proceed();
} finally {
sw.stop();
System.out.println(sw.prettyPrint());
}
}
@Pointcut("execution(public * foo..*.*(..))")
public void methodsToBeProfiled(){}
}
클 래스에 ProfilingAspect를 위빙하려는 AspectJ 위버를 알려주도록 'META-INF/aop.xml' 파일을 생성할 필요도 있을 것이다. 주로 ' META-INF/aop.xml'라는 파일을 자바 클래스패스에 두는 것이 표준 AspectJ의 관례이다.
<!DOCTYPE aspectj PUBLIC
"-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<!-- 어틀리케이션에 한정된 팩키지의 클래스만 위빙한다 -->
<include within="foo.*"/>
</weaver>
<aspects>
<!-- 이 관점을 위빙한다 -->
<aspect name="foo.ProfilingAspect"/>
</aspects>
</aspectj>
이 제 스프링에 설정을 할 차례다. LoadTimeWeaver 를(뒤에서 자세히 설명할 것이므로 지금은 그냥 알아만 둬라.) 설정해야 한다. 이 로드타임 위버가 하나 이상의 'META-INF/aop.xml' 파일의 관점 설정을 어플리케이션의 클래스들에 위빙하는 책임을 가지는 핵심 컴포넌트다. 다음에서 보듯이 많은 설명이 필요하지 않다는 것은 좋은 일이다.(지정할 수 있는 옵션은 더 있지만 자세한건 뒤에서 설명한다.)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!-- 서비스 객체로 이 객체의 메서드를 프로파일링 할 것이다. -->
<bean id="entitlementCalculationService"
class="foo.StubEntitlementCalculationService"/>
<!-- 로드타임 위빙을 킨다 -->
<context:load-time-weaver/>
</beans>
이제 필요한 것은 모두 준비되었다.(관점, 'META-INF/aop.xml' 파일, 스프링 설정) 동작할 때 LTW를 보여주기 위해 main(..) 메서드를 가진 간단한 드라이버 클래스를 생성하자.
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService =
(EntitlementCalculationService)
ctx.getBean("entitlementCalculationService");
// 프로파일링 관점은 이 메서드 실행의 주변에서(around) '위빙된다'
entitlementCalculationService.calculateEntitlement();
}
}
해 야할 작업이 하나 남았다. 스프링에서 ClassLoader마다 선택적으로 LTW를 적용할 수 있다고 이 섹션의 도입부에서 얘기한 것은 사실이다. 하지만 이 예제에서는 LTW를 적용하는데 자바 에이전트(스프링이 제공하는)를 사용할 것이다. 다음은 위의 Main 클래스를 실행하기 위해 사용하는 커맨드라인 명령어이다.
java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main
JVM상에서 동작하는 프로그램을 위한 에이전트를 지정하고 활성화하는 '-javaagent'는 Java 5+의 플래그이다. 스프링 프레임워크는 위의 예제에서 -javaagent 아규먼트와 같은 역할로써 제공하는 InstrumentationSavingAgent같은 에이전트를 spring-instrument.jar에 팩키징해서 제공하고 있다.
Main 프로그램을 실행하면 다음과 같이 출력된다. (calculateEntitlement() 구현부에 Thread.sleep(..)문을 추가했으므로 프로파일러가 0 밀리초 외의 것을 실제로 잡아낸다. 01234 밀리초는 AOP로 인한 오버헤드가 아니다. :))
Calculating entitlement
StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms % Task name
------ ----- ----------------------------
01234 100% calculateEntitlement
이 LTW가 완전한 AspectJ를 사용하기 때문에 스프링 빈을 어드바이징 하는데 제약이 없다. 약간 변형한 다음의 Main 프로그램은 같은 결과를 출력할 것이다.
package foo;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Main {
public static void main(String[] args) {
new ClassPathXmlApplicationContext("beans.xml", Main.class);
EntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService();
// 프로파일링 관점은 이 메서드 실행의 주변에서(around) '위빙된다'
entitlementCalculationService.calculateEntitlement();
}
}
위 의 프로그램에서 얼마나 간단하게 스프링 컨테이너를 구성하고 스프링 컨텍스트와 완전히 분리된 StubEntitlementCalculationService의 새로운 인스턴스를 생성했는지 주목해라. 프로파일링 어드바이드는 여전히 위빙된다.
확실히 이 예제는 극도로 단순하다... 하지만 스프링의 LTW 지원의 기본은 위 예제에서 모두 소개했고 이 섹션의 나머지 부분에서는 각 설정과 사용방법의 세부사항에 대한 '이유'를 살펴본다.
Note
이 예제에서 사용한 ProfilingAspect는 기초적인 것이지만 아주 유용하다. 개발자가 개발하는데 사용할 수 있는 개발시간(development-time) 관점의 좋은 예제이고 UAT나 프로덕션에 배포하는 어플리케이션 빌드에서 아주 쉽게 제외시킬 수 있다.
이 예제에서 사용한 ProfilingAspect는 기초적인 것이지만 아주 유용하다. 개발자가 개발하는데 사용할 수 있는 개발시간(development-time) 관점의 좋은 예제이고 UAT나 프로덕션에 배포하는 어플리케이션 빌드에서 아주 쉽게 제외시킬 수 있다.
8.8.4.2 관점
LTW 에서 사용하는 관점은 AspectJ 관점이어야 한다. 이 관점은 AspectJ 언어로 작성할 수도 있고 @AspectJ 방식으로도 작성할 수 있다. @AspectJ 방식은 당연히 자바 5이상을 사용할 때만 사용할 수 있지만 이는 관점이 유효한 AspectJ 이면서 스프링 AOP 관점이라는 의미이다. 게다가 컴파일된 관점 클래스는 클래스패스에서 사용할 수 있어야 한다.
8.8.4.3 'META-INF/aop.xml'
AspectJ LTW 인프라는 자바 클래스패스에 존재하는(파일을 바로 두거나 jar 파일안에 두는 것이 더 일반적이다) 하나 이상의 'META-INF/aop.xml' 파일을 사용해서 설정한다.
이 파일의 구조와 내용은 메인 AspectJ 레퍼런스 문서에 자세히 나와있고 관심있다면 해당 문서를 참고해 라. (이 섹션이 간결하지만 'aop.xml' 파일이 100% AspectJ라는 점을 인정한다. 여기에 적용할 수 있는 스프링에 한정된 정보나 시맨틱은 없으므로 여기서 제공할 수 있는 추가적인 가치는 없다. 그래서 AspectJ 개발자들이 작성한 충분히 만족스런 섹견을 다시 만들기 보다는 해당 문서를 참고하도록 하고 있다.)
8.8.4.4 필수 라이브러리 (JARS)
스프링 프레임워크의 AspectJ LTW 지원을 사용하려면 최소한 다음의 라이브러리들이 필요하다.
- spring-aop.jar (버전 2.5 이상과 필요한 모든 의존성 포함)
- aspectjweaver.jar (버전 1.6.8 이상)
- spring-instrument.jar
8.8.4.5 스프링 설정
스 프링 LTW 지원에서 LoadTimeWeaver 인터페이스가 핵심 컴포넌트다. (org.springframework.instrument.classloading 패키지에 있다) 이 인터페이스와 인터페이스를 구현한 수많은 구현체는 스프링 배포판에 포함되어 있다. LoadTimeWeaver는 런타임시에 ClassLoader에 하나 이상의 java.lang.instrument.ClassFileTransformers 추가에 대한 담당을 하고 해당 어플리케이션의 모든 방법을 가능하게 하는 것이 관점 LTW에서 이뤄지는 작업 중 하나이다.
Tip
런 타임 클래스 파일 변형의 개념에 익숙하지 않다면 계속 읽기 전에 java.lang.instrument 패키지에 대한 Javadoc API를 읽어보기를 권장한다. 이는 훌륭한 문서이므로 읽어보는 것이 아깝지 않다. 이번 섹션을 읽는내내 참조할 핵심 인터페이스와 클래스가 나와 있다.
런 타임 클래스 파일 변형의 개념에 익숙하지 않다면 계속 읽기 전에 java.lang.instrument 패키지에 대한 Javadoc API를 읽어보기를 권장한다. 이는 훌륭한 문서이므로 읽어보는 것이 아깝지 않다. 이번 섹션을 읽는내내 참조할 핵심 인터페이스와 클래스가 나와 있다.
특정 ApplicationContext에서 XML을 사용해서 LoadTimeWeaver를 설정하는 것은 한 줄만 추가하면 될 정도로 쉽다. (확실히 스프링 컨테이너로 ApplicationContext를 사용해야 할 것이다. 일반적으로 BeanFactory는 LTW 지원이 BeanFactoryPostProcessors를 사용하기 때문에 충분치 않다.)
스프링 프레임워크의 LTW 지원을 활성화하려면 LoadTimeWeaver를 설정해야 한다. LoadTimeWeaver는 일반적으로 <context:load-time-weaver/> 요 소를 사용해서 설정한다. 다음 예제에서 기본 설정을 사용하는 유효한 <context:load-time-weaver/> 정의를 볼 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:load-time-weaver/>
</beans>
위 의 <context:load-time-weaver/> 정의는 LoadTimeWeaver와 AspectJWeavingEnabler같은 LTW에 특화된 기반(infrastructure) 빈을 다수 정의하고 등록한다. 'context' 네임스페이스에서 어떻게 <context:load-time-weaver/>를 정의했는지 잘 봐라. 참조한 XML 스키마 파일은 스프링 2.5 이상의 버전에서만 사용할 수 있다는 것을 기억해라.
위의 설정은 개발자를 위해서 기본 LoadTimeWeaver 빈을 정의하고 등록한다. 기존 LoadTimeWeaver는 자동으로 탐지된 LoadTimeWeaver를 데코레이트하는 DefaultContextLoadTimeWeaver 클래스이다. '자동으로 탐지되는' LoadTimeWeaver의 정확한 타입은 런타임환경에 의존한다.(다음 표에 요약되어 있다.)
Table 8.1. DefaultContextLoadTimeWeaver LoadTimeWeavers
런타임 환경 | LoadTimeWeaver 구현체 |
---|---|
BEA의 웹로직 10에서 동작 | WebLogicLoadTimeWeaver |
IBM 웹스피어 어플리케이션 서버 7에서 동작 | WebSphereLoadTimeWeaver |
Oracle의 OC4J에서 동작 | OC4JLoadTimeWeaver |
글래스피쉬에서 동작 | GlassFishLoadTimeWeaver |
JBoss AS에서 동작 | JBossLoadTimeWeaver |
스프링 InstrumentationSavingAgent으로 시작된 JVM (java -javaagent:path/to/spring-instrument.jar) | InstrumentationLoadTimeWeaver |
폴백(Fallback), 기반하는 클래스로더가 다음의 일반적인 관례를 따르기를 기대한다. (예시: TomcatInstrumentableClassLoader와 Resin에 적용할 수 있다) | ReflectiveLoadTimeWeaver |
이 로드타임 위버들은 DefaultContextLoadTimeWeaver를 사용하는 경우 자동으로 탐지되는 LoadTimeWeavers이다. 물론 <context:load-time-weaver/> 요소의 'weaver-class' 속성값으로 정규화된 클래스이름을 지정함으로써 어떤 LoadTimeWeaver 구현체를 사용할 것인지를 정확히 지정하는 것도 가능하다. 다음 예제에서와 같이 지정한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:load-time-weaver
weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>
</beans>
<context:load-time-weaver/> 요소로 정의하고 등록한 LoadTimeWeaver는 잘 알려진 'loadTimeWeaver'라는 이름을 사용해서 스프링 컨테이너에서 나중에 획득할 수 있다. LoadTimeWeaver는 마치 하나 이상의 ClassFileTransformers를 추가하는 스프링의 LTW 인프라스트럭처 메카니즘처럼 존재한다는 것을 기억해라. LTW를 하는 ClassFileTransformer는 ClassPreProcessorAgentAdapter 클래스 (org.aspectj.weaver.loadtime 패키지에 있다)이다. 위빙이 실제로 어떻게 영향을 받는지에 대한 상세 내용은 이 섹션의 범위를 벗어나므로 더 자세한 내용은 ClassPreProcessorAgentAdapter 클래스에 대한 Javadoc의 클래스부분을 봐라.
이제 얘기할 마지막 속성은 <context:load-time-weaver/> 이다. 이 속성은 LTW를 활성화할 것인지 아닌지를 제어하는 간단한 속성이다. 이 속성은 아래에 정리해놓은 3가지 값을 지정할 수 있고 이 속성을 지정하지 않으면 기본값인 'autodetect'를 사용한다.
Table 8.2. 'aspectj-weaving' 속성 값
속성 값 | 설명 |
---|---|
on | AspectJ 위빙이 활성화되고 관점은 적절한 로드타임에 위빙될 것이다. |
off | LTW가 비활성화된다. 로드타임에 위빙되는 관점은 없다. |
autodetect | 스프링 LTW 인프라스트럭처가 최소 하나이상의 'META-INF/aop.xml' 파일을 발견하면 AspectJ 위빙이 활성화되고 발견하지 못하면 비활성화된다. 이 값이 기본값이다. |
8.8.4.6 Environment-specific configuration
이 마지막 부분은 어플리케이션 서버와 웹 컨테이너 같은 환경에서 스프링의 LTW 지원을 사용하는 경우 필요한 추가적인 설정을 설명한다.
톰캣
아파치 톰캣의 기본 클래스 로더는 클래스 변형(class transformation)을 지원하지 않기 때문에 스프링은 클래스 변형이 필요한 곳에 개선된 구현체를 제공한다. TomcatInstrumentableClassLoader라는 이름의 로더는 톰캣 5.0 이상에서 동작하고 다음과 같이 각 웹 어플리케이션마다 개별적으로 등록할 수 있다.
- 톰캣 6.0.x이나 그 상위 버전
- org.springframework.instrument.tomcat.jar를 $CATALINA_HOME/lib에 복사해라. ($CATALINA_HOME은 톰캣이 설치된 루트경로를 의미한다.)
- 웹 어플리케이션 컨텍스트 파일을 수정해서 톰캣이 커스텀 클래스로더(기본 클래스로더 대신)를 사용하도록 한다.
<Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/></Context>
아파치 톰캣 6.0.x (5.0.x/5.5.x와 비슷하다)는 다수의 컨텍스트 위치를 지원한다.- 서버 설정 파일 - $CATALINA_HOME/conf/server.xml
- 기본 컨텍스트 설정 - $CATALINA_HOME/conf/context.xml - 이 파일은 배포된 모든 웹 어플리케이션에 영향을 준다
- 웹 어플리케이션 개별 설정은 서버측의 $CATALINA_HOME/conf/[enginename]/[hostname]/[webapp]-context.xml이나 웹어플리케이션 아카이브내에 포함된 META-INF/context.xml에 배포할 수 있다.
- 톰캣 5.0.x/5.5.x
- org.springframework.instrument.tomcat.jar를 $CATALINA_HOME/server/lib에 복사해라. ($CATALINA_HOME은 톰캣이 설치된 루트경로를 의미한다.)
- 웹 어플리케이션 컨텍스트 파일을 수정해서 톰캣이 기본 클래스로더 대신 커스텀 클래스로더를 사용하도록 한다.
<Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/> </Context>
톰캣 5.0.x과 5.5.x은 다중 컨텍스트 위치를 지원한다.- 서버 설정 파일 - $CATALINA_HOME/conf/server.xml
- 기본 컨텍스트 설정 - $CATALINA_HOME/conf/context.xml - 이 파일은 배포된 모든 웹 어플리케이션에 영향을 준다.
- 웹 어플리케이션 개별 설정은 서버측의 $CATALINA_HOME/conf/[enginename]/[hostname]/[webapp]-context.xml이나 웹어플리케이션 아카이브내에 포함된 META-INF/context.xml에 배포할 수 있다.
톰캣 5.5.20 이전 버전에서는 클래스 로더를 지정하든지 공식 클래스 로더든 커스텀 클래스 로더든 상관없이 server.xml 설정내부에서 Loader 태그의 사용을 막는 XML 설정 버그가 있다. 자세한 내용은 톰캣의 버그질라를 참고해라.
톰캣 5.5.x의 5.5.20 이상버전에서는 이 문제를 해결하기 위해 useSystemClassLoaderAsParent를 false로 설정해야 한다.<Context path="/myWebApp" docBase="/my/webApp/location"> <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" useSystemClassLoaderAsParent="false"/> </Context>
이 설정은 톰캣 6 이상에서는 필요없다.
WebLogic, WebSphere, OC4J, Resin, GlassFish, JBoss
BEA 웹로직(버전 10 이상), IBM 웹스피어 어플리케이션 서버(버전 7 이상) Java EE에 대한 오라클 컨테이너(OC4J 10.1.3.1이상), 레진(3.1 이상), JBoss(5.x 이상)의 최신 버전들은 로컬 인스트루멘테이션의 기능을 가진 클래스로더를 제공한다. 앞에서 설명했듯이 context:load-time-weaver를 활성화해서 LTW를 사용할 수 있다. 특히 -javaagent:path/to/spring-instrument.jar를 추가하기 위해 실행스크립트를 수정할 필요가 없다.
글래스피쉬의 인스투르멘테이션 기능을 가진 클래스로더는 글래스피쉬의 EAR 환경에서만 사용할 수 있다. 글래스피쉬 웹 어플리케이션에서는 위에서 설명한 톰캣의 설정 가이드를 따른다.
JBoss 6.x에서는 어플리케이션이 실제로 시작되기 전에 클래스를 로딩하는 것을 막기 위해 어플리케이션 서버 스캐닝을 비활성화 해야 한다. 다음의 내용으로 WEB-INF/jboss-scanning.xml 라는 파일을 어플리케이션에 추가해서 간단히 회피할 수 있다.
<scanning xmlns="urn:jboss:scanning:1.0"/>
일반적인 자바 어플리케이션
클 래스 인스트루맨테이션을 지원하지 않는 환경에서 클래스 인스트루맨테이션이 필요하거나 존재하는 LoadTimeWeaver 구현체가 지원하지 않는 경우에는 JDK가 유일한 해결책이다. 이러한 경우에 스프링은 스프링에 한정된(하지만 아주 일반적인) VM 에이전트 InstrumentationLoadTimeWeaver를 org.springframework.instrument-{version}.jar에서 제공한다.(이전에는 spring-agent.jar라는 이름이었다.)
이를 사용하려면 반드시 다음의 JVM 옵션을 사용해서 스프링 에이전트로 가상머신을 시작해야 한다.
-javaagent:/path/to/org.springframework.instrument-{version}.jar
어플리케이션 서버 환경이(운영정책에 달려있다) 스프링의 VM 에이전트를 사용하는 것을 막을 수도 있으므로 VM 실행스크립트를 수정해야할 필요가 있다. 게다가 JDK 에이전트는 비용이 많이 들 수 있는 전체 VM을 사용할 것이다.
성능때문에 사용하는 환경(Jetty처럼)이 전용 LTW를 갖지 않은(또는 지원하지 않는) 경우에만 이 설정을 사용하기를 권한다.
8.9 추가 자료
AspectJ 웹사이트에서 AspectJ에 대한 더 자세한 내용을 볼 수 있다.
Adrian Colyer et. al.이 쓴 Eclipse AspectJ (Addison-Wesley, 2005)에 AspectJ 언어에 대한 광범위한 소개와 레퍼런스가 나와있다.
Ramnivas Laddad가 쓴 AspectJ in Action (Manning, 2003)를 강력히 추천한다. 이 책은 AspectJ에 초점을 맞추고 있지만 일반적인 AOP 주제들을 많이 다루고 있다.(꽤 상세하게)
Comments