AOP V3

Shaun·2024년 6월 27일
2

aop

목록 보기
4/4
post-thumbnail

AOP 적용

  • 이렇게 간단한 서비스, 레포 로직이 있다고 가정하자.

  • 스프링의 AOPAspectJ의 문법을 차용하고, 프록시 방식의 AOP를 제공한다. AspectJ를 직접 사용하는 것이 아니다.

  • 스프링 AOP를 사용할 때는 @Aspect를 사용하는데 이것 또한 AspectJ가 제공하는 애노테이션이다.

  • 포인트컷 범위 (Around)에 해당하는 모든 메서드들은 AOP에 적용이 된다.

@PointCut


  • 이런식으로 포인트컷 범위를 컨트룰 할 수도 있다.

@PointCut

  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처 라고 한다.
  • 메서드 반환타입은 void여야 한다.
  • 다른 Aspect에서 참조하려면 public로 열어줘야함
  • 포인트 컷은 && , || , ! 연산자들을 조합해서 사용 가능함

PointCut 클래스

  • 포인트컷만 따로 클래스로 만들어서 참조 가능하다.(단 public으로 열어줘야함)

어드바이스 순서

  • 어드바이스는 기본적으로 순서를 보장하지 않는다.

  • 순서를 보장하고 싶으면 포인트컷을 따로 클래스를 빼고 Order() 를 사용하면 된다.

어드바이스 종류

@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외
변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

  • @Around를 제외한 나머지 어드바이스들은 @Around가 할 수 있는 일의 일부만 제공할 뿐이다. 따라서 @Around 어드바이스만 사용해도 필요한 기능 수행 가능

  • 모든 어드바이스들JoinPoint를 파라미터로 사용가능

  • @Around 무조건 ProceedingJoinPoint를 사용해야함

  • proceed() : 다음 어드바이스타겟을 호출한다.

@Before

  • @Around와 다르게 작업 흐름을 변경할 수는 없다.
  • @Around는 ProceedingJointPoint.proceed() 를 호출해야만 다음 대상이 호출되지만, @Before은 종료시 자동으로 다음 타겟이 호출

@AfterReturning

  • returning 속성에 있는 이름어드바이스 메서드의 매개변수 이름과 일치해야함!!
  • returning에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행함
  • @Around 와 다르게 반환객체변경 불가, 변경하고 싶으면 @Around 사용

@AfterThrowing

  • throwing 에 사용된 이름과 어드바이스 메서드의 매개변수 이름과 일치해야함

@After

  • 메서드 실행이 종료되면 실행됨

@Around

  • 메서드 실행의 주변에서 실행, 메서드 실행 전후에 작업을 수행한다.
  • proceed()를 여러번 실행할 수도 있음(재시도)

JoinPoint

getArgs() : 메서드 인수를 반환
getThis() : 프록시 객체를 반환
getTarget() : 대상 객체를 반환
getSignature() : 조언되는 메서드에 대한 설명을 반환
toString() : 조언되는 방법에 대한 유용한 설명을 인쇄

-> 이렇게 많은 어드바이스들이 존재하는 이유는 @Around하나로 많은 기능을 제공하지만 그만큼 실수할 가능성이 있기때문, 또한 다들 개발자가 중간에 작업을 할경우 어드바이스 이름들이 명시적이여서 실수 가능성을 낮춰준다.

포인트컷

포인트컷 지시자의 종류

  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.

  • AspectJExpressionPointcut이 포인트컷 표현식을 처리해주는 클래스
  • AspectJExpressionPointcut PointCut인터페이스를 가진다

