티스토리 뷰

SPRING

토비의 스프링 7. AOP (2)

짜비 2022. 9. 18. 18:39

개선 목표

  • 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정정보를 추가해주는 부분 제거

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

빈 후처리기

  • 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해줌
  • 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈후처리기에 보내서 후처리 작업을 요청

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

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

확장된 포인트컷

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;
    //프록시를 적용할 클래시인지 확인 (New)
    ClassFilter getClassFilter();
    //어드바이스를 적용할 메소드인지 확인
    MethodMatcher getMethodMatcher();
}
  • 사실 포인트컷은 어드바이스를 적용할 메소드인지 확인하는 기능 외에, 프록시를 적용할 클래스인지 확인하는 ‘클래스 필터’ 역할도 담당

클래스 필터 기능 학습테스트

@Test
void classNamePointcutAdvisor() {
    NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
        public ClassFilter getClassFilter() {
            return new ClassFilter() {
                @Override
                public boolean matches(Class<?> clazz) {
                    return clazz.getSimpleName().startsWith("HelloT");
                }
            };
        }
    };
    classMethodPointcut.setMappedName("sayH*");

    checkAdviced(new HelloTarget(), classMethodPointcut, true);

    class HelloWorld extends HelloTarget{
    };
    checkAdviced(new HelloWorld(), classMethodPointcut, false);

    class HelloToby extends HelloTarget{};
    checkAdviced(new HelloToby(), classMethodPointcut, true);

}

private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) {
    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")).isEqualTo("HELLO TOBY");
        assertThat(proxiedHello.sayHi("Toby")).isEqualTo("HI TOBY");
        assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("Thank you Toby");
    } else {
        assertThat(proxiedHello.sayHello("Toby")).isEqualTo("Hello Toby");
        assertThat(proxiedHello.sayHi("Toby")).isEqualTo("Hi Toby");
        assertThat(proxiedHello.sayThankYou("Toby")).isEqualTo("Thank you Toby");
    }
}

Transaction 예제로의 적용

public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {

    public void setMappedClassName(String mappedClassName) {
        this.setClassFilter(new SimpleClassFilter(mappedClassName));
    }

    static class SimpleClassFilter implements ClassFilter {

        String mappedName;

        public SimpleClassFilter(String mappedName) {
            this.mappedName = mappedName;
        }

        @Override
        public boolean matches(Class<?> clazz) {
            //와일드카드가 들어간 문자열 비교를 지원하는 스프링의 유틸리티 메소드. *name | name* | *name*
            return PatternMatchUtils.simpleMatch(mappedName,
                    clazz.getSimpleName());
        }
    }

}
@Bean
public DefaultBeanFactoryPointcutAdvisor defaultBeanFactoryPointcutAdvisor() {
    return new DefaultBeanFactoryPointcutAdvisor();
}

@Bean
public UserService userService() {
    UserService userService = new UserServiceImpl(userDao(), mailSender());
    return userService;
}

@Bean(name = "transactionPointcut")
public NameMatchMethodPointcut transactionPointcut() {
    //NameMatchMethodPointcut transactionPointcut = new NameMatchMethodPointcut();
    NameMatchClassMethodPointcut transactionPointcut = new NameMatchClassMethodPointcut();
    transactionPointcut.setMappedName("upgrade*");
    transactionPointcut.setMappedClassName("*ServiceImpl");
    return transactionPointcut;
}

@Bean
public TransactionAdvice transactionAdvice() {
    TransactionAdvice transactionAdvice = new TransactionAdvice(transactionManager());
    return transactionAdvice;
}



@Bean(name = "transactionAdvisor")
public PointcutAdvisor transactionAdvisor() {
    PointcutAdvisor transactionAdvisor = new DefaultPointcutAdvisor( transactionPointcut(), transactionAdvice());
    return transactionAdvisor;
}



// @Bean(name = "userService")
// public ProxyFactoryBean proxyFactoryBean() {
//     ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
//     proxyFactoryBean.setTarget(userServiceImpl());
//     proxyFactoryBean.addAdvisor(transactionAdvisor());
//     return proxyFactoryBean;
// }

  • ProxyFactoryBean 제거
  • UserServiceImpl 빈의 이름을 userService 로 원상복구
  • Q. @Bean의 반환 타입이 UserServiceImpl 이 아니라 UserService로 되어 있어도 classFilter가 이를 감지할 수 있는 것인가?

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

