실전 AOP (feat. Slack 연동)

YEON·2023년 11월 8일
0

My Spring

목록 보기
4/4

AOP

Aspect Oriented Programming

'관점 지향 프로그래밍'이란 OOP로 독립적으로 분리하기 어려운 부가 기능모듈화하는 방식이다.
전체 로직 사이에서 핵심적인 관점(비지니스 로직), 부가적인 관점(Aspect)으로 나누어서 보고 그 관점을 기준으로 각각 모듈화(분리)함으로써 OOP를 보완하는 역할을 한다.

  • 핵심 관심사 → Object 기반으로 모듈화 (OOP)
  • 횡단 관심사 → Aspect 기반으로 모듈화 (AOP)

AOP는 관점 지향 프로그래밍이란 개념 그대로, Spring 프레임워크뿐만 아니라 다른 프로그래밍 환경에서도 사용될 수 있다.

Aspect (부가 기능 모듈)
ex. 핵심 비즈니스 로직 사이에서 반복되는 트랜잭션, 로깅같은 부가 기능
Aspect 의 구성은 부가될 기능을 정의한 Advice와, 해당 Advice를 어디에 적용할 지를 결정하는 Pointcut 정보를 가지고 있다.


주요 개념

Aspect

  • 위에서 설명한 흩어진 관심사를 모듈화 한 것. (주로 부가기능을 모듈화한다)
  • 여러 Advice + PointCut을 모듈화 한 것
  • Java에서는 AspectJ 라는 확장 기능을 통해 Aspect를 구현할 수 있다
  • Spring AOP에선 XML(스키마 기반), @AspectJ(어노테이션 기반) 두 가지 방식을 통해 Aspect를 구현할 수 있다.

Advice

  • 프록시가 호출하는 부가 기능. 실질적인 부가 기능을 담은 구현체
  • JointPoint 에 적용할 횡단 코드 의미
  • @Before, @After, @Around(전+후) 등의 어드바이스가 존재
  • Spring AOP 에선 하나의 PointCut+Advice 를 갖고 있는 것을 합쳐 Advisor 라고 부른다.

Target

  • Aspect가 가지고있는 Advice를 적용할 대상 (클래스, 메서드, …)

PointCut

  • 어떤 포인트(Point)에 부가 기능을 적용할지 안할지 구분(cut)하는 필터링 로직
  • Advice는 여러 JoinPoint중에서 Pointcut의 표현식과 일치하는 JoinPoint에서 실행된다.
  • 단, Spring AOP의 경우에는 Proxy 방식(메소드 interceptor 기반)이므로 항상 메소드 실행 시점만 가능하다

JoinPoint

  • Advice가 적용될 위치, 끼어들 수 있는 지점.
  • 일반적으로 AspectJ는 메서드 실행, 생성자 호출, 필드 값 접근 등 다양한 실행 시점에 가능하다.
  • 단, Spring AOP의 경우에는 Proxy 방식(메소드 interceptor 기반)이므로 항상 메소드 실행 시점으로 제한된다.

Weaving

  • 적용한다의 의미.
  • PointCut으로 결정된 target의 JoinPoint에 Advice를 적용하는 것
  • ex) 컴파일 타임, 로드 타임, 런 타임

AOP proxy

  • Aspect를 대신 수행하기 위해 AOP 프레임워크에 의해 생성된 객체(Object)
  • 이를 통해 횡단 관심 객체와 핵심 관심 객체의 느슨한 결합 구조를 만들고, 부가 기능 로직 구현에 대한 유연성 및 확장성 제공.
  • Spring AOP에선 JDK Dynamic Proxy, CGLIB Proxy 두가지 방식으로 proxy를 생성


적용 시점

그렇다면 AOP는 어느 시점에서 부가 기능이 실제 로직에 추가 될 수 있을까?
크게 3가지 방법이 있다.

  • 컴파일 시점
    AspectJ가 제공하는 컴파일러로 컴파일(.java.class)을 진행하며, 실제 대상 코드에 부가 기능 호출 코드가 포함된다. 런타임에는 부하가 없지만, 단, Aspect가 제공하는 컴파일러를 적용해야한다는 불편함이 있다.

  • 로드 타임
    .class 파일을 JVM의 클래스 로더에 보관하려는 시점에 조작을 한 후 후 JVM에 올리는 것이다. 컴파일 시점과 동일하게 실제 대상 코드에 부가 기능 코드가 붙게 된다. 단, 이를 위해 Java Agent 설정, Load Time Weaver 등 별도의 설정이 필요하다.

  • 런 타임 ( ⇢ Spring 방식)
    이미 실행되고 있는 시점에서 Spring 컨테이너, Proxy Bean 개념을 적용하여 실제 대상 코드는 그대로 유지되고 항상 Proxy를 통해서 부가 기능이 적용된다. 때문에 실행 시점에만 AOP를 적용할 수 있으며, 현재 우리가 사용하는 Spring AOP 방식이다.





Spring AOP


주요 개념

