Spring

[Spring] 프록시와 AOP에 대한 공부

하고있자 2024. 7. 6. 22:03

프록시

대리자라는 의미로 간접적으로 대신하는 것을 의미한다.

위 그림처럼 client가 server를 직접 호출할 수 있지만 client와 server사이에 대리자 즉 proxy를 두어 간접 호출할 수 있다. 아래처럼 간접 호출한 것을 대리자 proxy라고 한다.

 

위에 간접호출 부분에서 여러 프록시를 두어 호출이 가능하다. 이걸 프록시 체인이라고 한다.

 

프록시에서 중요한 점은 호출을 요청하는 객체가 서버를 호출한것인지, 프록시를 호출한것인지 몰라야한다. 어떤 것이 호출되는지 모르는것 즉 인터페이스를 사용해야한다. 또한 서버 객체를 프록시 객체로 변경해도 client의 코드를 변경해서는 안된다.

 

객체와 객체 사이에 프록시 객체가 중간에 있으면서 접근 제어와 부가 기능 추가를 수행 가능하다.

 

프록시는 2가지가 있다.

인터페이스 기반 프록시

위 그림처럼 인터페이스 기반 프록시는 server인터페이스를 구현한다. 그래서 인터페이스를 꼭 필요로 한다. 그래서 인터페이스 기반 프록시를 만들기 위해서는 인터페이스를 추가로 만들어야 한다.

 

클래스 기반 프록시

클래스 기반 프록시는 Target으로 하는 클래스를 상속한다. 그리고 부모 클래스의 생성자를 호출해야 한다. 클래스에 final 키워드가 붙으면 상속이 불가능하다. 또한 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. 그래서 클래스 기반 프록시를 사용하기 위해 final를 클래스와 메서드에 사용하지 않는다.

 

두 가지 방식을 보면 클래스 기반 프록시는 제한이 많다. 인터페이스 방식이 더 좋아 보인다. 실제로 역할과 구현을 구분하기에 더 좋다. 하지만 모든 클래스에 인터페이스를 적용할 수는 없다. 그래서 클래스 기반 프록시를 사용한다.

 

프록시를 위 방식으로 수정해야 할 프록시가 100개라면 100을 전부 수정해야 한다. 이를 해결하기 위한 것이 동적 프록시 이다.

 

동적 프록시

개발자가 직접 프록시 클래스를 만들지 않고 프록시 객체를 동적으로 런타임에 개발자 대신에 만드는 것이다.

JDK 동적 프록시

인터페이스를 기반으로 프록시를 동적으로 만들어준다. 인터페이스가 필수이다.

JDK 동적 프록시를 사용하려면  InvocationHandler인터페이스를 구현해야 한다.

public interface InvocationHandler {
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

InvocationHandler 인터페이스에서 각 파라미터가 의미하는 것은 다음과 같다.

Object proxy  : 프록시 자신

Method method : 호출한 메서드

Object[] args  : 메서드를 호출할 때 전달한 인수

위 그림처럼 JDK 동적 프록시 기술 덕분에 프록시가 100개라도 한 번만 개발하여 공통으로 적용이 가능하다. 하지만 JDK 동적 프록시는 인터페이스가 무조건 있어야 한다.

 

그래서 이를 해결하기 위해 사용할 수 있는 동적 프록시가 있다.

CGLIB

바이트코드를 조작해서 동적으로 클래스를 생성하는 라이브러리이다. 인터페이스가 없어서도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다. 원래를 외부 라이브러리이나, 스프링프레임워크가 내부 소스 코드에 포함하였다. CGLIB는 MethodInterceptor를 제공한다. 실제로는 ProxyFactory가 사용하기 편하게 도와준다. 

public interface MethodInterceptor extends Callback {
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

Object obj : CGLIB가 적용된 객체

Method method : 호출된 메서드 

Object[] args : 메서드를 호출하면서 전달된 인수

MethodProxy proxy : 메서드 호출에 사용

CGLIB 제약

