Spring - AOP (Proxy)

진주원(JooWon Jin)·2023년 10월 8일
0

Spring

목록 보기
1/3
post-thumbnail

AOP - 프록시

서론

AOP의 등장 배경으로는 핵심 기능부가 기능의 분리였다. 그리고 AOP를 학습하다 보면 프록시라는 개념이 많이 등장한다. 오늘은 프록시에 대한 설명과 Spring AOP에서 프록시를 사용하는 방법에 대해 기술하겠다.

프록시

목표 : 핵심 기능이 작성된 클래스와 부가 기능이 작성된 개체들을 분리해야 한다.

  • 분리 과정에 있어 두 클래스(핵심 ↔ 부가)가 양방향으로 알아야 할까?

    NO ! 부가 기능을 담당하는 쪽에서만 핵심 기능의 개체 정보를 알면 된다.

구체화

  • 부가 기능이 핵심 기능을 사용하는 것처럼 만들어야 한다 !

프록시

  • 클라이언트가 접근하려는 타켓인 것처럼 위장하여 요청을 대신 받는 대리자
    • 프록시는 요청을 가로채 타깃에게 다시 요청을 위임한다 !
  • 프록시에서는 부가 기능만 수행 → 핵심 기능은 타깃이 수행

Untitled

프록시 사용 목적

  1. 클라이언트가 타깃에 접근하는 방식을 제어 ( 프록시 패턴 )
    • Ex ) JPA에서의 지연 로딩 → 직접 타깃에 접근하기 전까지 접근을 지연
  2. 타깃에 부가적인 기능을 부여 ( 데코레이터 패턴 )
    • Ex ) 트랜잭션 , 시간 측정

프록시 적용

Untitled

  • 클라이언트에게 타깃에 대한 레퍼런스를 넘긴다 할 때 !

    실제 타깃 대신에 프록시를 넘긴다 !

프록시의 메소드를 통해 타깃 접근 시 → 타깃을 생성하고 요청 위임

  • 프록시와 타겟을 동일한 인터페이스를 사용하도록 !

간단한 예제

  • User Interface

Untitled

  • UserTarget (Target 클래스)

Untitled

  • UserProxy (Proxy 클래스)

Untitled

  1. 프록시 클래스는 타겟에 대한 레퍼런스를 가지고 있어야 한다.
    1. 동일한 인터페이스를 구현하고 있기 때문에 인터페이스로 대체
  2. 다음과 같이 say라는 메소드 호출 시
    1. 부가 기능을 사용하고 실제 메소드 호출은 타겟에게 위임 !

고찰

  • 메서드의 개수가 1개가 아닌 무수히 많아진다면 ?
    • 코드의 길이가 길어지고 중복 코드가 발생 !
  • 매번 새로운 클래스를 정의 !
    • 인터페이스 메소드 구현 + 위임 기능

동적 프록시

  • 동적 프록시란 Proxy 객체를 직접 생성하는 것이 아닌 Runtime중 Interface를 구현하는 인스턴스 생성

JDK Dynamic Proxy

  • 프록시 클래스를 직접 구현하지 않아도 된다 !
  • Invocation Handler를 통한 처리

방식

  • Interface를 기반으로 런타임 중 Proxy를 생성하는 방식
    • 리플렉션을 활용한 Proxy 클래스를 제공한다.
  • Java.lang.reflect.Proxy 클래스의 newProxyInstance() 메소드를 이용해 프록시 객체를 생성

리플렉션 ( Reflection )

  • 구체적인 클래스 타입을 알지 못하더라도 클래스 정보에 접근할 수 있는 자바 API

JVM이 실행되면 사용자가 작성한 자바 코드가 컴파일러를 거쳐 바이트 코드로 변환되어 JVM Memory에 저장된다. Reflection API는 이 정보를 활용해 필요한 정보를 가져온다.

클래스 정보

  • Class 클래스
    • 자바의 모든 클래스는 클래스 정보를 가지고 있는 Class 타입 오브젝트를 가지고 있다.
      • 클래스이름.class
      • 오브젝트.getClass()
  • Method
    • Class 오브젝트에서 메소드에 대한 정보를 추출 !
    • Method 타입의 invoke() 메소드 호출 !
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        Class<BTS> mClass = BTS.class; // BTS 클래스의 Class 오브젝트

        Method method = mClass.getMethod("sayHello"); // 메소드 정보 가져오기

        method.invoke(new BTS()); // 메소드 정보를 바탕으로 메소드 호출
    }
}
class BTS
{
    public void sayHello()
    {
        System.out.println("안녕하세요 BTS입니다 !");
    }
}

