[Spring] 토비의 스프링 6장 : AOP

헌치·2022년 11월 8일
0

Spring

목록 보기
11/13

들어가며

이번 장은 그 유명한 Spring AOP를 다룬다. 처음 6장을 읽었을 땐 텍스트를 읽으면서도 이해가 안갔는데, 이번에 다시 한번 읽으며 동작 원리에 대해 어느정도 이해하게 되었다.

💬
관련해 토프링 읽기모임에서 나왔던 얘기들을 공유한다.

읽기모임1
읽기모임2

생성자나 메소드 파라미터 등에서 스프링 AOP를 적용하려면 AspectJ를 사용해야 한다고 한다. 언젠가 코드에 적용해보고 싶다!

토비의 스프링 6장

0. 기존 코드

public interface UserService {

    User findById(final long id);

    void insert(final User user);

    void changePassword(final long id, final String newPassword, final String createBy);
}
public class AppUserService implements UserService {

    private final UserDao userDao;

    public AppUserService(final UserDao userDao) {
        this.userDao = userDao;
    }

    public User findById(final long id) {
        return userDao.findById(id);
    }

    public void insert(final User user) {
        userDao.insert(user);
    }

    public void changePassword(final long id, final String newPassword, final String createBy) {
        final var user = findById(id);
        user.changePassword(newPassword);
        userDao.update(user);
    }
}

🚀 1. JDK 다이나믹 프록시

  • 기존 코드는 타깃 클래스별 프록시 클래스를 구현해야 했다.
  • 여러 타깃 클래스 속 메서드에 공통으로 들어갈 관점을 어떻게 묶을 것인가?

자바 reflect(method.invoke())를 활용.

  • 프록시 클래스를 정의하지 않아도, 몇 API를 통해 프록시처럼 동작하는 오브젝트를 다이나믹하게 생성한다!

다이나믹 프록시 생성 방식!

final var userService = (UserService) Proxy.newProxyInstance(
                getClass().getClassLoader(), // 클래스 로딩을 위한 클래스 로더
                new Class[]{UserService.class}, // 프록시로 구현할 인터페이스
                new TxHandler(appUserService, transactionManager, "changePassword")); // 적용할 에스팩트, 포인트컷을 담은 핸들러

동적으로 트랜잭션 프록시를 만들어주는 TxHandler

public class TxHandler implements InvocationHandler {
    Object target;
    PlatformTransactionManager transactionManager;
    String pattern;

    public TxHandler(Object target, PlatformTransactionManager transactionManager, String pattern) {
        this.target = target;
        this.transactionManager = transactionManager;
        this.pattern = pattern;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        if (method.getName().startsWith(pattern)) {
            return invokeInTransaction(method, args);
        } else {
            return method.invoke(target, args);
        }
    }

