토비의스프링vol.1

[토비의 스프링vol.1 6장] AOP - 2

ohyujeong 2023. 4. 3. 10:04

6장 AOP-2 정리 노션 페이지

스프링 AOP

자동 프록시 생성

프록시 팩토리 빈 방식의 한계로 인한 문제점

  1. 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제

    ProxyFactoryBean 어드바이스 통해 해결

  2. 부가기능 적용이 필요한 타깃 오브젝트마다 비슷한 내용의 ProxyFactoryBean 설정정보 추가

    → 어떻게 중복을 제거할까?

중복 문제의 접근 방법

  • 다이내믹 프록시라는 런타임 코드 자동생성 기법을 통해 프록시 클래스 코드의 중복문제 해결

    ProxyFactoryBean설정 문제에도 자동등록/생성 기법 사용할 수 있을까?

    → 일정한 타깃 빈의 목록을 제공하면, 자동으로 각 타깃 빈에 대한 프록시를 만들어주는 방법

    → 하지만 지금까지는 위의 방법이 없었음

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

BeanPostProcessor 인터페이스를 구현해서 만드는 빈 후처리기

  • 스프링 빈 오브젝트로 만들어진 후, 빈 오브젝트 다시 가공할 수 있게 해줌

  • 빈 후처리기는 빈 오브젝트 프로퍼티 강제 수정, 별도 초기화 작업 수행 가능

  • 만들어진 빈 오브젝트 자체 바꿔치기 가능

    → 스프링이 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키기 가능

DefaultAdvisorAutoProxyCreator

  • 스프링이 제공하는 빈 후처리기 중 하나
  • 어드바이저를 이용한 자동 프록시 생성기

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

문제 해결!

  • 빈 후처리기 사용하여 일일이 ProxyFactoryBean 빈을 등록하지 않아도 타깃 오브젝트에 자동으로 프록시 적용

확장된 포인트컷

이상한 점

  • 지금까지의 포인트 컷 : 타깃 오브젝트의 메소드 중 어떤 메소드에 부가기능 적용할 지 선정하는 역할

→ 갑자기 어떤 빈에 프록시를 적용할지 선택하는 역할이 됨

→ 오브젝트 내 메소드가 아니라, 빈 오브젝트 자체를 선택하는 기능을 가진걸까?

→ 그건 아님

포인트 컷의 두 가지 기능

  1. 클래스 필터 돌려줌

    1. 프록시 적용할 클래스인지 확인해줌
  2. 메소드 매처 돌려줌

    → 지금까지 MethodMacther 라는 메소드 선별 기능만 사용해온 것

    → 어차피 PorxyFactoryBean 에서 포인트컷 사용할 때는 이미 타깃 정해져 있기 때문에

AOP란 무엇인가?

트랜잭션 서비스 추상화

트랜잭션 경계설정 코드를 비즈니스 로직에 담으며 특정 트랜잭션 기술에 종속되는 코드 문제점 → 서비스 추상화 기법을 통해 해결

  • 인터페이스DI를 통해 무엇을 하는지는 남기고, 그것을 어떻게 하는지를 분리한 것
  • 어떻게 할 지는 더 이상 비즈니스 로직 코드에 영향을 주지 않고 독립적으로 변경이 가능

프록시와 데코레이터 패턴

트랜잭션을 어떻게 다룰 지는 제거했지만,

트랜잭션이라는 부가기능어디에 적용할 것인가가 여전히 노출

→ DI 이용해 데코레이터 패턴 적용

  • 클라이언트인터페이스와 DI 통해 접근하도록 설계
  • 트랜잭션 처리 코드는 데코레이터에 담겨 타깃 클래스 사이에 존재함으로써 프록시 역할
  • 클라데코레이터 거쳐서 타깃에 접근

다이내믹 프록시

  1. 비즈니스 로직 인터페이스의 모든 메소드마다 트랜잭션 기능을 부여하는 코드를 넣어 프록시 클래스를 만들어야 한다는 문제점
  2. 트랜잭션 기능 부여하지 않아도 되는 메소드조차 프록시로서 위임 기능이 필요하기 때문에 일일이 다 구현해야 한다는 문제점

