프록시 기반 AOP

  • AOP는 모듈화된 부가기능(어드바이스)과 적용 대상(포인트컷)의 조합을 통해 여러 오브젝트에 산재해서 나타내는 공통적인 기능을 손쉽게 개발하고 관리할 수 있는 기능이다.
  • 프록시 방식의 AOP는 객체지향 디자인 패턴의 데코레이터 혹은 프록시 패턴을 응용해, 기존 코드에 영향을 주지 않은 채로 부가기능을 타깃 오브젝트에 제공할 수 있는 객체지향 프로그래밍 모델로부터 출발한다.

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

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

  • AOP 구성 요소를 모두 클래스로 개발하고 이를 빈으로 등록해 적용하는 방법이다.
  • 제일 원시적이지만 AOP의 동작방식을 이해하기 가장 좋다.
  • 스프링이 제공하는 인터페이스를 구현하는 방식으로 어드바이스와 포인트컷을 개발한다. 이 클래스를 빈으로 등록한 후, 어드바이스와 포인트컷의 조합을 어드바이저로 구성한다.
  • 어드바이저도 역시 빈으로 등록한다.
  • 마지막으로 준비된 어드바이저를 프록시로 만들어 빈에게 적용해주는 자동 프록시 생성기 빈을 등록해주면 된다.
  • 간단한 AOP 기능이라면 빈 클래스 구현과 등록 방식으로도 어렵지 않게 개발 가능하다.

AOP 인터페이스 구현과 aop 네입스페이스의 태그 이용 방법

  • 어드바이스 개발은 여전히 Advice 인터페이스를 구현하는 식으로 해야하지만 포인트컷 표현식을 적극 활용해서 포인트컷과 어드바이저, 그리고 이를 모두 적용하는 자동 프록시 생성기 빈 등록을 aop 스키마의 간결한 전용 태그만으로 가능하게 하는 방법이다.
<aop:config>
	<aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"    />
</aop:config>

<bean id="transactionAdvice" class="...">...</bean>
  • 태그는 pointcut 애트리뷰트를 통해 포인트컷 표현식을 직접 선언해주면, 어드바이저 비노가 함께 포인트컷 빈도 등록해준다.

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

  • 앞선 두번째와 마찬가지로 aop 네임스페이스를 사용한다. 하지만 기존의 어드바이스, 어드바이저 개념 대신 애스팩트라는 개념을 이용한다.
  • 애스팩트는 독립적인 AOP 모듈을 뜻하는 개념이다.
  • AspectJ로 대표되는 객체지향 기술의 확장 언어를 지향하는 AOP에서 애스팩트라는 AOP 모듈을 정의하는 방법이 따로 있다.
  • 애스팩트는 기본적으로 일반 자바 클래스를 이용해 정의할 수 있다. 특정 인터페이스를 구현할 필요가 없어 메소드는 유연하게 정의할 수 있고, 하나의 클래스 안에 여러 개의 애스펙트를 포함할 수 있다.
  • 인터페이스 구현 방식 AOP와 애스펙트 방식 AOP의 차이점은 스프링 MVC의 인터페이스 구현 컨트롤러와 어노테이션 방식 컨트롤러의 차이점과 같다고 볼 수 있다.

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

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

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

  • 스프링 AOP를 사용한다면 어떤 개발 방식을 적용하든 모두 프록시 방식의 AOP다.
  • 스프링의 프록시 개념은 데코레이터 패턴에서 나온 것이고, 동작 원리는 JDK 다이내믹 프록시와 DI를 이용한다.
  • 아래와 같이 Client, Target이 있을 때 Client가 Target을 DI 받아 사용하는 관계라고 생각해보자. 여기에 프록시를 이용해 Client, Target코드에는 전혀 영향을 주지 않은채로 부가기능을 제공하고 싶을 때는,
  • 아래와 같이 Client가 Target을 직접적으로 DI받으면 안된다.
public class Client {
	@Autowired Target target;
}
public class Target { ... }
  • 프록시는 DI에 기반을 두고 있고, 위 코드는 DI를 위반하고 있기 때문이다. (Client가 자신이 사용할 클래스를 이미 알고 있고, 구체 클래스에 의존한다)
  • 따라서 Client가 Target이라는 구체 클래스를 직접 의존하는 대신 Target이 구현하는 Interface에 의존하도록 해야 한다.
