티스토리 뷰

웹/Spring

스프링 볶음밥 - 6장 - AOP (2)

세댕댕이 2022. 7. 8. 16:52

# 이 게시글은 "토비의 스프링" 책을 보고 정리를 위해 기록해둔 게시글입니다.

 

 

[이전 글]

더보기

볶음밥 1장 - 1,2,3: 자바빈, 디자인 패턴(템플릿 메소드, 팩토리 메소드, 전략 패턴), 관심사의 분리, SOLID 및 객체지향 약간

볶음밥 1장 - 4,5,6: 제어의 역전, 프레임워크 vs 라이브러리, 스프링 IoC 및 용어 정리, 싱글톤, 동일성 vs 동등성, 빈의 스코프

볶음밥 1장 - 7: 의존관계 주입(DI), DL, IoC

볶음밥 2장: 테스트, TDD, jUnit

볶음밥 3장: 템플릿/콜백

볶음밥 4장: 예외

볶음밥 5장: 서비스 추상화, 테스트 대역

볶음밥 6장 - 1: AOP (1)

 

 

[6.4] 스프링의 프록시 팩토리 빈

스프링은 프록시 기술에 대해서도 서비스 추상화를 적용해준다. 

- 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다. (생성된 프록시는 스프링 빈으로 등록된다)

-> 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.

 

[ProxyFactoryBean]

- 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈.

- 순수하게 프록시 생성 작업에만 관여하고 부가기능은 포함하지 않는다.

(부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다)

 

[MethodInterceptor]

- InvocationHandler와 유사. 

- InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않기에 InvocatonHandler를 구현한 클래스가 직접 알고있어야 한다. 반면 MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean을 통해 타깃 오브젝트에 대한 정보까지 한번에 제공받는다. 

--> 타깃 오브젝트에 상관없이 독립적으로 만들 수 있다!

---> 타깃이 다른 여러 프록시에서도 함께 사용할 수 있고, 싱글톤 빈으로도 사용 가능하다.

 

    @Test
    public void proxyFactoryBean() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget()); // 타깃 설정
        pfBean.addAdvice(new UppercaseAdvice()); // 부가기능 추가 (어드바이스)

        Hello proxiedHello = (Hello) pfBean.getObject();
        assertThat(...);
    }

    private static class UppercaseAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            // 리플렉션 Method와 달리 타깃 오브젝트를 전달하지 않는다
            // MethodInvocation이 타깃 오브젝트를 이미 알고있기 때문
            String ret = (String)invocation.proceed();
            return ret.toUpperCase(); // 부가 기능 적용
        }
    }

 

* MethodInterceptor로 메소드 정보와 타깃 오브젝트가 포함된 MethodInvocation 오브젝트가 파라미터로 전달된다.

- MethodInvocation타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있기에 MethodInterceptor부가기능 제공에만 집중할 수 있다.

- MethodInvocation은 일종의 콜백 오브젝트로서 proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행시켜 준다. 

--> 사실상 공유 가능한 템플릿처럼 동작한다 == "싱글톤"으로 두고 공유할 수 있다!!!

 

* ProxyFactoryBean에 MethodInterceptor를 설정할 때는 addAdvice()를 이용하고, 여러개를 추가해줄 수 있다

- JDK 팩토리 빈의 문제였던 하나의 부가기능에 하나의 프록시와 프록시 팩토리에 대한 문제 해결

- 부가기능 여러개를 적용시켜도 하나의 팩토리 빈으로 처리 가능하다!

 

* ProxyFactoryBean은 인터페이스 자동 검출 기능을 이용해 타깃 오브젝트가 구현하고 있는 인터페이스를 알아내고 인터페이스를 모두 구현하는 프록시를 알아서 만들어준다. (정보를 제공해주지 않아도!!)

= 타깃 오브젝트가 구현하고 있는 모든 인터페이스를 동일하게 구현하고 있는 프록시를 만들어준다

 

[기존의 문제점]

InvocationHandler가 타깃과 메소드 선정 알고리즘 코드에 의존하고 있다.

 

[해결방안]

MethodInterceptor에는 순수한 부가기능 제공 코드만 남겨놓고, 메소드 선별 기능은 프록시가 대신하도록 한다.

- MethodInterceptor는 타깃의 정보를 담고있지 않기에 싱글톤으로 공유해 사용할 수 있다.

 

[어드바이스, Advice]

부가기능을 제공하는 오브젝트

- MethodInterceptor 타입의 오브젝트

 

[포인트컷, Pointcut]

메소드 선정 알고리즘을 담은 오브젝트

- Pointcut 인터페이스를 구현해서 만든다

 

[어드바이저, Advisor]

어드바이스와 포인트컷을 묶은 오브젝트

- 포인트컷과 어드바이스를 따로 등록하면 어떤 어드바이스가 어떤 포인트컷을 적용해야 할 지 애매해지기 때문에 둘을 묶어서 등록한다!

