[Spring] 토비의 스프링 Vol.2 5장 AOP와 LTW

Shiba·2023년 12월 1일
0

🍀 스프링 정리

목록 보기
19/21
post-thumbnail

📗AOP와 LTW

5장에서는 스프링의 또 다른 AOP 개발 방법인 @AspectJ AOP를 만드는 방법을 설명할 것이다. 또한, 스프링의 프록시 방식 AOP 대신 AspectJ 라이브러리를 직접 활용하는 방법과 로딩 시점의 바이트코드 조작을 통해 DI 기능을 확장하는 방법 등을 알아볼 것이다.

📖 애스펙트 AOP

📝 프록시 기반 AOP

◼ 프록시 기반 AOP 개발 스타일의 종류와 특징

AOP모듈화된 부가기능과 적용 대상의 조합을 통해 여러 오브젝트에 산재해서 나타나는 공통적인 기능을 손쉽게 개발하고 관리할 수 있는 기술
- 스프링은 자바 JDK에서 지원하는 다이나믹 프록시 기술을 이용해 AOP를 적용하는 프록시 기반 AOP 개발 기능을 제공.(Vol.1 6장)

스프링은 여러 가지 종류의 프록시 기반 AOP 개발 방법을 지원한다.

AOP 인터페이스 구현과 <bean>등록을 이용하는 방법

가장 기초적인 프록시 AOP 개발 방법
- AOP의 구성요소를 모두 클래스로 개발하고 이를 빈으로 등록해서 적용하는 방법
- 가장 오래되고 원시적인 방법이지만, AOP의 동작원리를 이해하기에는 가장 쉽다.
- 스프링이 제공한 인터페이스를 구현하는 방식으로 어드바이스와 포인트컷을 개발한 후, 이 클래스들을 빈으로 등록한 뒤 어드바이스와 포인트 컷의 조합을 어드바이저로 구성.

AOP 인터페이스 구현과 aop 네임스페이스의 <aop:advisor> 태그를 이용하는 방법

어드바이스 개발은 여전히 인터페이스를 구현하는 방식으로 하지만, 포인트컷 표
현식을 적극 활용
해서 포인트컷과 어드바이저, 그리고 이를 모두 적용하는 자동 프록시 생성기 빈 등록을 aop 스키마의 간결한 전용 태그만으로 가능하게 하는 방법
- 빈을 등록하는 것 보다 설정이 단순하고 이해하기 쉬워짐

임의의 자바 클래스와 aop 네임스페이스의 <aop:aspect>를 이용하는 방법

aop 네임스페이스를 사용하지만 어드바이스, 어드바이저 대신 애스펙트라는 개념을 사용한다.
- 스프링에는 AOP개념으로 접근한 독특한 모듈인 어드바이저를 가지고 있음.
하지만 AspectJ로 대표되는 객체지향 기술의 확장 언어를 지향하는 AOP에서는 AOP모듈을 정의하는 방법이 따로 존재
- 스프링 2.0에서부터 AOP 전용 기술을 대폭 도입하면서 애스펙트를 정의해서 AOP를 만드는 방법을 함께 제공하고 있다.

애스팩트는 인터페이스를 구현할 필요 없이 일반 자바 클래스에 애노테이션이나 XML의 <aop:aspectj> 태그를 이용해서 평범한 자바 클래스를 AOP 애스펙트로 만들 수 있다.
- 인터페이스 구현 방식 AOP와 애스펙트 방식 AOP의 차이점은 스프링 MVC의 인터페이스 구현 컨트롤러와 애노테이션 방식 컨트롤러의 차이점과 같다고 이해해도 좋다.

@AspectJ 애노테이션을 이용한 애스펙트 개발 방법

@AspectJ는 이름 그대로 AspectJ AOP 프레임워크에서 정의된 애노테이션을 이용해 애스펙트를 만들 수 있게 해준다.
- @AspectJ 문법과 애스펙트 정의 방법을 차용했을 뿐, AspectJ AOP를 사용하는 것은 아니다.
- @AspectJ도 여타 방법과 동일하게 스프링의 프록시 기반 AOP를 만들 때 사용

◼ 자동 프록시 생성기와 프록시 빈

스프링 AOP를 사용한다면 어떤 개발 방식을 적용하든 모두 프록시 방식의 AOP다.
프록시 방식의 AOP는 Vol.1에서 자세하게 설명했으니, 여기서는 프록시를 만드는 방법에 따라 등록되는 빈의 종류와 사용방법을 생각해보자.