public class Client {
	@Autowired Interface intf;
}
public class Target implements Interface { ... }

public class Proxy implements Interface {
	private Interface next;
	public void setNext(Interface next) { this.next = next; }
}
  • Proxy는 Interface를 구현했으므로 Client에 주입이 가능하다.
  • DI 설정을 조작해서 Client -> Proxy -> Target 순서로 의존관계를 맺게 하면 Proxy가 Client와 Target 호출 과정에 끼어들어 부가기능을 제공할 수 있게 된다.

발생할 수 있는 문제 ?

  • Proxy를 빈으로 등록할 때 @Autowired를 통한 자동 와이어링 불가
  • Interface를 구현한 빈이 두개이기 때문에, 자동 빈 선택이 불가능해진다. 빈 이름을 지정하거나 @Qualifier를 사용해 빈 선정 조건을 더 부여해주어야 한다.

자동 프록시 생성 기법

  • 스프링은 자동 프록시 생성기를 이용해 컨테이너 초기화 중 만들어진 빈을 바꿔치기해 프록시 빈을 자동으로 등록해준다.
  • 빈을 선택하는 로직은 포인트컷을 이용하고, XML 혹은 어노테이션을 통해 이미 정의된 빈의 의존관계를 바꿔치기하는 것은 빈 후처리기를 사용한다.
  • 이 자동 프록시 생성기가 바로 스프링의 프록시 기반 AOP의 핵심 동작 원리이다.
  • 자동 프록시 생성기는 빈을 별도로 추가하고 DI 설정만 바꿔주는게 아니라, 프록시를 적용할 대상 자체를 아예 자신이 포장해서 마치 그 빈처럼 동작한다는 점이다.
  • 자동 프록시 생성기가 만들어주는 프록시는 새로운 빈으로 추가되는 것이 아니라 AOP 대상 타깃 빈을 대체한다.
  • 스프링의 모든 프록시 기반 AOP는 이 자동 프록시 생성기를 통해 동작하기 때문에 어떤 방식을 쓰든 공통적으로 적용되는 특징이다.

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

  • 앞서 수동 빈 등록시 발생했던 문제를 해결해주었다.

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

  • 자동 프록시 생성기는 XML 혹은 어노테이션을 통해 빈으로 등록된 Target 클래스를 프록시 안에 감춰버린다.
  • 그렇기 때문에 Target 클래스를 직접적으로 의존하고 있는 클래스는 DI 할 수 없다고 예외가 발생하면서 컨테이너 초기화 작업을 중단시킨다.

프록시의 종류

  • 앞서 설명한 AOP의 프록시는 인터페이스를 구현한 프록시다. JDK의 다이내믹 프록시가 인터페이스를 이용하기 때문이기도 하고, 프록시의 원리인 데코레이터, 패턴과 DI도 인터페이스를 통한 의존관계를 사용하기 때문이다.
  • 스프링에서는 클래스를 직접 참조하며 강한 의존관계를 맺고 있는 경우에도 프록시를 적용할 수 있다.
  1. 아예 아무런 인터페이스도 구현하지 않은 타깃 클래스에 AOP를 적용하는 것
    • 스프링은 타깃 오브젝트에 인터페이스가 있다면 그 인터페이스를 구현한 JDK 다이내믹 프록시를 만들어주지만, 없다면 CGLib를 이용한 클래스 프록시를 만든다.
<aop:config proxy-target-class="true">
	...
</aop:config>
  1. 강제로 클래스 프록시를 만들도록 설정하는 것
    • 이 경우 인터페이스가 있더라도 무시하고 클래스 프록시를 만든다.
<tx:annotation-driven proxy-target-class="true"/>
  • 클래스 프록시 타입은 타깃 오브젝트의 클래스와 동일하다.

@AspectJ AOP

  • 애스팩트는 그 자체로 애플리케이션의 도메인 로직을 담은 핵심 기능은 아니지만, 많은 오브젝트에 걸쳐 필요한 부가기능을 추상화 해놓은 것이다.
  • 애스펙트는 하나 이상의 포인트 컷과 어드바이스로 구성된다.

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

  • XML 설정 파일에 aop 스키마 태그를 이용한 선언 추가
    • 해당 선언을 통해 빈으로 등록된 클래스 중에서 클래스 레벨에 @Aspect가 붙은 것을 모두 애스펙트로 자동 등록해준다.