- 여러 개의 어드바이스가 등록되더라도 각각 다른 포인트컷과 조합되어 사용할 수 있다.

 

어드바이스와 포인트컷은 프록시에 DI로 주입되어 사용된다.

두가지 모두 여러 프록시에서 공유 가능하도록 만들어졌으며, 싱글톤 빈으로 등록 가능하다.

- 전형적인 전략 패턴 구조를 사용. 부가기능 방식이나 메소드 선정 방식이 바뀐다면 구현 클래스만 갈아끼면 된다.

- 프록시와 ProxyFactoryBean의 변경 없이도 기능을 자유롭게 확장할 수 있다(DI를 통해) -> OCP를 만족하는 코드.

 

<동작 흐름>

1. 다이내믹 프록시가 클라이언트로부터 요청을 받는다

2. 프록시는 포인트컷에게 부가기능을 부여할 메소드인지를 확인해달라는 요청을 보낸다

3. 대상 메소드라면 어드바이스를 호출한다

4. 어드바이스가 부가기능을 부여하는 중 타깃 메소드의 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출한다. (어드바이스가 직접 호출하지 않는다 -- 타깃의 정보가 없기 때문)

 

p468.

 

<설정 코드>

    <bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="userServiceImpl"/>
        <property name="interceptorNames">
            <list>
                <value>transactionAdvisor</value>
            </list>
        </property>
    </bean>

    <bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="transactionAdvice"/>
        <property name="pointcut" ref="transactionPointcut"/>
    </bean>

    <bean id="transactionAdvice" class="springbook.learningtest.jdk.TransactionAdvice">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="upgrade*"/>
    </bean>

p475.

ProxyFactoryBean은 스프링 DI와 템플릿/콜백 패턴 서비스 추상화 기법이 모두 적용된 궁극의 오브젝트이다

- 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스 + 포인트컷으로 확장 기능을 분리할 수 있다

- 비즈니스 로직에 반복적으로 나타나던 코드를 <<기존 설계와 코드에 영향을 주지 않고>> 분리해냈다

(= 투명한 부가기능 형태로 제공된다)

 

 

[6.5] 스프링 AOP

기존 프록시 팩토리 빈의 문제점은 거의 해결되었으나, 아직 하나의 문제점이 남아있다

- 부가기능의 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 프록시 팩토리 빈 설정정보를 추가해 줘야한다.

-- 타깃 프로퍼티를 제외하면 빈 클래스 종류, 어드바이스, 포인트컷 설정 동일. 이 중복을 없앨 수 있을까??

 

다이내믹 프록시 런타임 코드 자동생성 기법을 이용해 특정 인터페이스를 구현한 오브젝트에 대해서 프록시 역할을 해주는 클래스를 턴타임시 내부적으로 만들어낸다. 

- 이 덕에 개발자가 인터페이스를 구현하는 프록시 클래스를 직접 만들지 않아도 됐다.

- 변하지 않는 타깃으로의 위임과 부가기능 적용 여부 판단은 다이내믹 프록시 기술에 맡기고, 변하는 부가기능 코드는 별도로 만들어 다이내믹 프록시 생성 팩토리에 DI 해주는 방식.

=> 의미있는 부가기능 로직은 직접 코드로 짜고(Advice), 기계적인 타깃 인터페이스 구현, 위임, 부가기능 연동 부분은 자동생성.

 

그럼 ProxyFactoryBean도 자동설정을 할 수 있을까?

= "빈 후처리기" 를 이용한 자동 프록시 생성기 사용 (BeanPostProcessor 인터페이스 구현체)

 

빈 후처리기스프링 빈 오브젝트가 만들어지고 난 이후에도 빈 오브젝트를 다시 가공할 수 있도록 해준다.

-- 빈 후처리기 자체를 빈으로 등록. 스프링은 빈 후처리기가 등록되어있으면 빈 오브젝트가 생성될 때마다 빈을 후처리기로 보내서 후처리 작업을 요청한다. 

-- 빈 후처리기는 빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고, 초기화 작업을 진행할 수도 있다. 심지어 빈 오브젝트를 바꿔치기 할 수도 있다!!

 

빈 후처리기 중 하나인 DefaultAdvisorAutoProxyCreator = 어드바이저를 활용한 자동 프록시 생성기.

1. DefaultAdvisorAutoProxyCreator 를 스프링 빈으로 등록

2. 스프링은 빈 오브젝트가 생성될 때 마다 빈 후처리기에게 빈을 보낸다.

3. 빈으로 등록된 모든 어드바이저 내 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지를 판별.

4. 프록시 적용 대상인 경우에 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들라고 요청. 만들어진 프록시에 어드바이저를 연결해준다.

5. 프록시가 생성되면 스프링이 전달해준 빈(Bean) 오브젝트 대신 프록시 오브젝트를 컨테이너에 반환한다.

6. 스프링(컨테이너)은 최종적으로 빈 후처리기가 반환한 프록시 오브젝트를 빈으로 등록해 사용한다.

 

