※ 해당 내용은 토비의 스프링3을 보고 정리한 내용이다.
6.5 스프링 AOP
직전 장까지 어드바이스와 포인트컷을 한번만 생성해서 이를 재사용할 수 있는 구조를 만들었지만 추가적으로 개선할 포인트가 남아있다.
어드바이스 적용이 필요한 타겟 오브젝트마다 비슷한 내용의 xml 빈 설정정보를 추가해줘야하는 부분이다. 코드의 수정은 발생하지 않지만 새로운 서비스 클래스에 어드바이스를 적용하고자 할때마다 중복된 내용의 설정이 추가되어야한다.
자동 프록시 생성
현재까지는 코드 반복에 대한 해결책으로 바뀌는 부분과 바뀌지 않는 부분을 구분해서 분리하고 템플릿, 콜백, 클라이언트로 나누는 방식으로 해결했다. (전략패턴과 DI활용)
하지만 반복적인 위임이 필요한 프록시 클래스 코드의 경우 위와는 다른 방식으로 문제를 해결했다. 다이나믹 프록시를 이용해서 런타임시에 필요한 코드를 가지는 프록시 클래스를 만들어서 변하지 않는 부분(위임, 부가기능 적용)은 다이나믹 프록시에 맡기고, 변하는 부분(부가기능)은 별도로 만들어서 다니아믹 프록시 생성 팩토리에 DI로 제공하는 방법이다.
- 빈 후처리기를 사용한 자동 프록시 생성
- 스프링에서는 DefaultAdvisorAutoProxyCreator라는 어드바이저를 이용한 자동 프록시 생성이 가능한 구조를 제공한다. 이를 통해서 스프링 컨테이너에서 빈 생성 이후, 후처리기에서 해당 빈 객체의 일부를 프록시로 감싸고 해당 프록시를 빈으로 대신 등록할 수 있다.
위 구조를 기준으로 프로세스를 살펴보면, 빈 후처리기가 빈 등록되어 있으면 스프링에서는 빈 생성시점마다 해당 후처리기에 빈을 보낸다. 이후 DefaultAdvisorAutoProxyCreator에서는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 사용해서 전달받은 빈의 프록시 적용대상여부를 판단하고, 적용 대상 빈이라면 프록시를 생성해서 스프링 컨테이너에 해당 프록시를 전달해준다.
이렇게 빈 후처리기를 사용함으로써 기존에 ProxyFactoryBean 빈을 수기로 등록하는 작업을 자동으로 프록시가 적용되도록 할 수 있다.
DefaultAdvisorAutoProxyCreator의 적용
실제 후처리기를 이용한 자동 프록시 적용을 하기위한 프로세스를 세분화해서 확인해볼 수 있다.
- 클래스 필터를 적용한 포인트컷 작성
- 포인트컷 인터페이스에는 클래스 필터, 메서드 매처 두 가지 메서드가 있고 이를 통해서 특정 조건을 만족하는 클래스, 메서드를 판별할 수 있다. 작성된 포인트컷은 빈으로 등록이 필요하다.
- 어드바이저를 이용하는 자동 프록시 생성기 등록1에서 등록된 포인트컷에 의해서
메서드에 대해서는 등록된 transactionAdvisor 어드바이저 빈에 의해서 트랜잭션 관련 부가기능을 수행하게 된다.
ServiceImpl로 끝나는 클래스의 upgrade - DefaultAdvisorAutoProxyCreator를 통해서 등록된 빈 중에서 어드바이저 인터페이스를 구현한 것을 모두 찾고, 어드바이저의 포인트컷을 적용해보면서 프록시 적용 대상을 선정한다. 적용 대상인 경우 프록시를 생성해서 원래의 빈 객체와 바꿔치기한다.
포인트컷 표현식을 이용한 포인트컷 적용
이전에 구현한 포인트컷은 클래스명, 메서드명을 각각 클래스 필터와 메서드 매처를 통해서 비교하는 방식이었기 때문에 이를 수기로 구현하거나 스프링에서 제공하는 값을 가져와서 프로퍼티에 설정해야하는 방식이었다.
스프링에서는 이를 간단하게 정규식 같이 일종의 표현식 언어를 사용해서 작성할 수 있도록 포인트컷 표현식을 지원한다.
- AspectJExpressionPointcut
- 포인트컷 표현식을 사용하려면 해당 클래스를 사용하고 이는 AspectJ 프레임워크에서 제공하는 클래스이다.
- 포인트컷 표현식 문법
- 구조는 아래와 같으며, 관련 내용은 실제 사용하는 시점에 좀 더 구체적으로 그때그때 알아보면 될 것 같다.
위와 같은 포인트컷 표현식을 통해서 기존에 클래스, 메서드별로 따로 프로퍼티를 셋팅했던 포인트컷을 하나의 프로퍼티값으로 명시해줄 수 있다.
다만 주의해야할 점은 표현식이 문자열이기 때문에 컴파일 시점에서는 오류 검증이 불가하기 때문에 다양한 테스트를 미리 만들어서 검증된 표현식을 사용하는 것이 중요하다.
<!-- ASIS -->
<property name="mappedClassName" value="*ServiceImpl" />
<property name="mappedName" value="upgrade*" />
<!-- TOBE -->
<property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))" />
AOP란 무엇인가?
비즈니스 로직을 담은 UserService에 트랜잭션 기능을 적용해온 과정을 정리해보면 다음과 같다.
- 트랜잭션 서비스 추상화
최초로 인지한 문제는 비즈니스로직이 특정 트랜잭션 기술에 종속된다는 것이었다. 이를 타개하기 위해서 트랜잭션 적용 관련 처리를 추상화하고 런타임 시점에 구체 구현을 다이나믹하게 연결함으로써(DI) 비즈니스 로직에 영향없이 독립적으로 변경할 수 있게 되었다. - 프록시와 데코레이터 패턴
트랜잭션 기술에 대한 종속은 끊어냈지만 비즈니스로직과 트랜잭션 적용 코드가 여전히 하나의 메서드내에 섞여있다. 이를 해결하기 위해 데코레이터 패턴을 사용해서 클라이언트가 인터페이스와 DI를 통해서 비즈니스로직에 접근하도록 하고, 트랜잭션 처리 부분은 그 사시에 존재시킴으로써 일종의 프록시 역할을 하는 트랜잭션 데코레이터를 거쳐서 타겟(비즈니스 로직)에 접근할 수 있도록 했다.
이러한 분리를 통해서 비즈니스로직과 트랜잭션 처리가 분리되고 단위 테스트를 작성하기에도 용이해진다. - 다이나믹 프록시와 프록시 팩토리 빈
비즈니스 로직 인터페이스의 모든 메서드에 대해서 트랜잭션 사용여부에 상관없이 프록시로서의 위임 기능을 넣어서 프록시 클래스를 만드는 작업이 복잡하다.
JDK의 다이나믹 프록시를 통해서 부가기능을 메서드별로 중복 구현해줘야하는 문제는 해결했으나, 동일 기능의 프록시를 여러 오브젝트에 적용하는 경우 오브젝트 단위의 중복문제는 해결되지 못했다.
최종적으로 Spring에서 제공하는 프록시 팩토리 빈을 이용해서 다이나믹 프록시의 생성을 DI를 통해서 처리하도록 했고, 어드바이스와 포인트컷을 프록시에서 분리하고 여러 프록시에서 공유해서 사용할 수 있는 구조로 개선했다. - 자동 프록시 생성 방법과 포인트컷
소스상으로는 문제가 없으나 스프링 설정상으로 트랜잭션 적용이 필요한 모든 빈 마다 프록시 팩토리 빈 설정을 해줘야하는 문제가 생겼다.
이에 대한 해결을 위해서 스프링의 빈 생성 후처리 기법을 활용해서 스프링 컨테이너 초기화 시점에 자동으로 프록시를 생성해주도록 개선했다. 그리고 실제 프록시를 생성할 대상 빈을 일일이 설정파일에서 지정하지 않고 포인트컷이라는 독립적인 정보를 통해서 조건에 맞는 빈을 자동으로 선택하도록 했다. - 부가기능(어드바이스)의 모듈화
최종적으로 기능적으로는 독립될 수 없는 트랜잭션 경계설정 부가기능을 TransactionAdvice라는 이름으로 모듈화 시킬 수 있었고, 위의 모든 작업은 최종적으로 핵심기능에 부여되는 부가기능을 효과적으로 모듈화하는 방법을 찾는 프로세스였다고 볼 수 있다.
그래서 AOP를 어떻게 정의할 수 있을까?
전통적인 객체지향 설계로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가기능을 어떻게 모듈화할지에 대한 고민이 있었고, 이러한 부가기능 모듈화 작업은 기존 객체지향 설계 패러다임과 구분되기 때문에 이 모듈을 객체가 아닌 aspect라고 명명한다.
객체지향적 설계의 핵심인 추상화를 적용해도 aspect는 6-21의 왼쪽 그림처럼 기존 구조와 분리되지 않는다. 이를 해결하기 위해서 aspect를 기존구조와는 아예 분리되는 측면으로 분리/설계 하는 방법을 AOP라고 정의할 수 있다. 이는 OOP를 대체할 새로운 패러다임이 아닌, OOP를 더 잘 사용하기 위한 보조 수단으로 보는게 맞다.
스프링의 AOP 적용기술
- 프록시를 사용하는 AOP 적용어드바이스가 구현하는 MethodInteceptor 인터페이스는 프록시로부터 요청정보를 전달받고 타겟 오브젝트 메서드를 호출하게 되는데, 이 호출 전후로 다양한 부가기능을 제공할 수 있다.
- AOP의 적용 핵심을 프록시의 사용이다. 프록시로 만들어서 DI로 연결된 클라이언트→타겟 사이에서 부가기능을 제공해주는것이 골자이다.
- 바이트코드 생성/조작을 통한 AOP 적용
- 이 외에 바이트코드 생성/조작을 통한 AOP 적용 방식도 존재한다. AspectJ 프레임워크는 프록시처럼 간접적인 방식이 아니라 실제 타겟 오브젝트를 수정해서 부가기능을 직접 넣어주는 방법을 사용한다. 이를 소스코드 수정으로는 해결이 불가하니 컴파일된 class파일을 수정하거나 JVM 로딩시점에 가로채서 바이트코드를 조작하는 방식을 사용한다.
이렇게 AOP 적용 시, 스프링 컨테이너가 사용되지 않는 환경에서도 AOP적용이 가능하고 타겟 오브젝트가 생성되는 시점에 부가기능을 적용시켜줄수도 있다.
6.6 트랜잭션 속성
TransactionAdvice에서는 타겟 메서드 호출이전에 트랜잭션을 가져오는 작업을 하고 있는데, 여기서 사용하는 DefaultTransactionDefinition에 대해서 좀 더 알아본다.
트랜잭션 정의
기본적으로 트랜잭션은 작업의 최소단위를 뜻하고 진행된 작업은 commit을 해서 모두 성공하든지 rollback을 통해서 모두 취소가 되어야 한다. TransactionDefinition 인터페이스를 구현하고 있는 구현체의 셋팅을 통해서 이외에 추가로 하기 트랜잭션 동작방식을 제어할 수 있다.
- 트랜잭션 전파
특정 트랜잭션 A가 진행중에 있고 중간에 B에서 새로운 트랜잭션이 생성된다면 B의 동작은 트랜잭션 전파 속성에 따라서 다르게 처리될 수 있다.- PROPAGATION_REQUIRED
진행중인 트랜잭션이 없으면 새로 시작하고, 있다면 이에 참여한다. - PROPAGATION_REQUIRES_NEW
항상 새로운 트랜잭션을 시작한다. - PROPAGATION_NOT_SUPPORTED
트랜잭션 처리를 하지 않도록 동작한다. AOP의 포인트컷중에서 일부 메서드는 이를 미적용 시키고 싶은 경우 사용한다.
- PROPAGATION_REQUIRED
- 격리수준
- 모든 DB 트랜잭션은 격리수준 관련 설정을 가지고 있고, 이는 메서드 레벨로 재정의할 수도 있다.
- 제한시간
- 읽기전용
트랜잭션 인터셉터와 트랜잭션 속성
- 메서드 이름 패턴을 이용한 트랜잭션 속성 지정
- 트랜잭션 인터셉터를 통해서 트랜잭션 속성을 메서드별로 다르게 가져갈수도 있다. 스프링에서 제공하는 TransactionInterceptor를 아래와 같이 설정함으로써 특정 메서드 명에 대한 조건으로 적용되는 트랜잭션 속성을 다르게 가져갈 수 있다.
- tx 네임스페이스를 이용한 설정 방법
설정파일내에서 비슷해보이는 태그 대신, 용도를 명확히 드러내주는 태그를 사용함으로써 가독성 측면에서 더 좋다.
포인트컷과 트랜잭션 속성의 적용 전략
- 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 사용
ex)executon(**..*ServiceImpl.(..))
- 공통된 메서드명을 사용해서 최소한의 트랜잭션 어드바이스와 속성을 정의
ex) insert, get 등 특정 동작에 대한 공통 접두어를 정의 - 프록시 방식의 AOP는 동일 타겟 객체내의 메서드 호출시에는 적용 불가
아래 구조와 같이 1,3은 프록시를 통해서 타겟 메서드가 호출됐기 때문에 트랜잭션 적용이 되지만 2의 경우는 트랜잭션 적용이 불가하다.
6.7 어노테이션 트랜잭션 속성과 포인트컷
@Transactional 어노테이션
직관적으로 사용이 가능하며 메서드 ,클래스, 인터페이스에 사용이 가능하다. 해당 어노테이션이 붙은 대상을 스프링은 자동으로 타겟 객체로 인식하며, 이때 TransactionAttributeSourcePointcut 포인트컷이 사용된다. 또한 어드바이저의 동작방식 또한 해당 어노테이션별로 구분해서 가져올 수 있는 AnnotationTransactionAttributeSource를 사용하기 때문에 메서드별로 유연한 트랜잭션 속성이 가능하다.
트랜잭션 어노테이션 적용
UserServie에 트랜잭션 어노테이션을 적용한다고 하면, 구체클래스가 아닌 인터페이스에 @Transactional 적용을 해두고, 읽기전용 설정이 필요한 단순 조회 메서드는 메서드 레벨에 @Transactional를 다시 적용하는 것이 일반적이다.
스프링에서는 @Transactional 적용 시 메서드 → 타겟 클래스 → 인터페이스 순서로 확인하기 때문에 아래와 같은 적용이 가능하다.
6.8 트랜잭션 지원 테스트
선언적 트랜잭션과 트랜잭션 전파 속성
UserService의 add() 메서드의 트랜잭션 전파 방식을 디폴트값이 REQUIRED 라고 봤을때, 이를 사용하는 영역에서는 좀 더 큰 단위의 트랜잭션을 이미 시작했을 수 있다. 예를 들어서 EventService의 processDailyEventRegistration() 메서드에서 이미 트랜잭션이 시작되었다면 add() 가 독립적인 트랜잭션으로 시작되는대신 이미 진행되는 트랜잭션의 일부로 참여하게 된다.
스프링에서는 이러한 트랜잭션의 전파 속성을 어노테이션을 통해서 선언적으로 간편하게 적용할 수 있기 때문에 불필요한 add() 메서드 코드 중복이 발생하지 않는다.
AOP를 통해서 외부에서 트랜잭션의 기능을 부여하고 속성을 지정할 수 있는 선언적 트랜잭션 방식이 가능하다.
트랜잭션 동기화와 테스트
디폴트 트랜잭션 전파속성인 REQUIRED의 경우 이미 진행중인 트랜잭션이 있으면 그 트랜잭션에 참여할 수 있돋록 하고, 이는 트랜잭션 동기화 기술에 의해서 가능하다. 보통 서비스로직에서는 선언적 트랜잭션을 통해서 이를 관리하는게 편하지만 테스트코드 작성의 경우, 트랜잭션 매니저를 통해서 이를 제어한다.
userService내의 메서드는 모두 전파속성이 REQUIRED 로 되어있기 때문에 해당 테스트 메서드 실행시 트랜잭션이 독립적으로 3개 생성되는게 맞다. 하지만 트랜잭션 매니저를 통해서 임의로 userService 메서드 호출이전에 트랜잭션 시작시 전파속성에 의해서 테스트 메서드내에서 1개의 트랜잭션만 생성된다.
테스트에서 트랜잭션을 조작할 수 있다면 트랜잭션 결과나 상태를 조작하면서 테스트하는게 가능하고 트랜잭션 전파속성에 따라서 여러 메서드를 조합해서 어떤 결과가 나오는지 미리 검증이 가능하다.
테스트를 위한 트랜잭션 어노테이션
- @Transactional
- @Rollback
테스트용 트랜잭션은 기본적으로 테스트가 종료되면 자동으로 롤백된다. 하지만 이러한 강제 롤백을 하지않고 테스트에서 진행한 작업을 그대로 DB에 반영하고 싶으면 @Rollback(false) 어노테이션을 사용한다. - @TransactionConfiguration
'Spring' 카테고리의 다른 글
[토비 스프링3] 6. AOP (1) (0) | 2023.03.04 |
---|---|
[토비 스프링3] 5. 서비스 추상화 (0) | 2023.02.13 |
[토비 스프링3] 4. 예외 (0) | 2023.01.23 |
[토비 스프링 3] 3. 템플릿 (0) | 2023.01.08 |
[토비 스프링 3] 2. 테스트 (0) | 2020.06.09 |