<aop:aspectj-autoproxy	/>
  • AspectJ 런타임 라이브러리를 클래스패스에 추가

@Aspect 클래스와 구성요소

  • 애스펙트는 자바 클래스에 @Aspect라는 어노테이션을 붙여 만든다.
  • @Configuration처럼 자바 코드로 만든 메타정보로 활용된다.

포인트컷 : @Pointcut

  • @Pointcut 안에 포인트컷 표현식을 넣어서 정의한다.
  • 메소드 내부에 코드를 작성할 필요는 없다. 단지 메소드의 선언부를 메타정보로 이용해 포인트컷의 이름과 파라미터를 정의하는 용도로만 사용한다.
@Pointcut("execution(* hello(..))")
private void all() {}

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

  • @AspectJ에서는 다섯 가지 종류의 어드바이스를 사용할 수 있다.
  • 어드바이스도 포인트컷과 마찬가지로 하나 이상을 정의할 수 있다. 어드바이스의 어노테이션에는 이에 적용할 포인트컷을 명시해야 한다.
@Aroung("all()")
public Object printParametersAndReturnVal(ProceedingJoinPoint pjp) throws Throwable {
	...
	Object ret = pjp.proceed();
	..
	return ret;
}
  • 위와 같이 포인트컷의 이름을 사용하는 대신, 포인트컷 표현식을 넣어 적용할 수도 있다.

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

  • 포인트컷은 @Pointcut 어노테이션과 메소드의 이름, 파라미터로 정의된다.
  • 메소드에 구현 코드는 필요 없으며, 리턴 타입은 항상 void형이여야 한다.
  • 포인트컷에서 조인포인트를 선별할 수 있는 방법은 아래와 같다 :
  • execution() : 가장 대표적이고 강력한 포인트컷 지시자이다. 접근제한자, 리턴 타입, 타입, 메소드, 파라미터 타입, 예외 타입 조건을 조합해 메소드 단위까지 선택 가능한 가장 정교한 포인트컷을 만들 수 있다.
  • within() : 타입 패턴만을 이용해 조인포인트 메소드를 선택한다. 선택된 타입의 모든 메소드가 AOP 적용 대상이 된다.(선택 대상이 타입 레벨에서 결정된다면 execution보다는 within을 사용하자)
  • this, target : this는 빈 오브젝트의타입을 확인하고, target은 타깃 오브젝트의 타입을 비교한다.
  • args ; 메소드의 파라미터 타입만을 이용해 포인트컷을 설정할 때 사용
  • @target, @within : @target 지시자는 타깃 오브젝트에 특정 애노테이션이 부여된 것을 선정하고, @within은 타깃 오브젝트의 클래스에 특정 애노테이션이 부여된 것을 찾는다.
  • @args : 파라미터 오브젝트에 지정된 애노테이션이 부여되어 있는 경우 선정 대상이 된다.
  • @annotation : 조인 포인트 메소드에 특정 애노테이션이 있는 것만 선정하는 지시자다.
  • bean : 빈 이름 또는 아이디를 사용해서 선정하는 지시자다.
  • && : 두 개의 포인트컷 또는 지시자를 AND 조건으로 결합한다.
  •   , ! :   는 OR조건이고 ,!는 NOT 조건이다.

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

  • 어드바이스를 정의하는 메소드는 어드바이스 어노테이션과 어드바이스에 적용되는 포인트컷 그리고 메소드 코드로 구성된다.
@Before("daoLayer()")
public void logDaoAccess(JoinPoint jp) { 
	....
}

