📙

[토비의 스프링] 6-2. AOP - 기본 용어

개요

AOP의 기본 용어와 ’AOP란 무엇인지?‘에 대해 조금 더 자세히 알아보고 스프링의 트랜잭션 속성에 대해 알아보겠습니다.

AOP의 기본 용어

AOP에서 많이 사용되는 몇 가지 용어를 먼저 살펴보고 갑시다.

타깃(Target)

타깃은 부가기능을 부여할 대상입니다.
핵심기능을 담은 클래스일수도, 부가기능을 제공하는 프록시 오브젝트일 수도 있습니다.

어드바이스(Advice)

어드바이스는 타깃에게 제공할 부가기능을 담은 모듈입니다.
어드바이스는 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의할 수도 있습니다.

조인 포인트(Join Point)

조인 포인트란 어드바이스가 적용될 수 있는 위치를 말합니다.
타깃 오브젝트가 구현한 모든 인터페이스의 모든 메소드는 조인 포인트가 됩니다.

포인트컷(Pointcut)

포인트컷이란 어드바이스를 적용할 조인 포인트를 선별하는 작업입니다.
클래스를 선정하고 그 안의 메소드를 선정하는 기능을 가집니다.

프록시(Proxy)

프록시는 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트입니다.
클라이언트의 메소드 호출을 대신 받아 부가기능을 제공한 뒤 타깃에 위임해줍니다.

어드바이저(Advisor)

어드바이저포인트컷어드바이스를 하나씩 가지고 있는 오브젝트입니다.

어드바이저 = 포인트컷 + 어드바이스

애스펙트(Aspect)

OOP의 클래스와 마찬가지로 애스펙트는 AOP의 기본 모듈입니다.
한 개 또는 그 이상 어드바이저로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재합니다.

포인트컷 표현식을 이용한 포인트컷

기존의 리플렉션 API를 통해 클래스와 메소드 이름, 패키지, 파라미터, 리턴 값을 물론 인터페이스, 상속한 클래스까지 알 수 있는 API를 이용하여 클래스나 메소드를 선정했습니다.
하지만, 리플렉션 API를 이용해 메타조건을 비교하는 방법은 조건이 달라질 때마다 포인트컷 구현 코드를 수정해야하는 번거로움이 있습니다.

스프링은 아주 간단하고 효과적인 방법으로 포인트컷의 클래스와 메소드를 선정하는 알고리즘을 작성할 수 있는 방법을 제공합니다.
이것을 포인트컷 표현식이라고 부릅니다.

포인트컷 표현식

  • 포인트컷 표현식을 지원하는 포인트컷을 적용하려면 AspectJExpressionPointcut 클래스를 사용하면 됩니다.
  • AspectJExpressionPointcut은 클래스와 메소드의 선정 알고리즘을 포인트컷 표현식을 이용해 한 번에 지정할 수 있게 해줍니다.
  • 스프링이 사용하는 포인트컷 표현식은 AspectJ라는 유명한 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것이고 그래서 이를 AspectJ 포인트컷 표현식이라고도 합니다.

학습 테스트를 작성해서 표현식의 사용법을 알아봅시다.

포인트컷 테스트용 클래스

Java
public class Target implements TargetInterface {

    public void hello() {}
    public void hello(String a) {}
    public int minus(int a, int b) throws RuntimeException { return 0; }
    public int plus(int a, int b) { return 0; }
    public void method() {}
}

포인트컷 테스트용 추가 클래스

Java
public class Bean {
    public void method() throws RuntimeException {}
}

포인트컷 표현식 문법

AspectJ 포인트컷 표현식은 포인트컷 지시자를 이용해 작성합니다.
execution() 지시자를 사용한 포인트컷 표현식의 문법구조는 기본적으로 아래와 같습니다.

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...) [throws 예외 패턴])
  • []는 생략 가능하고 ()는 파라미터 패턴입니다.
  • | 는 OR 조건입니다.

