티스토리 뷰

스프링 AOP

자동 프록시 생성

중복 문제의 접근 방법

반복적인 프록시의 메소드 구현을 코드 자동생성 기법을 이용해 해결했다면 반복적인 ProxyFactoryBean 설정 문제는 설정 자동등록 기법으로 해결할 수 없을까? 또는 실제 빈 오브젝트가 되는 것은 ProxyFactoryBean을 통해 생성되는 프록시 그 자체이므로 프록시가 자동으로 빈으로 생성되게 할 수는 없을까? 하지만 한 번에 여러 개의 빈에 프록시를 적용할 만한 방법은 없었다.

빈 후처리기를 이용한 자동 프록시 생성기

스프링은 OCP의 가장 중요한 요소인 유연한 확장이라는 개념을 스프링 컨테이너 자신에게도 다양한 방법으로 적용하고 있다. 그래서 스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다.

그중에서 관심을 가질 만한 확장 포인트는 바로 BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기다. 빈 후처리기는 말 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다.

빈 후처리기 중 하나인 DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기다. 빈 후처리기를 적용하는 방법은 간단하다. 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 바로 이것이 자동 프록시 생성 빈 후처리기다. DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인한다. 프록시 적용 대상이면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에 어드바이저를 연결해준다. 컨테이너는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용한다.

확장된 포인트컷


public interface Pointcut {

ClassFilter getClassFilter();

MethodMatcher getMethodMatcher();

}

이제까지의 포인트컷은 다깃 오브젝트의 메소드 중 선정된 메소드를 선별하는 역할을 해 왔다. 여기에 또 하나의 기능인 등록된 빈 중에서 어떤 빈에 프록시를 적용할지를 선택할 수 있다. 포인트컷은 ClassFilter, MethodMatcher를 돌려주는 메소드를 갖고 있다. 이제까지는 메소드 선정 기능만 필요하였지만 DefaultAdvisorAutoProxyCreator는 클래스와 메소드 선정 알고리즘 모두가 필요하다.

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

포인트컷 표현식 문법

포인트컷 표현식은 일종의 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법이다. 포인트컷 표현식을 지원하는 포인트컷을 사용하려면 AspectJExpressionPointcut 클래스를 사용하면 된다.

앞서 만들었던 포인트컷은 클래스와 메소드의 이름의 패턴을 독립적으로 비교하도록 만들어져 있다. 하지만 AspectJExpressionPointcut은 클래스와 메소드의 선정 알고리즘을 포인트컷 표현식을 이용해 한 번에 지정할 수 있게 해준다. 사실 스프링이 사용하는 포인트컷 표현식은 AspectJ라는 유명한 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것이다. 그래서 이를 AspectJ 포인트컷 표현식이라고도 한다.

AspectJ 포인트컷 표현식은 포인트컷 지시자를 이용해 작성한다. 포인트컷 지시자 중에서 가장 대표적으로 사용되는 것은 execution()이다. 문법은 다음과 같다.


execttuion([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...)[throws 예외 패턴])
  • 접근제한자 패턴 - public, private 같은 접근제한자. 생략가능

  • 타입패턴 - 리턴값의 타입 패턴

  • 타입패턴(2번째) - 패키지와 클래스 이름에 대한 패턴. 생략가능

  • 이름패턴 - 메소드 이름 패턴

(- 타입패턴 | "..", ...) - 파라미터의 타입 패턴을 순서대로 넣을 수 있다. 와일드카드를 이용해 개수에 상관없는 패턴 생성도 가능

포인트컷 표현식을 이용하는 포인트컷 적용

포인트컷 표현식은 execution() 이외에도 몇 가지 스타일이 더 있다. bean(*Service)라고 쓰면 아이디가 Service로 끝나는 모든 빈을 선택한다. 또 특정 어노테이션이 타입, 메소드, 파라미터에 적용되어 있는 것을 보고 메소드를 선정하게 하는 포인트컷도 만들 수 있다. 아래와 같이 쓰면 @Transactional이라는 어노테이션이 적용된 메소드를 선정하게 해준다.


@annotaion(opg.springfameework.transaction.annotation.Transactional)

이전에 Bean으로 생성하여 methodClassName, mappedName을 선정했던 부분들은 execution(* *..*ServiceImpl.upgrade*(..)**)로 간단히 설정할 수 있다.

타입 패턴과 클래스 이름 패턴

이전에 TestUserServiceImpl와 UserSerivceImpl 클래스 두 개의 빈이 선정되고 테스트는 성공했다. 포인트컷 표현식으로 적용하고 다시 테스트 해 봐도 두 개 모두 성공한다. execution(* ..*ServiceImpl.upgrade(..)**)로 등록되어 있는데 어떻게 성공한 것인가? 이유는 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입 패턴이기 때문이다. 이름은 TestUserServiceImpl이지만 타입은 UserServiceImpl 타입이기 때문이다.

