[Spring] 토비의 스프링 Vol.1 6장 AOP (하)

Shiba·2023년 9월 18일
0

🍀 스프링 정리

목록 보기
8/21
post-thumbnail

📗 AOP (하)

❗ 토비의 스프링 3.1 vol 1 정리입니다.
책을 읽지 않으셨다면 이해가 어려울 수 있습니다!

부제 : 스프링의 AOP

(상)편에서 우리는 코드의 수정 없이 트랜잭션 부가기능을 추가해주는 다양한 방법에 대해 알아보았다.
(하)편에서는 스프링은 이러한 문제를 어떻게 해결하였는지에 대해 알아볼 것이다.

📖 스프링의 프록시 팩토리 빈

스프링은 매우 세련되고 깔끔한 방식으로 문제에 대한 해법을 제공한다

📝 ProxyFactoryBean

스프링은 트랜잭션,메일 발송 기술에 적용했던 서비스 추상화를 프록시에도 사용
- 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈 제공

◼ ProxyFactoryBean의 기능

  • (상)편에서 만든 TxProxyFactoryBean과 달리 순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능별도의 빈에 둘 수 있다.
  • 부가기능은 MethodInterceptor인터페이스를 구현해서 만듦
    - MethodInterceptor는 타깃 오브젝트에 대한 정보까지도 함께 제공 받음
    => 독립적으로 생성이 가능 -> 타깃이 다른 여러 프록시에서 함께 사용가능

(상)편에서 만든 다이나믹 프록시 학습테스트를 ProxyFactoryBean을 이용하여 수정해보자.

package springbook.learningtest.jdk.proxy;
...
public class DynamicProxyTest{
	@Test
    public void simpleProxy() { //JDK 다이나믹 프록시 생성
    	Hello proxiedHello = (Hello)Proxy.newProxyInstance(
        		getClass().getClassLoader(),
                new Class[] { Hello.class },
                new UppercaseHandler(new HelloTarget()));
       ...
    }
    
    @Test
    public void proxyFactoryBean() {
    	ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget()); //타깃 설정
        pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스 추가
        
        //getObject()로 생성된 프록시 가져옴
        Hello proxiedHello = (Hello) pfBean.getObject();
        
        assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
        assertThat(proxiedHello.sayHello("Toby"), is("HI TOBY"));
        assertThat(proxiedHello.sayHello("Toby"), is("THANK YOU TOBY"));
    }
    
    static class UppercaseAdvice implements MethodInterceptor {
    	public Object invoke(MEthodInvocation invocation) throws Throwable {
        	//리플렉션의 Method와 달리 실행시 타깃 오브젝트를 전달할 필요 X
        	String ret = (String)invocation.proceed(); 
            return ret.toUpperCase(); //부가기능 적용
        }
    }
    
    static interface Hello {
    	String sayHello(String name);
        String sayHi(String name);
        String sayThankYou(String name);
    }
    
    static class HelloTarget implements Hello {
    	public String sayHello(String name) { return "Hello " + name; }
        public String sayHi(String name) { return "Hi " + name; }
        public String sayThankYou(String name) { return "Thank You " + name; }
    }
}

MethodInterceptor는 타깃의 메소드를 내부적으로 실행시켜주는 일종의 템플릿처럼 동작. - 템플릿/콜백 구조의 응용이므로 싱글톤으로 두고 공유 가능!!

◼ 📌어드바이스 : 타깃이 필요 없는 순수한 부가기능

타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트
- MethodInterceptor를 설정해줄때의 addAdvice()

◼ 포인트컷: 부가기능 적용 대상 메소드 선정 방법

(상)편에서 InvocatiionHandler는 부가기능 적용외에도 메소드 이름을 가지고 부가기능 적용 대상 메소드를 선정하는 작업이 존재

스프링의 MethodInterceptor은 이름을 통한 대상 메소드 선정이 불가능.
=> 메소드 선정 알고리즘을 담은 오브젝트인 포인트컷을 사용!
- 스프링은 Pointcut 구현클래스를 제공함
- 포인트컷 또한 프록시에 DI되어 사용되며 여러 프록시에서 공유가능 - 싱글톤 빈

//포인트컷까지 적용한 ProxyFactoryBean
@Test
public void pointcutAdvisor() {
	ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(new HelloTarget());
    
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedName("sayH*"); //sayH로 시작하는 모든 메소드 선택
    
    pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
    
    Hello proxiedHello = (Hello) pfBean.getObject();
    
    assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
    assertThat(proxiedHello.sayHello("Toby"), is("HI TOBY"));
    //메소드 이름이 포인트컷 선정조건과 맞지않음 - 부가기능 적용 X
    assertThat(proxiedHello.sayHello("Toby"), is("Thank you TOBY"));
}

포인트컷과 어드바이스를 묶어서 하나의 Advisor타입으로 호출
- 여러 개의 포인트컷과 어드바이스가 추가될 수 있기때문에 조합을 만들어 저장해두는 것

  • 어드바이저(Advisor) = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)

📝 ProxyFactoryBean 적용

기존의 TxProxyFactoryBean을 ProxyFactoryBean으로 수정해보자

