토비의 스프링 Vol.2 - 5장 AOP와 LTW

2020-11-04

AOP와 LTW

스프링은 AspectJ라는 뛰어난 AOP 프레임워크로부터 포인트컷 표현식과 함께 애노테이션을 이용해 AOP 모듈을 개발하는 방법도 도입했다. 또, 스프링의 프록시 방식 AOP 대신 AspectJ 라이브러리를 직접 활용하는 방법과 로딩 시점의 바이트코드 조작을 통해 DI 기능을 확장하는 방법등을 알아볼 것이다.


애스펙트 AOP

프록시 기반 AOP

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

AOP는 모듈화된 부가기능(어드바이스)과 적용 대상(포인트컷)의 조합을 통해 여러 오브젝트에 산재해서 나타나는 공통적인 기능을 손쉽게 개발하고 관리할 수 있는 기술이다. 스프링은 자바 JDK에서 지원하는 다이내믹 프록시 기술을 이용해, 복잡한 빌드 과정이나 바이트코드 조작 기술 없이도 유용한 AOP를 적용할 수 있는 프록시 시반 AOP 개발 기능을 제공한다.

프록시 방식의 AOP는 객체지향 디자인 패턴의 데코레이터 패턴 또는 프록시 패턴을 응용해서, 기존 코드에 영향을 주지 않은 채로 부가기능을 타깃 오브젝트에 제공할 수 있는 객체지향 프로그래밍 모델로부터 출발한다. 여기에 포인트컷이라는 적용 대상 선택 기법과 자동 프록시 생성이라는 적용 기법까지 접목하면, 비로소 AOP라고 부를 수 있는 효과적인 부가기능 모듈화가 가능해진다.

스프링은 가장 기초적이고 단순한 방법부터 최신 AOP 기술의 개발 방법을 차용한 방법에 이르기까지 여러 가지 종류의 프록시 기반 AOP 개발 방법을 지원한다.

  • AOP 인터페이스 구현과 <bean> 등록을 이용하는 방법
  • AOP 인터페이스 구현과 aop 네임스페이스의 <aop:advisor> 태그를 이용하는 방법
  • 임의의 자바 클래스와 aop 네임스페이스의 <aop:aspect>를 이용하는 방법
  • @AspectJ 애노테이션을 이용한 애스펙트 개발 방법
    자동 프록시 생성기와 프록시 빈

    스프링 AOP를 사용한다면 어떤 개발 방식을 적용하든 모두 프록시 방식의 AOP다. 스프링의 프록시 개념은 데코레이터 패턴에서 나온 것이고, 동작원리는 JDK 다이내믹 프록시와 DI를 이용한다.

    자동 프록시 생성기의 특징
  • AOP 적용은 @Autowired의 타입에 의한 의존관계 설정에 문제를 일으키지 않는다
  • AOP 적용은 다른 빈들이 Target 오브젝트에 직접 의존하지 못하게 한다
    프록시의 종류

    인터페이스를 구현한 프록시와 클래스를 직접 참조하면서 강한 의존관계를 맺고 있는 경우에 Client -> Target과 같이 직접적인 의존관계를 만든 경우에도 인터페이스 없이 프록시를 만들 수 있다.

  • 클래스를 이용한 프록시를 적용하는 방법
    1. 아예 아무런 인터페이스도 구현하지 않은 타깃 클래스에 AOP를 적용한다.
    2. 강제로 클래스 프록시를 만들도록 설정한다.

      @AspectJ AOP

      @AspectJ는 애스펙트를 자바 클래스와 메소드, 그리고 애노테이션을 이용해서 정의하는 방법을 가리키는 말이다.

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

      @AspectJ라는 이름의 애노테이션은 없다. 특정 AOP 개발 방법을 가리키는 용어일뿐이다. 애노테이션 방식의 MVC 개발 방법을 @MVC라고 부르는 것과 마찬가지다. @AspectJ 방식의 애스펙트를 사용하려면 XML 설정파일에 aop 스키마의 태그를 이용한 선언을 넣어줘야 한다. <aop:aspectj-autoproxy />

이 선언은 빈으로 등록된 클래스 중에서 클래스 레벨에 @Aspect가 붙은 것을 모두 애스펙트로 자동 등록해준다. @AspectJ 방식에서 사용하는 핵심 애노테이션은 @Aspect다.