주의사항

  • 일반적으로 메소드를 호출 시 컴파일 시점에 분석된 클래스를 사용하지만 리플렉션은 런타임에 클래스를 분석
    • Type Check가 컴파일 시점에 불가능하다.
    • 속도가 느리고 Reflection API 자체의 비용이 크다.

JDK Dynamic Proxy

Proxy 생성 방법

Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
  • 클래스 로더 , 타깃 인터페이스 , 타깃 정보가 있는 Handler

동작 방법

Untitled

  • 다이내믹 프록시 → 런타임 시 프록스 팩토리에 의해 만들어지는 동적 오브젝트 (프록시)

    • 다이내믹 프록시는 타깃의 인터페이스와 같은 타입
    • 다이내믹 프록시 객체에 InvocationHandler를 포함시켜 하나의 객체로 반환
      • 부가기능을 InvocationHandler에 작성
  • 내가 만든 프록시하고의 차이 ?

    부가기능에 대한 구현을 프록시 내부에 작성하는 것이 아니라 → InvocationHandler에게 위임 !

  • InvocationHandler

public Object invoke(Object proxy, Method method, Object[] args)
  1. 다이내믹 프록시는 클라이언트의 모든 요청을 리플렉션 정보로 변환
  2. InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘김

Untitled

사용 방법

public class UserHandler implements InvocationHandler {

    Object target;

    UserHandler(Object target)
    {
        this.target = target; // 다이내믹 프록시로부터의 요청을 다시 타깃에게 위임(타깃 오브젝트 주입)
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("say"))
            System.out.println("프록시가 사용자의 say 감지");

        else
            System.out.println("프록시가 사용자의 eat 감지");

				return method.invoke(target,args); // 타깃에게 요청 위임
    }
}

결과

Untitled

CGLIB

  • 클래스의 바이트 코드를 조작하여 Proxy 객체를 생성
    • 인터페이스가 아닌 타깃의 클래스에 대해서도 프록시 생성 가능 (상속을 이용하는 방식)
TeamService teamService = (TeamService) Enhancer.create(
                TeamService.class,
                new TeamInterceptor(new TeamService())
        );

Untitled

공식 문서

Classes in Java are loaded dynamically at runtime. Cglib is using this feature of Java language to make it possible to add new classes to an already running Java program.

  • Hibernate의 지연 로딩
  • Mockito의 mocking method

MethodIntercepter

package com.example.toyjava.module.account.service;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class TeamInterceptor implements MethodInterceptor {

    private Object target;
    public TeamInterceptor(Object target)
    {
        this.target = target; // target을 주입받는다.
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println("CGLIB Proxy 감지"); // 부가 기능
				return methodProxy.invoke(target,objects); // 위임
    }
}

// TeamService : class com.example.toyjava.module.account.service.TeamService$$EnhancerByCGLIB$$582c0b44
  • 메소드가 처음 호출되는 경우 동적으로 타깃의 클래스의 바이트 코드를 조작
  • 이후 호출 시 조작된 바이트 코드를 재사용

Untitled

CGLIB 고찰

  • 구체 클래스를 상속받기 때문에 final 키워드가 사용될 수 없다.
  • 대상 클래스의 기본 생성자 필수
    • 생성자가 2번 호출된다.

서론 -2

JDK 다이나믹 프록시와 CGLIB을 통해 우리는 부가 기능과 핵심 기능의 개체들을 분리할 수 있었다. 또한 위의 방법들은 런타임 중 동적으로 프록시 객체를 생성해주었기 때문에 불필요한 양의 코드 작성 또한 줄일 수 있었다.

  • 그렇다면 우리에게 남은 과제는 이러한 프록시를 ‘어떻게 Spring에 녹여낼 것인가’ 이다.

문제점

  • 한번에 여러 개의 클래스에 공통적인 부가기능 제공 할 수 없다.

    • JDK 다이나믹 프록시 , CGLIB의 핸들러와 인터셉터가 타겟 오브젝트를 프로퍼티로 가지고 있다.
      • 동일한 기능이라도 ? → 타겟이 다르다면 별도의 오브젝트를 또 만들어야 한다.
  • 스프링 관점 !

    Handler를 Bean으로 등록한다 했을 때 타겟의 개수만큼 중복 등록(생성) → 비효율적

    중복을 없애고 모든 타깃에 적용 가능한 싱글톤 빈으로 만들자 !