package springbook.user.service;
...
public class TransactionAdvice implements MethodInterceptor {
	PlatformTransactionManager transactionManager;
    
    public void setTransactionManager(PlatformTransactionManager
    		transactionManager) {
   		this.transactionManager = transactionManager;         
    }
    
    public Object invoke(MethodInvocation invocation) throws Throwable {
    	TransactionStatus status = 
        		this.transactionManager.getTransaction(new
                	DefaultTransactionDefinition());
        try {
        	Object ret = invocation.proceed(); //콜백을 호출해서 타깃의 메소드 실행
            this.transactionManager.commit(status);
            return ret;
        }
        //MethodInvocation을 통한 타깃호출은 예외가 포장되지 않고 타깃에서 보낸 그대로 전달
        catch (RuntimeException e){ 
        	this.transactionManager.rollback(status);
            throw e;
        }
    }
}

◼ 어드바이스와 포인트컷의 재사용

스프링의 ProxyFactoryBeanDI와 템플릿/콜백 패턴, 서비스 추상화 기법이 모두 적용되어있음

  • 독립적인 어드바이스와 포인트컷으로 확장기능을 분리할 수 있음
    - 새로운 서비스 클래스가 만들어져도 만들어둔 TransactionAdvice를 그대로 사용가능

📖 스프링 AOP

기존의 프록시 팩토리 빈의 한계라고 생각한 문제점들이 있었다

  • 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제
    - ProxyFactoryBean의 어드바이스를 통해 해결
  • 부가기능의 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 설정정보를 추가해주어야 함
    - 이번 파트에서는 이 문제를 해결해보도록하자!

📝 자동 프록시 생성

◼ 중복 문제의 접근 방법

우리는 프록시 클래스 코드의 중복문제를 해결할 때, 다이나믹 프록시라는 런타임 코드 자동생성 기법을 사용했다.
- 이 방법을 ProxyFactoryBean의 설정 자동등록기법으로 해결할수 있지 않을까?

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

빈 후처리기 : 스프링 빈 오브젝트로 만들어진 후, 빈 오브젝트를 다시 가공할 수 있게 해줌
- 빈 오브젝트의 프로퍼티를 강제로 수정하거나 별도의 초기화 작업 수행가능
- 만들어진 빈 오브젝트 자체를 바꿔치기할 수도 있음
- 구조와 자세한 사용법은 Vol.2에서 다룰 것

◼ 자동 프록시 생성기 - DefaultAdvisorAutoProxyCreator

빈 후처리기중 한 종류. 어드바이저를 이용한 자동 프록시 생성기
자기자신을 빈으로 등록하면 사용가능

기능

스프링이 생성하는 빈 오브젝트의 일부프록시로 포장하고, 프록시를 빈으로 대신 등록가능

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

  1. DefaultAdvisorAutoProxyCreator 빈 후처리기가 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 빈 후처리기에게 빈을 보냄
  2. DefaultAdvisorAutoProxyCreator빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인
  3. 적용대상이라면, 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 함
  4. 만든 프록시에 어드바이저를 연결해줌
  5. 빈 후처리기는 원래 컨테이너가 전달해준 빈 대신 프록시를 컨테이너에게 돌려줌
  6. 컨테이너는 후처리기가 돌려준 프록시를 빈으로 등록하고 사용하게됨

◼ 확장된 포인트컷

포인트컷두가지 기능을 가지고있음

  • 타깃 오브젝트의 메소드어떤 메소드에 부가기능을 적용할지 선정하는 역할
  • 등록된 빈 중에서 어떤 빈에 프록시를 적용할지 선택
    - 빈 후처리기를 사용하는 경우, 프록시 자동 적용 대상을 선별해야하기 때문에 기능을 모두 구현한 포인트컷과 결합된 어드바이저가 필요
public interface Pointcut {
	ClassFilter getClassFilter(); //프록시를 적용할 클래스인지 확인
    MethodMatcher getMethodMatcher(); // 어드바이스를 적용할 메소드인지 확인
}

포인트컷의 기능을 간단한 학습테스트로 확인해보자!

@Test
public void classNamePointcutAdvisor() {
	//포인트컷 준비
    NameMatchMetodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
    	public ClassFilter getClassFilter() {
        	return new ClassFilter() {
            	public boolean matches(Class<?> clazz){
                 	//클래스 이름이 'HelloT'인것만 선정
                	return clazz.getSimpleName().startsWith("HelloT");
                }
            };
        }
    };
    classMethodPointcut.setMappedName("sayH*"); //'sayH'로 시작하는 모든 메소드 선정
    
    //테스트
    checkAdviced(new HelloTarget(), classMethodPointcut, true); //적용 클래스
    
    class HelloWorld extends HelloTarget {};
    checkAdviced(new HelloWorld(), classMethodPointcut, false); //적용 클래스 X
    
    class HelloToby extends HelloTarget {};
    checkAdviced(new HelloToby(), classMethodPointcut, true); //적용 클래스
}