Spring AOP 핵심은 다음과 같이 크게 세가지로 요약할 수 있다.

  1. Spring AOP는 Proxy 기반의 AOP 프레임워크다.
  2. Spring AOP는 Spring IoC 컨테이너와 함께 사용된다.
  3. Proxy 기반의 Weaving은 Runtime weaving이다.

Spring AOP는 기존 실제 객체 대신 Proxy 기반의 객체를 통해 동작하게 된다.

Proxy 객체는 Aspect를 대신 수행하기 위해 생성된 객체로, 내부에 Advisor와 실제 호출해야할 대상 객체(target)을 알고 있다.
스프링은 프록시를 통해 횡단 관심 객체와 핵심 관심 객체의 느슨한 결합 구조를 만들고, 부가 기능 로직 구현에 대한 유연성 및 확장성 제공한다.

이러한 프록시 객체를 사용하기 위해선, Spring IoC 컨테이너에서 원본 객체 대신 프록시 객체가 Bean으로 등록되어야한다.

Spring Proxy 생성 과정

이때 스프링 부트는 자동으로 빈 후처리기(AnnotationAwareAspectJAutoProxyCreator)를 통해서 빈 저장소에 실제 객체 대신 ⇢ 프록시 객체 를 스프링 빈(Bean) 으로 등록해준다.
정확히는, 빈 후처리기는 자동으로 Advisor, @Aspect를 인식해서 필요한 곳(Pointcut)에 프록시를 만들고 AOP를 적용해준다. 프록시 적용 대상이 아니라면 원본 객체가 스프링 빈으로 등록되며 무분별한 프록시 생성 비용 낭비를 막는다.

때문에 우리는 이제 Advisor (PointCut + Advice) 만 스프링 빈으로 등록하면 되는데, 이를 @Aspect 어노테이션을 통해 자동으로 등록할 수 있다.

(만약 빈 후처리기가 없다면 우리는 매번 프록시 객체를 생성하거나, 컴포넌트 스캔에 의해서 프록시 객체가 아닌 원본 객체가 스프링 빈으로 자동으로 등록되어 프록시 적용이 불가능했을 것이다)

AOP proxy

Proxy 객체에 대한 자세한 설명은 이전에 작성한 포스팅을 참고하면 된다.
Proxy 의 이유, 원리에 대해 알아야 Spring AOP 원리를 이해할 수 있다.

Spring AOP 구현 방법

Spring AOP에선 1. XML(스키마 기반), 2. @AspectJ(어노테이션 기반) 두 가지 방법으로 Aspect를 구현할 수 있다.
하지만 XML 방식은 표현의 제한, 하나의 설정으로 관리 등이 어려우므로 먼저 @AspectJ 방식으로 살펴보겠다.

어노테이션 정리

  • @Aspect : Advisor (Advice + PointCut) 생성 기능 지원

요약

  • Spring AOP는 Proxy 기반의 AOP 프레임워크다.
  • Spring에서 @Aspect 어노테이션을 사용한 클래스는 Advisor로 인식되어 자동으로 빈 후처리기를 통해 Proxy 객체를 생성하고 AOP를 적용한다.
  • Spring은 @Aspect 어노테이션을 통해 Advisor가 스프링 빈으로 등록되는데, 그럼 스프링에서 해당 Advisor를 인식하여 자동으로 빈 후처리기를 통해 실제 객체 대신 Proxy 객체를 생성하고 알아서 AOP를 적용해준다.
  • 때문에 우리는 @Aspect만 구현하면 간단하게 AOP를 적용할 수 있다.



예제

초간단 예제 구현

원리를 이해하기 전에, Spring AOP를 어떻게 사용할 수 있는지 살펴보자.
다음은 서비스의 모든 Service 메소드 시작 전 print 로그를 남겨보는 부가 기능에 대한 예제이다.

우리는 이전에 학습했던 핸들러나 인터셉터 그리고 Proxy 등을 직접 구현할 필요 없이 Aspect 하나만 정의해주면 된다.

(1) Controller, Service 작성

@RestController
@RequestMapping("/pay")
public class PayController {
    @Autowired
    KakaoPayService kakaoPayService;

    @GetMapping("/kakao")
    public void kakaoPay(){
        kakaoPayService.pay();
    }
}

@Service
public class KakaoPayService implements PayService {

    @Override
    public void pay(){
        System.out.println("카카오페이");
    }
}

(2) Aspect 구현

@Aspect // Advisor가 Bean으로 등록되며, 알아서 Proxy 생성 및 AOP 적용
@Component
public class PayLogAspect {

    @Pointcut("execution(* com.tempspring.test.pay.service.*.*(..))")
    private void services() {}

    @Before("services()")
    public void serviceLog(JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getName();
        System.out.println(className + " 실행");
    }
}

(3) 적용된 결과



Spring @Transactional

Spring @Transactional 은 말그대로 AOP를 적용하여 트랜잭션 프록시를 통해 트랜잭션을 구현한 방법이다.
스프링이 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공한다.
스프링 부트를 사용 면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.