→ 프록시 클래스 없이 JDK 다이내믹 프록시 기술 적용하여

런타임 시에 프록시 오브젝트 만들어 해결

  • 부가 기능 부여 코드의 중복 문제 해결
  • 일부 메소드에만 적용 시 메소드 선정하는 등 패턴 이용
  • 클래스를 만들지 않고도 새로운 구현 기능 가진 오브젝트 다이내믹하게 만들어냄

프록시 팩토리 빈

동일한 기능의 프록시를 여러 오브젝트에 적용할 경우 오브젝트 단위의 중복 문제 해결

  • 내부적으로는 템플릿/콜백 패턴 활용하는 프록시 팩토리 빈 덕분에 부가기능을 담은 어드바이스와 부가기능 선정 알고리즘을 담은 포인트컷이 프록시에서 분리 → 여러 프록시에서 공유해서 사용할 수 있게 됨

자동 프록시 생성 방법과 포인트컷

트랜잭션 적용 대상이 되는 빈마다 일일이 프록시 팩토리 빈 설정해야한다는 문제점

→ 빈 후처리기 기법 활용하여 클래스 선정 기능 담은 확장된 포인트컷 사용

cf) 빈 후처리기 : IoC/DI 컨테이너의 빈 생성 작업 가로채서 빈 오브젝트를 프록시로 대체하는 기술

  • 트랜잭션 부가 기능 어디에 적용하는지에 대한 정보를 포인트컷이라는 독립적인 정보로 완전히 분리할 수 있었음
  • 포인트컷 표현식 활용해 간단한 설정만으로 적용 대상 손쉽게 설정

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

  • 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특징

  • 에스펙트

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

    • 부가기능 정의한 코드인 어드바이스, 어드바이스 어디에 적용할지 결정하는 포인트컷을 함께 갖고 있음

      왼쪽 : 모듈 분리 전 상태

      오른쪽 : 부가기능을 독립적인 모듈인 애스펙트로 분리

      AOP 적용기술

    • 프록시를 이용한 AOP*

    • 프록시로 만들어 DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여해서 부가기능 제공

    • 독립적으로 개발한 부가기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이내믹하게 적용해주기 위해 가장 중요한 역할이 프록시

    • 그래서 스프링 AOP는 프록시 방식의 AOP라 할 수 있음

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

    • AspectJ는 다이내믹 프록시 방식을 사용하지 않음

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

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

왜 사용할까?

  1. 바이트코드를 조작해서 타깃 오브젝트를 직접 수정해버리면 스프링과 같은 DI 컨테이너의 도움을 받아서 자동 프록시 생성 방식을 사용하지 않아도 AOP를 적용할 수 있기 때문.
    1. 스프링과 같은 컨테이너가 사용되지 않는 환경에서도 손쉽게 AOP 적용이 가능
  2. 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하다.
    1. 프록시를 AOP의 핵심 메커니즘으로 사용하면 부가기능을 부여할 대상은 클라이언트가 호출할 때 사용하는 메소드로 제한
    2. 바이트코드를 직접 조작해서 AOP를 적용하면 오브젝트의 생성, 필드 값이 조회와 조작, 스태틱 초기화 등의 다양한 작업에 부가기능을 부여해줄 수 있다.
    3. 타깃 오브젝트가 생성되는 순간 부가기능을 부여해주고 싶을 때도 있는데 프록시 방식에서는 불가능한 방법

AOP 용어

타깃 : 부가기능을 부여할 대상.

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

  • 오브젝트로 정의하거나 메소드레벨에서 정의
  • 메소드 호출 과정에 전반적으로 참여하거나, 예외 발생때만 동작하는 어드바이스처럼 호출 과정 일부에만 동작하는 어드바이스 등 여러가지 종류

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

  • 스프링의 AOP에서 조인포인트는 메소드의 실행 단계 뿐
  • 타깃 오브젝트가 구현한 인터페이스의 모든 메소드는 조인 포인트가 된다.

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

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

  • DI를 통해 타깃 대신 클라이언트에게 주입되며, 클라이언트의 메소드 호출을 대신 받아서 타깃에 위임해주면서, 그 과정에서 부가기능을 부여한다.