private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) { [3]
	ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(target);
    pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
    Hello proxiedHello = (Hello) pfBean.getObject();
    
    if(adviced) { // 메소드 선정 방식을 통해 어드바이스 적용
    	assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
        assertThat(proxiedHello.sayHello("Toby"), is("HI TOBY"));
        assertThat(proxiedHello.sayHello("Toby"), is("Thank You Toby"));
    }
    else{ // 어드바이스 적용 대상 후보에서 아예 탈락
    	assertThat(proxiedHello.sayHello("Toby"), is("Hello Toby"));
        assertThat(proxiedHello.sayHello("Toby"), is("Hi Toby"));
        assertThat(proxiedHello.sayHello("Toby"), is("Thank You Toby"));
    }
}

📝 DefaultAdvisorAutoProxyCreator의 적용

이제 실제로 적용해보도록하자

◼ 클래스 필터를 적용한 포인트컷 작성

메소드 이름만 비교하던 NameMatchMethodPointcut을 상속해서 프로퍼티로 주어진 이름 패턴을 가지고 클래스 이름을 비교하는 ClassFilter만 추가하면 된다.

package springbook.learningtest.jdk.proxy;
...
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
	public void setMappedClassName(String mappedClassName) {
    // 모든 클래스를 다 허용하던 디폴트 클래스 필터를 
    //	프로퍼티로 받은 클래스 이름을 이용해서 필터를 만들어 덮어씌움.
    	this.setClassFilter(new SimpleClassFilter(mappedClassName)); 
    }
    
    static class SimpleClassFilter implements ClassFilter {
    	String mappedName;
        
        private SimpleClassFilter(String mappedName){
        	this.mappedName = mappedName;
        }
        
        public boolean matches(Class<?> clazz) {
        	//와일드카드(*)가 들어간 문자열 비교를 지원하는 스프링의 유틸리티 메소드
        	return PatternMatchUtils.simpleMatch(mappedName,
            		clazz.getSimpleName()); 
        }
    }
}

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

리플렉션 API작성이 번거롭고, API를 이용해 메타정보를 비교하는 방법조건이 달라질때 마다 포인트컷 구현 코드를 수정해야하는 번거로움 존재
- 스프링에서는 정규식,JSP의 EL과 같은 일종의 표현식 언어를 이용해 작성가능
- 이를 포인트컷 표현식(pointcut expression)이라고 부른다.

◼ 포인트컷 표현식

AspectJExpressionPointcut를 사용해 포인트컷 표현식을 지원하는 포인트컷 적용

  • 클래스와 메소드의 선정 알고리즘표현식을 이용해 한 번에 지정 가능
  • AspectJ프레임워크에서 가져와 사용하여 AspectJ포인트컷 표현식이라고도 한다

학습테스트를 만들어 표현식의 사용 방법을 살펴보자

//포인트컷 테스트용 클래스
package springbook.learningtest.spring.pointcut;
...
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() {}
}

//포인트컷 테스트용 추가 클래스
public class Bean {
	public void method() throws RuntimeException {}
}

◼ 포인트컷 표현식 문법

포인트컷 표현식은 포인트컷 지시자를 이용해 작성 대표적으로 execution()사용

execution([접근제한자 패턴] 타입 패턴 [타입 패턴.]이름 패턴 (타입 패턴 | "..", ...)
[throws 예외 패턴]) //[]안 패턴은 생략가능
  • 접근제한자 패턴 : public protected, private등이 올 수 있다.
  • 타입 패턴 : 리턴 값의 타입을 나타내는 패턴이다. 반드시 하나의 타입을 지정
  • 타입 패턴. : 패키지와 타입 이름을 포함한 클래스의 타입 패턴. 패키지 이름과 클래스 또는 인터페이스 이름'*'사용 가능. '..'를 사용하면 한 번에 여러개의 패키지 선택 가능.
  • (메소드)이름 패턴 : 메소드 이름 패턴으로, 모든 메소드를 선택한다면 * 사용
  • (타입 패턴) : 메소드 파라미터의 타입 패턴이다. 메소드 파라미터의 타입을 ','로 구분하면서 순서대로 적으면 된다. 파라미터 타입과 갯수에 상관없이 모두 허용하는 패턴으로 만드려면 '..'을 넣으면 된다. '...'를 이용해서 뒷부분의 파라미터 조건만 생략 가능
  • 예외 패턴 : 예외 이름에 대한 타입 패턴이다.

Target클래스의 minus() 메소드만 선정해주는 포인트컷 표현식을 만들고 검증해보는 테스트를 작성해보자

@Test
public void methodignaturePointcut() throws SecurityException,
		NoSuchMethodException {
	AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("execution(public int " +
    	"springbook.learningtest.spring.pointcut.Target.minus(int,int)" + 
        "throws java.lang.RuntimeException)"); //Target 클래스 minus() 메소드 시그니처
        
    // Target.minus()
    //클래스 필터와 메소드 매처를 가져와 각각 비교
    assertThat(pointcut.geClassFilter().matches(Target.Class) && 
    	pointcut.getMethodMatcher().matches(
       	 	//포인트컷 조건 통과
            Target.class.getMethod("minus", int.class, int.class), null), is(true));
    // Target.plus()
    //클래스 필터와 메소드 매처를 가져와 각각 비교
     assertThat(pointcut.geClassFilter().matches(Target.Class) &&
    	pointcut.getMethodMatcher().matches(
        	//메소드 매처에서 실패
            Target.class.getMethod("plus", int.class, int.class), null), is(false));
    // Bean.method()
     assertThat(pointcut.geClassFilter().matches(Bean.Class) &&
    	pointcut.getMethodMatcher().matches(
            Target.class.getMethod("method"), null), is(false));
}

