본문 바로가기
Develop/Spring

Spring | Spring AOP의 동작원리와 JDK Dynamic Proxy vs CGLIB Proxy 비교 및 Spring AOP와 AspectJ 비교

by Hoya324 2024. 9. 5.

Spring AOP란

  • Spring AOP는 스프링 프레임워크에서 제공하는 관점 지향 프로그래밍 기술이다.
  • Spring AOP는 로깅, 보안, 트랜잭션 관리 등과 같은 공통적인 관심사를 모듈화하여 코드 중복을 줄이고 유지 보수성을 향상하는데 도움을 준다.

관심 지향 프로그래밍(Aspect-Oriented Programming, AOP)란

  • 객체 지향 프로그래밍 패러다임을 보완하는 기술
  • 메서드나 객체의 기능을 핵심 관심사(Core Concern)공통 관심사(Cross-cutting Concern) 로 나누어 프로그래밍을 하는 것을 뜻 함
    • 핵심 관심사 : 각 객체가 가져야 할 본래의 기능
    • 공통 관심사: 여러 객체에서 공통적으로 사용되는 코드
  • 여러 개의 클래스에서 반복해서 사용하는 코드가 있다면 해당 코드를 모듈화해서 공통 관심사로 분리한다.
    • 분리된 공통 관심사를 Aspect로 정의하고 Aspect를 적용할 메서드나 클래스에 Advice를 적용하여 공통 관심사와 핵심 관심사 분리
  • AOP는 이러한 방식으로 공통의 관심사를 별도의 모듈로 분리하여 관리하며, 이를 통해 재사용성과 유지 보수성을 높일 수 있습니다.

AOP 예시

  • A, B, C 3개의 클래스가 있다.
  • 각 클래스가 가진 색은 A, B, C 클래스에서 사용되는 메서드이며, 같은 색은 같은 코드(관심사)이다.
  • 유지보수 차원에서 각 클래스가 가진 같은 코드를 수정하게 될 때, 각각의 모든 코드를 수정해야한다는 불편함이 있다.
  • Aspect X처럼 공통 관심사를 묶어서 모듈화 시키면 코드의 재사용성과 유지 보수성을 강화할 수 있다. → AOP의 장점

Spring AOP 이해

주요 용어

용어 설명
Aspect 공통적인 기능들을 모듈화 한것을 의미
Target Aspect가 적용될 대상을 의미하며 메소드, 클래스 등이 이에 해당됨
Join point Aspect가 적용될 수 있는 시점을 의미하며 메소드 실행 전, 후 등이 될 수 있음
Advice Aspect의 기능을 정의한 것으로 메서드의 실행 전, 후, 예외 처리 발생 시 실행되는 코드를 의미
Point cut Advice를 적용할 메소드의 범위를 지정하는 것을 의미

주요 어노테이션

메서드 설명
@Aspect 해당 클래스를 Aspect로 사용하겠다는 것을 명시
@Before 대상 “메서드”가 실행되기 전에 Advice를 실행
@AfterReturning 대상 “메서드”가 정상적으로 실행되고 반환된 후에 Advice를 실행
@AfterThrowing 대상 “메서드에서 예외가 발생”했을 때 Advice를 실행
@After 대상 “메서드”가 실행된 후에 Advice를 실행
@Around 대상 “메서드” 실행 전, 후 또는 예외 발생 시에 Advice를 실행

AOP는 어디에 사용하는가

  • AOP는 공통적으로 관심 있는 기능을 구현할 때 사용한다.
  • 하나의 서버에 여러 개의 메서드가 있을 때,
    • 모든 메서드의 호출 시간을 측정하고 싶은 경우
    • 로그를 찍는 등, 들어오는 매개변수와 리턴되는 결과를 보고 싶은 경우
    • 메서드 실행 시간을 알고 싶은 경우
  • 모든 메서드에 해당 기능을 코드로 작성하면 코드가 길어지고 가독성이 안 좋아진다.
  • 이럴 때 AOP 기능 사용

횡단 관심사 (cross-cutting concerns)

  • 핵심 기능을 보조하기 위해 여러 클래스에 걸쳐 공통으로 사용되는 부가적인 기능
    • 중복 코드 생김 ( AOP 방식으로 해결 )
  • 핵심기능과 함께 사용 ( 단독 X )

Ex. 로깅, 보안, 트랜잭션, 로그 추적 로직 등

구체적인 예시

1. 들어오는 매개변수와 리턴되는 결과를 보고 싶은 경우

  • 컨드롤러의 매개변수를 로그에 찍어주는 ParameterAop 클래스를 작성
@Aspect
@Component
public class ParameterAop {

    //com/example/aop/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
    @Pointcut("execution(* com.example.aop.controller..*.*(..))")
    private void cut() {}

