본문 바로가기

Backend/Spring

스프링 AOP(Aspect-Oriented Programming)

목차
AOP란?
스프링 AOP에 대하여
프록시 객체와 AOP의 원리
AOP 기반 어노테이션 사용 시 주의점

 

 

 

AOP란?

AOP(Aspect-Oriented Programming)의 사전적 정의는 아래와 같다.

 

컴퓨팅에서 관점 지향 프로그래밍은 횡단 관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다.

 

 

말로만 들었을때는 조금 와닿지 않을 수 있으나 프로그래밍적으로 해석하자면,

AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)는 애플리케이션의 핵심 로직(Core Logic)과 부가 기능(Cross-Cutting Concern)을 명확히 분리하기 위한 프로그래밍 패러다임이다.

 

 

일반적으로 프로그램 코드를 작성할 때, 인증이 필요한지 검사하거나 로그를 찍는 등의 로직이 있고, 요청한 기능이 동작하는 비즈니스 로직이 있지 않은가? AOP란 이런 일반적인 기능과 비즈니스 로직을 확실히 분리하여 코드의 관점(Aspect)에 집중하자는 취지에서 비롯된 개념으로 볼 수 있다.

 

✔ AOP가 필요한 이유

앞서 언급된 인증이나 로깅외에도 애플리케이션을 개발하다 보면 다음과 같은 코드가 여러 곳에 반복된다.

  • 로깅
  • 트랜잭션 처리
  • 인증/인가 처리
  • 캐싱
  • 성능 측정(Execution Time)

이들은 비즈니스 로직이 아님에도 여러 메소드에서 반복되는 코드들이다. AOP는 이런 공통 기능을 따로 모아서 관리하고, 원하는 지점(Pointcut)에 자동으로 끼워 넣는 방식으로 동작한다.

 

 

 

 

스프링 AOP에 대하여

기존 네이티브 자바에서는 AspectJ 기반의 바이트코드를 조작하는 로우레벨의 AOP를 사용하였다. Spring AOP에서는 이런 AOP 개념을 좀더 쉽게 프로젝트에 적용할 수 있도록 전용 애노테이션들을 제공한다.

 

Aspect 정의

✔ @Aspect

  • AOP 기능을 담는 클래스임을 명시.
  • 해당 클래스 안에 Pointcut, Advice 등을 선언할 수 있음.
@Aspect
@Component
public class LoggingAspect { ... }
 

 

Pointcut 정의

✔ @Pointcut

  • Advice가 적용될 지점(Joint Point)을 문자열 표현식으로 정의.
  • 다른 Advice에서 재사용하기 위해 이름 붙은 Pointcut을 만드는 용도.
  • Pointcut의 표현식은 AspectJ의 것을 그대로 사용한다.
@Pointcut("execution(* com.example..*Service.*(..))")
public void allServiceMethods() {}

 

Advice 정의

✔ @Around

  • 대상 메소드 앞뒤를 모두 감싸서 실행 흐름을 전부 제어할 수 있는 가장 강력한 Advice(직접 joinPoint.proceed() 호출해야 함)
  • @Around 외에도 Before, After, AfterReturning, AfterThrowning 등이 있으나 Around가 실행제어 측면에서 가장 강력하며 가장 자주 쓰인다.
  • 나머지 애노테이션의 경우 필요한 경우가 많지는 않고 대부분 @Around로 해결이 가능하다.
@Around("allServiceMethods()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();

    Object result = joinPoint.proceed();

    long end = System.currentTimeMillis();
    System.out.println("Execution Time: " + (end - start) + "ms");

    return result;
}

✔ @Before

  • 대상 메소드가 실행되기 직전에 수행
@Before("allServiceMethods()")
public void before() {
    System.out.println("Before Method");
}

✔ @After

  • 대상 메소드의 실행이 끝난 후 실행됨
@After("allServiceMethods()")
public void after() {
    System.out.println("After Method");
}

✔ @ AfterReturning

  • 대상 메소드가 정상적으로 반환될 때 실행됨
@AfterReturning(pointcut = "allServiceMethods()", returning = "result")
public void afterReturning(Object result) {
    System.out.println("Return Value = " + result);
}