=> 어드바이저와 빈 후처리기를 빈으로 등록해놓으면 일일히 ProxyFactoryBean 빈을 설정하지 않아도 타깃 오브젝트에 자동으로 프록시가 적용되게끔 할 수 있다!

// 이런 것 일일히 안해줘도 된다는 것!! (자동으로 해준다!)
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="userServiceImpl"/>
        <property name="interceptorNames">
            <list>
                <value>transactionAdvisor</value>
            </list>
        </property>
</bean>

 

* 포인트컷으로 어떻게 빈 오브젝트가 프록시 대상인지 아닌지 알 수 있는가?

- 포인트컷은 프록시를 적용할 클래스인지를 확인하는 클래스 필터 어드바이스를 적용할 메소드인지를 확인하는 메소드 매처 두가지 기능을 갖고있다.

public interface Pointcut {
	ClassFilter getClassFilter();
	MethodMatcher getMethodMatcher();
}

-> 프록시를 적용할 클래스인지 판단하고 나서 적용 대상인 경우에만 어드바이스를 적용할 메소드인지 확인하도록 만들 수 있다. (= 클래스 필터에서 탈락하면 메소드 매처는 아예 사용되지 않는다)

 

<설정파일 예시>

    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

    <bean id="userService" class="springbook.user.service.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        <property name="mailSender" ref="mailSender"/>
    </bean>
    
    <bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="transactionAdvice"/>
        <property name="pointcut" ref="transactionPointcut"/>
    </bean>

    <bean id="transactionAdvice" class="springbook.learningtest.jdk.TransactionAdvice">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <bean id="transactionPointcut" class="springbook.learningtest.jdk.NameMatchClassMethodPointcut">
        <property name="mappedClassName" value="*ServiceImpl"/>
        <property name="mappedName" value="upgrade*"/>
    </bean>
    
    // 테스트용
    <bean id="testUserService"
          class="springbook.user.service.TestUserServiceImpl"
          parent="userService"/>
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
    // 포인트컷의 클래스 필터 설정(프로퍼티를 이용해 파라미터를 받은 다음 set)
    public void setMappedClassName(String mappedClassName) {
        this.setClassFilter(new SimpleClassFilter(mappedClassName));
    }
    
    static class SimpleClassFilter implements ClassFilter {
        String mappedName;
        // 외부에서 생성 불가
        private SimpleClassFilter(String mappedName) {
            this.mappedName = mappedName;
        }
        @Override
        public boolean matches(Class<?> clazz) {
            // *name, name*, *name*
            return PatternMatchUtils.simpleMatch(mappedName,
                    clazz.getSimpleName());
        }
    }
}

 

 

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

스프링은 간단한 방법으로 포인트컷의 클래스와 메소드를 선정하는 알고리즘을 작성할 수 있는 방법을 제공한다

- 포인트컷 표현식(Pointcut Expression)을 이용!

- AspectJExpressionPointcut 클래스 사용. (= AspectJ 포인트컷 표현식)

 

(전) 클래스 필터와 메소드 매처를 각각 구현한 다음 프로퍼티로 각각 넣어줘서 사용

-> (후) 클래스와 메소드의 선정 알고리즘을 포인트컷 표현식을 이용해 한번에 지정할 수 있도록 해준다.

 

<표현식 문법>

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | "..", ...) [throws 예외패턴])

<예시>

System.out.println(Target.class.getMethod("minus", int.class, int.class));

-> 

public int springbook.learningtest.spring.pointcut.Target.minus(int,int) throws java.lang.RuntimeException

 

1. 접근제한자 (public/protected/private) - 생략 가능

2. 리턴 타입 - 필수. " * " 를 붙여 모든 타입을 다 선택할 수 있음

3. 패키지와 타입 이름을 포함한 클래스의 타입 패턴 

- 생략 가능 (생략 시 모든 타입을 전부 허용하겠다는 뜻)

- 패키지 이름, 클래스, 인터페이스 이름에 " * " 을 사용할 수 있다.

- " .. " 를 사용하면 한번에 여러개의 패키지를 선택할 수 있다 (서브 패키지를 포함하겠다)

- 단순 클래스 이름 비교 뿐만 아니라 >>인터페이스, 슈퍼클래스의 타입도 인식 가능<<하다

4. 메소드 이름 패턴 - 필수. " * " 를 붙여 모든 메소드를 다 선택할 수 있음

5. 메소드 파라미터의 타입 패턴 - 필수

- 파라미터 타입/개수에 무관한 패턴이라면 " .. " 을 넣으면 된다.

