Chapter4. 애스팩트 지향 스프링
ex) 전략소비량 모니터링
횡단관심사 (cross-cutting concerns)
한 애플리케이션의 여러 부분에 걸쳐있는 기능
보통은 애플리케이션의 비즈니스 로직과 개념적으로 분리된다.
- 예: 로깅, 트랜젝션, 보안, 캐싱
애스팩트 지향 프로그래밍(AOP)
횡단관심사의 분리를 위한 것이다.
4.1 AOP란 무엇인가?
AOP(Aspect Oriented Programming)
주 목적은 횡단관심사의 모듈화에 있다.
애스펙트(Aspect) AOP에서 횡단 관심사를 애스팩트(Aspect)라는 특별한 클래스로 모듈화 한다.
애스펙트 vs 상속,위임
공통 기능을 재사용하기 위한 방법이라는 점에서는 동일하지만, 애스팩트는 상속이나 위임보다 더 깔끔한 해결책을 제공한다.
- 상속: 객체의 정적 구조에 의존하므로 복잡하고 깨지기 쉬운 구조가 되기 쉽다.
- 위임: 대상 객체에 대한 복잡한 호출로 인해 번거롭다.
애스팩트는 대상 클래스를 전혀 수정할 필요가 없다는 점이 큰 차이점이다.
상속이나 위임만으로는 불가능한 모듈화가 가능하다.
애스펙트 장점
- 전체 코드 기반에 흩어져 있는 관심사항이 하나의 장소로 응집된다.
- 서비스 모듈이 자신의 주요 관심사항(핵심기능)에 대한 코드만 포함한다. (코드가 깔끔해진다.)
4.1.1 AOP 용어 정의
어떤 애스펙트의 기능(어드바이스)은 하나 이상의 조인포인트 지점을 통해 프로그램의 실행(execution)에 위빙(weaving)된다.
어드바이스(advice)
애스펙트가 '무엇'을 '언제' 할지를 정의한다.
애스펙트가 해야 하는 작업과 언제 그 작업을 수행해야 하는지를 정의한 것이다.
어드바이스 종류 | 설명 |
---|---|
이전(before) | 어드바이스 대상 메소드가 호출되기 전에 어드바이스 기능을 수행한다. |
이후(after) | 결과에 상관없이 어드바이스 대상 메소드가 완료된 후에 어드바이스 기능을 수행한다. |
반환이후(after-returning) | 어드바이스 대상 메소드가 성공적으로 완료된 후에 어드바이스 기능을 수행한다. |
예외발생이후(after-throwing) | 어드비아스 대상 메소드가 예외를 던진 후에 어드바이스 기능을 수행한다. |
주위(around) | 어드바이스가 어드바이스 대상 메소드를 감싸서 어드바이스 대상 메소드 호출 전과후에 몇가지 기능을 제공한다. |
조인포인트(join point)
어드바이스를 적용할 수 있는 곳
애스펙트를 끼워 넣을 수 있는 지점을 말한다.
- 예: 메소드 호출 지점, 예외 발생, 필드값 수정 등
포인트커트(pointcut)
애스펙트가 어드바이스를 '어디서' 할지를 정의한다.
애스펙트가 어드바이스할 조인포인트의 영역을 좁히는 역할을 한다.
어드바이스가 위빙(weaving)되어야 하는 하나 이상의 조인포인트를 정의한다.
- 예: 클래스나 메소드명, 정규표현식, 동적 포인트커트(싱핼중에 얻는 정보 이용) 등
애스펙트(aspect)
어드바이스와 포인트커트를 합친 것
어드바이스와 포인트커트를 합치면 애스팩트가 무엇을 언제 어디서 할지 필요한 정보가 모두 정의된다.
인트로덕션(introduction)
기존 클래스에 코드 변경 없이 새메소드나 멤버변수를 추가하는 기능이다.
위빙(weaving)
타깃 객체에 애스펙트를 적용해서 새로운 프록시 객체를 생성하는 절차이다.
애스펙트는 타깃 객체의 조인포인트로 위빙된다.
위빙시점 | 설명 |
---|---|
컴파일시간 (compile time) |
타깃 클래스가 컴파일 될 때 애스펙트가 위빙되며, 별도의 컴파일러가 필요하다. 예: AspectJ 위빙 컴파일러 |
클래스로드시간 (classload time) |
클래스가 JVM에 로드될 때 애스펙트가 위빙된다. 이렇게 하려면 애플리케이션에서 사용되기 전에 타깃 클래스의 바이트 코드를 enhance하는 특별한 ClassLoader가 필요하다. 예: AspectJ5의 로드시간위빙 기능 |
실행시간 (runtime) |
애플리케이션 실행 중에 애스펙트가 위빙된다. 보통 타깃 객체에 호출을 위임하는 구조의 프록시 객체를 위빙중에 AOP 컨테이너가 동적으로 만들어낸다. 예: 스프링 AOP 애스펙트 위빙 방식 |
4.1.2 스프링의 AOP 지원
AOP 프레임워크마다 제공하는 조인포인트 모델 수준이 다르고, 애스펙트를 언제 어떻게 위빙하는지도 다르다.
그러나, 어드바이스를 어느 조인포인트에 위빙할지 정의하는 포인트커트를 생성한다는 개념은 같다.
스프링 AOP 지원
스프링에서 지원되던 AOP는 AspectJ프로젝트에서 많은 것을 가져왔다.
- 고전적인(classic) 스프링 프록시 기반 AOP -> 너무 무겁고 지나치게 복잡하므로 여기서는 다루지 않는다.
- Pure-POJO 애스펙트 -> 스프링의 aop 네임스페이스를 사용하여 POJO에서 애스펙트로 전환할 수 있다. XML 설정이 필요하지만 객체에서 애스펙트로 전환하는 가장 명확한 방법이다.
- @AspectJ 애너테이션 기반 애스펙트 -> 스프링은 프록시 기반 AOP이지만 프로그래밍 모델에서 AspectJ로 애너테이션된 애스펙트를 사용할 수 있다. XML 설정이 필요없다.
- AspectJ 애스펙트에 빈 주입 (스프링 모든버전에서 지원)
1,2,3번은 스프링 AOP 구현체에서 파생된 것이므로 동적 프록시 기반으로 만들어진다.
스프링의 AOP는 메소드 가로채기(interception)로만 제한된다.
메소드 가로채기 이상(생성자 또는 멤버변수에 대한 가로채기)의 능력이 필요하다면, 4번 방식인 ApectJ를 이용하여 애스펙트를 구현해야 한다.
스프링 어드바이스는 자바로 작성
스프링에서 생성하는 모든 어드바이스는 표준 자바 클래스로 작성한다.
포인트커트는 보통 스프링 XML 설정 파일에 정의하게 된다.
AspectJ는 자바 언어를 확장한 형태로 구현되어 있다.
AOP를 위한 특별한 언어를 갖게 되어 더 강력하고 세밀한 제어가 가능하며 풍부한 AOP 도구모음을 제공한다.
하지만 새로운 도구와 문법을 배워야만 한다는 부담도 존재한다.
실행 시간에 만드는 스프링 어드바이스
스프링에서는 빈을 감싸는 프록시 객체를 실행시간에 생성함으로써 애스펙트가 스프링 관리 빈에 위빙된다.
- 프록시 객체는 타깃 객체로 위장해서 어드바이스 대상 메소드의 호출을 가로챈 후 타깃 객체로 호출을 전달(forward)
- 애스펙트의 로직은 프록시가 메소드 호출을 가로채고나서 실행되며, 그 다음에 타깃 빈의 메소드가 호출된다.
스프링은 애플리케이션이 프록시 타깃 빈을 실제 필요로 할 때까지 프록시 타깃 객체를 생성하지 않는다.
스프링은 런타임시에 프록시를 생성하므로 위빙을 위한 별도의 컴파일러가 필요하지 않다.
스프링은 메소드 조인포인트만 지원
스프링은 동적 프록시를 기반으로 AOP를 구현하므로 메소드 조인포인트만 지원한다.
따라서, 갤체 필드 값의 수정이나 객체 인스턴스화에 어드바이스를 적용할 수 없다.
그러나 메소드 조인포인트만으로도 필요한 대부분이 충족된다.
- AspectJ나 JBoss는 필드나 생성자 조인포인트까지 제공한다.
4.2 포인트커트를 이용한 조인포인트 선택
스프링 AOP에서 포인트커트는 AspectJ의 포인트커트 표현식 언어를 이용해 정의된다.
스프링은 AspectJ에서 사용할 수 있는 포인트커트 지정자(designator)에 속하는 것만 지원한다.
스프링에서 지원되는 AspectJ의 포인트커트 표현식 언어
AspectJ 지정자 | 설명 |
---|---|
args() | 인자가 주어진 타입의 인스턴스인 조인포인트 매칭을 정의 |
@args() | 전달된 인자의 런타임 타입이 주어진 타입의 애너테이션을 갖는 조인포인트 매칭을 정의 |
execution() | 메소드 실행 조인포인트와 일치시키는데 사용 |
this() | AOP 프록시의 빈 레퍼런스가 주어진 타입의 인스턴스를 갖는 조인포인트를 정의 |
target() | 대상 객체가 주어진 타입을 갖는 조인포인트를 정의 |
@target() | 수행 중인 객체의 클래스가 주어진 타입의 애너테이션을 갖는 조인포인트를 정의 |
within() | 특정 타입에 속하는 조인포인트를 정의 |
@within() | 주어진 애너테이션을 갖는 타입 내 조인포인트를 정의 (스프링 AOP를 사용할 때 주어진 애너테이션을 사용하는 타입으로 선언된 메소드 실행) |
@annotation | 조인포인트의 대상 객체가 주어진 애너테이션을 갖는 조인포인트를 정의 |
AspectJ의 다른 지정자를 사용하면 IllegalArgumentExcetion 발생한다.
execution 지정자만 실제로 일치시키는 작업을 수행하고 나머지는 일치를 제한하는데 사용된다.
따라서, execution 지정자가 포인트커트의 기본 지정자이다.
4.2.1 포인트커트 작성
포인트커트 대상 정의
package concert;
publid interface Performance {
public void perform();
}
메소드 실행시 어드바이스하는 포인트커트 표현식
perform 메소드가 실행될 때마다 어드바이스를 하기 위한 포인트커트 표현식
- execution 지정자: Performance.perform 메소드를 선택
- *: 메소드 반환 타입이 무엇이든 상관없음
- (..): 인자 목록이 무엇이든지 간에 perform 메소드를 선택
execution(* concert.Performace.perform(..))
포인트커트 범위 제한
포인트커트 범위를 concert 패키지로만 제한한다.
- within 지정자: 포인트커트의 범위를 제한
- && 연산자: execution과 within 지정자를 and 관계로 결합시킨다.
- || 연산자: or 관계를 나타낼 수 있다.
- ! 연산자: 지정자의 영향을 부정하는데 사용한다.
- XML 기반의 설정에서 포인트커트를 지정할때는 && 대신 and를 사용한다. 마찬가지로, || 대신 or, ! 대신 not을 사용한다. (XML에서는 &이 특별한 의미가 있기때문)
execution(* concert.Performace.perform(..) && within(concert.*))
4.2.2 포인트커트에서 빈 선택하기
bean() 지정자
포인트커트 표현식 내에서 ID로 빈을 지정할 수 있다.
인자로 빈ID나 이름을 받고 특정 빈에 대한 포인트커트의 영향을 제한한다.
특정 빈으로 포인트커트를 좁히는 경우도 있지만, 특정 ID가 아닌 모든 빈에 적용하기 위해 부정(!)을 사용할 수도 있다.
ex1) Performance에 있는 perform() 메소드의 실행시 ID가 woodstock인 빈에 제한하여 어드바이스를 적용한다.
execution(* concert.Performace.perform(..) and bean('woodstock'))
ex2) 애스팩트의 어드바이스가 ID가 woodstock이 아닌 모든 빈에 위빙된다.
execution(* concert.Performace.perform(..) and !bean('woodstock'))
4.3 애스펙트 애너테이션 만들기
애너테이션을 사용하여 애스펙트를 만들 수 있는 기능. (@AspectJ)
어드바이스: @After, @AfterReturning, @AfterThrowing, @Around, @Before
포인트커트: @Pointcut
오토프록싱: @EnableAspectJAutoProxy
인트로덕션: @DeclareParents
4.3.1 애스펙트 정의하기
// 애스펙트로 선언
@Aspect
public class Audience {
// 포인트커트 execution(...)
// 어드바이스 Before, silenceCellPhone() {...}
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhone() {
System.out.println("Silencing cell phones");
}
...
}
@Configuration
// AspectJ 오토-프록싱 활성화
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
4.3.2 around 어드바이스 만들기
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phones");
System.out.println("Taking seats");
// 반드시 proceed()를 호출해야 한다.
jp.proceed();
System.out.println("CLAP CLAP CLAP!!!");
} catch (Throwable throwable) {
System.out.println("Demanding a refund");
}
}
4.3.3 어드바이스에서 파라미터 처리하기
// playTrack(int) int 인자
// args(trackNumber) 인자 스펙
@PointCut("execution(* soundsystem.CompactDisc.playTrack(int)) && args(trackNumber)")
public void trackPlayed(int trackNumber) {}
@Before("trackPlayed(trackNumber))
public void countTrack(int trackNumber)
4.3.4 인트로덕션 애너테이션
@Aspect
public class EncoreableIntroducer {
// value: 적용할 대상, Performance+ Performance 의 서브타입.
// defaultImpl: 인트로덕션 구현체를 제공하는 클래스, DefaultEncoreable.class
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
4.4 XML에서 애스펙트 선언하기
4.4.1 before 어드바이스와 after 어드바이스 선언하기
<aop:config>
<!-- 빈 참조 -->
<aop:aspect ref="auidence">
<!-- before 어드바이스 -->
<aop:before
<!-- 포인트커트 -->
pointcut="execution(** concert.Performance.perform(..))"
method="silenceCellPhones"/>
...
</aop:aspect>
</aop:config>
4.4.2 around 어드바이스 선언
<aop:config>
<aop:aspect ref="auidence">
<!-- around 어드바이스 -->
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))"/>
<aop:around
pointcut="performance"
method="watchPerformance"/>
...
</aop:aspect>
</aop:config>
4.4.3 어드바이스에 파라미터 전달
<aop:config>
<aop:aspect ref="trackCounter">
<!-- around 어드바이스 -->
<aop:pointcut
id="trackPlayed"
expression="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)"/>
<aop:before
pointcut-ref="trackPlayed"
method="countTrack"/>
</aop:aspect>
</aop:config>
4.4.4 애스펙트를 이용한 새로운 기능 도입
<aop:config>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"
/>
</aop:config>
4.5 AspectJ 애스펙트 주입
Aspect 정의
public aspect CriticAspect {
pointcut performance() : execution(* perform(..));
after() : returning() : perfermance() {
System.out.println(criticismEngine.getCriticism());
}
...
}
CriticAspect - CriticismEngineImpl 와이어링.
<bean id="criticismEngine"
class="com.springinaction.springidol.CriticismEngineImpl">
<property name="criticisms">
<list> ... </list>
</property>
</bean>
<bean class="com.springinaction.springidol.CriticAspect"
factory-method="aspectOf">
<property name="criticEngine" ref="criticismEngine" />
</bean>