스프링 AOP

zunzero·2022년 8월 11일
0

스프링, JPA

목록 보기
5/23

스프링 aop에 대해 아주아주 간략하게 포스팅하는 글

프록시

이전 글에서도 언급했지만, 클라이언트와 서버를 컴퓨터 개념이 아닌 요청을 주고 받는 역할로 생각해보자.
지금까지 나 혹은 우리는 주로 클라이언트가 서버에 직접 요청을 보내고 직접 요청을 처리하는 것에 익숙하다.
클라이언트와 서버의 통신 사이에 proxy 라는 대리자를 놓게 되면 어떻게 될까?
대리자에게 자율성을 부여하게 되면, 대리자는 클라이언트가 서버에 부탁한 일 이외에도 자율적인 행위를 할 수 있다.

1. 접근 제어, 캐싱
2. 부가 기능 추가
3. 프록시 체인

위의 세가지에 대한 예시를 들어보자.
1. 접근 제어, 캐싱
엄마에게 라면을 사달라고 부탁했는데(불효자ㅋㅋ), 엄마는 그 라면이 집에 있다고 한다.
그렇다면 기대한 것보다 더 빨리 우리는 라면을 먹을 수 있다.
2. 부가 기능 추가
아빠한테 자동차 주유를 부탁했는데(불효자 진짜 ㅋㅋ;), 아빠가 세차까지 해서 오셨다.
내 부탁 이외에 세차라는 부가 기능을 수행하게 되었다.
3. 프록시 체인
내 후임에게 아이스크림 좀 사오라 했는데, 후임이 또 다른 후임에게 아이스크림을 사오라고 할 수 있다.
나는 내 대리자인 내 후임에게 요청을 했고, 그 이후의 과정은 모른 채 아이스크림을 받기만 하면 된다.

대체 가능

그렇다고 해서 아무나, 아무 객체나 프록시가 될 수 있는 것은 아니다.
객체에서 프록시가 되려면, 클라이언트는 인터페이스에 요청을 보내고 그 이후의 구현 과정은 몰라야 한다.
즉 클라이언트는 해당 요청을 프록시에 보낸 것인지 서버에 보낸 것인지 알 수 없어야 한다.
따라서 서버와 프록시는 같은 인터페이스를 사용해야 한다.런타임(애플리케이션 실행 시점) 객체 의존 관계는 다음과 같다.
런타임에 클라이언트 객체에 DI를 사용해서 Client -> Server 에서 Client -> Proxy로 객체 의존 관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다.

프록시의 주요 기능

  • 접근 제어
    - 권한에 따른 접근 차단
    • 캐싱
    • 지연 로딩
  • 부가 기능 추가
    - 원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.

프록시를 이용한 디자인 GOF 디자인 패턴으로 프록시 패턴데코레이터 패턴이 있는데 디자인 패턴에 대해선 나중에 따로 정리하겠다.

동적 프록시 기술

동적 프록시 기술은 프록시 객체를 동적으로 런타임에 개발자 대신 만들어주는 기술이다.
그리고 이렇게 만들어진 동적 프록시에 원하는 실행 로직을 지정할 수 있다.

JDK 동적 프록시

자바 언어가 기본으로 제공하는 JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어주기 때문에 인터페이스가 필수이다.
InvokationHandler 인터페이스를 구현해서 프록시에서 실행할 로직을 기술하고, 실제 객체인 target을 invoke하여 호출하게 되는데, 이에 대한 자세한 동작 과정에 대한 설명은 생략하겠다.

CGLIB (Code Generator Library)

CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
CGLIB는 원래 외부 라이브러리이지만, 스프링 프레임워크가 스프링 내부 소스코드에 포함했다.
MethodInterceptor 인터페이스를 구현해서 프록시에 실행할 로직을 기술하고, 실제 객체인 target을 invoke하여 호출하게 되는데, 이에 대한 자세한 동작 과정 또한 자세히 설명하지 않겠다.

스프링이 지원하는 프록시