6. 예외 타입 - 생략 가능

 

    @Test
    public void methodSignaturePointcut() throws NoSuchMethodException {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(public int " +
                "springbook.learningtest.spring.pointcut.Target.minus(int,int) " +
                "throws java.lang.RuntimeException)");

        // 클래스 + 메소드가 표현식과 모두 일치
        assertThat(pointcut.getClassFilter().matches(Target.class) &&
                pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null)).isTrue();

        // 메소드명이 포인트컷 표현식과 불일치
        assertThat(pointcut.getClassFilter().matches(Target.class) &&
                pointcut.getMethodMatcher().matches(Target.class.getMethod("plus", int.class, int.class), null)).isFalse();

        // 클래스부터 표현식과 불일치
        assertThat(pointcut.getClassFilter().matches(Bean.class) &&
                pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null)).isFalse();
    }

 

<<필수 항목 = 리턴 타입 / 메소드 이름 패턴 / 메소드 파라미터 타입 패턴>>

 

execution(int minus(int, int)) 

- 어떤 접근 제한자를 가지든 / 어떤 클래스에 정의됐든 / 어떤 예외를 던지든 관계 X

 

execution(* minus(int, int))

- 위에것 + 어떤 리턴 타입을 가지든 관계 X

 

execution(* minus(..))

- 위에것 + 파라미터의 개수와 타입에 관계 X

 

execution(* *(..))

- 조건 없음. 모든 메소드를 전부 허용하겠다.

 

execution(* *..Tar*.*(..)) -> 리턴타입 / 클래스 타입 패턴 / 메소드 이름 패턴 / 파라미터 타입 패턴

- 패키지에 관계 없이 이름이 Tar로 시작하는 모든 클래스에 적용하겠다

 

execution(* com..*.*(..))

- com 하위 패키지의 모든 클래스에 적용하겠다

 

execution(* *(..) throws Runtime*)

- Runtime으로 시작하는 예외를 던지는 메소드에만 적용하겠다

 

<포인트컷 표현식을 사용한 빈 설정>

    <bean id="transactionPointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut">
        <property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))"/>
    </bean>

장점 = 짧고 간결하고 강력함.

단점 = 문자열 표현식이기 때문에 오타, 문법 실수의 가능성이 매우 높음

 

포인트컷 표현식은 expression() 이외에도 다양한 표현식 스타일이 존재

bean() : 빈 선정 포인트컷

- bean(*Service) = Service로 끝나는 모든 빈을 선택해라

 

@annotation() : 특정 애노테이션이 타입/메소드/파라미턴에 적용되어있는 것을 보고 메소드 선정

- @annotation(org.springframework.~~.Transactional) = @Transactional 애노테이션이 붙은 메소드만 선택해라

-> 애노테이션만 걸어놓고 포인트컷을 통해 선정해서 부가기능을 제공하도록 만든다!!

 

 

관심사가 같은 코드를 분리하고 한 군데로 모으는 것(모듈화)이 소프트웨어 개발의 가장 기본 원칙이다.

- 하지만 트랜잭션과 같은 부가기능은 핵심기능과는 달리 모듈화하기가 쉽지 않았다. 부가기능이기에 혼자 독립적인 방식으로 존재해서는 적용되기가 어렵기 때문. (타깃이 존재해야만 의미가 있다. 그렇기에 타깃의 코드 내에 침투하거나 긴밀하게 연결되어 있어야 한다)

-> 어드바이스와 포인트컷을 결합한 어드바이저를 이용해 부가기능도 독립적으로 모듈화 시킬 수 있었다!

 

위와같은 부가기능 모듈화 작업은 기존 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했고, 이에 부가기능 모듈을 특별한 이름으로 부르게 되는데 그것이 애스펙트(Aspect)이다!!!!

- 애스팩트란 그 자체로 애플리케이션의 핵심기능을 담고있지는 않지만, 애플리케이션을 구성하는 중요한 요소로 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.

- 부가 기능을 정의한 어드바이스와, 어디에 적용될지를 결정하는 포인트컷을 모두 (=어드바이저) 가지고 있다

 

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

- AOP는 OOP(객체지향 프로그래밍)을 돕는 보조적인 기술일 뿐 대체하는 개념이 아니다.

-- 애스팩트를 분리함으로써 핵심기능 설계 및 구현 시 객체지향적인 가치를 지킬 수 있도록 보조하는 개념

 

애플리케이션을 다양한 측면에서 독립적으로 모델링/설계/개발할 수 있도록 만들어준다.

= 애플리케이션을 특정한 관점을 기준으로 바라볼 수 있게 해준다

= 관점 지향 프로그래밍

= AOP

 

스프링은 IoC/DI 컨테이너와 다이내믹 프록시, 데코레이터 패턴, 프록시 패턴, 자동 프록시 생성 기법, 빈 오브젝트의 후처리 조작 기법 등 다양한 기술을 조합해 AOP를 지원한다. 그중 핵심은 바로 "프록시"

- 프록시를 이용해 DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여하고 부가기능을 제공해주도록 만듬.

- 이외에도 바이트코드 생성 및 조작을 이용한 AOP 방식이 존재한다 (AOP 프레임워크인 AspectJ 방식)

 

AspectJ는 스프링 AOP같이 다이내믹 프록시 방식을 사용하지 않는다. 