AOP란 무엇인가?

비즈니스 로직을 담은 UserService에 트랜잭션을 적용해온 과정을 정리해보자.

트랜잭션 서비스 추상화

트랜잭션을 처리하는 구체적인 방법이 변하면, 트랜잭션과 직접 관련이 없는 코드가 담긴 많은 클래스를 일일이 수정해야 했다.

그래서 트랜잭션 적용이라는 추상적인 작업내용은 유지한 채로 구체적인 구현 방법을 자유롭게 바꿀 수 있도록 서비스 추상화 기법을 적용했다.

트랜잭션 추상화란 격국 인터페이스와 DI를 통해 무엇을 하는지 남기고, 그것을 어떻게 하는지 분리한 것이다. 어떻게 할지는 더 이상 비즈니스 로직 코드에는 영향을 주지 않고 독립적으로 변경할 수 있게 됐다.

프록시와 데코레이터 패턴

하지만 트랜잭션의 경계설정을 담당하는 코드의 특성 때문에 단순한 추상화와 메소드 추출 방법으로는 더이상 제거할 방법이 없었다.

그래서 도입한 것이 바로 DI를 이용해 데코레이터 패턴을 적용하는 방법이었다. 클라이언트가 인터페이스와 DI를 통해 접근하도록 설계하고 데코레이터 패턴을 적용해서, 트랜잭션 부가기능을 자유롭게 적용할 수 있게 되었다. 클라이언트가 일종의 대리자인 프록시 역할을 하는 트랜잭션 데코레이터를 거쳐서 타깃에 접근할 수 있게 됐다.

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

프록시를 이용해서 트랜잭션 코드는 모두 제거할 수 있었지만, 비즈니스 로직 인터페이스의 모든 메소드마다 트랜잭션 기능을 부여하는 코드를 넣어 프록시 클래스를 만드는 작업이 오히려 큰 짐이 됐다.

JDK 다이내믹 프록시와 같은 프록시 기술을 추상화한 스프링의 프록시 팩토리 빈을 이용해서 다이내믹 프록시 생성 방법에 DI를 도입했다. 덕분에 부가기능을 다은 어드바이스와 부가기능 선정 알고리즘을 담은 포인트컷은 프록시에서 분리될 수 있었고 여러 프록시에서 공유해서 사용할 수 있게 됐다.

자동 프록시 생성 방법과 포인트컷

트랜잭션 적용 대상이 되는 빈마다 일일이 프록시 팩토리 빈을 설정해줘야 한다는 부담이 남아 있었다.

이를 해결하기 위해서 스프링 컨테이너의 빈 생성 후처리 기법을 활용해 컨테이너 초기화 시점에서 자동으로 프록시를 만들어주는 방법을 도입했다. 또한, 클래스를 선정하는 기능을 담은 확장된 포인트컷을 사용했다. 결국 트랜잭션 부가기능을 어디에 적용하는지에 대한 정보를 포인트컷이라는 독립적인 정보로 완전히 분리할 수 있었다.

부가기능의 모듈화

지금까지의 DI, 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷은 여기저기 흩어져 있는 트랜잭션 기능을 독립적인 모듈로 만들기 위한 방법이었다. 덕분에 TransactionAdvice라는 이름으로 모듈화될 수 있었다. 때문에 코드가 중복되지 않고 변경이 필요한 한 곳만 수정하면 되었다.

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

부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다. 그래서 이런 부가기능 모듈을 객체지향 기술에서는 주로 사용하는 오브젝트와는 다르게 특별한 이름으로 부르기 시작했다. 그것이 바로 애스펙트(aspect)다. 애스펙트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.

애스펙트는 부가될 기능을 정의한 코드인 어드바이스와, 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있다.

독립된 측면에 존재하는 애스팩트로 분리한 덕에 핵심기능은 순수하게 그 기능을 담은 코드로만 존재하고 독립적으로 살펴볼 수 있도록 구분된 면에 존재하게 된 것이다.

이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지향 프로그래밍(Aspect Oriented Programming) 또는 약자로 AOP라고 부른다.

AOP는 OOP를 돕는 보조적인 기술이지 OOP를 완전히 대체하는 새로운 개념은 아니다. AOP는 애스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것이라고 보면 된다. AOP를 관점 지향 프로그래밍이라고도 한다.

AOP 적용기술

프록시를 이용한 AOP

스프링의 AOP는 프록시 방식이라고 할 수 있다. 프록시의 역할은 독립적으로 개발한 부가기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이내믹하게 적용해준다.

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

프록시를 사용하지 않는 AOP 기술은 AspectJ가 있다. AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용한다. 이 방법은 소스를 수정할 수 없기 때문에 JVM에 로딩되는 시점에 가로채서 바이트코드를 조작하는 방법을 사용한다.