JDK 동적 프록시와 CGLIB 동적 프록시를 보면, 인터페이스 여부에 따라 적용 가능 여부가 달라진다.
개발자 입장에선 귀찮다...
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 ProxyFactory라는 기능을 제공한다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다. 이에 대한 설정은 변경 가능하다.
이 때 Advice라는 새로운 개념이 등장한다.
JDK 동적 프록시가 제공하는 InvokationHandler나 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 개발해야 하는 개발자의 수고를 덜어주기 위해, 스프링은 두 인터페이스 모두 Advice를 호출하게 한다.
개발자는 Advice만 신경 써서, 프록시에 적용한느 부가 기능 로직을 작성하면 된다.
Advice는 MethodInterceptor 인터페이스를 구현해서 만들 수 있는데, 이 때의 MethodInterceptor는 CGLIB가 제공하는 것과는 다른 것이다.
(org.aopalliance.intercpet.MethodInterceptor)

프록시 팩토리

  • 대상에 인터페이스가 있는 경우
    JDK 동적 프록시, 인터페이스 기반 프록시
  • 대상에 인터페이스가 없는 경우
    CGLIB, 구체 클래스 기반 프록시
  • proxyTargetClass=True 옵션
    CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관 없음.
    스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 ㅎ사용한다.

포인트컷, 어드바이스, 어드바이저

  • 포인트컷 Poincut
    어디에 부가 기능을 적용할 지, 어디에 부가 기능을 적용하지 않을 지 판단하는 필터링 로직이다.
    주로 클래스와 메서드 이름으로 필터링한다.
  • 어드바이스 Advice
    프록시가 호출하는 부가 기능이다.
    프록시 로직이라고 생각하면 편리하다.
  • 어드바이저 Advisor
    하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다.

역할과 책임의 구분을 통해 포인트컷과 어드바이스를 나누었다.

어드바이저는 다음과 같이 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
만약 여러 어드바이저를 하나의 target에 적용하려면 어떻게 해야할까?
여러 프록시를 만들어서 프록시 체인을 만들고, 각 프록시가 어드바이저를 조회하여 포인트컷을 확인해 어드바이스를 호출하게 하면 될까?
물론 불가능한 방법은 아니지만, 적용해야할 어드바이저가 수백개라면 프록시팩토리에서 프록시를 만드는 코드를 수백번 작성해야 한다.
스프링은 이 문제를 해결하기 위해 하나의 프록시에 여러 어드바이저를 적용할 수 있도록 만들었다.

ProxyFactory factory = new ProxyFactory(target);
factory.addAdvisor(advisor2);
factory.addAdvisor(advisor1);
Object proxy = factory.getProxy();

위 소스코드처럼 프록시 팩토리에 원하는 만큼 addAdvisor() 메서드를 통해 어드바이저를 등록하면 된다.

빈 후처리기

@Bean이나 컴포넌트 스캔으로 스프링 빈을 등록하면 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다.
이후, 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용할 수 있다.

빈 후처리기 - BeanPostProcessor

스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 무언가를 조작하고 싶다면 빈 후처리기를 사용하면 된다.
빈 후처리기는 말 그대로 빈 생성 후 무언가를 처리하는 용도로 사용된다.
이 때, 객체를 조작할 수도 있고, 완전 다른 객체로 바꿔칠 수도 있다. 위 그림은 빈 후처리기를 이용해 실제 객체가 아닌 프록시 객체를 빈 저장소에 저장한 그림이다.
따라서 스프링 빈 저장소에 프록시를 적용하고 싶은 객체에 대해서는 위의 과정을 통해 프록시 객체를 스프링 빈으로 등록할 수 있다.

하지만 빈으로 등록될 모든 클래스에 대해 프록시를 사용하는 것은 올바르지 않다.
그래서 스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공하는데, 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
따라서 포인트컷은 다음 두 가지의 경우에 사용된다.

1. 빈 후처리기 - 자동 프록시 생성
    프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다.
2. 프록시 내부
	프록시의 어떤 메서드가 호출 되었을 때, 어드바이스를 적용할 지 판단한다.
    

자동 프록시 생성기 - AutoProxyCreator

자동으로 프록시를 생성해주는 빈 후처리기이다.
해당 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
Advisor 안에는 Pointcut과 Advice가 이미 모두 포함되어 있어, Advisor만 알고 있으면 Pointcut을 이용해 어떤 스프링 빈에 프록시를 적용해서 Advice로 부가 기능을 적용할 지 알 수 있다.