ex) Client가 Target을 DI받아 사용하는 관계
다음과 같이 Client가 Target을 알고 있으면 안됨

public class Client {
	@Autowired Target target;
    ...
}

public class Target { ... }

따라서, Target이 구현하고 있는 인터페이스를 이용해 의존하도록 만들어야 한다.

public class Client {
	@Autowired Interface intf;
    ...
}

interface Interface { ... }

public class Target implements Interface { ... }

이제 프록시 클래스를 인터페이스를 구현하여 만들면 된다.

public class Proxy implements Interface {
	private interface next;
    public void setNext(interface next) { this.next = next; }
}

DI 설정을 조작하여 Client -> Proxy -> Target 순서로 의존관계를 맺게 하면 Proxy가 Client와 Target의 호출과정에 끼어들어서 부가기능을 제공할 수 있게 해준다.

위 방식에서는 proxy 빈이 추가가 되는데 이 때문에 문제가 생긴다.
바로 @Autowired의 자동 와이어링을 사용할 수 없게 된다. 프록시와 타겟이 둘다 같은 인터페이스를 구현했기 때문에 @Autowired는 자동으로 빈을 선택할 수 없게된다.
그렇다면 프록시를 자동 프록시 생성 기법을 이용해 AOP로 확장해준 경우에는 어떨까?
프록시 자체는 AOP가 아니다. 그래서 스프링은 자동 프록시 생성기를 이용해서 컨테이너 초기화 중에 만들어진 빈을 바꿔치기해 프록시 빈을 자동으로 등록해준다.

자동 프록시 생성을 통해 만들어진 프록시는 수동으로 등록한 프록시와 다른 점이 존재하는데 자동 프록시 생성기는 프록시 빈을 별도로 추가하고 DI 설정만 바꾸는 것이 아닌, 프록시를 적용할 대상 자체를 아예 자신이 포장해서 마치 그 빈처럼 동작한다는 것이다. 즉, 자동 프록시 생성기가 만들어주는 프록시는 AOP 대상 타겟 빈을 대체한다.

AOP를 사용하려면 자동 프록시 생성기의 특징을 잘 기억해두어야 한다. 스프링의 모든 프록시 기반 AOP는 자동 프록시 생성기를 통해 동작하기 때문에 어떤 방식을 쓰든 공통적으로 적용되는 특징이다.

🔷 자동 프록시 생성기의 특징

AOP 적용은 @Autowired의 타입에 의한 의존관계 설정에 문제를 일으키지 않는다

직접 타깃 오브젝트의 인터페이스를 구현한 프록시를 적용한 경우에는 클라이언트가 @Autowired를 사용할 수 없었다. 하지만, 자동 프록시 생성기를 사용하는 프록시 AOP에서는 이런 문제가 발생하지 않음

AOP 적용은 다른 빈들이 Target 오브젝트에 직접 의존하지 못하게 한다

자동 프록시 생성기는 Target 클래스를 프록시 안에 감춰버린다.
- Target이라는 클래스 타입으로 의존하는 빈이 있는 경우, Target이라는 빈이 감춰져있어 빈을 찾을 수 없기 때문에 DI가 되지 않는다.

❗ 이 특징은 꼭 기억해두자. 잘 동작하던 코드가 AOP를 적용하면 DI관련 에러가 생길 수 있다.

◼ 프록시의 종류

스프링은 클래스를 직접 참조하면서 강한 의존관계를 맺고 있는 경우에도 프록시를 적용할 수 있다. 이를 이용하면 Client → Target과 같이 직접적인 의존관계를 만든 경우에도 프록시를 적용할 수 있다.

타깃 클래스 자체를 인터페이스처럼 사용한다.
- 타깃 클래스를 상속한 서브클래스를 프록시로 사용한다.

위 방식에는 제약이 따른다.

  • final 클래스와 final 메소드는 적용이 안됨.
    - 상속 및 오버라이딩이 불가능
  • 타깃 클래스의 생성자가 두 번 호출됨.
    - 같은 타깃 클래스 타입의 빈이 두 번 만들어지기 때문
  • 클래스 프록시를 만들기 위해서는 CGLib 라는 바이트코드 생성 라이브러리가 필요.
    - 외부 라이브러리에 대한 의존도가 증가
  • 인터페이스처럼 프록시로 노출할 메소드를 선별할 수도 없음.