어드바이스 종류는 아래 총 5가지가 있다 :

  • @Around : 프록시를 통해 타깃 오브젝트의 메소드가 호출되는 전 과정을 모두 담을 수 있는 어드바이스다.
    • 파라미터로는 ProceedingJoinPoint 타입의 오브젝트를 받는다. 이 오브젝트를 이용해 타깃 오브젝트의 메소드를 실행하고 그 결과를 받을 수 있다.
    • @Around는 가장 강력한 기능을 가진 어드바이스다. @Around 어드바이스 내에서 원한다면 타깃 오브젝트의 메소드를 여러 번 호출하거나, 호출 파라미터를 바꿔치기 하거나, 심지어 타깃 오브젝트 메소드를 실행하지 않도록 만들 수도 있다.
    • 나머지 어드바이스를 먼저 검토해서 어드바이스 로직을 적용할 수 있는지 확인하고, 적용 가능한 어드바이스가 없을 때만 최후의 선택으로 남겨두는 것이 바람직하다.
@Around("myPointcut()")
public Object doNothing(ProceedingJoinPoint pjp) throws Throwable {
	Object ret = pjp.proceed();
	return ret;
}
  • @Before : 타깃 오브젝트의 메소드가 실행되기 전에 사용되는 어드바이스다.
    • 타깃 오브젝트 메소드를 호출하는 방식을 제어할 수 없다.
    • 리턴 값에 별 관심이 없을 때 사용하며, 어떤 메소드가 호출되고 어떤 파라미터를 사용하는지 참조해서 필요한 부가 작업을 수행할 수 있다.
    • 파라미터로는 JoinPoint타입을 사용한다.( ProceedingJoinPoint의 슈퍼 클래스로, 메소드를 실행하는 proceed() 메소드는 없다)
@Before("myPointcut()")
public void logJoinPoint(JoinPoint jp) {
	System.out.println(jp.getSignature().getDeclaringTypeName());
	System.out.println(jp.getSignature().getName());
	for(Object arg : jp.getArgs()) { System.out.println(args); }
}
  • @AfterReturning : 타깃 오브젝트의 메소드가 실행을 마친 뒤에 실행되는 어드바이스다.(예외 발생하지 않고 정상적으로 종료한 경우에만)
    • 메소드의 리턴 값을 참조할 수 있다. 이 때 returning 엘리먼트를 이용해 리턴 값을 담을 파라미터 이름을 지정해야 한다.
    • 리턴 값 자체를 변경할 수 없다. 변경하려면 @Around를 이용해야 한다.
@AfterReturning(pointcut="myPointcut()", returning="ret")
public void logReturnValue(Object ret) {
	...
}
  • @AfterThrowing : 타깃 오브젝트의 메소드가 예외를 발생하면 실행되는 어드바이스다.
    • thworing 엘리먼트를 이용해 예외를 전달받을 메소드 파라미터 이름을 지정할 수 있다.
@AfterThrowing(pointcut="daoLayer()", throwing="ex")
public void logDAException(DataAccessException ex) { 
	....
}
  • @After : 메소드 실행이 정상 종료됐을 때와 예외 발생했을 때 모두 실행되는 어드바이스다. finally와 비슷한 용도라고 생각하면 된다.
    • 반드시 반한돼야 하는 리소스가 있거나, 로그를 남겨야 할 경우 사용한다.

파라미터 선언과 바인딩

  • 포인트컷 표현식의 타입 정보를 파라미터와 연결할 수 있다. 이 방식을 사용하면 포인트컷 표현식 내 길게 선언했던 타입정보를 파라미터 정보로 대신할 수 있어 표현식이 간결해진다.
@Before("@target(com.epril.myproject.special.annotation.BatchJob)")
public void beforeBatch() { ... }
  • 타깃 오브젝트에 @BatchJob이라는 어노테이션이 붙은 것을 선정해 @Before 어드바이스를 적용하는 코드다. 아래와 같이 타입정보를 자바 코드로 작성하면 훨씬 깔끔해진다.
import com.epril.myproject.special.annotation.BatchJob;
...
@Before("@target(bj)")
public void beforeBatch(BatchJob bj) { ... }	// 포인트컷은 이름이 일치하는 메소드 파라미터의 타입 정보를 참조하고, 어드바이스 메소드는 타깃 오브젝트의 어노테이션을 전달받는다.
  • 포인트컷 표현식 내 파라미터 이름은 포인트컷 메소드 또는 어드바이스 메소드의 파라미터 이름과 일치해야 한다.

oksusutea's blog

꾸준히 기록하려고 만든 블로그