- 대신 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 방식을 이용한다. 

-> 컴파일된 타깃의 클래스 파일 자체를 수정하거나, 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용. (소스코드는 수정되지 않음)

 

<왜 이렇게 복잡한 방식을 사용하는가?>

1. 바이트코드를 조작해서 타깃 오브젝트를 직접 수정하면 스프링과 같은 컨테이너가 사용되지 않는 환경에서도 사용 가능

- DI 컨테이너를 통한 자동 프록시 생성 방식을 사용하지 않아도 된다.

 

2. 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하다.

- 프록시를 통한 AOP는 클라이언트가 호출할 때 사용하는 메소드로 제한되지만, 바이트코드를 통한 AOP는 오브젝트의 생성, 필드값의 조회와 조작, 스태틱 초기화, private 메소드의 호출, 스태틱 메소드의 호출 및 초기화, 필드 입출력 등 다양한 작업에 기능을 부여해줄 수 있다.

 

대부분의 경우는 프록시를 통한 스프링 AOP 기술로도 충분하다.

 

 

[AOP 용어 정리]

타깃: 부가기능을 부여할 대상. 핵심 기능을 담은 클래스일 수도 있으나, 다른 부가기능을 제공하는 프록시일 수도 있다.

 

어드바이스: 타깃에게 재공할 부가기능을 담은 모듈

 

조인 포인트: 어드바이스가 적용될 수 있는 위치.

- 스프링 AOP에서 조인 포인트는 메소드의 실행 단계 뿐이다. 

- 타깃 오브젝트가 구현한 인터페이스의 모든 메소드가 조인 포인트가 된다.

 

포인트컷: 어드바이스를 적용할 조인 포인트를 선별하는 작업 또는 그 기능을 정의한 모듈.

 

프록시: 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트

 

어드바이저: 어드바이스 + 포인트컷을 조합한 오브젝트

 

애스펙트: OOP의 클래스가 있다면 AOP에는 애스펙트. AOP의 기본 모듈으로 한개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어진다.

- 보통 싱글톤 형태의 도브젝트로 존재.

 

 

스프링 AOP를 적용하기 위해 사용하는 어드바이저, 자동 프록시 생성기와 같은 빈은 일반 애플리케이션 로직을 담은 빈들과는 성격이 다르다. 따라서 이들 빈은 AOP 네임스페이스를 선언(xml 설정파일)해 별도로 관리하면 편리하다.

- 개선 전 = 자동 프록시 생성기 / 어드바이스 / 포인트컷 / 어드바이저를 모두 빈으로 각각 등록

- 개선 후 = 전용 태그를 사용해 정의, 이해도 쉽고 코드도 간결.

// AOP 설정을 담는 부모태그, AspectJAdvisorAutoProxyCreator를 빈으로 등록해준다
<aop:config>
    // AspectJExpressionPointcut을 빈으로 등록해준다
    <aop:pointcut id="transactionPointcut" expression="execution(* *..*ServiceImpl.upgrade*(..))"/>
    
    // advice와 pointcut의 ref를 프로퍼티로 갖는 DefaultFactoryPointcutAdvisor를 등록해준다.
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut"/>
    
    /********************/
    // 위에 것을 한줄로도 대체 가능
    <aop:advisor advice-ref="transactionAdvice" 
                 pointcut="execution(* *..*ServiceImpl.upgrade*(..))"/>
    /********************/
</aop:config>

 

 

 

[6.6] 트랜잭션 속성

[트랜잭션 전파 (Transaction Propagation)]

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

Q) 각자 독자적인 트랜잭션 경계를 가진 두개의 코드 A, B가 있다고 했을 떄 A의 트랜잭션 중 B의 메소드를 호출하고 난 뒤 예외가 발생했다면?

Case 1. A의 트랜잭션 중 B의 메소드 호출 -> B가 A의 트랜잭션에 참여 -> B 작업을 마친 이후 A 작업 도중 예외 발생

=> A와 B는 같은 트랜잭션으로 묶여있기 때문에 A, B 모두 롤백

 

Case 2. A 트랜잭션 중 B 메소드 호출 -> B가 독립적인 트랜잭션 생성 -> B 작업을 마친 이후 A 작업 도중 예외 발생

=> A는 롤백, B는 정상 커밋

 

위와 같이 독자적인 트랜잭션 경계를 가진 코드에 대해 이미 진행중인 트랜잭션이 어떻게 영향을 미칠 수 있는가를 정의하는 것.

<옵션>

* PROPAGATION_REQUIRED : 가장 많이 사용. 진행중인 트랜잭션이 없다면 새로 시작하고 있다면 이에 참여한다.

* PROPAGATION_REQUIRES_NEW : 항상 새로운 트랜잭션을 시작한다

* PROPAGATION_NOT_SUPPORTED : 트랜잭션 없이 동작한다.

 

(+) 왜 트랜잭션 없이 동작하는 옵션이 있는지?