포인트컷 표현식

  • 정규식과 같은 표현식 언어를 사용해서 포인트컷을 작성할 수 있도록 하는 방법.
  • 필요성
    • 단순히 메소드 이름을 비교하는 것 이외에 정의된 패키지, 파라미터, 리턴 값, 어노테이션, 구현한 인터페이스, 상속한 클래스 등의 정보로 포인트컷 적용대상 결정
    • Reflection API를 활용할 수도 있지만 너무 번거로움.

execution()

  • 메소드 시그니처를 비교하는 방식
  • execution(* minus(int,int))
    • 리턴 타입은 상관 없이 minus 라는 메소드 이름, 두 개의 int 파라미터를 가진 모든 메소드를 선정
  • execution(* minus(..))
    • 리턴 타입, 파라미터 종류, 개수 상관 없이 minus라는 메소드 이름 가진 모든 메소드를 선정
  • execution(* *(..))
    • 리턴 타입, 파라미터, 메소드 이름에 상관없이 모든 메소드 조건을 다 허용

bean()

  • 빈의 이름으로 비교하는 방식
  • bean(*Service)
    • bean 의 이름이 Service로 끝나는 모든 빈을 선택

@annotation()

  • 어노테이션으로 포인트컷 적용 메소드를 선정하는 방식
  • @annotation(org.springframework.transaction.annotation.Transactional)
    • @Transactional 이라는 어노테이션이 붙은 메소드만 선택
@Bean(name = "transactionPointcut")
public AspectJExpressionPointcut transactionPointcut() {
    // NameMatchClassMethodPointcut transactionPointcut = new NameMatchClassMethodPointcut();
    // transactionPointcut.setMappedName("upgrade*");
    // transactionPointcut.setMappedClassName("*ServiceImpl");
    AspectJExpressionPointcut transactionPointcut = new AspectJExpressionPointcut();
    transactionPointcut.setExpression(
            "execution(* *..*ServiceImpl.upgrade*(..))"
    );
    return transactionPointcut;
}
  • 클래스 이름은 ServiceImpl로 끝나고, 메소드 이름은 upgrade로 시작하는 모든 클래스에 적용.
  • 기존처럼 ClassName 필터링을 위한 클래스를 별도로 적용할 필요가 없어짐
  • Q. AspectJ Deprecate 되지 않았나..?

AOP란 무엇인가?

부가기능의 모듈화

  • 부가 기능
    • 다른 모듈의 코드에 부가적으로 부여되는 기능
    • 한데 모을 수 없고 어플리케이션 전반에 여기저기 흩어져 있음
  • 부가 기능을 어떻게 독립적인 모듈로 만들 수 있을까
    • == 부가 기능 코드를 중복되지 않게 하고 변경이 필요할 시 한 곳만 수정하면 되도록 만든다.
    • 지금까지 해온 모든 작업은 핵심기능에 부여되는 부가기능을 효과적으로 모듈화하는 방법을 찾는 과정이었음

애스펙트 (Aspect)

  • 부가기능 모듈
  • 그 자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만 애플리케이션을 구성하는 중요한 한 가지 요소이고 핵심기능에 부가되어 의미를 갖는 특별한 모듈
  • 애스펙트는 부가될 기능을 정의한 코드인 어드바이스와 어드바이스를 어디에 적용할지를 결정하는 포인트컷을 함께 갖고 있음

AOP (Aspect Oriented Programming)

  • 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법
  • AOP는 OOP를 돕는 보조적인 기술
    • 애스펙트를 분리함으로써 핵심기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것

AOP 적용 기술

  • 프록시를 이용한 AOP
  • 바이트코드 생성과 조작을 통한 AOP
    • e.g. AspectJ 프레임워크
    • 프록시처럼 간접적인 방법이 아니라 타깃 오브젝트를 뜯어고쳐서 부가 기능을 직접 넣어주는 방법 사용
    • DI 컨테이너 없이도 사용 가능
    • 부가기능을 부여할 수 있는 대상이 클라이언트가 호출하는 메소드로만 제한되지 않고 오브젝트 생성, 필드 값 조회와 조작, 스태틱 초기화 등 다양한 작업에 부가기능 부여 가능

AOP 네임스페이스

  • 기계적으로 적용하는 빈을 간편하게 등록하기 위함.
    • 어드바이저
    • 포인트컷
    • 자동 프록시 생성기

applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"

    xsi:schemaLocation="http://www.springframework.org/schema/beans 

                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

                        http://www.springframework.org/schema/aop 

                        http://www.springframework.org/schema/aop/spring-aop-3.0.xsd

                        http://www.springframework.org/schema/tx 

                        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<aop:config>
    <aop:pointcut id="transactionPointcut"
                expression="execution(* *..*ServiceImpl.upgrade*(..))" />
    <aop:advisor advice-ref="transactionAdvice" pointcut-ref="transactionPointcut" />
</aop:config>
  • 태그를 사용했을 때보다 이해하기 쉬우며 코드의 양이 대폭 줄어 들었다.

트랜잭션 속성

트랜잭션 속성 4가지

트랜잭션 전파

  • 트랜잭션 경계에서 이미 진행 중인 트랜잭션이 있을 때 혹은 없을 때 어떻게 동작할 것인가를 결정하는 방식.
  • PROPAGATION_REQUIRED
    • 진행 중인 트랜잭션이 없으면 새로 시작하고 이미 시작된 트랜잭션이 있으면 이에 참여
  • PROPAGATION_REQUIRES_NEW
    • 앞에서 시작된 트랜잭션이 있든 없든 상관없이 새로운 트랜잭션을 만들어서 독자적으로 동작
  • PROPAGATION_NOT_SUPPORTED
    • 트랜잭션 없이 동작. (트랜잭션 무시)
    • 특정한 메소드만 트랜잭션 적용에서 제외하고자 할 때 유용

격리수준

  • 동시에 여러 트랜잭션이 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것.
  • Read Uncommitted(0), Read Committed(1), Repeatable Read(2), Serializable(3)

제한시간

  • 트랜잭션을 수행하는 제한시간

    읽기전용

  • 트랜잭션 내에서 데이터를 조작하는 시도를 금지시킴

메소드별로 다른 트랜잭션 정의를 적용하려면 어떻게 해야할까?

TransactionInterceptor

  • 동작방식은 기존에 만들었던 TransactionAdvice와 동일
  • 트랜잭션 정의를 메소드 이름 패턴을 이용해서 다르게 지정할 수 있는 방법을 추가적으로 제공

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

<bean id="transactionAdvice" class="org.springframework.transaction.interceptor.TransactionInterceptor">
        <property name="transactionManager" ref="transactionManager"/>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
  • PROPAGATION_NAME, ISOLATION_NAME, readOnly, timeout_NNNN, -Exception1, +Exception2
  • 위 설정을 tx 네임스페이스를 이용해서 바꾸면 다음과 같다.
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            https://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/aop
                            http://www.springframework.org/schema/aop/spring-aop.xsd
                            http://www.springframework.org/schema/tx
                            http://www.springframework.org/schema/tx/spring-tx.xsd">

....

    <!--
        tx태그를 사용할 경우
        - transactionManager는 트랜잭션 매니저 빈 아이디가 transactionManager가 있으면 생략 가능
    -->
    <tx:advice id="transactionAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="upgrade*" read-only="true"/>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

    <aop:config>
        <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)"/>
    </aop:config>

포인트컷, 트랜잭션 속성 적용 팁

  • 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름을 이용한다.
    • 일반적으로 트랜잭션의 경우 메소드 단위까지 세밀하게 포인트컷을 정의할 필요 X
    • 클래스들이 모여있는 패키지를 통째로 선택하거나 클래스 이름에서 일정한 패턴을 찾아서 표현식으로 만드는 것이 바람직
  • 공통된 메소드 이름 규칙을 통해 최소한의 트랜잭션 어드바이스와 속성을 정의한다.
    • 트랜잭션 속성은 최소(모든 메소드에 기본 트랜잭션 속성)로 시작해서 점차 확대하는 방식을 권장.
    • e.g. 디폴트 속성을 일괄 부여 -> 조회용 메소드에 ‘Read-only’ 속성 추가 -> ….
  • 프록시 방식 AOP 는 같은 타깃 오브젝트 내의 메소드를 호출할 때는 적용되지 않는다.
    • 프록시를 거치지 않고 직접 타깃의 메소드가 호출되기 때문.
    • 해결 방법
      • AspectJ 적용 (바이트코드 조작하는 방식의 AOP 기술)

트랜잭션 속성 -> UserService 적용

부가기능을 어느 layer에 적용할 것인가.

  • 비즈니스 로직을 담고 있는 서비스 계층 오브젝트의 메소드가 가장 이상적.
    • Dao의 메소드를 UserService에 위임.
    • 다른 레이어나 모듈에서 Dao로 직접 접근 제한