Execution 문법

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

  • 메소드 실행 조인포인트 매칭
  • ?는 생략 가능
  • *같은 패턴을 지정할 수 있다.

  • AspectJExpressionPointcut 에 pointcut.setExpression 을 통해서 포인트컷 표현식을 적용할 수 있다.

  • pointcut.matches(메서드, 대상 클래스)를 실행하면 지정한 포인트컷 표현식의 매칭여부를 boolean 으로 반환

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: come.example.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략

  • 접근제어자?: 생략
  • 반환타입: *
  • 선언타입?: 생략
  • 메서드이름: *
  • 파라미터: (..)
  • 예외?: 없음
  • * 은 아무 값이 들어와도 된다는 뜻
  • .. 은 파라미터의 타입과 파라미터 수 상관 없다는 뜻

  • 메서드 이름 앞뒤에 * 사용 가능

  • . 정확하게 해당 위치 패키지
  • .. 해당 위치의 패키지와 그 하위 패키지도 포함

Execution2

  • Execution에서 다형성에서 배운것처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다.

  • 부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭된다

  • (String) : 정확하게 String 타입 파라미터

  • () : 파라미터가 없어야 한다.

  • (*) : 정확히 하나의 파라미터, 단 모든 타입을 허용한다.

  • (*, *) : 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.

  • (..) : 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다. 0..* 로 이해하면 된다.

  • (String, ..) : String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
    예) (String) , (String, Xxx) , (String, Xxx, Xxx) 허용

Within

  • within 사용시 정확하게 타입이 맞아야함, 부모타입 x

Args

  • String은 Object, Serializable의 하위 타임
  • execution : 파라미터 타입이 정확하게 매칭 되어야함, 클래스에 선언된 정보를 기반으로 판단
  • args : 부모타입 혀용, 실제 넘어온 파라미터의 객체 인스턴스를 보고 판단

@target, @within

  • @target(com.example.member.annotation.ClassAop)
  • @within(com.example.member.annotation.ClassAop)

  • 주로 클래스 aop 적용범위 설정할때 사용

  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트, 인스턴스의 모든 메서드조인 포인트로 적용

  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트, 해당 타입 내에 있는 메서드만 조인포인트로 적용

  • @target은 부모 클래스의 메서드까지 어드바이스를 적용하고 @withnin은 자기 자신의 클래스에 정의된 메서드만 어드바이스 적용

!!!주의!!!

  • args , @args, @target단독으로 사용하면 안됨 왜냐 이것들은 실제 객체 인스턴스가 생성되고 실행될때(런타임) 어드바이스 적용 여부를 확인할 수 있음
  • 프록시가 있어야 실행 시점에 판단 가능, 프록시 생성 시점은 애플리케이션 로딩시점
  • 프록시가 없으면 모든 스프링빈에 AOP프록시를 적용하려고함, 스프링빈 중에는 final로 지정된 빈들도 있어서 여기서 오류발생 가능성 있음
  • 그래서 적용 대상을축소하는 표현식과 함께 사용해야함

@annotation, @args


  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

bean

  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.

매개변수 전달

  • this, target, args,@target, @within, @annotation, @args 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.

  • this : 스프링 빈에 등록된 프록시를 가져온다
  • target: 실제타겟을 가져옴

AOP 적용(애노테이션)


  • Trace 애노테이션 AOP

  • Retry 애노테이션 AOP

  • @annotation(retry) , Retry retry를 통해 어드바이스에 애노테이션을 파라미터로 전달함


  • 이런식으로 바로 메서드 위에 적용가능

AOP 한계 - 내부 호출

  • 스프링은 프록시 방식의 AOP 를 사용한다. 즉 AOP를 사용하려면 프록시를 통해서 대상객체(target)을 호출해야한다.

  • AOP를 적용하면 스프링은 대상 객체 대신에 프록시를 스프링 빈으로 등록한다. 따라서 스프링은 의존관계 주입시에 항상 프록시 객체를 주입한다. 프록시 객체가 주입되기 때문에 대상 객체를 직접 호출하는 문제는 일반적으로 발생하지 않 는다.


  • 이렇게 외부에서 호출이아닌 내부에서 자기자신 인스턴스 메서드 호출시에는 실제 대상 객체의 메서드를 호출한다.

  • 스프링 AOP는 메서드 내부 호출에 프록시를 적용할 수 없다. 이를 해결하기 위해 AOP를 직접 적용하는 AspectJ를 사용하면 해결 되지만 이 방식은 난이도가 상당히 높기 때문에 추천하지는 않음.