- 트랜잭션 경계설정은 보통 AOP를 이용해 한꺼번에 많은 메소드에 동시 적용해 사용한다. 그렇기에 모든 메소드에 트랜잭선 AOP가 적용되도록 기본으로 깔아놓고 특정 메소드에 대해서만 트랜잭션 없이 동작하도록 만들어버릴 때 사용.

 

이전에 사용한 DefaultTransactionDefinition으로 getTransaction() 메소드를 사용할 시 트랜잭션 전파 옵션으로 PROPAGATION_REQUIRED 옵션을 사용. 이미 진행중인 트랜잭션이 있다면 거기에 참여하고, 없다면 새로운 트랜잭션을 만든다.

 

[격리 수준 (Isolation Level)]

서버 환경에서는 여러 개의 트랜잭션이 동시에 진행될 수 있다. 가능하다면 모든 트랜잭션이 순차적으로 진행되어 다른 트랜잭션 작업에 독립적인 것이 좋지만 성능 이슈 상 그럴수 없기에 격리 수준을 조절해 가능한 한 많은 트랜잭션이 동시에 진행시키면서도 장애가 없도록 제어해야 한다.

- 기본적으로 DB에 설정되어 있고 JDBC 드라이버, DataSource 등에서 재설정도 가능

- 트랜잭션 단위로도 격리 수준을 조절할 수 있다. (기본값: ISOLATION_DEFAULT)

 

[제한 시간 (Timeout)]

트랜잭션 수행 제한시간을 설정할 수 있다.

 

[읽기 전용 (Read Only)]

읽기 전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하려는 시도를 막을 수 있으며, 데이터 접근기술에 따라 성능향상도 기대 가능

 

..

그래서 트랜잭션 속성은 어떻게 바꾸나요?

-> 외부에서 정의된 TransactionDefinition 오브젝트를 DI 받아서 사용할 수도 있으나, 이 경우에는 트랜잭션 속성을 변경할 경우 이를 사용하는 어드바이스에 해당하는 모든 메소드가 전부 속성이 변경된다는 문제점이 있다.

 

원하는 메소드만 독자적인 트랜잭션 정의를 적용할 수는 없을까??

-> TransactionInterceptor를 이용하면 메소드 이름 패턴을 이용해 트랜잭션 속성을 다르게 지정할 수 있는 방법을 추가로 제공한다.

- PlatformTranscationManager와 Properties 타입의 두 가지 프로퍼티를 가짐. Properties 타입이 바로 transactionAttributes로 트랜잭션 속성을 정의한 프로퍼티. (TransactionAttribute 인터페이스 구현체컬렉션으로 전달(Map 타입)받는다)

--> 메소드 패턴에 따라 각기 다른 트랜잭션 속성을 부여하기 위해

 

트랜잭션 속성은 TransactionAttribute 기본 4가지 항목에 rollbackOn() 메소드를 하나 더 가진 인터페이스로 정의된다.

- rollbackOn() 메소드는 어떤 예외가 발생했을 때 롤백할 것인지를 결정하는 메소드.

- 이 TransactionAttribute 인터페이스를 이용해 트랜잭션 부가기능의 동작방식을 제어한다

 

스프링이 제공하는 TransactionInterceptor는 기본적으로 런타임 예외가 발생하면 트랜잭션을 롤백하고, 체크 예외를 던지는 경우에는 이것을 예외 상황으로 인식하지 않고 비즈니스 로직에 따른 리턴 방식으로 생각해 트랜잭션을 그냥 커밋한다.

- 기타 예외적인 상황의 경우 rollbackOn() 메소드를 이용해 특정 체크 예외는 롤백시키고, 특정 런타임 예외의 경우는 커밋시켜 줄 수도 있다.

 

 

[메소드 이름 패턴을 이용한 트랜잭션 속성 지정 방법]

트랜잭션 전파방식, 격리수준, 읽기전용 유무, 제한시간, -롤백 대상, +커밋 대상

 

- 트랜잭션 전파방식 제외하고는 모두 생략 가능. (생략 시 디폴트 속성 부여)

- 예외 앞에 ' + ' 를 붙이면 커밋하겠다는 뜻 / ' - ' 를 붙이면 롤백하겠다는 뜻

 

    <bean id="transactionAdvice" class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED, readOnly, timeout_30</prop>
                <prop key="upgrade*">PROPAGATION_REQUIRES_NEW, ISOLATION_SERIALIZABLE</prop>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
    
//// tx 네임스페이스를 이용해 단축 가능

    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="get*" propagation="REQUIRED" read-only="true" timeout="30"/>
            <tx:method name="upgrade*" propagation="REQUIRES_NEW" isolation="SERIALIZABLE"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

- 메소드 이름이 하나 이상 일치하는 경우 가장 정확히 일치하는 것이 적용된다

 

 

[포인트컷과 트랜잭션 속성 적용 전략]

1. 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 사용하자