두 번째로, AspectJ의 런타임 라이브러리를 클래스패스에 추가해줘야 한다.

@Aspect 클래스와 구성요소

애스펙트는 자바 클래스에 @Aspect라는 애노테이션을 붙여서 만든다. @Aspect 클래스는 기본적으로 @Configuration처럼 자바 코드로 만든 메타정보로 활용된다. 클래스를 애스펙트로 사용하려면 먼저 빈으로 등록해야 한다. <bean> 태그나 @Component를 붙여서 자동스캔 방식으로 등록해도 된다.

  • 포인트컷: @Pointcut
    • 포인트컷은 @Pointcut 애노테이션이 달린 메소드를 이용해 선언한다. 선택 로직은 @Pointcut 안에 포인트컷 표현식을 넣어서 정의한다. 메소드의 내부에 코드를 작성할 필요는 없다. 단지 메소드의 선언부를 메타정보로 이용해서 포인트컷의 이름과 파라미터를 정의하는 용도로만 사용한다.
  • 어드바이스: @Before, @AfterReturning, @AfterThrowing, @After, @Around
    • 어드바이스도 포인트컷과 마찬가지로 애노테이션이 붙은 메소드를 이용해 정의한다. @AspectJ에서는 다섯 가지 종류의 어드바이스를 사용할 수 있다. 각 종류별로 애노테이션이 하나씩 정의되어 있다. 어드바이스 로직은 메소드 내의 자바 코드로 작성한다. 메소드의 파라미터와 리턴 값은 어드바이스 종류와 포인트컷에서 선언한 파라미터에 따라 달라질 수 있다.
      포인트컷 메소드와 애노테이션
      @Aspect 클래스 안에서 포인트컷을 정의하는 방법

      포인트컷은 @Pointcut 애노테이션과 메소드의 이름, 파라미터로 정의된다. 포인트컷 메소드에는 구현 코드는 필요 없다. 단, 포인트컷 메소드의 리턴 타입은 항상 void형이어야 한다. @Pointcut("execution(* sayHello(..))") private void hello();

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

포인트컷 표현식은 execution()을 포함해서 여러 종류의 포인트컷 지시자(PCD, Pointcut Designator)를 이용해 정의할 수 있다.

  • execution()
    • 가장 대표적이고 가장 강력한 포인트컷 지시자다. 접근제한자, 리턴 타입, 타입, 메소드, 파라미터 타입, 예외 타입 조건을 조합해서 메소드 단위까지 선택 가능한 가장 정교한 포인트컷을 만들 수 있다.
  • within()
    • within()은 타입 패턴만을 이용해 조인 포인트 메소드를 선택한다. 패턴을 이용할 수 있기 때문에 자바 패키지 단위의 선택이 가능하다.
  • this, target
    • this와 target은 여러 개의 타입을 고를 수 있는 타입 패턴이 아니라 하나의 타입을 지정하는 방식이다. this와 target은 오브젝트를 선별한다. this는 빈 오브젝트의 타입을 확인하고, target은 타깃 오브젝트의 타입과 비교한다.
  • args
    • args 지시자는 메소드의 파라미터 타입만을 이용해 포인트컷을 선정할 때 사용한다. execution() 지사자의 () 안에 들어가는 파라미터 타입과 동일하다고 보면 된다. 보통 args는 다른 지시자와 함께 사용한다. 하나의 포인트컷 표현식 안에 여러 개의 지시자를 함께 사용할 수 있다. args는 포인트컷 파라미터를 적용하기 위해서도 자주 사용된다.
  • @target, @within
    • @target 지시자는 타깃 오브젝트에 특정 애노테이션이 부여된 것을 선정한다.
    • @within은 타깃 오브젝트의 클래스에 특정 애노테이션이 부여된 것을 찾는다.
    • @within은 @target과 유사하게 타깃 오브젝트의 클래스에 애노테이션이 부여된 것을 찾지만, 선택될 조인 포인트인 메소드는 타깃 클래스에서 선언되어 있어야 한다. 따라서 슈퍼클래스의 메소드는 해당이 되지 않는다. within과 다르게 패턴을 사용하지 않고 특정 타입을 지정한다.
  • @args
    • @args는 args와 유사하게 파라미터를 이용해 선정한다. 파라미터 오브젝트에 지정된 애노테이션이 부여되어 있느 ㄴ경우 선정 대상이 된다.
  • @annotation
    • @annotation은 조인 포인트 메소드에 특정 애노테이션이 있는 것만 선정하는 지시자다.
  • bean
    • bean은 빈 이름 또는 아이디를 이용해서 선정하는 지시자로, 와이드카드(*)를 사용할 수 있다.

      AOP를 학습하기 어려운 이유 중의 하나는 기억해야 할 포인트컷 지시자의 종류가 많고, 이를 적절히 활용해서 깔끔한 포인트컷을 만들기가 쉽지 않기 때문이다. 조건이 복잡한 포인트컷을 만들 때는 하나의 표현식에 모든 내용을 담기보다는 의미있는 작은 단위로 분리해서 정의한 후에 이를 조합하는 것이 좋다.