    private Object invokeInTransaction(Method method, Object[] args)
            throws Throwable {
        TransactionStatus status = this.transactionManager
                .getTransaction(new DefaultTransactionDefinition());
        try {
            Object ret = method.invoke(target, args);
            this.transactionManager.commit(status);
            return ret;
        } catch (InvocationTargetException e) {
            this.transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

이를 통해 특정 이름으로 시작하는 메소드에 트랜잭션을 적용할 수 있게 된다!

프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다. 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 건 어렵지 않게 가능했다. 하지만 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 지금까지 살펴본 방법으로는 불가능하다.

  • 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서

🚀 2.프록시 팩토리 빈

  • 말그대로, 프록시를 생성하고, 빈으로 등록해주는 빈이다.
  • 프록시의 생성만을 담당한다.
  • 추상화된 빈이다. 프록시 생성 방식은 달라질 수 있다.

프록시 팩토리 빈을 통한 프록시 호출

스프링 내장 빈인 ProxyFactoryBean 을 통해 동적으로 프록시 생성 및 빈 등록이 가능하다!

// ProxyFactoryBean 구조 간략화

public class ProxyFactoryBean implements FactoryBean<Object> {

  private Object singletonInstance;
  private Advisor advisor;

  public ProxyFactoryBean(Object singletonInstance) {
    this.object = singletonInstance
  }

	public void addAdvisor(Advisor advisor) {}

  //...
}
ProxyFactoryBean pfBean = new ProxyFactoryBean(new UserServiceImpl());
// 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("changePassword*"); // 이름 비교조건 설정. 

pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
// 포인트컷과 어드바이스를 advisor로 묶어서 한 번에 추가

UserService proxiedTxUserService = (UserService) pfBean.getObject();

🚀 3. 빈 후처리기를 통한 자동 프록시 생성

스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있다. 바로 이것이 자동 프록시 생성 빈 후처리기다.

  • 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서

아직 트랜잭션 적용 대상이 되는 빈마다, 일일이 프록시 팩토리 빈을 설정해줘야 한다는 부담이 남아 있다.

이를 해결하기 위해, DefaultAdvisorAutoProxyCreator 를 사용한다.

자동으로 Advisor를 호출하여 pointcut에 부합한다면 advice 를 호출한다.

과정

  1. 빈을 만들 때마다, 스프링 DI 컨테이너는 후처리기에 빈을 보낸다.
  2. 빈의 어드바이저 속 포인트컷을 통해 해당 빈이 프록시 적용 대상인지 확인한다.
  3. 적용 대상일 시, 내장된 프록시 생성기에게 프록시를 생성하게 한다.
  4. 후처리기는 DI 컨테이너에게 프록시를 돌려준다.
  5. DI 컨테이너는 프록시를 빈으로 등록한다!

장점

번거로운 프록시 팩토리 빈 설정을 안해도 된다!

포인트컷 확장

포인트컷에는 클래스 정보 뿐 아니라, 어드바이스를 적용할 메소드에 대한 정보도 필요하다!

public interface Pointcut {
    ClassFilter getClassFilter(); //프록시를 적용할 클래스인지 확인
    MethodMatcher getMethodMatcher(); //어드바이스를 적용할 메소드인지 확인
}

포인트컷 표현식

포인트컷 표현식은 자바의 RegEx 클래스가 지원하는 정규식처럼 간단한 문자열로 복잡한 선정조건을 쉽게 만들어낼 수 있는 강력한 표현식을 지원한다.

사실 스프링이 사용하는 포인트컷 표현식은 AspectJ라는 유명한 프레임워크에서 제공하는 것을 가져와 일부 문법을 확장해서 사용하는 것이다. 그래서 이를 AspectJ 포인트컷 표현식이라고도 한다.

적용 예시

@Test
public void methodSignaturePointcut() throws NoSuchMethodException {
    AspectJExpressionPointcut pointcut =new AspectJExpressionPointcut();

    pointcut.setExpression("execution(public int springbook.learningtest.pointcut.Target.minus(int,int) throws java.lang.RuntimeException)");

    assertThat(pointcut.getClassFilter().matches(Target.class), is(true));
    assertThat(pointcut.getMethodMatcher().matches(Target.class.getMethod("minus", int.class, int.class), null), is(true));
    assertThat(pointcut.getMethodMatcher().matches(Target.class.getMethod("plus", int.class, int.class), null), is(false));
}
  • bean() : 특정 이름의 빈을 선정하는 포인트컷
  • @annotation : 특정 애노테이션이 적용된 메소드를 선정하는 포인트컷

주의사항

런타임 시점까지 문법검증/기능 확인이 되지 않는다. 검증된 표현식을 쓰자!

포인트컷 표현식 속 클래스 이름은 타입 패턴이다.

🚀 4. AspectJ

AspectJ는 프록시처럼 간접적인 방법이 아니라, 타깃 오브젝트를 뜯어고쳐서 부가기능을 직접 넣어주는 직접적인 방법을 사용한다

컴파일된 타깃의 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용한다. 트랜잭션 코드가 UserService 클래스에 비즈니스 로직과 함께 있었을 때처럼 만들어버리는 것이다.

물론 소스코드를 수정하지는 않으므로 개발자는 계속해서 비즈니스 로직에 충실한 코드를 만들 수 있다.

왜 바이트코드를 조작?

  • 스프링 없이도 AOP 조작 가능!
  • 프록시 방식보다 유연한 AOP가 가능!(오브젝트 생성, 필드값 조작, 스태틱 초기화 등…)

주의사항

  • 번거로운 작업 필요 : JVM 실행옵션 변경, 별도 바이트코드 컴파일러 사용, 특수 클래스로더 사용…

🚀 5. AOP 용어

  1. 타깃 : 부가기능 부여 대상
  2. 어드바이스 : 타깃에게 제공할 부가기능을 담은 모듈
  3. 조인포인트 : 어드바이스가 적용될 위치. 스프링 AOP에서는 메소드
  4. 포인트컷 : 어드바이스를 적용할 조인포인트를 선별하는 작업을 정의한 모듈
  5. 프록시 : 부가기능 제공 오브젝트.
  6. 어드바이저 : 포인트컷과 어드바이스가 있는 오부젝트. AOP 기본 모듈. 스프링 AOP에서만 사용되는 용어
  7. 에스펙트 : 한개 이상의 포인트컷과 어드바이스의 조합. 싱글톤 형태. 어드바이저 역시 단순한 형태의 에스팩트!

🚀 6. 스프링 AOP 용어

자동 프록시 생성기

  • 스프링의 DefaultAdvisorAutoProxyCreator 클래스를 빈으로 등록한다. 다른 빈을 DI 하지도 않고 자신도 DI 되지 않으며 독립적으로 존재한다. 따라서 id도 굳이 필요하지 않다. 애플리케이션 컨텍스트가 빈 오브젝트를 생성하는 과정에 빈 후처리기로 참여한다. 빈으로 등록된 어드바이저를 이용해서 프록시를 자동으로 생성하는 기능을 담당한다.

어드바이스

  • 부가기능을 구현한 클래스를 빈으로 등록한다. TransactionAdvice는 AOP 관련 빈 중에서 유일하게 직접 구현한 클래스를 사용한다.

포인트컷

  • 스프링의 AspectJExpressionPointcut을 빈으로 등록하고 expression 프로퍼티에 포인트컷 표현식을 넣어주면 된다. 코드를 작성할 필요는 없다.

어드바이저

  • 스프링의 DefaultPointcutAdvisor 클래스를 빈으로 등록해서 사용한다. 어드바이스와 포인트컷을 프로퍼티로 참조하는 것 외에는 기능은 없다. 자동 프록시 생성기에 의해 자동 검색되어 사용된다.

🚀 7. 트랜잭션 속성

트랜잭션 전파

트랜잭션 경계에서, 이미 진행중인 트랜잭션이 있을 때, 없을 때 어떻게 동작할 것인가?


Required

기존 트랜잭션(부모 트랜잭션)이 있을 시 합류한다.

Required New

부모 트랜잭션과 관계없는 새 트랜잭션을 생성한다.두 트랜잭션은 별개로 처리된다.

Not Supported

트랜잭션 없이 동작

트랜잭션을 무시하는 속성을 두는 데는 이유가 있다.
트랜잭션 경계설정은 보통 AOP를 이용해 한 번에 많은 메소드에 동시에 적용하는 방법을 사용한다. 그런데 그중에서 특별한 메소드만 트랜잭션 적용에서 제외하려면 어떻게 해야 할까?
물론 포인트컷을 잘 만들어서 특정 메소드가 AOP 적용 대상이 되지 않게 하는 방법도 있겠지만 포인트컷이 상당히 복잡해질 수 있다.
그래서 차라리 모든 메소드에 트랜잭션 AOP가 적용되게 하고, 특정 메소드의 트랜잭션 전파 속성만 PROPAGATION_NOT_SUPPORTED로 설정해서 트랜잭션 없이 동작하게 만드는 편이 낫다.

  • 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서

트랜잭션 격리

  • DefaultTransactionDefinition에 설정된 격리수준은 ISOLATION_DEFAULT다.
  • 이는 DataSource에 설정되어 있는 디폴트 격리수준을 그대로 따른다는 뜻이다.

제한시간

  • 트랜잭션 수행 제한시간

읽기전용

  • 트랜잭션 내의 데이터 조작 제한.
  • 데이터 액세스 기술에 따라 성능이 향상되기도 함.

RollBackOn

스프링의 트랜잭션은 기본적인 예외처리 원칙에 따라 비즈니스적인 의미가 있는 예외상황에만 체크 예외를 사용하고, 그 외의 모든 복구 불가능한 순수한 예외의 경우는 런타임 예외로 포장돼서 전달하는 방식을 따른다고 가정한다.

  • 해당 속성을 통해, 기본 예외처리 원칙과 다른 예외처리가 가능하다.
  • 특정 체크예외를 롤백시키거나, 특정 런타임 예외시 트랜잭션을 커밋할 수 있다.

🚀 8. 주의사항

해당 타겟 클래스 안에서 호출한 메소드에는 AOP가 적용되지 않는다.

🚀 9. 선언적 트랜잭션

정의

  • 선언적 트랜잭션 : AOP를 통해 코드 외부에서 트랜잭션 기능 부여, 속성 지정
  • 프로그래머틱 트랜잭션 : 개별 데이터 기술의 트랜잭션 API로 직접 코드에서 사용

대체 정책

  • 메소드의 속성을 확인할 때
  • 타깃 메소드 → 타깃 클래스 → 선언 메소드 → 선언 타입(클래스, 인터페이스)의 순서에 따라서
  • @Transactional이 적용됐는지 차례로 확인하고, 가장 먼저 발견되는 속성정보를 사용한다!

테스트 내 사용

테스트에서 트랜잭션을 시작하거나 조작할 수 있는 기능은 매우 유용하다. 테스트 코드에서 미리 트랜잭션을 시작해놓으면 직접 호출하는 DAO 메소드도 하나의 트랜잭션으로 묶을 수 있다. 트랜잭션 결과나 상태를 조작하면서 테스트하는 것도 가능하다. 예를 들어 하이버네이트 같은 ORM에서 세션에서 분리된detached 엔티티의 동작을 확인할 때도 유용하다.

  • 알라딘 eBook <토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리> (이일민 지음) 중에서
@Test
public void transactionSync(){
    userDao.deleteAll();
    assertThat(userDao.getCount(), is(0)); // 롤백테스트를 위해 초기 상태 지정

    DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
    // 트랜잭션 매니저에게 트랜잭션을 요청한다. 기존에 진행되는 트랜잭션이 없으니 새 트랜잭션을 시작한다.
    // 트랜잭션을 반환함과 동시에 동기화한다.

    // ... service 메서드 호출
    userService.add(users.get(0));
    userService.add(users.get(2));
    assertThat(userDao.getCount(), is(2));

    transactionManager.rollback(txStatus); // 강제 롤백

    assertThat(userDao.getCount(), is(0)); // 롤백 이전의 상태로 돌아가는지 확인.
}

롤백테스트

테스트가 끝날 시 트랜잭션을 롤백하는 테스트.

DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다.

만약 트랜잭션에서 특정 메소드를 제외하고 싶다면?

  • @NotTransactional 대신 @Transactional의 트랜잭션 전파 속성을 사용!


추가자료

궁금증

  • 다이나믹 프록시와 CGLib를 각각 언제 사용하는지 궁금하다. 다이나믹 프록시의 단점이 인터페이스 구현 이외에 없는지?
  • AOP는 메소드 단위로 이뤄지는데, 생성자나 메소드 파라미터 등에서 스프링 AOP를 적용할 수 있는 방법이 없는지 궁금하다. 아니면 해당 케이스에서 스프링 AOP 대신 고려되는 방식이 궁금하다.
    • 객체를 new로 생성할 시 들어가는 파라미터에 대해 암호화해주는 로직을 AOP로 할 수 있는지 궁금했다.

2차 궁금증

  • aspectJ에서 어떻게 바이트코드를 조작하는지 궁금하다.
    • 텍스트에디터 보면, 알수없는 바이트코드들이 나옴. 사실 명확히 정의되어잇음. 고도의 노가다성 작업으로 수정, 변경 가능함. 에스팩트 제이 전문가들이 뛰어나 잘 작업함. 같은이름 파일을 바꿔치기도 하고, 런타임 위빙도 사용. 클래스를 읽어야 사용 가능한데, 읽는 순간, 로딩된 클러스를 바꿔치기하는 방법이 있다. 자바 API 이용하는 방식. 다 장단점이 있다. 런타임 위빙은 추가작업이 필요하다.
  • 롤백 테스트 시, 기존 비즈니스 로직 속 트랜잭션을 검증할 수 없다는 단점이 있지 않은가?
    • 커밋 완료시 트랜잭션 단위 끝나고 DB 반영 확인…
    • 커밋 완료되지 않아도 확인 가능하다!!
    • 쿼리 날아가면, 엔티티들의 롤백된다해도 DB에 쿼리는 전달된다!!
    • 플러시되면, DB에 실제로 반영은 됨. 커밋 완료된 후, 결과 검증이 가능하다.
  • JPA 롤백테스트 주의사항
    • JPA의 persistant context 안에서 엔티티를 추가, 세이브할 때 커밋 직전까지 DB에 인서트 쿼리가 안나가다가 나중에 한번에 날아감. 쓰기지연.
    • ID 조회 시 데이터를 바로 읽어가게 해서 성능 향상도..
    • 롤백테스트 시, 인서트 실행 후 조회, 테스트 끝날 시 DB에는 쿼리가 안날아가고 테스트가 끝날지도…
    • 매핑에 문제가 있다… 인서트쿼리가 DB에 날아가야 확인이 가능할텐데, JPA 테스트를 롤백으로 만들 시 매핑 문제 상황에서도 테스트가 성공하기도 한다!
    • 플러시모드 관련 설명들이 많이 있다. 잘 찾아봐라.
    • 테스트 만들 때, DB 모든 작업들이 다 플러시가 되었는지 확인해라. 플러시를 강제로 해라. 그것만 주의하면 된다!!
    • 테스트용 디비를 통해 테스트 전후 데이터 변화를 확인. 트랜잭션 롤백 못시키면 테스트에서 디비 바꾼걸 복귀작업을 테스트 해줘야 함. 번거로움.
    • 롤백테스트를 잘 활용해라. DB까지의 동작과정을 잘 알아봐야.

참고자료

토비의 스프링 6장

https://rlawls1991.tistory.com/m/entry/AOP-스프링의-프록시-팩토리-빈

profile
🌱 함께 자라는 중입니다 🚀 rerub0831@gmail.com

0개의 댓글