메소드의 풀 시그니처를 문자열로 비교하는 개념이라고 생각하면 이해하기 쉽습니다.

예시

public int springbook.learning.spring.pointcut.Target.minus(int, int) throws java.lang.RuntimeException

public

  • 접근제한자입니다.
    public, protected, private 등이 올 수 있습니다.
    생략이 가능합니다.

int

  • 리턴값을 나타내는 패턴입니다.
    생략은 불가능합니다.

springbook.learning.spring.pointcut.Target

  • 패키지와 타입 이름을 포함한 클래스의 타입 패턴입니다.
    생략이 가능합니다.

minus

  • 메소드 이름 패턴입니다.
    필수항목이기 때문에 반드시 적어야 합니다.
    모든 메소드를 선택하겠다면 *(와일드카드)를 작성합니다.

(int, int)

  • 메소드 파라미터의 타입 패턴입니다.

throws java.lang.RuntimeException

  • 예외 이름에 대한 타입 패턴입니다.

사용법

포인트컷 표현식은 다음과 같은 헬퍼 메소드를 만들어 사용하면 좋습니다.

Java
public void pointcutMatches(String expression, Boolean expected, Class<?> clazz, String methodName, Class<?>... args) throws Exception {
    AspectJExpressionPointcut pointcut =new AspectJExpressionPointcut();
    pointcut.setExpression(expression);

    assertThat(pointcut.getClassFilter().matches(clazz)
        && pointcut.getMethodMatcher().matches(clazz.getMethod(methodName, args), null), is(expected));
}

사용 예제

Java
public void targetClassPointcutMatches(String expression, boolean... expected) throws Exception {
    pointcutMatches(expression, expected[0], Target.class, "hello");
    pointcutMatches(expression, expected[1], Target.class, "hello", String.class);
    pointcutMatches(expression, expected[2], Target.class, "plus", int.class, int.class);
    pointcutMatches(expression, expected[3], Target.class, "minus", int.class, int.class);
    pointcutMatches(expression, expected[4], Target.class, "method");
    pointcutMatches(expression, expected[5], Bean.class, "method");
}

targetClassPointcutMatches("excution(* *(..))", true, true, true, true, true);

AOP란 무엇일까?

트랜잭션 서비스 추상화

트랜잭션 경계설정 코드를 비즈니스 로직을 담은 코드에 넣으면서 맞닥뜨린 첫 번째 문제는 특정 트랜잭션 기술에 종속되는 코드가 돼버린다는 것입니다.
그래서 트랜잭션 적용이라는 추상적인 작업 내용은 유치한 채로 구체적인 구현 방법을 자유롭게 바꿀수 있도록 서비스 추상화 기법을 적용했다.
덕분에, 비즈니스 로직 코드는 트랜잭션을 어떻게 처리해야 한다는 구체적인 방법과 서버환경에서 종속되지 않습니다.
결국 트랜잭션 추상화란 인터페이스와 DI를 통해 무엇을 하는지는 남기고, 그것을 어떻게 하는지를 분리한 것입니다.

프록시와 데코레이터 패턴

트랜잭션을 어떻게 다룰 것인가는 추상화를 통해 코드에서 제거했지만, 여전히 대부분의 비즈니스 로직에는 트랜잭션이 들어가있습니다.
단순한 추상화와 메소드화로는 두가지를 분리할 수 없습니다.
그래서 도입한 것이 바로 DI를 이용해 데코레이터 패턴을 적용하는 방법이였습니다.
투명한 부가기능 부여를 가능하게하는 데코레이터 패턴의 적용 덕분에 비즈니스 로직을 담당하는 클래스도 자신을 사용하는 클라이언트와 DI 관계 맺을 이유를 찾게 됐습니다.

다이내믹 프록시와 프록시 팩토리 빈