포인트컷 표현식은 논리연산 기호를 이용해서 여러 개의 포인트컷 지시자 또는 포인트컷 자체를 조합할 수 있다. 포인트컷 표현식 안에 다른 포인트컷 이름을 사용하는 것도 가능하다.

  • &&
    • 두 개의 포인트컷 또는 지시자를 AND 조건으로 결합한다.
  •   , !
    •   는 OR 조건이다. 두 가지 지시자 또는 포인트컷의 대상을 모두 포함하는 포인트컷을 정의할 때 사용한다.
    • !는 NOT 조건이다. ! 뒤에 나오는 조건에 해당하는 것을 제외할 때 쓴다.
      어드바이스 메소드와 애노테이션

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

  • @Around
    • @Around는 프록시를 통해서 타깃 오브젝트의 메소드가 호출되는 전 과정을 모두 담을 수 있는 어드바이스다.
    • @Around는 가장 강력한 기능을 가진 어드바이스다. @Around 어드바이스 내에서 원한다면 타깃 오브젝트의 메소드를 여러 번 호출하거나, 호출 파라미터를 바꿔치기 하거나, 심지어 타깃 오브젝트 메소드를 호출하지 않도록 만들 수도 있다.
    • @Around는 나머지 어드바이스를 먼저 검토해서 어드바이스 로직을 적용할 수 있는지 확인하고, 적용 가능한 어드바이스가 없을 때만 최후의 선택으로 남겨두는 것이 바람직하다.
  • @Before
    • @Before는 타깃 오브젝트의 메소드가 실행되기 전에 사용되는 어드바이스다. @Before 어드바이스로는 타깃 오브젝트 메소드를 호출하는 방식을 제어할 수 없다. @Before를 적용해도 타깃 오브젝트 메소드 호출은 정상적으로 일어난다.
    • @Before에는 JoinPoint 타입의 파라미터를 사용할 수 있다.
    • 파라미터 자체를 변경할 수는 없어도 파라미터가 참조하는 오브젝트의 내용을 변경할 수는 있다. 파라미터 자체를 변경하려면 @Around를 사용해야 한다.
  • @AfterReturning
    • @AfterReturning은 타깃 오브젝트의 메소드가 실행을 마친 뒤에 실행되는 어드바이스다. 단, 예외가 발생하지 않고 정상적으로 종료한 경우에만 해당된다. 따라서 메소드에서 예외가 던져졌다면 이 어드바이스는 적용되지 않는다.
    • 타깃 오브젝트의 메소드가 정상 종료된 후에 호출되기 때문에 메소드의 리턴 값을 참조할 수 있다.
    • @AfterReturning은 리턴 값 자체를 바꿀 수는 없다. 리턴 값을 변경하려면 @Around를 사용해야 한다. 하지만 리턴 값이 레퍼런스 타입이라면 참조하는 오브젝트를 조작할 수는 있다.
    • 리턴 값을 전달받을 파라미터의 타입을 구체적으로 지정해주면 리턴 값의 타입이 일치하는 경우에만 어드바이스가 실행된다.
  • @AfterThrowing
    • @AfterThrowing은 타깃 오브젝트의 메소드를 호출했을 때 예외가 발생하면 실행되는 어드바이스다.
    • @AfterThrowing 애노테이션의 throwing 엘리먼트를 이용해서 예외를 전달받을 메소드 파라미터 이름을 지정할 수 있다. throwing으로 지정한 파라미터의 타입이 발생한 예외와 일치할 경우에만 어드바이스가 호출된다.
    • 모든 예외를 다 전달받으려면 Throwable로 파라미터 타입을 지정한다.
  • @After
    • @After는 메소드 실행이 정상 종료됐을 때와 예외가 발생했을 때 모두 실행되는 어드바이스다. 코드에서 finally를 사용했을 때와 비슷한 용도라고 생각하면 된다.
    • 반드시 반환돼야 하는 리소스가 있거나 메소드 실행 결과를 항상 로그로 남겨야 하는경우에 사용할 수 있다. 하지만 리턴 값이나 예외를 직접 전달받을 수는 없다.
      파라미터 선언과 바인딩

      어드바이스 메소드에는 JoinPoint, ProceedingJoinPoint를 기본적으로 사용할 수 있다. 또, 어드바이스 종류에 따라 returning이나 throwing을 이용해서 선언된 리턴 값 또는 예외 파라미터를 이용할 수 있다. 이 외에도 포인트컷 표현식의 타입 정보를 파라미터와 연결하는 방법이 있다.