프록시 기술 한계 해결방안

1. 자기자신을 의존관계 주입 받기

  • 생성자 주입은 빈 순환 참조를 만듬(자기자신을 생성하면서 주입해야하니까).

  • 수정자 주입은 스프링이 생성된 이후에 주입할 수 있기 때문에 에러발생 x

  • 이렇게 해서 주입받은 자기 자신은 타겟(실제객체)가 아닌 프록시를 주입받게 된다.

  • 내부 메서드 호출시 프록시를 통해 호출하게 된다.

2. 지연조회

  • ObjectProvider(Provider), ApplicationContext를 사용해 스프링 빈을 지연해서 조회하면 된다.

  • ObjectProvider는 객체를 스프링컨테이너에서 조회하는 것을 스프링 빈 생성 시점이 아니라 실제 객체를 사용하는 시점으로 지연할 수 있다.

  • getObject()를 호출하는 시점에 스프링 컨테이너에서 빈을 조회한다.

3. 구조 변경


  • 내부 호출을 클래스로 별도로 분리한뒤 주입받아서 사용한다.

프록시 기술 한계 - 타입 캐스팅

  • MemberServiceImpl은 JDK동적 프록시를 사용했으며 MemberService를 구현했다. 그러므로 MemberService 인터페이스 기반으로 프록시를 생성함(JDK동적 프록시 이기 때문에)

  • 인터페이스 기반으로 프록시를 생성하기 때문에 여기서 생성된 프록시를 대상 클래스(Impl)로 캐스팅하면 예외가 발생한다.

  • CGLIB 프록시는 구체클래스(Impl)기반으로 프록시를 생성하기 때문에 인터페이스, 구체클래스 두 타입으로 캐스팅 가능함.

프록시 기술 한계 - 의존관계 주입

  • spring.aop.proxy-target-class=false 설정을 사용해서 스프링 AOP가 JDK 동적 프록시를 사용하 도록 했다.

  • 이런식으로 테스트 해보면 이런 에러가 나온다.

    BeanNotOfRequiredTypeException: Bean named 'memberServiceImpl' is expected to be
    of type 'hello.aop.member.MemberServiceImpl' but was actually of type
    'com.sun.proxy.$Proxy54'

  • JDK 프록시는 인터페이스 기반으로 만들어지므로 MemberServiceImpl 주입시 에러발생

  • 이런식으로 AOP가 CGLIB 프록시를 사용하도록 설정하면 문제 없다.

  • 이렇게 보면 CGLIB가 만병통치약 같지만 단점들도 존재한다.

프록시 기술 한계 - CGLIB

1. 대상 클래스에 기본 생성자 필수

  • CGLIB 프록시는 대상 클래스를 상속 받고, 생성자에서 대상 클래스의 기본 생성자를 호출한다. 따라서 대상 클래스의 기본 생성자를 만들어야 한다.

2. 생성자 2번 호출 문제

  • 실제 타겟의 객체를 생성할때
  • 프록시 객체를 생성할 때 부모 클래스의 생성자 호출

3. final 키워드 클래스, 메서드 사용 불가

  • final 이 있으면 상속불가, 오버라이딩불가 -> CGLIB는 상속을 기반으로 하기 떄문에 두 경우 프록시가 정상동작 하지 않는다.

스프링에서 CGLIB 해결방안

  • 스프링 3.2, CGLIB를 스프링 내부에 함께 패키징

  • 기본생성자 문제objenesis 라는 특별한 라이브러리를 사용해서 기본 생성자 없이 객체 생성이 가능하다. 참고로 이 라이브러리는 생성자 호출 없이 객체를 생성할 수 있게 해준다.

  • 생성자 2번 호출문제objenesis 라는 특별한 라이브러리 덕분에 가능해졌다.

  • AOP를 적용할때는 final을 잘 안쓰기 때문에 이 부분은 크게 문제가 되지 않는다.

profile
호주쉐프에서 개발자까지..

0개의 댓글