중복을 없애고 모든 타깃에 적용 가능한 싱글톤 빈으로 만들자 !

ProxyFactoryBean

Untitled

  • JDK Dynamic Proxy와 CGLIB 등 스프링이 사용하는 프록시 방식은 다양
    • 일관된 방법으로 프록시를 만들수 있게 추상 레이어 제공
  • 클라이언트의 요청이 들어오면 인터페이스 유무에 따라
    • 있다면 → JDK Dynamic Proxy
    • 없다면 → CGLIB

ProxyFactoryBean

  • 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
  • 순수하게 프록시를 생성 , 부가 기능은 별도의 빈으로 둘 수 있다.
    • 프록시 추상화 기능 제공

MethodIntercepter

  • 이름은 동일하지만 CGLIB의 MethodIntercepter와는 조금 차이가 있다.
  • MethodIntercepter는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지 함께 제공
    • 타깃 오브젝트에 상관없이 독립적 존재 가능 → 싱글톤 빈으로 등록 가능
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    @Nullable
    Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}

// MethodInvocation (콜백 오브젝트) : proceed() 메소드 호출 시 -> 타깃 오브젝트의 메소드를 내부적 실행

Untitled

예제

TeamAdvice

package com.example.toyjava.module.account.service;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class TeamAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("MethodInterceptor 동작");
        invocation.proceed(); // 타깃 오브젝트 메소드 실행
        
				return null; // proceed()의 결과값을 반환하는 것도 가능
    }
}

프록시 팩토리 빈 사용

public class ProxyTest {
    @Test
    public void test() {

        ProxyFactoryBean pf = new ProxyFactoryBean();
        pf.setTarget(new TeamService()); // 타켓 저장
        pf.addAdvice(new TeamAdvice()); // 부가 기능 저장 (별도의 싱글톤 빈으로 관리 가능)

        TeamService ts = (TeamService) pf.getObject(); // 프록시 객체 반환
        ts.testing();
     }
}
  • 프록시 팩토리 빈 !

    부가 기능을 담당하는 핸들러에서 타깃의 정보를 가지고 있지 않아도 되었다. → 타깃에 구애받지 않고 적용 가능

마지막 욕심

  • ProxyFactoryBean을 사용 시 타겟과 부가기능의 적용에 유연성은 확대했지만 결국 개발자가 계속해서 생성해야 한다는 것이다.
    • 불필요한 중복 작업이 방대 !
  • 타깃 빈의 목록을 제공 → 스프링이 자동으로 타깃 빈에 대한 프록시를 만들어줘 !

자동 프록시 생성기 (빈 후처리기)

  • 빈 후처리기의 사용

    • 스프링 빈 오브젝트가 생성되고 나서 → 빈 오브젝트를 다시 가공할 수 있다 !
      • 빈 오브젝트의 프로퍼티를 강제 수정
        • 아예 빈 자체를 바꿔치기 가능
      • 별도의 초기화 작업 수행
  • 빈 후처리기 사용 !

    스프링이 생성한 빈 오브젝트의 일부를 프록시로 포장 → 프록시를 빈으로 대신 등록하자 !

  • AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈으로 자동 등록됨 !

동작 과정

  1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성
  2. 전달: 빈 후처리기에 생성된 객체를 전달
  3. 모든 Advisor 빈 조회: 자동 프록시 생성기가 스프링 컨테이너에서 모든 Advisor을 조회
  4. 프록시 적용 대상 체크Advisor내에 있는 Pointcut을 이용해 모든 클래스와 메서드를 매칭 , 조건이 하나라도 만족하면 프록시 적용 대상
  5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 Advisor 연결
    • 컨테이너에 프록시를 전달 (바꿔치기)
      • 컨테니어는 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록 , 사용

Spring Bean VS CGLIB

  • 기본적으로 Bean 객체를 프록시로 바꿔치기 하는 과정에서 비용이 발생
    • Spring Context에서 관리되는 빈이 모두 프록시 객체는 아니다 !

스프링 컨텍스트 관점

Untitled

참고 서적

  • 토비의 스프링 VOL 1 (AOP , 프록시)

참고 블로그

https://www.youtube.com/watch?v=MFckVKrJLRQ&t=922s

https://www.baeldung.com/cglib

https://woooongs.tistory.com/99

https://velog.io/@hyun6ik/프록시-기술과-한계-CGLIB

https://velog.io/@gmtmoney2357/스프링-부트-빈-후처리기와-프록시-자동-생성기

profile
Young , Wild , Free

0개의 댓글