포인트컷 표현식 내의 파라미터 이름은 포인트컷 메소드 또는 어드바이스 메소드의 파라미터 이름과 일치해야 한다. 스프링은 자바 클래스의 디버깅 정보를 이용해 파라미터 이름을 확인한다. 만약 디버깅 정보를 모두 제거했다면 argsName 엘리먼트를 이용해 직접 파라미터 이름을 지정할 수도 있다.


AspectJ와 @Configurable

AspectJ AOP

AspectJ는 가장 강력한 AOP 프레임워크다. AspectJ는 아예 타깃 오브젝트 자체의 코드를 바꿈으로써 애스펙트를 적용한다. 따라서 프록시를 사용하지 않는다. 대신 타깃 오브젝트의 자바 코드에 처음부터 애스펙트가 적용되어 있던 것처럼 클래스 바이트코드를 변경하는 작업이 필요하다. 프록시 방식으로는 어드바이스를 적용할 수 없는 조인 포인트와 포인트컷 지시자를 지원하기 위해서다. AspectJ의 조인 포인트는 메소드 실행 지점 외에도 필드 읽기와 쓰기, 스태틱 초기화, 인스턴스 생성, 인스턴스 초기화 등도 지원한다.

빈이 아닌 오브젝트에 DI 적용하기

어디서든지 도메인 오브젝트가 생성되면 자동 DI 작업을 수행해주는 어드바이스를 적용해주는 것이다. 스프링의 빈이 아니더라도 수정자 메소드를 가진 임의의 오브젝트에는 스프링의 API의 도움을 받아서 간단히 DI를 할 수 있다. 문제는 특정 오브젝트가 생성되는 지점에 자동으로 DI 기능을 가진 어드바이스를 적용해줘야 한다는 점이다. 오브젝트 생성은 스프링 AOP에서 지원하는 조인포인트가 아니다. 따라서 다양한 조인 포인트를 지원하는 AspectJ AOP의 도움이 필요하다.

DI 애스펙트

스프링은 AspectJ 기술로 만들어진 DependencyInjectionAspect라는 애스펙트를 제공한다. DependencyInjectionAspect 애스펙트가 적용되면 @Configurable이 붙은 도메인 오브젝트가 어디서든 생성될 때마다 이 어드바이스가 적용되어 자동 DI 작업이 일어난다. 또, 빈의 초기화 메소드가 정의되어 있다면 실행된다. DI 방식은 도메인 오브젝트를 빈으로 선언해서 명시적으로 지정할 수도 있고 자동와이어링 방식을 이용할 수도 있다.

@Configurable

@Configurable을 통해서 DI 애스펙트가 오브젝트 생성자에 적용되는 예

@Configurable
public class User {
  ...
}

