ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] AOP 심화 - JoinPoint와 ProceedingJoinPoint / 동작 원리 / 프록시 / JDK Proxy와 CGLib Proxy
    DEV/Spring 2024. 5. 17. 15:33

    지난번 스프링 특징 중 AOP에 관하여 조금 더 조사해보았다.

     

    📌JoinPoint와 ProceedingJoinPoint의 차이

    • JoinPoint : Aspect가 적용될 수 있는 시점을 의미. JoinPoint 인터페이스는 호출되는 대상 객체, 메서드, 전달파라미터 목록에 접근할 수 있는 메서드를 제공
    메서드 설명
    Signature getSignature() 호출되는 메서드 정보 반환
    Object getTarget() 대상 객체 반환
    Object[] getArgs() 파라미터 목록 반환
    getThis() 프록시 객체 반환
    • Signature : 호출되는 메서드 정보
    메서드 설명
    String getName() 메서드 이름 반환
    String toLongName() 메서드를 완전하게 표현한 문장 반환 (반환타입,파라미터타입)
    String getArgs() 파라미터 목록 반환
    • Proceeding JoinPoint : JoinPoint를 상속받아 추가 기능을 제공. @Around에서만 지원되며 target 실행제어가 가능하다.
    메서드 설명
    Object proceed() 다음 어드바이스나 타겟을 호출

     

    📌AOP (Advice) 사용 예시

    AOP는 개발자가 공통적인 코드를 주로 작성하는데 자주 사용되는 예 몇가지는 아래와 같다.

    1. 로깅 -요청, 응답에 대한 클래스, 메서드, 파라미터를 로깅
    2. 소요된 시간 로깅 - 각 작업 수행에 걸린 시간을 로깅할 수 있음 (작업수행 전~후 까지 로깅이 가능하므로)
    3. 값 변경 - AOP에서 요청된 데이터, 응답하는 데이터를 필터링 할 수 있음
    4. 커스텀 어노테이션을 이용한 Reflaction 구현

    지난 포스팅에서 @Before, @After, @AfterReturning, @AfterThrowing, @Around 어노테이션을 다뤘는데,

    @Around는 메서드를 전반적으로 제어하기 때문에 Before와 After로 나누지 않고 하나에서 처리가 가능하다.

     

    *주의* 하나의 @Aspect 클래스 내에서 여러 Advice가 존재한다면 (@Before 가 2개 이상) 순서를 보장 받지 못한다.

    AOP Advice의 순서를 보장 받고 싶다면 @Aspect 단위로 분리하여 @Order 어노테이션으로 순서 지정이 필요하다.

     

    간단한 @AfterReturning, @AfterThrowing, @Around의 예시를 준비했다.

    • 메서드 정상 호출 완료 후 실행
    //returning 속성값과 ResponseObject 매개변수 이름이 일치해야함
    @AfterReturning(value = "execution(* com.test.controller.TestController.*(..))", returning = "returnValue")
    public void writeSuccessLog(JoinPoint joinPoint, ResponseObject returnValue) throws RuntimeException {
        //logging
        //returnValue 는 해당 메서드의 리턴객체를 그대로 가져올 수 있다.
        if(returnValue instanceof String) {
               System.out.println("the return value is "+(String)returnValue);
        } 
    }
    
    • 메서드 예외 발생 시 실행
    //throwing 속성값과 Exception 매개변수 이름이 일치해야함
    @AfterThrowing(value = "execution(* com.test.controller.TestController.*(..))", throwing = "exception")
    public void writeFailLog(JoinPoint joinPoint, Exception exception) throws RuntimeException {
        //logging
        //exception 으로 해당 메서드에서 발생한 예외를 가져올 수 있다.
        exception.printStackTrace();
    }  
    
    • 메서드 호출 전 후로 실행
    @Around("execution(public * com.sparta.springcore.controller..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        
        // 앞에서 부가 기능 수행
        System.out.println("Before "+proceedingJoinPoint.getSignature().toShortString());
        
        // 핵심기능 수행
        Object output = joinPoint.proceed(); // 들어온 요청을 controller 로 보냄
        
        // 뒤에서 부가 기능 수행 가능
        System.out.println("After "+proceedingJoinPoint.getSignature().toShortString());
        
        return output; // controller 에서 처리된 요청을 반환
    }

     

    위 예시에서는 실행 범위를 지정하는 Pointcut 중 execution만 사용했는데, 다양한 지시자가 존재한다.

    cf. pointcut 지시자와 표현식 종류

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

     

    📌Spring AOP 동작 원리

    Spring에서 AOP는 프록시 방식으로 동작한다. 프록시 패턴이란 어떤 객체(타겟)를 사용하고자 할 때, 객체를 직접적으로 참조 하는 것이 아니라, 해당 객체를 대행하는 객체(=proxy)를 통해 대상 객체에 접근하는 방식을 말한다.

     

     

    - 왜 프록시 방식을 사용할까 ?

    비즈니스로직의 핵심 기능(서비스)과 부가 기능(로그, 보안, 트랜잭션 등)을 분리하기 위해서!

    → 유지보수 용이

    → 핵심 로직에만 집중 가능

     

    - 정적(수동) 프록시 VS 동적(자동) 프록시

    정적 프록시란 개발자가 직접 프록시 패턴을 이용하여 AOP를 구현하는 방법.

    즉, 한 서비스 로직의 실행 전후로 AOP관련 프록시 객체를 추가하여 관리하는 것인데

    클래스 수가 많아지면 그에 맞는 프록시 객체를 일일히 추가해주기 번거롭고 관리도 힘들다.

    따라서 프록시를 조건에 따라 자동으로 만들어주는 동적 프록시를 사용한다.

     

    - Spring AOP 동적 프록시

    • Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다
      • 컴파일 시점이 아닌 런타임 시점에 프록시를 자동으로 생성
    • Spring AOP에서는 JDK Dynamic Proxy 와 CGlib 을 통해 Proxy화 한다

    - JDK Proxy와 CGLib Proxy

    두 방식의 가장 큰 차이점은 Target의 어떤 부분을 상속 받아서 프록시를 구현하느냐에 있다.

    JDK Proxy는 Target의 상위 인터페이스를 상속 받아 프록시를 만든다. 따라서 인터페이스를 구현한 클래스가 아니면 의존할 수 없다. Target에서 다른 구체 클래스에 의존하고 있다면, JDK 방식에서는 그 클래스(빈)를 찾을 수 없어 런타임 에러가 발생한다.

    CGLib Proxy는 Target 클래스를 상속 받아 프록시를 만든다. JDK 방식과는 달리 인터페이스를 구현하지 않아도 되고, 구체 클래스에 의존하기 때문에 런타임 에러가 발생할 확률도 상대적으로 적다. 또한 JDK Proxy는 내부적으로 Reflection을 사용해서 추가적인 비용이 들지만, CGLib는 그렇지 않다고 한다. 여러 성능 상 이점으로 인해 Spring Boot에서는 CGLib를 사용한 방식을 기본으로 채택하고 있다.

    *주의 : 상속이 불가능한 final과 private은 Aspect가 적용되지않는다.

     

    - @EnableAspectJAutoProxy

    ProxyFactory를 한번더 추상화한 Layer로 Aspect를 Annotation을 이용해 쉽게 AOP를 등록할 수 있다.

    @Import(AspectJAutoProxyRegistrar.class)
    public @interface EnableAspectJAutoProxy {
    
    	boolean proxyTargetClass() default false;
    	..
    }
    

    proxyTargetClass : 인터페이스를 구현하도록 할 것인지(JDK 동적 프록시) 또는 해당 클래스를 바이트 조작하여 직접 구현하도록 할 것인지(CGLib)에 대한 옵션

    • false = Interface기반 Reflection AOP
    • true = Class기반 CGLIB AOP
    @EnableAspectJAutoProxy(proxyTargetClass = true)

    일반적으로 인터페이스 주입에 의한 문제를 예방하고자 proxyTargetClasstrue로 주는 경우가 많은데, SpringBoot를 사용하고 있다면 더이상 이러한 옵션을 부여하지 않아도 된다.

    SpringBoot에서는 CGLib 라이브러리가 안정화되었다고 판단하여 proxyTargetClass 옵션의 기본값을 true로 사용하고 있다.

     

     

    참고 포스팅

    https://mangkyu.tistory.com/175 https://sabarada.tistory.com/97 https://velog.io/@ha0kim/2020-12-28-AOP-특징-및-동작원리
    https://steady-coding.tistory.com/608

    https://zigo-autumn.tistory.com/m/entry/AOP-심화-feat-Life-Cycle-of-Bean-Advice-예제

    댓글

Designed by Tistory.