◼ 포인트컷 표현식 테스트

메소드 시그니처를 그대로 사용한 포인트 표현식을 정리해보자

// 다양한 활용방법을 보기위한 테스트 보충
// 포인트컷과 메소드를 비교해주는 테스트 헬퍼 메소드
public void pointcutMatches(String expression, Boolean expected, Class<?> clazz, 
		String methodName, Class<?>... args) throws Exception {
	AspectJExpressinPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression(expression);
    
    assertThat(pointcut.getClassFilter().matches(clazz)
    	&& pointcut.getMethodMatcher().matches(clazz.getMethod(methodName,
        args), null), is(expected));
}       
//타깃 클래스의 메소드 6개에 대해 포인트컷 선정 여부를 검사하는 헬퍼 메소드
public void tagetClassPointcutMatches(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");
 }
//포인트컷 표현식 테스트
@Test
public void pointcut() throws Exception {
	tagetClassPointcutMatches("execution(* *(..))", true, true, true, true, true, true);
   //나머지는 생략 -  아래의 표에 정리
}

결과를 가리고 직접 O표를 쳐보면서 익숙해지도록 하자!

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

◼ 애스펙트(Aspect)

핵심기능을 담고 있지는 않지만, 하나의 구성요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈
- 어드바이저는 아주 단순한 형태의 애스펙트라고 볼 수 있다

애스펙트는 말 그대로 애플리케이션을 구성하는 한 가지 '측면'이라고 볼 수 있다.

2차원적 설계 시에는 해결할 수 없던 중복코드들이 3차원적 구조로 설계하면서 부가기능 코드를 독립적으로 분리하여 중복을 제거하였다

◼ 애스펙트 지향 프로그래밍(Aspect Oriented Programming)

애플리케이션의 핵심 기능에서 부가기능을 분리하여 애스펙트라는 독특한 모듈로 만들어 설계하고 개발하는 방법

📝 AOP 적용기술

◼ 프록시를 이용한 AOP

스프링다양한 기법들을 사용해 AOP를 지원하는데, 그 중 핵심은 프록시이다.
- 프록시로 만들어 DI로 연결된 빈 사이에 적용타깃 메소드 호출 과정에 참여부가기능을 제공
=> 스프링 AOP는 프록시 방식의 AOP라 할 수 있다.

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

AspectJ와 같이 직접적으로 타깃 오브젝트를 뜯어고쳐 부가기능을 직접 넣어주는 방식
- 컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM의 로딩되는 시점을 가로채 바이트코드를 조작

프록시가 있음에도 이 방법을 사용하는 이유

  1. 스프링의 DI 컨테이너의 도움을 받아 자동 프록시 생성 방식을 사용하지 않아도 AOP적용 가능
  2. 프록시 방식보다 훨씬 강력하고 유연한 AOP가 만들어짐
    - 프록시와 달리 오브젝트 생성, 필드 값 조회와 조작, 스태틱 초기화등 다양한 작업에 부가기능 부여 가능

📝 AOP의 용어

◼ 타깃

부가기능을 부여할 대상.
- 핵심기능을 담은 클래스, 경우에 따라 다른 부가기능을 제공하는 프록시

◼ 어드바이스

타깃에게 제공할 부가기능을 담은 모듈.
- 오브젝트로 정의하기도 하지만, 메소드레벨에서 정의할 수 있음
- 전반적으로 참여하는 것, 예외발생시에만 참여하는 것 등 여러 종류 존재

◼ 조인포인트

어드바이스가 적용될 수 있는 위치
- 스프링의 프록시 AOP에서 조인 포인트는 메소드 실행단계 뿐.

◼ 포인트컷

어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈.
스프링의 포인트컷은 메소드를 선정하는 기능을 가짐

◼ 프록시

클라이언트와 타깃 사이투명하게 존재하면서 부가기능을 제공하는 오브젝트.
- DI를 통해 타깃 대신 클라이언트에게 주입

◼ 어드바이저

포인트컷과 어드바이스를 하나씩 갖고있는 오브젝트.
-어떤 부가기능을 어디에 전달할 것인가를 알고있는 AOP의 가장 기본적 모듈
- 스프링에서만 사용되는 용어

◼ 애스펙트

AOP의 기본 모듈. 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어짐.
- 싱글톤 형태로 존재.

📝 AOP 네임스페이스

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

◼ 자동 프록시 생성기

스프링의 DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록.
- DI를 하지도, 되지도 않으며 독립적으로 존재 - id가 굳이 필요없음
- 빈으로 등록된 어드바이저를 이용해 프록시를 자동으로 생성하는 기능