AOP의 용어

타깃

부가기능을 부여할 대상이다

어드바이스

타깃에게 제공할 부가기능을 담은 모듈이다. 오브젝트 정의,메소드 레벨에서 정의할 수 있다. 어드바이스는 메소드 호출 과정에 전반적으로 참여하는 것도 있지만, 예외가 발생했을 때만 동작하는 어드바이스도 있다.

조인 포인트

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

포인트컷

어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈을 말한다. 스프링 AOP의 조인 포인트는 메소드의 실행이므로 스프링의 포인트컷은 메소드를 선정하는 기능을 갖고 있다.

프록시

클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트다. DI를 통해 타깃 대신 클라이언트에게 주입되며, 클라이언트의 메소드 호출을 대신 받아서 타깃에 위임해주면서, 그 과정ㅇ에서 부가기능을 부여한다.

어드바이저

어드바이저는 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트다. 어드바이저는 어떤 부가기능(어드바이스)을 어디에(포인트컷) 전달할 것인가를 알고 잇는 AOP의 가장 기본이 되는 모듈이다.

애스펙트

AOP의 기본 모듈. 한 개 또는 그이 상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재한다. 모듈 정의와 오브젝트와 같은 실체(인스턴스)의 구분이 특별히 없다.

AOP 네임스페이스

스프링의 프록시 방식 AOP를 적용하려면 최소한 네 가지 빈을 등록해야 한다.

자동 프록시 생성기

DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록한다.

빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성하는 기능을 담당한다.

어드바이스

부가기능을 구현한 클래스를 빈으로 등록한다.

포인트컷

스프링의 AspectJExpressionPointcut을 빈으로 등록하고 expression 프로퍼티에 포인트컷 표현식을 넣어주면 된다.

어드바이저

스프링의 DefaultPointcutAdvisor 클래스를 빈으로 등록해서 사용한다.

어드바이스와 포인트컷을 프로퍼티로 참조하는 것 외의 기능은 없다.

자동 프록시 생성기에 의해 자동 검색된다.

트랜잭션 속성

트랜잭션 정의

트랜잭션이라고 모두 같은 방식으로 동작하는 것은 아니다. 물론 트랜잭션의 기본 개념인 더 이상 쪼갤 수 없는 최소 단위의 작업이라는 개념은 항상 유효하다. 따라서 트랜잭션은 commit(), rollback()을 통해 처리가 되어야 한다. 이 밖에도 트랜잭션의 동작방식을 제어할 수 있는 몇가지 조건이 있다.

트랜잭션 전파

트랜잭션 전파(transaction propagation)이란 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식을 말한다. 대표적으로 당므과 같은 트랜잭션 전파 속성을 줄 수 있다.

PROPAGATION_REQUIRED

진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 이에 참여한다. 가장 많이 사용하고, DefaultTransactionDefinition도 이 속성을 사용한다.

PROPAGATION_REQUIRES_NEW

항상 새로운 트랜잭션을 시작한다.

PROPAGATION_NOT_SUPPORTED

트랜잭션 없이 동작하도록 만든다. 하나의 트랜잭션 내에 여러 메소드 중 특별한 메소드만 트랜잭션 적용에서 제외하려고 할 때 유용하게 쓸 수 있다.

격리수준

모든 DB 트랜잭션은 격리수준(isolation level)을 갖고 있어야 한다. 서버 환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있다. 따라서 적절하게 격리수준을 조정해서 가능한 한 많은 트랜잭션을 동시에 진행시키면서도 문제가 발생하지 않도록 제어가 필요하다. 격리 수준은 기본적으로 DB에 설정되어 있지만, 재설정도 가능하다.

제한시간

트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다. 제한시간은 트랜잭션을 직접 시작할 수 있는 PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW와 함께 사용해야만 의미가 있다.

읽기전용

읽기전용(read only)으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 엑세스 기술에 따라서 성능이 향상될 수도 있다.

트랜잭션 정의를 수정하려면 어떻게 해야 할까? TransactionDefinition 오브젝트를 생성하고 사용하는 코드는 트랜잭션 경계설정 기능을 가진 TransactionAdvice다. DefaultTransactionDefinition을 사용하는 대신 외부에서 정의된 TransactionDefinition 오브젝트를 DI 받아서 사용하도록 만들면 된다. 하지만 이 방법으로 트랜잭션 속성을 변경하면 TransactionAdvice를 사용하는 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 잇다. 원하는 메소드만 선택해서 독자적인 트랜잭션 정의를 적용할 수 있는 방법은 없을까?

트랜잭션 인터셉터와 트랜잭션 속성

TransactionInterceptor