프록시를 이용해서 비즈니스 로직에서 트랜잭션 코드는 모두 제거할 수 있었지만, 비즈니스 로직 인터페이스의 모든 메소드마다 트랜잭션 기능을 부여하는 코드를 넣어 프록시 클래스를 만드는 작업이 오히려 큰 짐이 됐습니다.
트랜잭션이 필요 없는 메소드 조차 프록시로서 위임 기능이 필요하기 때문에 일일히 다 구현해주어야 했습니다.
그래서 프록시 클래스 없이도 프록시 오브젝트를 런타임 시에 만들어주는 JDK 다이내믹 프록시 기술을 적용해서 일부 해결했지만, 동일한 기능의 프록시를 여러 오브젝트에 적용할 경우 오브젝트 단위의 중복을 해결할 수 없었습니다.

이런 문제점은 스프링의 프록시 팩토리 빈을 이용해 다이내믹 프록시 생성 방법에 DI를 도입해, 내부적으로 템플릿/콜백 패턴을 활용해 부가 기능을 담은 어드바이스와 부가기능 선정 알고리즘을 담은 포인트컷을 프록시에서 분리시키고 여러 프록시에서 공유해서 사용할 수 있게 되었습니다.

부가기능의 모듈화

관심사가 같은 코드를 분리해 한데 모으는 것은 소프트웨어 개발의 가장 기본이 되는 원칙입니다.
인터페이스를 통한 DI 기법을 사용하면 낮은 결합도로 인해 대부분의 문제를 해결 할 수 있었습니다.
하지만, 이 트랜잭션 적용 코드는 기존에 써왔던 방법으로는 간단하게 분리해서 독립된 모듈로 만들 수 없었습니다.

트랜잭션 같은 부가기능은 핵심기능과 같은 방식으로는 모듈화하기가 매우 힘듭니다.
트랜잭션 부가기능이란 트랜잭션 기능을 추가해 줄 다른 대상, 즉 타깃이 존재해야만 의미가 있고, 따라서 각 기능을 부가할 대상인 각 타깃의 코드 안에 침투하거나 긴밀하게 연결되어 있지 않으면 안 됩니다.

AOP: 애스펙트 지향 프로그래밍

부가 기능 모듈화 작업을 연구하는 사람들은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했습니다.
이런 부가기능 모듈을 애스펙트(aspect)라고 부릅니다.
핵심 기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 요소이고, 핵심 기능에 부가되어 의미를 갖는 특별한 모듈을 가리킵니다.

이러한 부가 기능들은 런타임 시에는 자기가 필요한 위치에 다이내믹하게 참여하게 될 것이고 이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지항 프로그래밍 또는 약자로 AOP라고 부릅니다.

또는 ‘특정한 관점을 기준으로 바라본다’하여 관점 지향 프로그래밍이라고도 합니다.

AOP 적용 기술

프록시를 이용한 AOP

  • 프록시로 만들어서 DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여해서 부가기능을 제공해주도록 만들었습니다.
    프록시 방식을 사용했기 때문에 메소드 호출 과정에 참여해서 부가기능을 제공해주게 되어 있습니다.
  • 독립적으로 개발한 부가기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이내믹하게 적용해주기 위해 가장 중요한 역할을 맡고 있는 게 바로 프록시입니다.
    그래서 스프링 AOP는 프록시 방식의 AOP라고 할 수 있습니다.

바이트코드 생성과 조작을 통한 AOP

  • AspectJ는 스프링처럼 다이내믹 프록시 방식을 사용하지 않습니다.
    AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어 고쳐서 부가 기능을 직접 넣어주는 방법을 사용한다.
  • 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용합니다.

장점

  • 자동 프록시 생성 방식을 사용하지 않아도 AOP를 적용할 수 있습니다.
  • 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능합니다.
  • 프록시를 AOP의 핵심 메커니즘으로 사용하면 부가기능을 부여할 대상은 클라이언트가 호출할 때 사용하는 메소드로 제한된다.
    하지만 바이트 코드를 직접 조작해서 AOP를 적용하면 오브젝트의 생성, 필드 값의 조회와 조작, 스태틱 초기화 등의 다양한 작업에 부가 기능을 부여해 줄 수 있다.