ex. 트랜잭션 프록시

public class TransactionProxy {
	private PayService target;
    
	public void logic() {
		TransactionStatus status = transactionManager.getTransaction(..); //TM을 통한 트랜잭션 시작
		try {
			target.pay(); //실제 대상 호출 (= 비즈니스로직)
            transactionManager.commit(status); //성공시 커밋 (= 부가로직)
		} catch (Exception e) {
        	transactionManager.rollback(status); //실패시 롤백 (= 부가로직)
            throw new IllegalStateException(e);
		}
	}
}

위 코드처럼 트랜잭션 프록시는 말 그대로 트랜잭션 시작,커밋,롤백 등의 로직이 부가로직으로 작성되어있고 target을 통해 실제 비즈니스 로직을 호출한다.

이제 Spring @Transactional에서 AOP가 어떻게 적용되어있는지 내부를 간단하게 살펴보자.

https://mangkyu.tistory.com/312



Spring @Cacheable

목적

  • Spring Cache에서 Proxy는 캐싱 기능을 구현하기 위한 핵심 구성 요소이다.
  • 주된 목적은 메서드의 결과를 캐시하여 동일한 입력에 대한 중복 계산을 방지하고, 성능을 향상시키는 것이다.
  • Proxy를 통해 Spring Cache는 캐싱과 관련된 부가적인 로직을 모듈화하고, 재사용 가능한 캐싱 모듈을 만들어 성능 향상을 제공한다.

동작

  • Spring Cache에서 Proxy는 @Cacheable, @CacheEvict, @CachePut 등의 어노테이션을 사용하여 대상 메서드에 캐싱 동작을 적용한다.
  • 이때, Proxy는 대상 객체를 래핑하며, 캐싱된 데이터가 존재하는지 여부를 확인하고, 캐시된 데이터가 없는 경우에만 대상 메서드를 호출하여 결과를 캐시한다.
  • Spring은 기본적으로 JDK 동적 프록시 또는 CGLIB을 사용하여 Proxy를 생성하며, 이를 통해 캐싱 동작이 메서드 호출을 가로채고 캐시를 처리한다.

내부

Spring @Cacheable 에서 AOP가 어떻게 적용되어있는지 내부를 간단하게 살펴보자.

https://alwayspr.tistory.com/42













ProxyFactory

Spring은 AOP를 적용할 때, ProxyFactory를 통해 하나의 프록시에 여러 어드바이저를 적용할 수 있다.

하나의 target에 여러 AOP를 동시에 적용한다고해도, 최적화를 통해 target마다 하나의 proxy만 생성하고 하나의 proxy에 여러 어드바이저를 적용한다.

프록시 팩토리에 각각의 target과 advisor를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.

이때 프록시 팩토리에서는 자동으로 인터페이스가 존재하면 JDK 동적 프록시를 적용하고, 구현체가 있으면 CGLIB를 적용한다.

Spring의 PointCut

다음과 같이 포인트컷은 크게 ClassFilter, MethodMatcher 둘로 이뤄져있다. 이때 둘다 true를 반환해야 Advice(부가기능)를 적용할 수 있다.

  • ClassFilter 에서는 클래스가 맞는지 확인한다
  • MethodMatcher 에서는 메서드가 맞는지 확인한다


1. 만약 AOP 기능을 제공하는 프레임워크/라이브러리가 없다면?

2. AOP 기능을 제공하는 프레임워크/라이브러리를 사용한다면?

번거로운 프록시 클래스 작성없이 핵심 비즈니스 로직에서 부가 기능 관심사(ex.트랜잭션)를 간편하게 분리할 수 있다.
더불어 다양한 클래스가 Aspect를 재활용하며 공통 사용할 수 있다.





실전 @Slack AOP 적용

이제 위에서 학습했던 내용을 바탕으로 직접 AOP를 적용해보자
목표는 @Slack 어노테이션을 통해 Exception 발생시 slack으로 알람을 가게 하는 것이다.

  1. 이를 통해 Exception이 발생하면 개발자가 서버 접속 없이도 바로 로그를 확인할 수 있고 장애 대응이 가능하다.
  2. AOP를 적용함으로써 매번 Slack 알람을 불필요하게 핵심 비즈니스 로직 사이에서 붙이지 않아도 된다.

아래 코드는 실제 실무에서 적용했던 코드를 local에서 다시 작성하였다.

@Slack 어노테이션

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Slack {
}

SlackAspect 구현

Aspect 적용

@Slack 적용










[참고]
https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(1)
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(2)
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(3)![업로드중..](blob:https://velog.io/01072c74-df60-490c-be6d-2c886a03ded4)

https://invicredible.tistory.com/entry/Spring-AOP%EB%8A%94-%ED%94%84%EB%A1%9D%EC%8B%9C-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EB%8A%94

https://gmoon92.github.io/spring/aop/2019/03/01/spring-aop-choosing.html

profile
- 👩🏻‍💻

0개의 댓글