✔ @ AfterThrowing

  • 대상 메소드에서 예외가 발생했을 때만 발생
@AfterReturning(pointcut = "allServiceMethods()", returning = "result")
public void afterReturning(Object result) {
    System.out.println("Return Value = " + result);
}

 

 

 

 

프록시 객체와 AOP의 원리

스프링 AOP는 프록시 객체(Proxy)를 생성하여 메소드 호출을 가로채는 방식으로 동작한다. 여기서 프록시 객체란 무엇인지에 대해 알아야한다.

 

스프링은 실제 빈 대신 빈 클래스를 감싼 프록시 객체를 만들어서 제공한다.

 

 

스프링 프로젝트에서 빈으로 등록된 객체들은 프로젝트 빌드 후 실행 시 프록시 객체로 교체되어 제공된다. 결과적으로 우리가 빈으로 선언한 클래스들(컨트롤러, 서비스, Config 객체등등)은 프록시 객체로 교체된다.

 

 

📌 Weaving(위빙)

애스펙트(Advice)를 대상 코드(타겟)와 합쳐서 실제로 실행 가능한 형태의 객체 또는 바이트코드를 만들어내는 과정을 의미.

 

weave의 경우 직역하면 '직조하다', '엮어서 만들다' 등의 의미인데, 위에서 언급되었듯이 Aspect와 대상 코드를 엮어서 새로운 클래스를 만들어 내기때문에 붙은 용어라고 이해할 수 있다.

 

 

 

 

위빙을 하기 위해서는 시점에 따라 컴파일, 로드타임, 런타임 등이 있으며, 네이티브 자바의 AspectJ에서는 직접 바이트 코드를 조작하여 위빙을 제공하기도 한다.

 

 

물론 스프링 AOP에서는 바이트 코드를 조작하는 등의 로우레벨까지 들어가지는 않는다. 스프링 AOP에서는 프록시 기반의 런타임 위빙(Weaving)을 한다.

 

 

 

AOP 기반 어노테이션 사용 시 주의점

트랜잭션 보장을위해 사용하는 @Transactional, 비동기 동작 처리를 위한 @Async의 경우 스프링 AOP에서 제공하는 프록시 객체를 기반으로 동작한다. 메소드 호출 전후를 기점으로 특정 작업이 진행되어야하기 때문에 프록시를 기반으로 동작하게 되며, 스프링 AOP 동작원리에 대해 이해하여야 이와 같은 AOP 기반 어노테이션 동작원리에 대해 이해할 수 있다.

 

 

  • @Async: 메소드 호출을 “별도 스레드에서 실행”하도록 제어
  • @Transactional: 메소드를 호출하기 전에 "트랜잭션을 시작/종료" 하도록 제어

 

 

따라서 AOP 기반 어노테이션 사용을 위해서는 아래와 같은 주의가 필요하다.

 

 

✔ 1) private 메소드에는 적용 안 됨

@Transactional
private void saveUser() {}

 

프록시 객체는 public 메소드에만 접근 가능하기 때문에 private 메소드에 대해서는 intercept할 수 없다.

 

 

✔ 2) 같은 클래스 내부에서 메소드 호출 시 적용 안 됨 (self-invocation)

public void outer() {
    inner();  // 실제 객체의 메소드 호출 → 프록시 거치지 않음 → AOP 미적용
}

 

프록시 객체 -> 원본 객체로 요청을 하는 구조이기 때문에 원본 객체에서 본인을 호출하는 구조는 프록시가 개입할 틈이 없다.

 

 

 

 

✔ 3) 스프링 빈이 아니면 AOP 절대 적용 안 됨

new MyService().doSomething();

 

정확히 말하면 프록시 객체가 아니면 적용이 안된다. @Transactional, @Async는 스프링 AOP에서 제공하는 프록시 객체를 기반으로 동작하기 때문에 그렇다고 설명할 수 있다.

 

 

 

위의 내용들을 다시 정리하자면 아래와 같다.

AOP는 반드시 “프록시 객체 → 원본 객체” 호출 구조를 거칠 때만 실행된다.
private 메소드나 self-invocation처럼 프록시가 개입할 수 없는 호출은
트랜잭션/로깅/비동기 같은 AOP 기능이 적용되지 않는다.