1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.
4. 프록시 적용 대상 체크: 
앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 
클래스 정보와 메서드 정보를 포인트 컷에 하나하나 모두 매칭하여, 조건이 하나라도 만족하면 프록시 적용 대상이 된다.
예를 들어 10개의 메서드 중 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
5. 프록시 생성 & 빈 등록: 
프록시 적용 대상이면 프록시를 생성 후 반환해서 프록시를 스프링 빈으로 등록한다.
만약 적용 대상이 아니라면 원본 객체를 반환해서 스프링 빈으로 등록한다.
@Configuration
public class AutoProxyConfig {
	@Bean
    public Advisor advisor1(Target target) {
    	NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        Advice advice = new Advice(target);
        return new DefaultPointcutAdvisor(pointcut, advice);
	}
}    

또한, 앞서 이야기한 것과 마찬가지로 하나의 프록시에 여러 Advisor를 적용할 수 있다.

@Aspect AOP

Advisor를 하나하나 만드는 대신 @Aspect 애노테이션이 붙은 클래스에 포인트컷과 어드바이스를 하나씩 작성해서 Advisor를 여러 개 만들 수 있다.

@Aspect
public class TestAspect {
	private final Target target;
    
    @Around("AspectJ 표현식을 사용한 Pointcut")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    	// 부가 기능 로직
        
        // target 로직 호출
        Object result = joinPoint.proceed();
        
        return result;
    }
}    

@Aspect는 org.aspectj에서 제공하는 애노테이션으로 애노테이션 기반 프록시를 적용할 때 필요하다.
@Around 애노테이션이 붙은 메서드는 어드바이스가 되어, AspectJ 표현식을 활용한 Pointcut과 짝지어 하나의 어드바이저가 된다.
물론 해당 클래스도 스프링 빈으로 등록해주어야 한다. @Component를 사용하던 @Bean을 사용하던.

앞서 자동 프록시 생성기를 학습할 때, 자동 프록시 생성기는 Advisor를 자동으로 찾아와서 필요한 곳에 프록시를 생성하고 적용해준닥고 했다.
자동 프록시 생성기는 여기에 추가로 @Aspect를 찾아서 이것을 Advisor로 만들어주는 역할도 한다.

자동 프록시 생성기는 영어로 AnnotationAwareAspectJAutoProxyCreator이다.

따라서 자동 프록시 생성기는 2가지의 일을 하는 것이다.

1. @Aspect 애노테이션을 보고 어드바이저로 변환해서 저장한다.
2. 어드바이저를 기반으로 프록시를 생성한다.

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

1. 실행: 스프링 애플리케이션 로딩 시점에 자동 프록시 생성기를 호출한다.
2. 모든 @Aspect 빈 조회: 
자동 프록시 생성기는 스프링 컨테이너에서 @Aspect 애노테이션이 붙은 스프링 빈을 모두 조회한다.
3. 어드바이저 생성: @Aspect 어드바이저 빌더를 통해 @Aspect 애노테이션 정보를 기반으로 어드바이저를 생성한다.
4. @Aspect 기반 어드바이저 저장: 생성한 어드바이저를 @Aspect 어드바이저 빌더 내부에 저장한다.

@Aspect 어드바이저 빌더

BeanFactoryAspectJAdvisorBuilder 클래스로, @Aspect의 정보를 기반하여 포인트컷&어드바이스&어드바이저를 생성하고 보관하는 것을 담당한다.
@Aspect의 정보를 기반으로 어드바이저를 만들고, @Aspect 어드바이저 빌더 내부 저장소에 캐시한다. (캐시에 어드바이저가 이미 만들어져 있는 경우에는 캐시에 저장된 어드바이저를 반환한다.)

스프링 AOP

횡단 관심사