◼ 어드바이스

부가기능을 구현한 클래스를 빈으로 등록
- TransactionAdvisor는 AOP 관련 빈중 유일하게 직접 구현된 클래스 사용

◼ 포인트컷

스프링의 AspectJExpressionPointcut을 빈으로 등록하고 expression 프로퍼티에 포인트컷 표현식 삽입
- 코드 작성이 필요없음

◼ 어드바이저

스프링의 DefaultPointcutAdvisor 클래스를 빈으로 등록.
- 어드바이스와 포인트컷을 프로퍼티로 참조
- 자동 프록시 생성기에 의해 자동 검색되어 사용

◼ AOP 네임스페이스

네가지의 빈들을 간편한 방법으로 등록하기위해 스프링은 aop스키마를 제공

<aop:config>
	//expression의 표현식을 프로퍼티로 가진 AspectJExpressionPointcut을 빈으로 등록
	<aop:pointcut id="transactionPointcut" 
    				expression="execution(* *..*ServiceImpl.upgrade*(..))" />
    //advice와 pointcut의 ref를 프로퍼티로 갖는 DefaultBeanFactoryPointcutAdvisor를 등록                
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
</aop:config> 

<aop:config>, <aop:pointcut>, <aop:advisor> 태그를 정의해두면 세 개의 빈 등록

📖 트랜잭션 속성

트랜잭션 추상화를 하면서 사용한 DefaultTransactionDefinition의 용도를 알아보자

public Object invoke(MethodInvocation invocation) throws Throwable {
    	TransactionStatus status = 
        		this.transactionManager.getTransaction(new 		
                	DefaultTransactionDefinition()); // 트랜잭션 정의
        try {
        	Object ret = invocation.proceed(); 
            this.transactionManager.commit(status);
            return ret;
        } catch (RuntimeException e){ 
        	this.transactionManager.rollback(status);
            throw e;
        }
    }

📝 트랜잭션 정의

트랜잭션의 원자성은 항상 유효하지만, commit(), rollback()말고도 동작을 제어할 수 있는 조건들이 존재한다
- DefaultTransactionDefinition구현하고 있는 TransactionDefinition 인터페이스는 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의

◼ 트랜잭션 전파(Transaction Propagation)

트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 떼 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식

  1. 트랜잭션 A가 (1)수행 후, B.method() 실행
  2. B.method()에서 트랜잭션 B 실행 후 종료
  3. A가 (2)수행 중 예외 발생 – 롤백


    B가 트랜잭션 A에 참여한 경우, A,B 모두 롤백.
    B를 독립적인 트랜잭션으로 만들어 참여한 경우, B는 독립적으로 종료되었으므로 롤백 되지 않음


    이처럼 이미 진행중인 트랜잭션독자적인 트랜잭션 경계를 가진 코드에 대해 어떻게 영향을 미칠 수 있는가를 정의한 것이 트랜잭션 전파 속성

트랜잭션 전파 속성의 종류

  • PROPAGTION_REQUIRED
    가장 많이 사용하는 속성. 진행 중인 트랜잭션이 없으면 새로 시작, 있다면 이에 참가한다.
    - 두 트랜잭션 코드 모두 PROPAGTION_REQUIRED라면 다양한 방식으로 결합 가능 (A, B, A→B, B→A)
    - DefaultTransactionDefinition의 전파 속성

  • PROPAGTION_REQUIRES_NEW
    이미 진행 중인 트랜잭션에 유무에 상관없이 항상 새로운 트랜잭션을 시작한다.

  • PROPAGTION_NOT_SUPPORTED
    트랜잭션 없이 동작하도록 만들 수 있다. 진행 중인 트랜잭션은 무시한다.
    - 특별히 트랜잭션 적용에서 제외해야하는 메소드가 존재한다면 사용

getTransaction()이라는 메소드를 사용하는 이유가 바로 전파 속성이 존재하기 때문
- 항상 트랜잭션을 새로 시작하는 것이 아닌 진행 중인 트랜잭션 존재와 전파 속성에 따라 새로 시작할수도, 참가할 수도 있음

◼ 격리수준(Isolation Level)

모든 DB트랜잭션은 격리수준을 갖고 있어야 한다.
- 적절하게 격리수준을 조정하여 많은 트랜잭션을 동시에 진행시키면서 문제가 없도록 제어가 필요하다.
- DefaultTransactionDefinition의 격리수준은 ISOLATION_DEFAULT이다

◼ 제한시간(Timeout)

트랜잭션을 수행하는 제한시간 설정 가능
- DefaultTransactionDefinition의 기본설정은 제한시간이 없는 것

◼ 읽기전용(Read Only)

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

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

트랜잭션 정의를 수정하기위해 DefaultTransactionDefinition대신 외부에서 정의된 TransactionDefinition 타입의 빈을 정의해두면 원하는 속성을 지정해줄 수 있다. 하지만 이 방법을 사용하면 모든 트랜잭션의 속성이 한꺼번에 바뀐다는 문제가 존재한다. 독자적인 트랜잭션 정의를 적용할 수 있는 방법은 없을까?

◼ TransactionInterceptor