클래스 프록시는 그렇다면 왜 지원하는 것일까?

인터페이스를 만들지 않고 개발한 레거시 코드외부에서 개발한 인터페이스 없는 라이브러리의 클래스 등에도 원한다면 AOP를 적용할 수 있게 해주려는 것일 뿐이다.

클래스를 이용한 프록시를 적용하는 방법

  • 아무런 인터페이스도 구현하지 않은 타깃 클래스에 AOP를 적용하는 것
    - 인터페이스가 없는 경우에는 CGLib을 통해 클래스 프록시를 만듦
  • 강제로 클래스 프록시를 만들도록 설정
    - 인터페이스가 있더라도 강제로 클래스 프록시를 만듦
<aop:config proxy-target-class="true">
	...
</aop:config>

📝 @AspectJ AOP

◼ @AspectJ를 이용하기 위한 준비사항

@AspectJ는 특정 AOP 개발 방법을 가리키는 용어이지, 애노테이션이 아니다.
@AspectJ 방식의 애스펙트를 사용하려면 XML 설정파일에 다음과 같이 aop 스키마의 태그를 이용한 선언을 넣어줘야 한다.

<aop:aspectj-autoproxy />

위 선언은 빈으로 등록된 클래스 중에서 클래스 레벨에 @AspectJ가 붙은 것을 모두 애스펙트로 자동 등록해준다.

두번째로 AspectJ의 런타임 라이브러리를 클래스패스에 추가해줘야 한다.
포인트컷 표현식을 사용했다면 AspectJ 라이브러리가 추가되어 있을 것이다.

◼ @Aspect 클래스와 구성 요소

애스펙트는 자바 클래스에 @Aspect라는 애노테이션을 붙여서 만든다.

@Aspect
public class SimpleMonitoringAspect {
	...
}

이 클래스를 애스펙트로 사용하려면 빈으로 등록해야 한다.

@Aspect 클래스에는 포인트컷과 어드바이스를 정의할 수 있다. 두 가지 모두 애노테이션이 달린 메소드를 사용해 정의한다.

포인트컷 : @Pointcut

포인트컷은 @Pointcut 애노테이션이 달린 메소드를 이용해 선언.
- 선택 로직@Pointcut 안에 포인트컷 표현식을 넣어서 정의한다.
- 메소드의 선언부를 메타정보로 이용포인트컷의 이름과 파라미터를 정의하는 용도로 사용
- 하나의 @Aspect 클래스 안에 여러 개의 포인트컷을 선언할 수 있음.

@Pointcut("execution(* hello(..))")
private void all() {}

어드바이스 : @Before, @AfterReturning, @AfterThrowing, @After, @Around

어드바이스도 포인트컷과 마찬가지로 애노테이션이 붙은 메소드를 이용해 정의한다.
@AspectJ에서는 다섯 가지 종류의 어드바이스를 사용할 수 있으며, 각 종류별로 애노테이션이 하나씩 정의되어 있다.
- 어드바이스의 애노테이션에는 이에 적용할 포인트컷을 명시해야 한다.
- 메소드의 파라미터와 리턴 값은 어드바이스 종류와 포인트컷에서 선언한 파라미터에 따라 달라질 수 있다.
- 하나의 @Aspect 클래스 안에 여러 개의 어드바이스 정의 가능

public Object printParameterAndReturnVal(ProceedingJoinPoint pjp) throws
		Throwable {
    ...
    Object ret - pjp.proceed();
    ...
    return ret;
}

@Aspect 클래스에는 포인트컷과 어드바이스를 정의하기 위한 메소드 외에도 일반 필드나 단순 메소드를 포함할 수 있다. 또, 상속과 인터페이스 구현 또는 DI를 통한 다른 빈의 참조도 모두 가능하다.

◼ 포인트컷 메소드와 애노테이션

@Aspect 클래스 안에서 포인트컷을 정의하는 방법을 자세히 살펴보자.

포인트컷은 @Pointcut 애노테이션과 메소드의 이름, 파라미터로 정의된다.

@Pointcut("execution(* sayHello(..))") private void hello();
//리턴타입은 반드시 void로 한다.
// ()까지 포함해서 포인트컷 이름으로 사용된다.