메소드별로 다른 트랜잭션 정의를 적용하려면 어드바이스의 기능을 확장해야 한다. 스프링에는 편리하게 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 만들어진 TransactionInterceptor가 존재한다. TransactionInterceptor 어드바이스의 동작방식은 기존의 TransactionAdvice와 크게 다르지 않고, 다만 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가로 제공해줄 뿐이다.

애노테이션 트랜잭션 속성과 포인트컷

트랜잭션 애노테이션

이제까지 일괄적으로 적용하는 방식을 살펴봤다. 하지만 경우에 따라 세밀하게 트랜잭션을 적용해 주어야 할 때가 있다. 이러한 상황에서 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법이 있다.

@Transactional


package org.springframework.transaction.annotation;

...



@Target({ElementType.METHOD, ElementType.TYPE}) //애노테이션을 사용할 대상을 지정. 여기에 사용된 '메소드와 타입(클래스, 인터페이스)'처럼 한 개 이상의 대상을 지정할 수 있다.

@Retention(RetentionPolicy.RUNTIME) //애노테이션 정보가 언제까지 유지디는지를 지정. 이렇게 설정하면 런타임 때도 애노테이션 정보를 리플랙션을 통해 얻을 수 있다.

@Inherited

@Documented

public @interface Transactional {

String value() default "";

Propagation propagation() default Propagation.REQUIRED;

Isolation isolation() default Isolation.DEFAULT;

int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

boolean readOnly() default false;

Class<? extends Throwable>[] rollbackFor() default {};

String[] rollbackForClassName() default {};

Class<? extends Throwable>[] noRollbackFor() default {};

String[] noRollbackForClassName() default {};

}

@Transactional 애노테이션의 타깃은 메소드와 타입이다. 따라서 메소드, 클래스, 인터페이스에 사용할 수 있다. 사용되는 포인트컷TransactionAttributeSourcePointcut이다. TransactionAttributeSourcePointcut은 스스로 표현식과 같은 선정기준을 갖고 있지 않다. 대신 @Transactional이 타입 레벨이든 메소드 레벨이든 상관없이 부여된 빈 오브젝트를 찾아서 포인트컷의 선정 결과로 돌려준다.

트랜잭션 속성을 이용하는 포인트컷

TransactionInterceptor는 메소드 이름 패턴을 통해 부여되는 일괄적인 트랜잭션 속성정보 대신 @Transactional 애노테이션의 엘리먼트에서 트랜잭션 속성을 가져오는 AnnotationTransactionAttributeSource를 사용한다. 이를 통해 @Transactional은 메소드마다 다르게 설정하는 유연함을 제공한다. 동시에 포인트컷도 @Transactional을 통한 트랜잭션 속성정보를 참조하도록 만든다.

대체 정책

스프링은 @Transactional을 적용할 때 4단계의 대체(fallback) 정책을 이용하게 해준다. 메소드의 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스)의 순서에 따라서 @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법이다.


[1]

public interface Service {

[2]

void method1();

[3]

void method2();

}

[4]

public class ServiceImpl implements Service {

[5]

public void method1() {

}

[6]

public void method2() {

}

}
  • 타깃 오브젝트의 메소드부터 시작해서 @Transactional 애노테이션이 존재하는지 확인힌다. 따라서 [5], [6]이 첫 번째 후보이다.
  • 여기서 발견되지 못하면 다음은 [4]에 존재하는지 확인한다.
  • 구현 메소드가 가장 우선이고 그 다음이 구현 클래스로 대체 정책이 넘어간다.
  • 그 다음은 인터페이스의 메소드 [2], [3]을 확인한다.
  • 마지막 순서는 인터페이스의 클래스 [1]을 확인한다.
  • 인터페이스를 사용하는 프록시 방식의 AOP가 아닌 방식으로 트랜잭션을 적용하면 인터페이스에 정의한 @Transactional은 무시되기에 타깃 클래스에 애노테이션을 두는 것이 바람직하다.

기타 트랜잭션 애노테이션

@Rollback

테스트 클래스에서 rollback은 기본 적용이 true이다. 만약 테스트를 끝내고 변경된 사항을 그대로 적용하고 싶다면 @Rollback(false)라고 해줘야 한다.

@TransactionConfiguration

@Rollback 애노테이션은 메소드 레벨에만 적용 가능하다. @TransactionConfiguration(defaultRollback=false)를 클래스 부분에 선언해 주면 메소드에 일일이 선언할 필요없이 한번에 가능하다.

@NotTransactional / Propagation.NEVER

@NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional 설정을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행한다. 이는 3.0에서 deprecated 되었기 때문에 사용하기에는 별루이다. 따라서 @Transactional(propagation=Propagation.NEVER)을 지정해주면 동일한 효과를 가질 수 있다.


참고

  • 토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함