타깃 오브젝트로의 위임 코드와 부가기능을 제공하기 위한 코드가 프록시가 구현해야 하는 모든 인터페이스 메소드마다 반복적으로 필요했다. 이 문제를 해결하기 위해 다이내믹 프록시를 사용하였다. 타깃의로의 위임과 부가기능 적용 여부 판단은 다이내믹 프록시가 담당하고, DI를 통해 제공하였다.
BeanPostProcessor
이라는 인터페이스를 구현하여 빈 후처리기를 생성할 수 있고, 책에서는DefalutAdvisorAutoProxyCreator
을 이용한다. 빈 후처리기를 빈으로 등록하여 사용할 수 있고, 빈 후처리기를 통해 빈 오브젝트를 수정하고 별도의 작업을 수행할 수 있다.
빈 후처리기를 자동 프록시 생성에 적용해보자.
DefalutAdvisorAutoProxyCreator
가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때 마다 후처리인DefalutAdvisorAutoProxyCreator
에게 빈을 보낸다.기존 포인트컷은 어떤 메소드에 부가기능을 적용할지 선정하는 역할로 사용되었다. 빈 후처리기가 자동 프록시 생성을 하는 과정에서는 어떤 빈에 프록시를 적용할지를 선정하는 역할을 한다. Pointcut
인터페이스는 다음과 같이 정의된다.
public interface Pointcut{
ClassFilter getClassFilter();
MethodFileter getMethodMatcher();
}
빈 후처리기는 getClassFilter()
메소드를 통해 클래스 레벨에서 프록시를 적용할 클래스인지 판단할 수 있다. 그 이후 getMethodMatcher()
메소드를 통해 어드바이스를 적용할 메소드인지 확인한다. 만약 클래스 레벨에서 프록시를 적용할 메소드로 선정되지 않는다면 부가 기능 역시 적용될 가능성이 없다.
포인트컷을 빈에 등록하기 위해 클래스 이름 패턴과 메소드 이름 패턴을 각각 지정하여 등록한다. 테스트시에 지정된 클래스만 프록시가 적용되는지 확인하고 싶다면 이 이름 패턴을 수정하며 간단하게 확인해 볼 수 있다.
지금까지는 포인트컷을 메소드의 이름 패턴과 클래스 이름 패턴을 비교하는 단순한 방법을 사용할 수 있다. 리플렉션 API를 통해 더 복잡하고 상세한 클래스와 메소드의 메타정보를 얻어올 수 있고, 이를 포인트컷에 적용할 수 있다. 하지만 리플렉션 API 코드를 작성하는 것은 번거로운 일이고, 비교 조건이 변경되었을 번거롭게 수정해야 한다. 스프링은 이보다 더 효과적으로 포인트컷의 클래스와 메소드를 선정하는 알고리즘을 작성할 수 있는 방법을 제공한다.
execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...)[thows 예외 패턴])
[]로 감싸진 부분은 옵션 항목이고, 각 이름 패턴에 대해 와일드카드('*','..') 형식을 적용할 수 있다.
포인트컷 표현식에서 클래스 이름에 적용되는 것은 해당 클래스의 이름 패턴이 아니라 타입 패턴이다.
UserLevelUp 기능을 구현하기 위해 트랜잭션 경계 설정 코드가 필요하였고 해당 코드로 부터 비즈니스 로직을 구현한 코드가 분리되기 위해 다음과 같은 시도가 있었다.
트랜잭션 경계설정과 같은 부가기능은 핵심기능과 같은 방법으로 모듈화하기 어렵다. 이러한 부가기능을 모듈화할지 연구해온 사람들이 Aspect라는 개념을 도입하였다. Aspect는 애플리케이션의 핵심기능을 담고 있지 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.
Aspect는 부가기능을 정의한 어드바이저와, 부가기능을 적용할 대상을 결정하는 포인트컷을 가지고 있다.
스프링은 프록시와 관련된 여러 기술을 조합하여 AOP를 지원한다. 따라서 자바의 기본 JDK와 스프링 컨테이너 이외에 특별한 기술과 환경이 요구되지않는다.
AspectJ는 프록시를 사용하지 않는 대표적인 AOP 기술이다. 프록시를 이용한 방법처럼 간접적으로 타깃 오브젝트에 영향을 주는 것이 아니라 타깃 오브젝트를 직접 건드리는 방식을 통해 부가기능을 넣는다. 바이트코드를 통한 복잡한 방식을 통해 핵심기능 코드와 부가기능 코드가 함께 있었을 때 처럼 만들수 있다.
바이트코드 방식의 장점
바이트 코드 방식을 통한 AOP는 이러한 장점이 있지만 대부분의 AOP를 적용하기 위해서 프록시를 통한 방식도 충분하다.
트랜잭션은 더 이상 쪼갤 수 없는 최소 단위이며 commit()와 rollback()이 모두 이 트랜잭션 단위로 이루어져야 한다. 이 외의 트랜잭션 동작방식을 제어할 수 있는 조건이 있고 DefaultTransactionDefinition
이 구현하는 TransactionDefintion
인터페이스는 다음 네 가지 속성을 정의한다.
트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할지 결정하는 방식이다. 두 개의 다른 트랜잭션이 다른 트랜잭션 경계를 가지고 있다면 다른 트랜잭션에 참여하여 실행될 수도 있고 아닐 수도 있다. 만약 같은 트랜잭션으로 실행된다면 역시 commit()와 rollback()에 대해 같은 단위로 적용된다.
이렇게 다른 트랜잭션 경계 설정을 가진 코드에 대해 이미 진행중인 트랜잭션이 어떻게 영향을 미치는지 정의하는 것을 트랜잭션 전파 속성이라고 한다.
트랜잭션 전파 속성
PROPAGATION_REQURIED
PROPAGATION_REQURIES_NEW
PROPAGATION_NOT_SUPPORTED
서버 환경에서는 여러 개의 트랜잭션이 동시에 실행될 수 있고, 적절한 격리 수준을 두어 여러 트랜잭션이 동시에 실행되며 문제가 발생하지 않도록 해야 한다. 격리 수준은 기본적을 DB에 설정되어 있고, JDBC 드라이버나 DataSource 등에서 재설정할 수 있다.
트랜잭션을 수행하는 제한시간이다.
읽기전용으로 설정하면 트랜잭션 내에서 데이터를 조작하는 시도를 막을 수 있고, 데이터 액세스 기술에 따라 성능 향상을 기대할 수 있다.
트랜잭션의 정의를 수정하기 위해서는 TransactinoDeinition
오브젝트를 DI 받아 사용할 수 있다. 하지만 이런 방법으로는 모든 트랜잭션의 정의가 한 번에 바뀌고, 메소드에 대해 선택적으로 적용할 수 없게 된다.
메소드 별로 다른 트랜잭션을 정의를 적용하기 위해 어드바이스의 기능을 확장할 수 있다. 메소드 이름 패턴에 따라 다른 트랜잭션 정의를 적용한다.
TransactionAdvice
와 다르지 않고 PlatformTransactionManager
와 Properties
타입의 transactionAttributes
를 가진다.
transactionAttributes
는 트랜잭션의 네 가지 속성과 rollbackOn()
메소드를 가지는 TransactionAttribute
인터페이스로 정의된다.
스프링은 두 가지 종류의 예외 처리 방식이 있다. 런타임 예외가 발생하면 반드시 트랜잭션을 롤백한다. 체크 예외의 경우 이것을 예외 상황이 아닌 비즈니스 로직에 따른 의미 있는 리턴 방식으로 인식하여 트랜잭션을 커밋한다.
하지만 특정 체크 예외는 트랜잭션을 롤백해야 할 경우가 있고, rollbackOn()
메소드를 통해 어떤 체크 예외가 발생했을 때 롤백을 해야할 지 결정한다.
첫 번째 속성인 트랜잭션 전파 항목은 필수적이고 나머지는 생략 가능하다. 생략된 속성은 모두 default 값을 갖는다.
예외 속성에서 '+'는 런타임 예외중에서도 해당 예외는 커밋하게 한다. '-'는 체크 예외이지만 해당 예외는 롤백되게 한다.
클라이언트가 인터페이스를 통해 타깃 오브젝트를 사용할 때는 프록시가 관여하고, 프록시의 방식의 AOP가 적용된다. 하지만 타깃이 직접 자신의 오브젝트를 호출하면 프록시는 관여하지 않게 되고 부가기능이 부여되지 않게 된다. 이 점을 유의하며 코드를 작성해야 한다.
이러한 문제를 해결할 수 있는 방법은 두 가지가 있다.
세밀한 트랜잭션 속성의 제어가 필요한 경우 일일이 포인트컷과 어드바이스를 추가해 주어야 한다. 스프링에서는 이를 위해 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법을 제공한다.
메소드, 클래스, 인터페이스에 애노테이션을 적용할 수 있고, 애노테이션이 적용된 모든 오브젝트에는 트랜잭션이 적용된다.
메소드의 트랜잭션 속성을 확인할 때 타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입(클래스, 인터페이스) 순으로 확인한다.