포인트컷은 적용할 조인 포인트를 선별하는 것이다. 조인 포인트는 어드바이스로 정의된 부가기능을 적용할 수 있는 위치이다. 스프링에서는 프록시 방식의 AOP를 사용하기 때문에 조인 포인트는 메소드 실행 지점뿐이다.
따라서, 포인트컷 설명에서 조인 포인트라고 하면 메소드를 가리킨다고 이해하면 된다.

지금부터는 포인트컷 표현식에서 사용할 수 있는 여러 포인트컷 지시자에 대해 알아보자

execution()

가장 대표적이고 가장 강력한 포인트컷 지시자.
접근제한자, 리턴 타입, 타입, 타입메소드, 파라미터 타입, 예외 타입 조건을 조합해서 메소드 단위까지 선택가능한 가장 정교한 포인트컷을 만들 수 있음

within()

타입 패턴만을 이용해 조인 포인트 메소드를 선택.
- 타깃 클래스의 타입에만 적용되며 조인 포인트는 타깃 클래스 안에서 선언된 것만 선정된다는 점이 다르다. 선택된 타입의 모든 메소드가 AOP 적용 대상이 된다.

this, target

여러 개의 타입을 고를 수 있는 타입 패턴이 아니라 하나의 타입을 지정하는 방식
- 오브젝트를 선별한다.
- this는 빈 오브젝트의 타입, target은 타깃 오브젝트의 타입과 비교

args

메소드의 파라미터 타입만을 이용해 포인트컷을 설정할 때 사용
- execution 지시자의 () 안에 들어가는 파라미터 타입과 동일하다고 보면 된다.

@target, @within

@target : 타깃 오브젝트에 특정 애노테이션이 부여된 것을 선점
@within : 타깃 오브젝트의 클래스에 특정 애노테이션이 부여된 것을 찾는다.
- @target과 비슷하지만 조인 포인트인 메소드는 타깃 클래스에 선언되어 있어야 한다. ( 슈퍼 클래스의 메소드는 해당 되지 않는다.)
- within과 다르게 패턴을 사용하지 않고 특정 타입을 지정한다는 것에 주의

@args

args와 유사하게 파라미터를 이용해 선정. 파라미터 오브젝트에 지정된 애노테이션이 부여되어 있는 경우 선정 대상이 된다.

@annotation

조인 포인트 메소드에 특정 애노테이션이 있는 것만 선정하는 지시자.

bean

빈 이름 또는 아이디를 이용해서 선정하는 지시자
- 와일드 카드 사용 가능

포인트컷 표현식은 논리표현을 통해 조합하는 것도 가능하다!

&&

두 개의 포인트컷 또는 지시자를 AND로 결합한다.

||, !

|| : OR 조건.
! : NOT 조건.

◼ 어드바이스 메소드와 애노테이션

@AspectJ 클래스에 정의하는 어드바이스도 포인트컷과 마찬가지로 애노테이션과 메소드를 사용한다.
어드바이스는 다섯 가지 종류가 존재한다.
- 어드바이스를 쉽게 작성할 수 있도록, 메소드 실행 과정의 일부분에만 적용하도록 만든 어드바이스가 있기 때문

어드바이스가 프록시의 메소드 전체를 구현하게 할 수도 있지만, 그 중 일부분에만 참여하도록 만들 수도 있다. 다음은 어드바이스를 적용할 수 있는 지점을 표현한 것이다.

어드바이스가 어느 단계에 적용되느냐에 따라 어드바이스의 종류가 달라진다. 어드바이스의 종류에 따라서 다섯 가지 어드바이스 애노테이션 중 하나를 선택해서 사용한다.

@Around

프록시를 통해서 타깃 오브젝트의 메소드가 호출되는 전 과정을 모두 담을 수 있는 어드바이스.
MethodInterceptor 인터페이스와 유사함.

@Around("myPointcut()")
public Object doNothing(ProceedingJoinPoint pjp) throws Throwable {
	Object ret = pjp.proceed();
    return ret;
}

- 파라미터로는 ProceedingJoinPoint 타입의 오브젝트를 받는다.
타깃 오브젝트의 메소드를 실행하고 그 결과를 받을 수 있다.

@Around는 가장 강력한 기능을 가진 어드바이스이다.
- 타깃 오브젝트의 메소드를 여러 번 호출하거나, 호출 파라미터를 바꿔치기하거나, 심지어 타깃 오브젝트 메소드를 호출하지 않도록 만들 수도 있다.

@Before