  • 부모 클래스의 생성자를 체크해야 한다. > CGLIB는 자식 클래스를 동적으로 생성하기에 기본 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다.> CGLIB에서는 예외가 발생한다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. CGLIB에서는 프록시 로직이 동작하지 않는다.

 

JDK 동적 프록시와 CGLIB는 InvocationHandler와 MethodInterceptor를 각각 만들어 프록시를 만든다. 우리는 프록시를 구현할때 매번 인터페이스인지 클래스인지 확인하고 어떤것을 구현해야하는지 생각해야한다. 이것을 줄이는 방법은 없을까?

프록시 팩토리(ProxyFactory)

스프링에서 동적 프록시를 통합해서 편하게 만들어주는 기능이다.

 

 

스프링 팩토리는 위 그림처럼 의존한다. InvocationHandler와 MethodInterceptor를 구현하는 문제를 해결하기 위해서 스프링에서는 Advice라는 개념을 도입했다. 간단한 게 말하면 개발자는 Advice만 만들면 된다. 그러면 프록시 팩토리에서 InvocationHandler, MethodInterceptor를 내부에서 사용한다.

 

또한 스프링은 Pointcut이라는 개념을 도입하여 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되도록 만들었다.

위 그림처럼 프록시는 하나의 프록시만으로 여러 어드바이저가 적용이 가능하게 개발되도록 만들었다.

빈 후처리기 (BeanPostProcessor)

일반적인 스프링 빈 등록

위 그림처럼 스프링 빈 등록은

1. 스프링에서 객체를 생성

2. 객체를 스프링 빈 저장소에 저장 

빈 후처리기 과정

빈 후처리기를 사용하면

1. 스프링에서 객체를 생성

2. 생성된 객체를 빈 후처리기에 전달

3. 전달받은 빈 후처리기는 객체를 조작하거나 다른 객체로 변경이 가능

4. 빈을 스프링 빈 저장소에 전송하여 빈을 등록한다.

위 그림처럼 빈 후처리기를 통해서 프록시 적용이 가능하다. 

AutoProxyCreator (자동 프록시 생성기)

자동 프록시 생성기를 사용하면

1. 스프링에서 제공하는 스프링 빈 대상이 되는 객체를 생성 

2. 생성한 객체를 빈 후처리기에 전달

3. 빈 후처리기는 스프링 컨테이너에 모든 advisor 빈을 조회

4. 앞서 조회한 advisor에 포함되어 있는 포인트컷을 사용해서 객체가 프록시 대상인지 아닌지 확인

5. 프록시 적용대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 등록

6. 반환된 객체는 스프링 빈으로 등록

 

하나의 프록시로 여러 advisor들을 포함하는 것이 가능하다. 그래서 하나의 객체에 여러 advisor의 pointcut이 포함되어도 하나의 프록시만 생성한다. 

@Aspect
public class testAspect{
    @Around("execution(* example.aop.test.*(..))")
    public Object test(ProceedingJoinPoint joinPoint) throws Throwable{
    	.....
    }
    
    @Around("allOrder()")
    public Object testAll(ProceedingJoinPoint joinPoint) throws Throwable{
    	.....
    }
}

위 코드에서 "execution(* example.aop.test.*(..))")부분이 Pointcut이다. 메서드 부분이 부가기능인 Advice이다. 위 코드에서 allOrder()은 모든 기능을 대상으로 하는 Pointcut이다.

 

문법에 대해서 간단하게만 설명하겠다. 먼저 excution은 포인트컷 지시자로 포인트컷로 시작한다. excution은 메서드 실행 조인 포인트를 매칭한다. within, args, this 등 여러 포이트컷 지시자가 있지만 간단하게 설명하고 넘어갈 것이기에 더 자세한 내용은 다른 블로그를 참조해줬으면 한다. 

포인트컷의 문법은 execution(접근제어자 반환타입 선언타입 메서드이름(파라미터) 예외) 순이다. 여기서 굵은 글씨는 생략이 가능하다.  * example. aop. test..*를 예시로 한번 보자. 처음 부분 *로 어떤 반환타입이든 대상이 가능하다. 접근제어자는 와 선언 타입, 예외는 생략되었다. 다음으로 example.aop.test.*(..) 부분을 보면 test패키지에 모든 메서드로 지정이 가능하다는 의미이다. 마지막에 파라미터 부부인(..)은 파라미터의 타입과 수가 상관이 없다는 의미이다.

 

위 그림은 @Aspect를 어드바이저로 변환해서 저장하는 과정이다. 

1. 스프링 애플링케이션 로딩 시점에 자동 프록시 생성기를 호출

2. 자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 어노테이션이 붙은 스프링 빈을 모두 조회

3. @Aspect 어드바이저 빌더를 통해 @Aspect 어노테이션 정보를 기반으로 Advisor 생성

4. 생성한 Advisor를 @Aspect 어드바이저 빌더 내부에 저장

 

위 그림은 어드바이저를 기반으로 프록시를 생성하는 과정이다.
1. 스프링이 스프링 빈 대상이 되는 객체를 생성

2. 생성된 객체를 빈 후 처리기에 전달

3. 스프링 컨테이너에서 Advisor 빈을 모두 조회

4. @Aspect 어드바이저 빌더 내부에 저장된 Advisor을 조회

5. Adviosr에 포함되어 있는 포인트컷을 사용해서 객체가 조건에 하나라도 만족하면 프록시 적용 대상이 된다. 

6. 프록시를 빈으로 등록한다. 프록시가 아니면 원본 데이터를 저장한다. 스프링 빈으로 등록

AOP

부가 기능을 핵심 기능에서 분리하고 관리한다. 애플리케이션을 바라보는 관점을 하나의 기능에서 횡단 관심사 관점으로 달리 보는 것이다.  관점 지향 프로그래밍이라고도 한다.

 

핵심기능 - 해당 객체가 제공하는 고유의 기능

부가 기능 - 핵심 기능을 보조하기 위해 제공하는 기능

AOP 적용 방식

aop의 적용 방식은 크게 3가지가 있다. 

- 컴파일 시점 

- 클래스 로딩 시점

- 런타임 시점

 

컴파일 시점과 클래스 로딩 시점은 AspectJ 프레임워크를 직접 사용한다. 하지만 실제로 AspectJ를 사용하기에는 어렵다. 그래서 스프링 AOP는 주로 런타임 시점에 적용하는 방식을 사용한다. 

런타임 시점은 실제 대상 코드는 그대로 유지한다. 대신에 프록시를 통해 부가 기능이 적용된다. 따라서 항상 프록시를 통해야 부가 기능을 사용할 수 있다. 스프링 AOP는 이 방식을 사용한다. 스프링 AOP는 런타임 시점에 적용한다. 그래서 실제로 AOP의 적용 위치는 메서드 실행으로 제한된다.  

 

 

 

 

출처

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

https://giron.tistory.com/129