User는 DI 애스펙트의 포인트컷에 의한 선정 대상이 됐다. 남은 것은 어드바이스에서 자동 DI를 수행할 때 어떤 방식으로 사용할지 결정하는 일이다.

  • @Configurable이 적용된 클래스의 DI를 설정하는 방법
    • <bean> 설정
    • 자동와이어링
    • 애노테이션 의존관계 설정
      로드타임 위버와 자바 에이전트
  • DI 애스펙트를 사용하려면 두 가지 작업이 필요하다. 먼저 AspectJ AOP가 동작할 수 있는 환경설정이 필요하고, 다음은 DI 애스펙트 자체를 등록해서 @Configurable 오브젝트에 어드바이스가 적용되게 해야 한다.
  • AspectJ를 사용하려면 클래스를 로딩하는 시점에 바이트코드 조작이 가능하도록 로드타임 위버를 적용해줘야 한다.
  • 스프링은 간단히 @Configurable을 이용하는 DI 애스펙트를 등록할 수 있도록 <context:spring-configured />태그를 제공한다. 이 태그를 추가하면 DI 애스펙트가 등록된다.

로드타임 위버(LTW)

<context:load-time-weaver>는 단순히 @Configurable을 위해서만 사용되는 건 아니다. <context:load-time-weaver>를 통해 등록되는 로드타임 위버는 스프링에서 여러가지 기능에 활용된다.

  1. @Configurable 지원
  2. <tx:annotation-driven mode="aspectj" />로 트랜잭션 AOP의 모드를 AspectJ로 설정하기 위함
  3. AspectJ가 아니라 JPA에서 필요로 하는 로드타임 위버로 사용되는 것.

    스프링 로드타임 위버는 JPA와 AspectJ를 위한 로드타임 위버 기능을 대신해준다.

주요 WAS는 클래스 로더 레벨에 로드타임 위빙 기능을 적용할 수 있는 방법을 제공한다. 클래스 로더를 사용하면 JVM 레벨에 적용되는 자바에이전트처럼 서버의 모든 클래스에 다 적용되지 않고 특정 애플리케이션 모듈에만 적용이 가능하므로 서버에 부담을 주지 않고 설정에서도 부담이 없다. 스프링은 자동으로 다음의 로드타임 위버 구현 방법 중에서 하나를 적용해준다.

  • WAS 전용 로드타임 위버
  • JVM 자바에이전트
  • 관례를 따르는 클래스 로더

    스프링의 로드타임 위버는 환경에 따라 최적화된 방식을 자동으로 선정해주기 때문에 환경에 따라 설정을 바꿔야 한다는 부담을 덜어준다. 장기적으로 바이트코드 조작이 필요한 새로운 기술이 추가되더라도 일관된 방식으로 로드타임 위버를 적용할 수 있다.


스프링 3.1의 AOP와 LTW

AOP와 LTW를 위한 애노테이션

  • @EnableAspectJAutoProxy
    • @EnableAspectJAutoProxy는 @Aspect로 애스펙트를 정의할 수 있게 해주는 @AspectJ AOP 컨테이너 인프라 빈을 등록해준다.
  • @EnableLoadTimeWeaving
    • @EnableLoadTimeWeaving은 XML의 <context:load-time-weaver>처럼 환경에 맞는 로드타임 위버를 등록해주는 애노테이션이다.

정리

  • 스프링에서는 프록시 기반의 AOP 기술을 네 가지 접근 방법을 통해 활용할 수 있다. 각 접근 방법의 장단점을 파악하고 자신에게 맞는 방법을 선택할 수 있어야 한다.
  • @AspectJ는 AspectJ 스타일의 POJO 애스펙트 작성 방법이다. @AspectJ는 유연하고 강력한 기능을 가진 애스펙트를 손쉽게 만들게 해주지만, 복잡한 AspectJ 문법과 사용 방법을 익혀야 하는 부담이 있다.
  • 스프링은 또한 AspectJ를 스프링 애플리케이션 내에서 간접적으로 활용할 수 있는 몇 가지 방법을 제공한다. @Configurable은 스프링 빈이 아닌 오브젝트에 DI를 적용할 수 있게 해준다.
  • 스프링은 다양한 서버환경에서 사용 가능한 편리한 로드타임 위버를 제공해준다.

출처 : https://github.com/Masssidev/toby-vol2