스프링은 트랜잭션 경계설정 어드바이스로 사용할 수 있도록 만들어진 TransactionInterceptor가 존재.

  • 기존의 TransactionAdvice와 동작원리가 동일함. 다만 트랜잭션 정의메소드 이름 패턴을 이용다르게 지정할 수 있는 방법을 추가로 제공
  • PlatformTransactionManager와 Properties타입의 두 가지 프로퍼티를 가짐
    - Properties 타입인 두번째 프로퍼티는 transactionAttributes로 트랜잭션의 속성을 정의한 프로퍼티.
  • 런타임 예외가 발생하는 경우에만 트랜잭션 롤백. 그외의 경우는 트랜잭션 커밋
    - 이러한 예외처리 기본 원칙을 따르지 않는 경우, rollbackOn()이라는 속성을 두어 기본원칙과 다른 예외처리가 가능

◼ 메소드 이름 패턴을 이용한 트랜잭션 속성 지정

Properties 타입의 transactionAttributes 프로퍼티메소드 패턴과 트랜잭션 속성을 키와 값으로 갖는 컬렉션. 다음과 같이 문자열로 정의가능

  • 트랜잭션 전파 속성만 필수이고, 나머지는 모두 생략가능
    - 생략하면 모두 DefaultTransactionDefinition에 설정된 디폴트 속성이 부여
  • 순서가 바뀌어도 상관이 없음

트랜잭션 속성 정의 예

...
<props>
	//get으로 시작하는 모든 메소드에 대한 속성
	<prop key="get*">PROPAGATION_REQUIRED,readOnly,timeout_30</prop> 
    //upgrade로 시작하는 모든 메소드에 대한 속성
    <prop key="upgrade*">PROPAGATION_REQUIRES_NEW,ISOLATION_SERIALIZABLE</prop>
    //위 두가지 조건에 포함되지 않은 나머지 메소드들의 속성
    <prop key="*">PROPAGATION_REQUIRED</prop>
...

📝 포인트컷과 트랜잭션 속성의 적용 전략

포인트컷 표헌식과 트랜잭션 속성을 정의할 때 따르면 좋은 몇 가지 전략을 생각해보자

◼ 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용

비즈니스 로직을 담고 있는 클래스라면 메소드 단위까지 세밀하게 적용할 필요X

  • 트랜잭션 포인트컷 표현식에는 메소드나 파라미터, 예외에 대한 패턴정의하지 않는게 바람직함.
  • 클래스 이름에서 일정한 패턴을 찾아 표현식으로 만들기
    - Service, ServiceImpl
  • 일정한 패턴을 찾기 어려운 경우, bean()을 이용해 빈의 아이디를 통해 적용
    - bean(*Service) - 빈의 아이디가 Service로 끝나는 모든 빈에 적용

◼ 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의

  • 기준이 되는 몇 가지 트랜잭션 속성을 정의하고 그에 따라 적절한 메소드 명명 규칙을 만들어 사용
  • 간단한 디폴트 속성으로 시작하여 개발과정에서 필요하다면 하나씩 추가하기

❗ 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않음

주의사항!

  • 프록시 방식의 AOP에서는 프록시를 통한 부가기능의 적용클라이언트로부터 호출이 일어날 때만 가능
    - 타깃 오브젝트가 자기 자신의 메소드를 호출할 때는 부가기능의 적용이 일어나지 않음. (타깃의 delete()메소드에서 update()실행시 적용 X)
    - 이를 해결하기 위해 타깃의 바이트코드를 조작하는 방식을 적용하는 것

📝 트랜잭션 속성 적용

트랜잭션 속성과 그에 따른 트랜잭션 전략을 UserService에 적용해보자

◼ 트랜잭션 경계설정의 일원화

비즈니스 로직을 독자적으로 두고 테스트하기 위해 인터페이스로 서비스계층을 만들어 사용

public interface UserService {
	void add(User user);
    
    //추가된 메소드
    User get(String id); 
    void deleteAll();
    void update(User user);
    
    void upgradeLevels();
}
//추가된 메소드들의 구현 코드 넣어주기
public class UserServiceImpl implements UserService {
	UserDao userDao;
    ...
    //DAO로 위임하도록 만듦
    public void deleteAll() { userDao.deleteAll(); }
    public User get(String id) { return userDao.get(id); }
    public List<User> getAll() { return UserDao.getAll(); }
    public void update(User user) { userDao.update(user); }
    ...
}

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

가끔 클래스나 메소드에 따라 세밀하게 튜닝된 트랜잭션 속성을 적용해야 하는 경우가 존재하는데, 이름 패턴으로 속성을 부여하는 방식은 적합하지 않음.
- 스프링은 이러한 방법 대신 직접 타깃에 트랜잭션 속성정보를 가진 애노테이션을 지정하는 방법이 존재

📝 트랜잭션 애노테이션

주요 메타애노테이션에 대해 알아보자

◼ @Transactional

package org.springframework.transaction.annotation;
...