    //cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
    @Before("cut()")
    public void before(JoinPoint joinPoint) {

        //실행되는 함수 이름을 가져오고 출력
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println(method.getName() + "메서드 실행");

        //메서드에 들어가는 매개변수 배열을 읽어옴
        Object[] args = joinPoint.getArgs();

        //매개변수 배열의 종류와 값을 출력
        for(Object obj : args) {
            System.out.println("type : "+obj.getClass().getSimpleName());
            System.out.println("value : "+obj);
        }
    }

    //cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
    //@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
    @AfterReturning(value = "cut()", returning = "obj")
    public void afterReturn(JoinPoint joinPoint, Object obj) {
        System.out.println("return obj");
        System.out.println(obj);
    }
}

JoinPoint 인터페이스

  • 어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다.
  • 예를들면 예외가 터졌는데, 예외발생한 메소드의 이름이 뭔지 등을 기록할 필요가 있을 수 있다. 이럴때 JoinPoint 인터페이스가 제공하는 유용한 API들이 있다.
메소드 설명
Signature getSignature() 클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴
Object getTarget() 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴
Object[] getArgs() 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열 로 리턴
  • 컨트롤러 작성
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
    //서비스 로직
    return id + " " + name;
}

@PostMapping("/post")
public User post(@RequestBody User user) {
    //서비스 로직
    return user;
}
  • GET 방식으로 request
get 메서드 실행
type : Long 
value : 100 
type : String 
value : paul 
return obj
100 paul
  • POST 방식으로 request
post 메서드 실행
type : User
return obj
value : User{id='paul', pw= '1234', email='paulkim1997@naver.com '}
User{id= paul', pw= '1234', email= paulkim1997@naver.com '}

2. 메서드 실행 시간을 알고싶은 경우

  • TimeTraceAop 클래스 작성
@Component
@Aspect
public class TimeTraceAop {
        @Around("execution(* study.studySpring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

                // 시작 전
        long start = System.currentTimeMillis();


        System.out.println("START: " + joinPoint.toString());

        try {
                        // 메서드 시작 시점
            return joinPoint.proceed();
        } finally {
                        // 메서드 종료 시점
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
        }
    }
}
  • 로그
    • 첫 START-END는 spring bean에 등록되면서 뜬 것이다.
    • 나머지는 MemberController→MemberService→repository 순으로 동작하고 각 동작 시간을 측정한 것이다.

Spring AOP 동작 방식

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

AOP 적용 전 의존 관계

  • memberController가 memberService를 의존하고 있다.
  • 때문에 memberController를 호출하면 memberService도 메서드를 호출한다.

AOP 적용 후 의존 관계

  • AOP를 적용하면 Spring은 스프링 컨테이너에 스프링 빈을 등록할 때 프록시 객체를 등록한다.
  • 프록시 객체가 모두 실행되면 이후에 joinPoint.proceed() 가 실행되고 나서야 실제 객체가 실행된다.
  • 때문에 controller가 호출하는 service는 프록시 service인 것이다.(이후의 의존 관계에서도 마찬가지)

왜 Spring AOP는 프록시 방식을 사용하는가?

  • 프록시 객체가 없이 memberService를 사용하면, Aspect에 정의된 부가 기능을 사용하기 위해 원하는 위치에 직접 Aspect 클래스를 호출해야한다.
  • 이런 경우 Target 클래스 안에 부가 기능을 호출하는 로직이 포함되기 때문에, AOP를 적용하는 의미가 없다.
  • 즉, 여러 곳에서 반복적으로 Aspect를 호출해야 하고, 그로 인해 유지보수성이 크게 떨어진다.
  • 이러한 이유로 Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다.

CGLib Proxy

  • 클래스 기반으로 바이트코드를 조작하여 프록시를 생성하는 방식

클래스 기반이기 때문에 인터페이스 기반의 JDK Dynamic Proxy보다 성능이 우수하다고 한다.

  • memberService를 MemberController에 의존성 주입되는 시점에 실제로 memberService.getClass()로 로그를 찍어보면 그냥 memberService가 아닌 CGLIB에 의해 생성된 프록시 객체임을 알 수 있다.

JDK Dynamic Proxy vs CGLIB Proxy

JDK Dynamic Proxy

  • proxy 생성을 위해 interface가 필요하다.
  • Refelction을 이용해 proxy를 생성한다.
  • JDK에서 지원하는 프록시 생성 방법
    • 외부 라이브러리에 의존하지 않는다
  • 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
  • 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스 오브젝트를 자동으로 생성
  • 인터페이스가 반드시 존재해야한다
  • Invocation Hanlder를 재정의한 invoke 코드를 직접 구현해줘야 부가기능이 추가된다