public interface UserService {
    void add(User user);

    // 새로 추가된 4개의 메소드
    User get(String id);

    List<User> getAll();

    void deleteAll();

    void update(User user);

    void upgradeLevels();
}

//UserServiceImpl

//Dao 로 위임
@Override
public User get(String id) {
    return userDao.get(id);
}

@Override
public List<User> getAll() {
    return userDao.getAll();
}

@Override
public void deleteAll() {
    userDao.deleteAll();
}

@Override
public void update(User user) {
    userDao.update(user);
}
    <aop:config>
        <aop:advisor advice-ref="transactionAdvice" pointcut="bean(*Service)" />
    </aop:config>


    <tx:advice id="transactionAdvice">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*" />
        </tx:attributes> 
    </tx:advice>

어노테이션 트랜잭션 속성 & 포인트컷

  • 문제의식
    • 클래스나 메소드에 따라 제각각 속성이 다른, 세밀하게 튜닝된 트랜잭션 속성을 적용해야 하는 경우도 있다.
    • 위 경우는 메소드 이름 패턴을 이용해서 일괄적으로 트랜잭션 속성을 부여하는 방식은 적합하지 않음

트랜잭션 어노테이션 @Transactional

  • 스프링은 @Transactional 이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식.

    • TransactionalAttributeSourcePointcut
    • @Transactional 이 붙은 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려준다.
    • 그림 6-24 참고
  • 문제의식

    • 메소드마다 @Transactional 을 붙이면 코드가 지저분해지고 동일한 속성 정보를 가진 어노테이션이 반복된다.

대체 정책(fallback)

  1. 타깃 메소드에 @Transaction 이 있는지 확인
  2. 없다면 타깃 클래스에 있는지 확인
  3. 없다면 타깃 클래스 상위 인터페이스 메소드에 있는지 확인
  4. 없다면 인터페이스에 있는지 확인

UserService 에 적용

@Transactional
public interface UserService {
    void add(User user);

    @Transactional(readOnly = true)
    User get(String id);

    @Transactional(readOnly = true)
    List<User> getAll();

    void deleteAll();

    void update(User user);

    void upgradeLevels();
}
  • 인터페이스 레벨에 디폴트 속성 부여
  • 메소드별 디테일한 속성 설정은 메소드 레벨로 설정

트랜잭션 테스트

트랜잭션 동기화 기술

  • 트랜잭션 정보를 저장소에 보관해뒀다가 DAO 에서 공유
  • 이 기술 덕분에 트랜잭션 전파가 가능
  • 트랜잭션 매니저를 이용해서 트랜잭션에 참여하거나 트랜잭션을 제어할 수 있음.
  • e.g. 테스트 코드에서 세 개의 트랜잭션을 하나의 트랜잭션으로 묶기
@ExtendWith(SpringExtension.class)
class UserServiceTest {
  @Autowired
  PlatformTransactionManager transactionManager;


  @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);
  }

}
  • 위와 같이 임의의 트랜잭션 묶음을 만들어 놓고 트랜잭션 동기화가 잘 이루어지는지, 롤백은 정상적으로 되는지 등을 테스트해볼 수 있다.

롤백 테스트

  • 테스트 내의모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백하는 테스트

테스트를 위한 트랜잭션 어노테이션

  • @Transactional 어노테이션을 붙여서 위와 동일하게 여러 트랜잭션을 하나의 트랜잭션으로 묶을 수 있음
  • 주의사항 : 테스트용 트랜잭션은 테스트가 끝나면 자동으로 롤백된다.
    • 만약 DB에 그대로 커밋하고 싶다면?
    • @Rollback(false) 어노테이션을 붙인다.
    • @TransactionConfiguration(defaultRollback=false) 을 테스트 클래스에 붙여서 여러 테스트 메소드에 일괄 적용 가능. —> deprecated?
@Test
@Transactional
public void transactionSync() {
    userService.deleteAll();
    userService.add(users.get(0));
    userService.add(users.get(1));
}

Q. AOP - 실무와의 연관성

  • Transaction을 예로 들면 트랜잭션이 필요한 메소드에 @Transactional 을 붙이는 방식으로 쓰이는 것 같은데.. AOP (pointcut, advice) 등을 직접 구현해서 사용하는 경우가 있는지?
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
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
글 보관함