- 비즈니스 로직을 담고있는 클래스라면 메소드 단위까지 세밀하게 포인트컷을 정해줄 필요가 없다.

->  조회의 경우에는 읽기 전용으로 설정해 성능향상 / 비즈니스 로직은 다른 트랜잭션에 참여하게될 가능성이 높다

- 클래스들이 모여있는 패키지를 통째로 선택하거나 클래스 이름에서 일정한 패턴을 찾아서 표현식으로 사용하자

- 가능하면 클래스보다는 변경 빈도가 적은 인터페이스 타입을 기준으로 하는 것이 더 좋다. 

 

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

- 한 애플리케이션 내에서 사용되는 트랜잭션 속성의 종류가 막상 그렇게 다양하지 않다. 그렇기에 기준이 되는 몇가지 트랜잭션 속성을 정의해두고 그에따른 메소드 명명규칙을 사용하자.

- 모든 메소드에 대해 일단 기본 속성을 깔아둔다 (" * ")

- 조회라면 get-, find- 의 접두어를 붙여서 사용

- 일반화 하기에 적당하지 않은 특별한 트랜잭션 속성이 필요한 타깃 오브젝트의 경우에는 별도의 어드바이스와 포인트컷 표현식을 사용하자.

 

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

- 프록시 방식의 AOP에서는 프록시를 통한 부가기능 적용은 클라이언트로부터 호출이 일어났을 때만 가능하다

(* 클라이언트: 인터페이스를 통해 타깃 오브젝트를 사용하는 다른 모든 오브젝트)

- 일단 타깃 오브젝트로 들어돈 이후 타깃 오브젝트의 다른 메소드를 호출하는 경우에는 프록시를 거치지 않고 직접 타깃 메소드를 호출하기 때문에 부가기능을 적용받지 못함

 

<해결방법>

1. AspectJ와 같은 바이트코드 조작 방식의 AOP 기술을 사용

2. 프록시 오브젝트에 대한 레퍼런스를 따로 가져와서 프록시 이용을 강제하는 방법 (비추)

 

 

[트랜잭션 속성 적용]

=> 트랜잭션 경계설정을 일원화 하자

- 트랜잭션 경계설정의 부가기능을 여러 계층에서 중구난방으로 사용하는 것은 좋지않다. 특정 계층의 경계를 트랜잭션 경계와 일치시키는 것이 바람직.

- 특별한 이유가 없다면 다른 계층이나 모듈에서 DAO에 직접 접근하는 것을 차단해야 한다.

- 트랜잭션은 보통 서비스 계층의 메소드 조합을 통해 만들어지기 때문에 DAO가 제공하는 주요 기능은 서비스 계층에 위임 메소드를 만들어 사용하는 것이 좋다.

= 다른 모듈의 DAO에 접근할 때에는 서비스 계층을 거치도록 만들자

 

포인트컷 표현식과 트랜잭션 속성을 이용해 트랜잭션을 일괄적으로 적용하는 방식은 복잡한 트랜잭션 속성이 요구되지 않는 한 대부분 상황에서 잘 사용할 수 있다.

- 하지만 가끔은! 클래스나 메소드에 따라 제각각 속성이 다른 세밀하게 튜닝된 트랜잭션 속성을 적용해야 할 때가 있다. 이때 사용하면 좋은 것이 타깃에 직접 트랜잭션 속성정보를 가진 애노테이션(@Transactional)을 지정하는 방법이다.

 

@Target({ElementType.TYPE, ElementType.METHOD}) // 애노테이션 사용 타깃(클래스/인터페이스)
@Retention(RetentionPolicy.RUNTIME) // 애노테이션 정보가 언제까지 유지되는지
@Inherited // 상속을 통해서도 얻을 수 있는지
@Documented
public @interface Transactional {
...
}

- @Transactional 애노테이션의 타깃은 메소드와 타입(=클래스or인터페이스) 이기에 메소드, 클래스, 인터페이스에 사용될 수 있다.

- 스프링은 @Transactional이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식, 이때 사용되는 포인트컷으로TransactionAttributeSourcePointcut이 이용된다.

(기본적으로 트랜잭션 속성을 정의하는 것이지만 포인트컷도 등록한다 = 포인트컷과 트랜잭션 속성을 애노테이션 하나로 지정할 수 있다.)

- @Transactional은 메소드마다 다르게 설정할 수도 있으므로 매우 유연하고 세밀한 트랜잭션 속성 설정이 가능해진다.

 

또한 스프링은 @Transactional 애노테이션에 대해 4단계의 대체(Fallback) 정책을 제공한다,

-> 타깃 메소드 - 타깃 클래스 - 선언 메소드 - 선언 타입의 순서에 따라 @Transactional이 있는지를 순서대로 확인하고 가장 먼저 발견되는 속성정보를 사용하는 방법. (선언 메소드 = 인터페이스에 정의된 메소드)