어드바이저 : 포인트컷과 어드바이스를 하나씩 갖고 있는 오브젝트

  • 스프링 AOP에서만 사용되는 특별한 용어이고, 일반적인 AOP에서는 사용되지 않는다.

애스펙트 : AOP의 기본 모듈

  • 한 개 또는 그 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재한다.

트랜잭션 속성

트랜잭션이 모두 같은 방식으로 동작하는 건 아님

DefaultTransactionDefinition이 구현하고 있는TransactionDefinition 인터페이스는 트랜잭션 동작방식에 영향을 줄 수 있는 4가지 속성을 정의

  • 트랜잭션 전파
    • 트랜잭션의 경계에서 이미 진행 중인 트랜잭션이 있을 때 또는 없을 때 어떻게 동작할 것인가를 결정하는 방식
    • PROPAGATION_REQUIRED
      • 가장 많이 사용되는 전파 속성
      • 진행 중인 트랜잭션이 없으면 새로 시작하고, 이미 시작된 트랜잭션이 있으면 참여한다.
    • PROPAGATION_REQUIRES_NEW
      • 항상 새로운 트랜잭션 실행
    • PROPAGATION_NOT_SUPPORTED
      • 진행 중 트랜잭션 있어도 무시
      • 포인트컷을 복잡하게 만들지 않고 특정 기능에만 트랜잭션 적용이 안되도록 설정하고자 할 때 사용
  • 격리수준
    • 모든 DB 트랜잭션은 격리수준(isolation level)을 갖고 있다. 격리수준은 기본적으로 DB에 설정되어 있지만 JDBC 드라이버DataSource 등에서 재설정할 수 있고, 필요하다면 트랜잭션 단위로 격리수준을 조정할 수 있다.
  • 제한시간
    • 트랜잭션을 수행하는 제한시간(timeout)을 설정할 수 있다. DefaultTransactionDefinition의 기본 설정은 제한시간 없음
  • 읽기전용
    • 읽기전용(read only)으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아줄 수 있다. 또한 데이터 액세스 기술에 따라서 성능이 향상될 수도 있다.

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

  1. 트랜잭션 포인트컷 표현식은 타입 패턴이나 빈 이름 이용
  2. 공통된 메소드 이름 규칙 통해 최소한의 트랜잭션 어드바이스와 속성 정의
  3. 프록시 방식 AOP는 같은 타깃 오브젝트 내의 메소드 호출할 때는 적용되지 않음
    • 전략이라기보다는 주의 사항
    • 프록시 방식의 AOP에서는 프록시를 통한 부가 기능의 적용은 클라이언트로부터 호출이 일어날 때만 가능
    • 타깃 안에서의 호출에는 프록시가 적용되지 않는 문제를 해결할 수 있는 방법 두 가지
      1. 스프링 API를 이용해 프록시 오브젝트에 대한 레퍼런스를 가져온 뒤에 같은 오브젝트의 메소드 호출도 프록시를 이용하도록 강제하는 방법
        1. 스프링 API와 프록시 호출 코드 등장하며 순수한 비즈니스 로직이 아니게 됨. 추천 X
      2. AspectJ와 같은 타깃의 바이트코드를 직접 조작하는 방식의 AOP 기술을 적용하는 방법

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

  • 자바 5에서 등장
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited 
@Documented
// 디폴트 값이 설정되어 있으므로 모두 생략 가능
public @interface Transactional {
    @AliasFor("transactionManager")    String value() default "";
    @AliasFor("value") String transactionManager() default "";
    Propagation propagation() default Propagation.REQUIRED;
    Isolation isolation() default Isolation.DEFAULT;
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    boolean readOnly() default false;
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
    Class<? extends Throwable>[] noRollbackFor() default {};
    String[] noRollbackForClassName() default {};      
}