타깃 오브젝트의 메소드가 실행되기 전에 사용되는 어드바이스

@Before("myPointcut()")
public void logJoinPoint(JoinPoint jp) {
	System.out.println(jp.getSignatue().getDeclaringTypeName());
    System.out.println(jp.getSignatue().getName());
    for(Object org : jp.getArgs()) { System.out.println(arg); }
}

@AfterReturning

타깃 오브젝트가 실행을 마친 뒤 실행하는 어드바이스. 단 예외가 발생하지 않고 정상적으로 종료한 경우에만 해당. 메소드의 리턴 값 참조 가능

@AfterReturning(pointcut="myPointcut()", returning="ret")
public void logReturnValue(Object ret) {
	...
}

- 애노테이션의 returning 엘리먼트를 이용해서 리턴 값을 담을 파라미터 이름을 지정해야 한다.

@AfterThrowing

타깃 오브젝트의 메소드를 호출했을 때 예외가 발생하면 실행되는 어드바이스

@AfterThrowing("pointcut="daoLayer()". throwing="ex")
public void logDAException(DataAccessException ex) {
	...
}

- throwing 엘리먼트를 이용해서 예외를 전달받을 메소드 파라미터 이름을 지정할 수 있다.
- throwing으로 지정한 파라미터의 타입이 발생한 예외와 일치할 경우에만 어드바이스가 호출된다.

@After

메소드의 실행이 정상 종료됐을 때와 예외가 발생했을 때 모두 실행되는 어드바이스
- 코드에서 finally를 사용했을 때와 비슷한 용도
- 반환돼야 하는 리소스가 있거나 메소드 실행 결과를 항상 로그로 남겨야 하는 경우에 사용할 수 있다. 하지만 리턴 값이나 예외를 전달받을 수는 없다.

◼ 파라미터 선언과 바인딩

포인트컷 표현식의 타입 정보를 파라미터와 연결하는 방법이 존재
- 포인트컷 표현식 내에 길게 선언했던 타입 정보를 파라미터 타입 정보로 대신하게 할 수 있어서 포인트컷 표현식이 간결해짐
- 어드바이스의 메소드 파라미터로 해당 값을 전달받아 활용할 수 있기 때문에 편리

//기존의 선언 방식
@Before("@target(com.epril.myproject.special.annotation.BatchJob)")
public void beforeBatch() { ... }
//
//파라미터 선언
import com.epril.myproject.special.annotation.BatchJob;
...
@Before("@target(bj)")
public void beforeBatch(BatchJob bj) { ... }

포인트컷 표현식 내에 파라미터 이름은 포인트컷 메소드 또는 어드바이스 메소드의 파라미터 이름과 일치해야 한다.

📖 AspectJ와 @Configurable

📝 AspectJ AOP

AspectJ에서는 왜 애스펙트 적용을 위해 재컴파일이나 바이트코드 조작 같은 것이 필요할까?

프록시 방식으로는 어드바이스를 적용할 수 없는 조인 포인트와 포인트컷 지시자를 지원하기 위해서
- AspectJ의 조인 포인트는 메소드 실행 지점 외에도 필드 읽기와 쓰기, 스태틱 초기화, 인스턴스 생성, 인스턴스 초기화 등도 지원

그렇다면 스프링 애플리케이션에 AspectJ AOP가 필요할까?

99퍼센트의 애플리케이션은 메소드 실행 지점을 조인 포인트로 사용해도 충분히 부가기능을 제공할 수 있으므로 스프링의 프록시 방식 AOP로 충분하다.
- 간혹 필요한 경우에는 두 AOP를 같은 지점에 동시에 적용하는 것은 삼가하도록 하자.

📝 빈이 아닌 오브젝트에 DI 대응하기

도메인 오브젝트가 DI가 필요한 상황에서 도메인 오브젝트를 프레임워크나 @MVC의 핸들러 어댑터 내부에서 도메인 오브젝트를 생성하는 경우, AOP가 필요
- 어디서든지 도메인 오브젝트가 생성되면 자동 DI를 해주는 어드바이스를 적용해주면 된다.
- 특정 오브젝트가 생성되는 지점에 자동으로 DI 기능을 가진 어드바이스를 적용해줘야 한다는 문제가 있음 -> AspectJ AOP의 도움이 필요

◼ DI 애스펙트

스프링은 AspectJ 기술로 만들어진 DependencyInjectionAspect라는 애스펙트를 제공

//DependencyInjectionAspect 애스펙트의 포인트컷
public pointcut beanConstruction(Object bean) :
		initialization(ConfigurableObject+.new(..)) && this(bean);
        //new() : 오브젝트가 생성되는 지점
//DependencyInjectionAspect 애스펙트의 어드바이스
//@After 어드바이스와 동일
after(Object bean) returning :
	beanConstruction(bean) && postConstructionCondition() && inConfigurableBean() {
    	configureBean(bean); }

DependencyInjectionAspect 애스펙트가 적용되면 @Configurable이 붙은 도메인 오브젝트가 어디서든 생성될 때마다 이 어드바이스가 적용되어 자동 DI 작업이 일어남.
- DI방식은 도메인 오브젝트를 빈으로 선언하거나 자동와이어링 방식을 이용

◼ Configurable

@Configurable을 통해서 DI 애스펙트가 생성자에 적용되는 예를 살펴보자.

도메인 오브젝트가 빈으로 선언되는 것이 아닌 코드내에서 직접 생성하거나 ORM 프레임워크 등에서 만들어지는 경우, 도메인 오브젝트 내의 필드에 null값이 생겨버림.

이를 @Configurable을 이용해 DI 애스펙트를 적용해보자.
간단히 @Configurable 애노테이션을 도메인 오브젝트에 붙여주기만 하면 된다.

@Configurable
public class User {

이러면 User는 포인트컷에 대한 선정 대상이 됐다. 이제 어드바이스에서 자동 DI를 수행할 때 어떤 방식을 사용할지 결정하면 된다. DI를 설정하는 방법은 세 가지가 있다.

<bean> 설정

@Configurable이 붙은 클래스는 스프링의 빈이 아니고 빈으로 등록될 필요도 없고, 등록할 수도 없다. 그럼에도 <bean>을 사용해 오브젝트 설정에 포함해주기도 하는데, DI 설정정보를 제공해주기 위해서다.
- 빈 오브젝트가 불필요하게 만들어지는 일을 피하기 위해 abstract="true"를 넣어서 추상 빈으로 등록하는 방법을 사용하는 편이 좋다.

<bean class="springbook...User" abstract="true">
	<property name="userPolicyDao" ref=" userPolicyDao " />
  	<property name="emailService" ref="emailService" />
</bean>

자동와이어링

수정자 메소드를 등록해놨다면 자동와이어링 방식을 적용할 수도 있다.
이때는 @Configurable 애노테이션의 autowire 엘리먼트에 자동와이어링 방식을 지정해주면 된다. BY_NAME 대신 BY_TYPE을 사용할 수도 있다

@Configurable(autowire=Autowire.BY_NAME)
public class User {

자동와이어링의 단점은 모든 수정자 메소드에 DI를 시도한다는 점이다.
- 불필요한 정보가 주입되거나 불필요한 시간을 소모하는 경우가 생김

애노테이션 의존관계 설정

애노테이션 방식의 의존관계를 설정하게 되어 있다면 @Autowired나 @Resource를 이용할 수도 있다.
필드 주입 방식을 이용해 DI하도록 User 클래스 코드를 작성했다면 수정자 메소드도 생략 가능하다.
- 단 필드 주입 방식을 선택했다고 해도 단위 테스트를 위해서라면 수정자 메소드는 생략하지 않는게 좋다.

public class User {
	@Autowired private UserPolicyDao userPolicyDao;
    @Autowired private EmailService emailService;

◼ 로드타임 위버와 자바 에이전트

이제 DI 애스펙트를 젹용하는 부분만 남았다. DI 애스펙트를 사용하려면 두 가지 작업이 필요하다.

  • AspectJ AOP가 동작할 수 있는 환경설정
  • DI 애스펙트 자체를 등록해서 @Configurable 오브젝트에 어드바이스가 적용되도록 하기

AspectJ를 사용하려면 클래스를 로딩하는 시점에 바이트코드 조작이 가능하도록 로드타임 위버를 적용해줘야 한다.
스프링에서는 스프링이 직접 제공하는 로드타임 위버 방식을 사용할 수 있다.

<!--자바 에이전트 등록-->
-javaagent:lib/org.springframework.instrument-3.0.7.RELEASE.jar
<!--스프링의 로드타임 위버를 사용한다는 설정을 추가-->
<context:load-time-weaver />

📖 로드타임 위버(LTW)

<context:load-time-weaver>를 통해 등록되는 로드타임 위버는 스프링에서 여러 가지 기능에 활용된다.

@Configurable 지원

<context:spring-configured />로 등록한 DI 애스펙트가 동작하도록 AspectJ 로드타임 위버 기능을 대신해준다

트랜잭션 AOP의 모드를 AspectJ로 설정했을 때

<tx:annotation-driven mode="aspectj" />로 트랜잭션 AOP의 모드를 AspectJ로 설정했을 때, 로드타임 위버가 필요
- 직접 만든 AspectJ AOP를 적용한다면 역시 로드타임 위버가 필요

JPA에서 필요로 하는 로드타임 위버로 사용

JPA는 구현 제품에 따라 다르긴 하지만 대부분 로드타임 위버를 이용한 바이트코드 조작을 필요로 한다.

위와 같은 로드타임 위버의 사용의 문제점

  • AspectJ와 JPA의 로드타임 위버를 모두 적용해야 한다면 문제가 된다.
    - JVM의 자바에이전트 옵션은 한 번에 한 가지만 적용 가능
  • 자바에이전트는 JVM 레벨에 적용된다
    - JVM을 통해 로딩되는 모든 클래스를 다 조사하고 클래스 바이트코드 조작 대상으로 삼기 때문에 서버에 적용하기에는 매우 부담스럽다.

=> 스프링이 제공하는 로드타임 위버는 두 가지 문제를 모두 해결해준다!

  • 스프링 로드타임 위버는 JPA와 AspectJ를 위한 로드타임 위버 기능을 대신 해준다
  • JVM 레벨의 자바 에이전트를 대체할 수 있는 로드타임 위버 적용 방법을 자동으로 찾아줌으로써 두 번째 문제를 해결

스프링은 자동으로 다음의 로드타임 위버 구현 방법 중에서 하나를 적용해준다

  • WAS전용 로드타임 위버
    - 애플리케이션 서버가 WebLogic, OC4J, GlassFish, JBossAS 네 가지 중의 하나라면 각 서버에서 제공하는 특별한 클래스 로더를 이용하는 전용 로드타임 위버를 설정해줌
    - JVM에 자바에이전트를 설정할 필요가 없어짐

  • JVM 자바에이전트
    - 위의 4가지 WAS 환경이 아니라면, 스프링이 제공하는 자바에이전트가 설정되어있는지 확인
    - 스프링 자바에이전트가 등록되어 있으면, 이를 활용하는 로드타임 위버를 적용

  • 관례를 따르는 클래스 로더
    - 위의 모든 방법이 적용 불가능한 경우, 리플렉션을 이용해서 클래스 로더에 로드타임 위버 기능을 추가할 수 있는 메소드를 갖고 있는지 확인
    - 스프링이 직접 해당 클래스 로더 타입을 지원하진 않더라도, 이 관례를 따르는 이름을 가진 메소드를 제공하는 클래스 로더라면 로드타임 위버 기능 적용 가능
    🔷 스프링은 톰캣을 위해 특별히 이 관례를 따르는 전용 클래스 로더를 제공

📖 스프링 3.1의 AOP와 LTW

스프링 3.1의 AOP와 LTW 기능에 별다른 변화는 없다. 자바 코드 설정에서 사용할 수 있는 전용 애노테이션이 추가됐을 뿐이다

📝 AOP와 LTW를 위한 애노테이션

◼ @EnableAspectJAutoProxy

@Aspect로 애스펙트를 정의할 수 있게 해주는 @AspectJ AOP 컨테이너 인프라 빈을 등록해준다.

다음과 같이 애스펙트가 있다고 할 때,

@Aspect
public class MyAspect {
	...
}

자바 코드를 이용해 MyAspect가 애스펙트로 동작하게 하려면 다음과 같이 @Configuration 클래스에 @EnableAspectJAutoProxy를 붙여주고 MyAspect를 빈으로 등록해주면 된다.

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
	@Bean MyAspect myAspect() {
    	return new MyAspect();
    }
    ...
}

◼ @EnableLoadTimeWeaving

환경에 맞는 로드타임 위버를 등록해주는 애노테이션

디폴트 조건으로 로드타임 위버를 등록하려면 다음과 같이 @Configuration 클래스에 붙여주면 된다.

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
	...
}
profile
모르는 것 정리하기

0개의 댓글