@Target({ElementType.METHOD, ElementType.TYPE}) // 애노테이션을 사용할 대상 지정
@Retention(RetentionPolicy.RUNTIME) // 애노테이션 정보가 언제까지 유지되는지를 지정
@Inherited //상속을 통해서도 애노테이션 정보를 얻을 수 있게 함
@Documented

public @interface Transactional { //트랜잭션 속성의 모든 항목을 엘리먼트로 지정
	String value() default "";
    Propagation isolation() default Isolation.DEFAULT;
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    boolean readOnlt() default false;
    Class<? extends Trowable>[] noRollbackFor() default {};
    /string[] noRollbackForClassName() default {};
}

메소드, 클래스, 인터페이스에 사용가능
@Transactional 애노테이션을 속성정보로 사용하도록 지정하면 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식

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

@Transactional 애노테이션을 사용했을 때 어드바이저의 동작방식을 살펴보자

  • TxInterceptor@Transactional 애노테이션의 엘리먼트에서 트랜잭션 속성을 가져오는 AnnotationTransactionAttributeSource 사용
  • 포인트컷 또한 @Transactional을 통한 트랜잭션 설정정보를 참조하도록 만든다

트랜잭션 부가기능 적용 단위는 메소드이기 때문에 메소드마다 @Transactional을 사용할 수 있다. 하지만 이렇게 모든 메소드를 제어한다면 코드는 지저분해지고, 동일한 속성정보를 반복적으로 부여해주는 결과가 나타날 것이다.

◼ 대체정책

위의 문제때문에 스프링은 @Transactional을 사용할 때 4단계의 대체 정책을 이용하게 해준다.


타깃 메소드, 타깃 클래스, 선언 메소드, 선언 타입의 순서에 따라서 @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용하게 하는 방법

//@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() {
    }
}

📖 트랜잭션 지원 테스트

📝 선언적 트랜잭션과 트랜잭션 전파 속성

스프링은 트랜잭션 전파 속성을 선언적으로 적용할 수 있는 기능 제공
- 중복된 코드를 관리할 필요 없이 선언을 통해 속성을 부여할 수 있음

◼ 선언적 트랜잭션과 프로그램에 의한 트랜잭션

  • 선언적 트랜잭션 : AOP를 이용코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법
  • 프로그램에 의한 트랜잭션 : TransactionTemplate개별 데이터 기술의 트랜잭션 API를 사용직접 코드 안에서 사용하는 방법


    =>스프링은 두 가지 방법 모두 지원. 특별한 경우가 아니라면 선언적 방식 권장

📝 트랜잭션 동기화와 테스트

트랜잭션 추상화 기술의 핵심은 트랜잭션 매니저와 트랜잭션 동기화다.

◼ 트랜잭션 매니저와 트랜잭션 동기화

  지금은 모든 트랜잭션을 선언적으로 AOP로 적용하고있지만, 필요하다면 프로그램에 의한 트랜잭션 방식을 함께 사용할 수도 있다. 스프링의 테스트 컨텍스트를 이용한 테스트에서는 @Autowired를 통해 애플리케이션 컨텍스트에 등록된 빈을 가져와 테스트 목적으로 활용할 수 있었다.
  즉, 트랜잭션 매니저 빈도 가져올 수 있는 것이다!

//트랜잭션 매니저를 참조하는 메소드
@RunWith(SpringJUnir4ClassRummer.class)
@ContextConfiguration(location = "/test-applicationContext.xml")
public class UserServiceTest{
	@Autowired
    PlatformTransactionManager transactionManager;
}


//간단한 테스트 메소드
@Test
public void transactionSync() {
	userService.deleteAll(); //트랜잭션 생성
    
    userService.add(users.get(0)); //트랜잭션 생성
    userService.add(users.get(1)); //트랜잭션 생성
}

◼ 트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어

위 테스트 메소드에서 만들어지는 세 개의 트랜잭션을 하나로 통합할 수는 없을까?

  메소드들이 호출되기전에 트랜잭션이 시작되게만 한다면 가능하다. UserService에 새로운 메소드를 만들어 그안에서 위 메소드들을 실행하면된다.
  그런데, 메소드를 추가하지 않고도 테스트 코드만으로 세 메소드 트랜잭션을 통합하는 방법이 있다! UserService 메소드 호출 전트랜잭션을 미리 시작해주면 된다.

@Test
public void transactionSync() {
	//트랜잭션 정의는 디폴트 값 사용
	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    //트랜잭션 매니저에게 트랜잭션 요청
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
    
    //트랜잭션 생성
    userService.deleteAll();
    
    userService.add(users.get(0)); 
    userService.add(users.get(1)); 
    //트랜잭션 종료
    
    transactionManager.commit(txStatus); //트랜잭션 커밋
}

◼ 트랜잭션 동기화 검증

테스트는 성공한다. 하지만 실제로 하나의 트랜잭션으로 통합되어 실행되는지는 확인할 수 없다. 이를 트랜잭션 속성을 변경하여 검증해보도록하자.

deleteAll()의 트랜잭션 속성은 쓰기가능이다. 이때, 먼저 실행된 트랜잭션이 읽기전용이라면 어떨까? 참여하려고한다면 예외가 발생할 것이다. 먼저 실행된 트랜잭션의 속성을 따라가기 때문이다.