-> 이를 잘 활용하면 애노테이션 사용을 최소화 하고도 세밀한 제어가 가능하다. (타입 레벨에 기본 @Transactional을 일단 깔아놓고 특별한 메소드에 대해서만 다시 @Transactional을 걸어서 사용하는 방식)

- 인터페이스에 @Transactional을 두면 구현 클래스가 바뀌더라도 트랜잭션 속성을 유지할 수 있다는 장점이 있다.

 

 

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

트랜잭션 전파 속성은 아주 유용한 개념이다

- REQUIRED 속성을 이용할 경우, 앞에서 진행 중인 트랜잭션이 있다면 참여하고 없다면 새로운 트랜잭션을 시작함으로써 REQUIRED 속성을 가진 메소드를 결합해서 사용할 수 있고, 다양한 크기의 트랜잭션 작업을 만들 수 있다.

-> 불필요한 코드 중복도 사라지고, 애플리케이션을 작은 기능 단위로 쪼개서 개발할 수도 있다.

 

AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정해줄 수 있도록 하는 방법"선언적 트랜잭션 (Declarative Transaction)"이라고 한다.

반대로 개별 트랜잭션 API를 사용해 직접 코드 안에서 사용하는 방법을 "프로그램에 의한 트랜잭션"이라고 한다.

- 스프링은 두가지 방법을 모두 지원하나, 선언적 트랜잭션을 사용하는 것이 좋다.

(앞서서 했던 <tx:advice ~> or @Transactional 모두 선언적 트랜잭션 방법이다)

- AOP 덕분에 프록시를 이용한 트랜잭션 부가 기능을 간단하게 애플리케이션 전반에 적용할 수 있었고, 트랜잭션 추상화 덕분에 데이터 액세스 기술에 상관없이 DAO에서 일어나는 작업들을 하나의 트랜잭션으로 묶고, 추상 레벨에서 관리하도록 해줬다.

 

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

- PlatformTransactonManager 인터페이스를 구현한 트랜잭션 매니저를 통해 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능했다. 

- 트랜잭션 동기화 기술이 있었기에 시작된 트랜잭선 정보를 저장소에 보관해 뒀다가 DAO에서 공유할 수 있었다.

(진행중인 트랜잭션이 있는지 확인하고, 전파 속성(REQUIRED)에 따라서 이에 참여할 수 있도록 만들어주는 것도 역시 트랜잭션 동기화 기술 덕분이다.)

 

테스트 코드에서는 직접 트랜잭션 매니저를 가져와서 사용해 DB 작업이 포함된 테스트도 원하는 대로 제어하면서 효과적인 테스트를 만들어낼 수 있다. 대표적인 예시가 바로 "롤백 테스트"

- 롤백 테스트는 테스트 내의 모든 DB작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트를 말한다.

- DB작업이 포함된 테스트라도 DB에 영향을 주지 않음. 테스트가 성공하든 실패하든 관계없이 트랜잭션을 커밋하지 않음.

- 테스트에 따라 필요한 고유 테스트 데이터 초기화 작업을 진행하더라도 그것까지 모두 롤백함.

-> 여러 개발자가 하나의 공용 테스트용 DB를 사용할 수 있도록 해준다

 

테스트에서도 @Transactional 애노테이션을 사용할 수 있다

- AOP를 위해 사용되는 것이 아님

- 테스트 프레임워크에 의해 트랜잭션을 부여해주는 용도로 사용

** 테스트용 트랜잭션은 테스트가 끝나면 자동적으로 롤백 됨. 테스트에 적용된 @Transactional은 기본적으로 트랜잭션을 강제 롤백 시키도록 되어있다.

-> 롤백을 원하지 않는 경우, @Rollback 애노테이션을 이용하면 된다

 

@Rollback(false)

- 기본 값은 true이기 때문에 롤백을 원치 않는 경우 (false) 넣어줘야 함.

- @Rollback 애노테이션은 메소드 레벨에만 적용된다.

 

@TransactionConfiguration(defaultRollback=false)

- 테스트 클래스의 모든 메소드에 트랜잭션을 적용하면서 롤백되지 않게 하고자 할때 사용

 

@NotTransactional (deprecated)

- 클래스 레벨에 @Transactional이 붙었는데 클래스 내 특정 메소드에는 트랜잭션이 적용되면 안되는 경우 사용

- 트랜잭션 테스트와 비 트랜잭션 테스트를 처음부터 구분하는 것이 제일 좋다

- @Transactional(propagation=Propagation.NEVER)로 설정해도 트랜잭션이 시작되지 않는다.

 

 

' > Spring' 카테고리의 다른 글

스프링 볶음밥 - 8, 9장  (0) 2022.07.12
스프링 볶음밥 - 7장  (0) 2022.07.11
스프링 볶음밥 - 6장 - AOP  (0) 2022.07.06
스프링 볶음밥 - 5장 - 서비스 추상화  (0) 2022.07.04
스프링 볶음밥 - 4장 - 예외  (0) 2022.07.04
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함