@Target : 애노테이션 사용할 대상 지정, 여기에 사용된 메소드와 타입(클래스, 인터페이스) 처럼 한 개 이상의 대상 지정 가능

@Retention: 애노테이션 정보가 언제까지 유지되는지를 지정. 이렇게 설정하면 런타임 때도 애노테이션 정보를 리플렉션을 통해 얻을 수 있음

@Inherited: 상속을 통해서도 애노테이션 정보를 얻을 수 있게 한다.

@Transactonal 애노테이션 타깃은 메소드와 타입

  • 메소드,클래스,인터페이스에 사용 가능

  • 트랜잭션 속성정보로 사용하도록 지정하면 스프링은 애노테이션이 부여된 모든 오브젝트를 자동으로 타깃 오브젝트로 인식

  • 이때 사용되는 포인트컷은 TracnsactionAttributeSourcePointCut

    • 스스로 표현식과 같은 선정기준 갖고 있진 않음
    • 대신 @Transactonal이 타입 레벨이든 메소드 레벨이든 상관없이 부여된 빈 오브젝트를 모두 찾아서 포인트컷의 선정 결과로 돌려줌
  • 기본적으로 트랜잭션 속성 정의, 동시에 포인트컷의 자동등록에도 사용

  • 메소드마다 부여하면 코드가 지저분

    → 스프링은 4단계의 대체 정책 이용

    → 타깃 메서드, 타깃 클래스, 선언 메서드, 선언 타입(클래스,인터페이스) 순서에 따라 애노테이션 적용됐는지 확인함

    → 가장 먼저 발견되는 속성 정보 사용

6장 정리

  • 트랜잭션 경계설정 코드를 분리해서 별도의 클래스로 만들고 비즈니스 로직 클래스와 당일한 인터페이스를 구현하면 DI의 확장 기능을 이용해 클라이언트의 변경 없이도 깔끔하게 분리된 트랜잭션 부가기능을 만들 수 있다.
  • 트랜잭션처럼 환경과 외부 리소스에 영향을 받는 코드를 분리하면 비즈니스 로직에만 충실한 테스트를 만들 수 있다.
  • 목 오브젝트를 활용하면 의존관계 속에 있는 오브젝트도 손 쉽게 고립된 테스트로 만들 수 있다.
  • DI를 이용한 트랜잭션의 분리는 데코레이터 패턴과 프록시 패턴으로 이해될 수 있다.
  • 번거로운 프록시 클래스 작성은 JDK의 다이내믹 프록시를 사용하면 간단하게 만들 수 있다.
  • 다이내믹 프록시는 스태틱 팩토리 메소드를 사용하기 때문에 빈으로 등록하기 번거롭다. 따라서 팩토리 빈으로 만들어야 한다. 스프링은 자동 프록시 생성 기술에 대한 추상화 서비스를 제공하는 프록시 팩토리 빈을 제공한다.
  • 프록시 팩토리 빈의 설정이 반복되는 문제를 해결하기 위해 자동 프록시 생성기와 포인트 컷을 활용할 수 있다. 자동 프록시 생성기는 부가기능이 담긴 어드바이스를 제공하는 프록시를 스프링 컨테이너 초기화 시점에 자동으로 만들어준다.
  • 포인트컷은 AspectJ 포인트컷 표현식을 사용해서 작성하면 편리하다.
  • AOP는 OOP만으로는 모듈화하기 힘든 부가기능을 효과적으로 모듈화하도록 도와주는 기술이다.
  • 스프링은 자주 사용되는 AOP 설정과 트랜잭션 속성을 지정하는데 사용할 수 있는 전용 태그를 제공한다.
  • AOP를 이용해 트랜잭션 속성을 지정하는 방법에는 포인트컷 표현식과 메소드 이름 패턴을 이용하는 방법과 타깃에 직접 부여하는 @Transactional 애노테이션을 사용하는 방법이 있다.
  • @Transactgional을 이용한 트랜잭션 속성을 테스트에 적용하면 손쉽게 DB를 사용하는 코드의 테스트를 만들 수 있다.