public void transactionSync() {

	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    txDefinition.setReadOnly(true); //읽기전용 트랜잭션으로 정의
    
    //트랜잭션 매니저에게 트랜잭션 요청
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
    
    //트랜잭션 생성
    userService.deleteAll();  예외 발생!
    ...
}

이러한 방법은 선언적 트랜잭션이 적용된 서비스 메소드에만 적용되는 것이 아니다.
JdbcTemplate과 같이 스프링이 제공하는 데이터 액세스 추상화를 적용한 DAO에도 동일한 영향을 미친다. 따라서 다음과 같이 DAO를 직접 호출해도 동일한 결과를 얻을 수 있다!

public void transactionSync() {
	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    txDefinition.setReadOnly(true); //읽기전용 트랜잭션으로 정의
    
    //트랜잭션 매니저에게 트랜잭션 요청
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
    
    //트랜잭션 생성
    userDao.deleteAll();  예외 발생!
}

◼ 롤백 테스트

테스트 코드로 트랜잭션을 제어해서 적용할 수 있는 테스트 기법
- 테스트 내의 모든 DB작업을 하나의 트랜잭션 안에서 동작하게하고 테스트가 끝나면 무조건 롤백해버리는 테스트

public void transactionSync() {
	DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
    
    try { // 테스트 메소드를 하나의 트랜잭션으로 통합
    	userService.deleteAll();
        userService.add(users.get(0));
        userService.add(users.get(1));
    }
    finally {
    	transactionManager.rollback(txStatus); //결과가 어떻든 무조건 롤백
    }
}

장점

  • 테스트가 수행되어도 DB에 영향을 주지 않음
    - DB를 사용하는 테스트는 항상 시작전에 번거로운 초기화 작업이 필요했음
    - 롤백을 이용한다면 테스트를 시작하기위한 초기화 작업이 필요없어짐
    - 파일을 전부 날려버려도, 예외가 발생해도 롤백되기때문에 DB가 꼬이거나 손상되는 문제에 대해 생각하지 않아도됨
  • 여러 개발자가 하나의 공용 테스트용 DB를 사용할 수 있게해줌
    - 모두가 테스트의 마지막엔 롤백을 시키기때문에 하나의 DB만 존재해도 됨

📝 테스트를 위한 트랜잭션 애노테이션

@Transactional과 같은 애노테이션을 테스트 클래스와 메소드에도 적용할 수 있다!

◼ @Transactional

테스트에도 @Transactional을 적용할 수 있다. 테스트 클래스 또는 메소드에 @Transactional 애노테이션을 부여해주면 타깃 클래스나 인터페이스에 적용된 것 처럼 테스트 메소드에 트랜잭션 경계가 자동으로 설정된다.
- 테스트 내에서 진행하는 모든 트랜잭션 관련 작업을 하나로 묶을 수 있다.

@Test
@transactional //앞서 만들었던 테스트와 같은 결과를 가져옴!
public void transactionSync() {
	userService.deleteAll();
    userService.add(users.get(0));
    userService.add(users.get(1));
}

◼ @Rollback

@Transactional은 테스트에 적용하면 강제 롤백이 되도록 설정되어있다.
- @Transactional을 쓰는것으로 롤백테스트가 되는 것
그렇다면 롤백을 원하지 않으면 직접 트랜잭션을 설정해야할까?
- @Rollback 애노테이션을 사용하면 된다. - 롤백 여부를 지정

트랜잭션 롤백이 아닌 커밋을 시키도록 설정한 테스트

@Test
@Transactional
@Rollback(false) // 롤백 시키지않도록 설정
public void transactionSync() {
	...
}

◼ TransactionConfiguration

@Transactional은 테스트 클래스에 넣어 모든 테스트 메소드에 적용가능
@Rollback은 메소드 레벨에만 적용가능
- 모든 메소드에 트랜잭션을 적용하면서 롤백하지 않으려면 어떻게 해야 할까?
- 클래스 레벨에 부여할 수 있는 @TransactionConfiguration을 이용

@RunWith(SpringJUnir4ClassRummer.class)
@ContextConfiguration(location = "/test-applicationContext.xml")
@Transactional
@TransactionConfiguration(defaultRollback=false) // 공통 속성 지정가능.
public class UserServiceTest {
	@Test
    @Rollback // 공통 속성이 아닌 다른 롤백 방법 설정가능
    public void add() throws SQLException { ... }
    ...
}

◼ NotTransactional과 Propagation.NEVER

@Transactional을 적용할 때, 트랜잭션이 적용되면 안되는 메소드가 있다면 어떻게 해야 할까?
- @NotTransactional을 테스트 메소드에 부여하면 Transactional의 기본 설정 무시 가능.
❗ @NotTransactional이 스프링 3.0에서 제거대상이 되었기에 트랜잭션 테스트와 비 트랜잭션 테스트를 따로 만들도록 권장
- @Transactional의 속성NEVER로 하면 트랜잭션이 시작되지않음

@Transactional(propagation=Propagation.NEVER)
profile
모르는 것 정리하기

0개의 댓글