내가 공부한 예제에서는 애플리케이션 전반에 로그를 남기는 기능을 부가 기능으로 하여 코드를 작성하는 과정을 공부했다.
로그를 남기는 기능은 특정 기능 하나에 관심이 있는 기능이 아니라, 애플리케이션의 여러 기능들 사이에 걸쳐서 들어가는 관심사이다.
이것을 바로 횡단 관심사 cross-cutting concerns라고 한다.
위 사진은 횡단 관심사를 잘 표현한 그림이라 생각해서 다른 블로그에서 캡쳐해왔다. (문제가 된다면 삭제를... 기꺼이...) (출처: https://engkimbs.tistory.com/746)

애플리케이션 로직은 크게 핵심 기능부가 기능으로 나눌 수 있다.

핵심기능: 해당 객체가 제공하는 고유의 기능
부가 기능: 핵심 기능을 보조하기 위해 제공되는 기능으로 단독으로 사용되지 않고, 핵심기능과 함께 사용됨.

대게 부가 기능은 아래 그림처럼 여러 클래스에 걸쳐서 함께 사용되기 때문에 이를 횡단 관심사라고 하는 것이다. 부가 기능을 적용하기 위해 부가 기능이 필요한 객체마다 부가 기능 관련 로직을 작성하면 너무 번거롭다.
아주 많은 반복이 필요하고, 중복 코드를 별도의 유틸리티 클래스나 메서드로 뽑아내기도 번거롭다.
특히 수정이 필요하다면... 최악이다.

애스펙트

애스펙트는 부가 기능과, 해당 부가 기능을 어디에 적용할 지 정의한 하나의 모듈이다.
즉, 우리가 이전에 알아본 @Aspect가 바로 그것이다.
AOP는 애스팩트를 사용한 프로그래밍 방식이다.

AOP 관점 지향 프로그래밍 Aspect-Oriented Programming

AOP는 OOP를 대체하기 위한 것이 아닌, 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.
AOP를 사용하면 핵심 기능과 부가 기능이 코드상 완전히 분리되어서 관리된다.
AOP를 사용해서 부가 기능 로직을 실제 로직에 추가하는 방법은 크게 3가지 방법이 있다.

  1. 컴파일 시점에 AspectJ 컴파일러를 활용하기
  2. 클래스 로딩 시점에 AspectJ 클래스 로더 조작기를 이용해 바이트 코드를 조작하기
  3. 런타임 시점에 위빙하기 (Weaving: 옷감을 짜다, 애스팩트와 실제 코드를 연결해서 붙이는 것)

1번과 2번의 방법은 복잡하고 단점이 많아, 우리는 3번만 공부한다. 그리고 3번이 우리가 지금까지 공부해 온 방식이다.
또한 1번과 2번은 AspectJ를 직접 사용해야 하지만 3번은 프록시를 이용해 부가기능 로직을 적용하기 때문에 AspectJ를 직접 사용하지 않아도 된다.
물론 AspectJ를 직접 사용하지 않기 때문에 AOP 기능 일부에 제약이 있으나 딱히 상관 없다.
우리는 스프링만 있으면 AOP를 편리하게 사용할 수 있다.

AOP 적용 위치

사실 AOP는 지금까지 우리가 학습한 메서드 실행 위치 뿐만 아니라 다른 곳(생성자, 필드값 접근, static 메서드 접근)에도 적용할 수 있다.
AOP를 적용할 수 있는 지점을 조인 포인트 (Join Point)라고 하는데, AspectJ를 이용해서 컴파일 시점과 클래스 로딩 시점에 적용하는 AOP는 바이트 코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다.

프록시 방식을 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다.
프록시는 메서드 오버라이딩 개념으로 동작하기 때문에, 생성자나 static 메서드, 필드값 접근에는 프록시 개념이 적용될 수 없고, 스프링 AOP의 조인 포인트는 메서드 실행 시점으로 제한된다.

AOP 용어 정리

  • 조인 포인트
    - 어드바이스가 적용될 수 있는 위치로 메서드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실행 중 지점을 말한다.
    • 조인 포인트는 추상적인 개념으로, AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
    • 스프링 AOP는 프록시 방식을 사용하므로 조인포인트는 항상 메서드 실행 지점으로 제한된다.
  • 포인트컷
    - 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
    • 주로 AspectJ 표현식을 사용해서 지정
    • 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별 가능
  • 타겟
    - 어드바이스를 받는 객체, 포인트컷으로 결정
  • 어드바이스
    - 부가 기능
    • 특정 조인포인트에서 Aspect에 의해 취해지는 조치
    • Around, Before, After과 같은 다양한 종류의 어드바이스가 있음
  • 애스펙트
    - 어드바이스 + 포인트컷을 모듈화 한 것
    • @Aspect 애노테이션을 생각하면 됨
    • 여러 어드바이스와 포인트컷이 함께 존재한다.
  • 어드바이저
    - 하나의 어드바이스와 하나의 포인트컷으로 구성된다.
    • 스프링 AOP에서만 사용되는 특별한 용어이다.
  • 위빙
    - 포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
    • 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있음
    • AOP 적용을 위해 애스팩트를 객체에 연결한 상태
      • 컴파일 타임
        • 클래스 로딩 시점
        • 런타임 => 스프링 AOP의 동작 방식 (프록시 방식)
  • AOP 프록시
    - AOP 기능을 구현하기 위해 만든 프록시 객체로, 스프링에서는 JDK 동적 프록시 또는 CGLIB 동적 프록시이다.

어드바이스 종류

@Around 외에도 여러 종류의 어드바이스가 있으나, 간단히만 알아보자

  • @Around: 메서드 호출 전후에 수행
  • @Before: 조인 포인트 실행 이전에 실행 (조인포인트는 알아서 시행됨.)
  • @AfterReturning: 조인 포인트가 정상 완료 후 실행 (조인포인트는 알아서 시행됨.)
  • @AfterThrowing: 메서드가 예외를 던지는 경우 실행 (조인포인트는 알아서 시행됨.)
  • @After: 조인포인트가 정상 또는 예외에 관계 없이 실행 (조인포인트는 알아서 시행됨.)

위에서 알 수 있듯이, @Around를 제외한 어드바이스는 조인포인트가 알아서 시행되기 때문에, @Around는 ProceedingJoinPoint를 사용해서 proceed() 메서드를 통해 타겟의 로직을 실행해야 한다.

이에 대한 자세한 내용은 필요할 때 알아서 보자. 어렵지 않으니까! 그리고 지금 졸리다.ㅎ

따지고 보면, @Around만 있으면 되는데 왜 이렇게 제약을 두는가 싶다.

좋은 설계는 제약이 있는 것이다.

제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 하는 것이다.
또한 제약 덕분에 역할이 명확해진다.
@Before라는 애노테이션을 보는 순간 '아 ~ 타겟 실행 전에 한정해서 해당 로직을 실행하고 싶은 거구나' 하고 말이다.

정리

우리는 우선 핵심 로직에 집중한다.
그 후, 횡단 관심사에 해당하는 로직은 따로 @Aspect 애노테이션을 이용해서 작성한다.
@Aspect가 붙은 클래스 안에 포인컷과 어드바이스로 이루어진 어드바이저를 작성하고, 이 때 어드바이스에는 부가 기능 로직을 작성한다.
포인트컷에 해당하는 메서드를 가진 클래스는 모두 해당 객체 대신 프록시가 스프링 빈으로 등록된다. 물론 이 때, 프록시는 객체마다 하나씩만 생성되고, 프록시에는 여러 어드바이저가 적용될 수 있다.

두 개 이상의 어드바이스를 적용하는 경우, 순서를 지정할 수 있다.
기본적으로 어드바이스는 순서를 보장하지 않는다.
따라서 순서를 지정하고자 한다면 @Aspect 적용 단위로 @Order(1), @Order(2) 등을 적용해야 한다.

public class Aspect {
	@Aspect
    @Order(2)
    public static class SecondAspect {
    	@Around("pointcut 표현식")
        public Object advisor2(ProceedingJoinPoint joinPoint) throws Throwable {
        	// 부가 기능 로직
            
            // target 호출
            Object result = joinPoint.proceed();
            
            return result;
	}
}    

순서가 상관 없는 경우라면 하나의 애스펙트 안에 어드바이저를 두개 만들었겠지만, 순서가 중요한 경우에는 @Aspect를 위한 static class를 만들고 그 안에 어드바이저를 하나 작성할 수 있다.

참고

인프런 김영한 강사님의 스프링 핵심원리 - 고급편 강의를 참고하여 포스팅하였습니다.

profile
나만 읽을 수 있는 블로그

0개의 댓글