CGLIB Proxy

  • 바이트 코드를 조작해 프록시 생성
  • Hibernate의 lazy loading, Mockito의 모킹 메서드 등에서 사용
  • 프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
  • 클래스 상속을 이용하여 프록시 구현. 인터페이스가 존재하지 않아도 가능
    • 바이트 코드를 조작해서 프록시 생성함
    • 인터페이스에도 강제로 적용 가능. 이 경우 클래스에도 프록시를 적용해야 한다
  • Dynamic Proxy 보다 약 3배 가까이 빠르다
    • 메서드가 처음 호출되었을 때 동적으로 타깃 클래스의 바이트 코드를 조작
    • 이후 호출 시엔 조작된 바이트 코드를 재사용
  • MethodInterceptor를 재정의한 intercept를 구현해야 부가 기능이 추가된다
  • 메서드에 final을 붙이면 오버라이딩이 불가능

Spring AOP와 AspectJ

Spring AOP

  • 런타임 시점에 동적으로 변할 수 있는 프록시 객체를 이용하기에 앱 성능에 영향을 끼칠 수 있다.

AspectJ

  • AspectJ 는 자바에서 완벽한 AOP 솔루션 제공을 목표로하는 기술이다.
  • [ .aj 파일]을 이용한 assertj 컴파일러를 추가로 사용하여 컴파일 시점이나 JVM 클래스 로드시점에 조작한다.
  • 런타임 시점에는 영향끼치지 않는다. 즉 컴파일이 완료된 이후에는 앱 성능에 영향이 없다.

차이 비교

1. 기능과 목표가 다르다.

  • Spring AOP는 프로그래머가 직면하는 일반적인 문제 해결을 위해 Spring IoC에서 제공하는 간편한 AOP 기능이다. 어디에나 쓸 수 있는 완벽한 AOP 솔루션이 아니라, [Spring 컨테이너가 관리하는 Bean]에만 사용하려고 만들었고, 실제로 여기에만 AOP를 적용 할 수 있다.
  • AspectJ는 [자바코드에서 동작하는 모든 객체]에 대해 완벽한 AOP 솔루션 제공을 목표로 하는 기술이다. 성능이 뛰어나고 기능이 매우 강력하지만 그만큼 Spring AOP에 비해 사용방법이나 내부 구조가 훨씬 더 복잡하다.

2. Weaving 방법이 다르다.

  • Weaving은 공통관심사항(Aspect)의 동작코드(Advice)를 대상 객체(Target)에 연결시켜 관점지향을 구현한 객체로 만드는 과정이다. 좀 더 쉽게말하면 AOP를 구현하기 위한 바이트코드 조작 방법을 의미한다고 생각하면 된다.
  • Spring AOP는 위에서 설명한 런타임 위빙(다이나믹 프록시)를 사용한다.
    • 자바의 리플렉션 API와 CGlib등의 도구로 런타임시 동적인 프록시 객체를 만든다.
    • 동적인 프록시 객체란, 클래스의 정보에 따라 런타임시에 다르게 구현되도록 만드는 객체를 의미한다.
    • 복잡한 설정없이 Spring 빈 등록을 하게되면 자동으로 등록되어 사용하기 매우 편하다.
    • 근데 런타임에 바이트코드를 조작하는 만큼, 오버헤드가 심하고 성능에 영향을 크게 미칠 수 있다.
    • 벤치마킹상 AspectJ가 Spring AOP보다 최소 8배, 최대 35배정도 빠르다

  • AspectJ는 3가지 유형의 Weaving을 제공한다.
    1. Compile-Time 위빙 : AspectJ 전용 컴파일러를 이용하여 Aspect 부분과 Target 코드 부분을 입력으로 받고 하나로 엮인 바이트코드(.class)를 생성한다. 컴파일이 완료된 이후에는 앱의 성능에 전혀 영향을 끼치지 않는다.
    2. Post-Complie 위빙 : 외부 라이브러리를 Weaving 할 때 사용한다. [Compile-Time 위빙]과 거의 동일한 동작을 한다. 보통 클래스파일을 JAR와 엮기 위해서 사용해서 [Binary 위빙]이라고 부르는게 일반적이다.
    3. Load-time 위빙 : 전용 컴파일러를 사용하지않고, 조작되지 않은 바이트코드(.class)를 가 JVM에 로드 될 때 ClassLoader를 이용하여 바이트코드를 조작하는 위빙 방식이다. 객체를 로드할 때 위빙이 일어나는 거라 앱 성능의 하락을 발생시킬 수 있다.

결론

  • Spring AOP가 성능과 기능은 매우 부족하지만, Spring Bean에 자동으로 적용되고 설정하기 매우 편리하다. 또한 AspectJ와 다르게 컴파일 시점에 건드리는게 없어서 각종 라이브러리(Lombok)과 호환성이 뛰어나다.

하지만 Spring AOP는 컨테이너 안의 Bean만 조작할 수 있고, JoinPoint를 메서드호출시점밖에 적용 못한다는 단점이 있다. 반면에 AspectJ는 런타임이 아닌 컴파일 시점에 동작하는 도구라서 다음과 같이 다양한 시점을 지정할 수 있다.

Reference