[Spring AOP] AOP

스프링 AOP(Aspect-Oriented Programming)란?

스프링 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 핵심 비즈니스 로직과 부가적인 관심사(공통 기능)를 분리하여 관리할 수 있도록 도와주는 개념입니다.
주요 목적은 반복적인 코드(중복 코드)를 제거하고 유지보수를 용이하게 하는 것입니다.


1. AOP의 핵심 개념

🔹 Aspect (애스펙트)

  • 부가적인 관심사(공통 기능)를 의미합니다.
  • 예: 로깅, 트랜잭션 관리, 보안, 성능 모니터링 등

🔹 Join Point (조인 포인트)

  • 애스펙트가 적용될 수 있는 실행 지점을 의미합니다.
  • 예: 메서드 호출, 예외 발생 시점, 필드 접근 등

🔹 Pointcut (포인트컷)

  • 특정 Join Point를 필터링하는 표현식입니다.
  • 예: execution(* com.example.service.*.*(..))service 패키지 내 모든 메서드 실행 시

🔹 Advice (어드바이스)

  • 실제로 실행되는 부가 기능(로직)입니다.
  • Advice 종류:
    • Before: 대상 메서드 실행 전에 실행됨
    • After Returning: 정상 실행 후 실행됨
    • After Throwing: 예외 발생 시 실행됨
    • After: 예외 여부와 상관없이 항상 실행됨
    • Around: 대상 메서드를 감싸서 실행됨 (가장 강력한 어드바이스)

🔹 Weaving (위빙)

  • Pointcut에 의해 선택된 Join Point에 Advice를 적용하는 과정입니다.
  • 스프링 AOP는 런타임 위빙을 사용하여 프록시 기반으로 AOP를 적용합니다.

핵심 개념 적용 예제

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect  // 📌 [Aspect] : 이 클래스가 AOP 기능을 수행하는 클래스임을 나타냄
@Component // 스프링 빈으로 등록
public class LoggingAspect {

    // 📌 [Pointcut] : 특정 메서드 패턴을 지정하여 Advice가 실행될 대상(Join Point)을 필터링
    @Pointcut("execution(* com.example.service.*.*(..))") 
    public void serviceMethods() {}

    // 📌 [Advice - Before] : Join Point(메서드 실행) 전에 실행됨
    @Before("serviceMethods()")
    public void beforeMethod() {
        System.out.println("[LOG] Method execution started...");
    }

    // 📌 [Advice - After Returning] : 메서드가 정상적으로 실행된 후 실행됨
    @AfterReturning("serviceMethods()")
    public void afterReturningMethod() {
        System.out.println("[LOG] Method executed successfully.");
    }

    // 📌 [Advice - After Throwing] : 예외 발생 시 실행됨
    @AfterThrowing("serviceMethods()")
    public void afterThrowingMethod() {
        System.out.println("[LOG] Exception occurred during method execution.");
    }

    // 📌 [Advice - After] : 예외 발생 여부와 관계없이 항상 실행됨
    @After("serviceMethods()")
    public void afterMethod() {
        System.out.println("[LOG] Method execution ended.");
    }

    // 📌 [Advice - Around] : Join Point(메서드 실행)를 감싸서 실행 (가장 강력한 어드바이스)
    @Around("serviceMethods()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 📌 [Join Point] : 대상 메서드가 실행되는 실제 지점
        long end = System.currentTimeMillis();
        
        System.out.println(joinPoint.getSignature() + " execution time: " + (end - start) + "ms");
        
        return result;
    }
}


2. 스프링 AOP 사용 방법

1️⃣ 의존성 추가 (Spring Boot 기준)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2️⃣ Aspect 클래스 생성

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    // 대상 메서드 실행 전에 실행되는 Advice
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeMethod() {
        System.out.println("메서드 실행 전 로깅...");
    }

    // 정상 실행 후 실행되는 Advice
    @AfterReturning("execution(* com.example.service.*.*(..))")
    public void afterReturningMethod() {
        System.out.println("메서드 정상 실행 후 로깅...");
    }

    // 예외 발생 시 실행되는 Advice
    @AfterThrowing("execution(* com.example.service.*.*(..))")
    public void afterThrowingMethod() {
        System.out.println("메서드 실행 중 예외 발생!");
    }

    // 항상 실행되는 Advice
    @After("execution(* com.example.service.*.*(..))")
    public void afterMethod() {
        System.out.println("메서드 실행 완료 후 로깅...");
    }
}

3️⃣ AOP 적용 대상 클래스

import org.springframework.stereotype.Service;

@Service
public class SampleService {

    public void execute() {
        System.out.println("비즈니스 로직 실행 중...");
    }
}

4️⃣ 테스트 실행

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class TestRunner implements CommandLineRunner {

    private final SampleService sampleService;

    public TestRunner(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @Override
    public void run(String... args) {
        sampleService.execute();
    }
}

출력 결과

메서드 실행 전 로깅...
비즈니스 로직 실행 중...
메서드 정상 실행 후 로깅...
메서드 실행 완료 후 로깅...

3. @Around 어드바이스 (메서드 실행 시간 측정)

@Around 어드바이스를 사용하면 메서드를 직접 감싸서 실행할 수 있습니다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 대상 메서드 실행
        long end = System.currentTimeMillis();
        
        System.out.println(joinPoint.getSignature() + " 실행 시간: " + (end - start) + "ms");
        
        return result;
    }
}

4. AOP의 장점과 단점

장점

  • 중복 코드 제거: 공통 기능(로깅, 트랜잭션 등)을 분리하여 유지보수 용이
  • 핵심 로직 집중: 비즈니스 로직과 부가 기능을 분리하여 가독성 증가
  • 변경 용이성: 특정 기능을 수정할 때 여러 클래스에서 수정할 필요 없음

단점

  • 디버깅 어려움: 코드가 자동으로 감싸지기 때문에 흐름을 파악하기 어려울 수 있음
  • 성능 저하 가능성: @Around를 과도하게 사용하면 불필요한 오버헤드 발생 가능
  • 지나친 남용 주의: 모든 로직을 AOP로 처리하려 하면 코드가 복잡해질 수 있음

5. AOP가 주로 사용되는 곳

  • 트랜잭션 관리 (@Transactional)
  • 로깅 & 모니터링
  • 보안 (권한 체크)
  • 성능 측정 (메서드 실행 시간)
  • 예외 처리 & 에러 핸들링
  • 캐싱 적용 (@Cacheable)

6. AOP와 프록시 방식

스프링 AOP는 프록시 기반으로 동작합니다.

  • Spring AOP는 기본적으로 JDK 동적 프록시를 사용 (인터페이스 기반)
  • 클래스 기반 프록시가 필요하면 CGLIB 사용

💡 프록시 기반 AOP의 한계

  • 같은 클래스 내에서 AOP 메서드를 호출하면 적용되지 않음
  • 해결책: @EnableAspectJAutoProxy(exposeProxy = true) 사용 후 AopContext.currentProxy() 활용
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    public void execute() {
        System.out.println("비즈니스 로직 실행 중...");
        ((SampleService) AopContext.currentProxy()).anotherMethod();
    }

    public void anotherMethod() {
        System.out.println("다른 메서드 실행...");
    }
}

🔥 정리

  • AOP(관점 지향 프로그래밍)은 중복되는 로직(로깅, 트랜잭션, 보안 등)을 분리하여 코드 유지보수를 쉽게 하는 기법
  • 주요 개념: Aspect, Join Point, Pointcut, Advice, Weaving
  • 스프링 AOP는 런타임 프록시 기반으로 동작
  • 대표적인 활용 사례: 로깅, 트랜잭션 관리, 보안, 성능 모니터링
  • 지나친 남용은 코드 복잡성을 증가시키므로 주의

@Aspect란?

@Aspect스프링 AOP(Aspect-Oriented Programming)에서 해당 클래스가 하나 이상의 AOP 기능(Advice, Pointcut 등)을 포함하고 있음을 선언하는 어노테이션입니다.

즉, 이 어노테이션이 붙은 클래스는 AOP를 적용할 대상(Aspect 클래스)이 됩니다.


📌 @Aspect의 역할

스프링이 해당 클래스에 AOP 기능이 포함되어 있음을 인식하고, Advice(부가 기능)를 원하는 대상(Pointcut)에 적용할 수 있도록 해주는 역할을 합니다.

@Aspect가 없으면 스프링이 해당 클래스를 AOP로 동작시키지 않기 때문에, Advice가 실행되지 않습니다.


📌 @Aspect의 사용 예시

1️⃣ @Aspect 없이 로깅 기능을 구현하는 경우 (중복 코드 발생)

import org.springframework.stereotype.Service;

@Service
public class SampleService {
    public void execute() {
        System.out.println("[LOG] Method execution started..."); // 중복 로깅
        System.out.println("Business logic execution...");
        System.out.println("[LOG] Method execution ended..."); // 중복 로깅
    }
}

위처럼 각 서비스 메서드마다 로깅을 추가하면 코드 중복이 심해지고, 유지보수도 어려워집니다.


2️⃣ @Aspect를 사용하여 로깅 기능을 분리

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.springframework.stereotype.Component;

@Aspect  // AOP 클래스임을 선언
@Component // 스프링 빈으로 등록
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))") 
    public void beforeMethod() {
        System.out.println("[LOG] Method execution started...");
    }

    @After("execution(* com.example.service.*.*(..))")
    public void afterMethod() {
        System.out.println("[LOG] Method execution ended...");
    }
}

위처럼 @Aspect를 사용하면, 서비스 클래스(SampleService)는 본연의 비즈니스 로직에 집중하고, 로깅 기능을 별도로 관리할 수 있습니다.

이제 SampleService에서는 메서드를 실행할 때마다 AOP가 자동으로 로깅을 수행합니다.


📌 @Aspect의 동작 방식

스프링이 @Aspect가 붙은 클래스를 AOP 컨텍스트에서 특별하게 인식하여, Advice(부가 기능)와 Pointcut(적용 위치)을 연결해 줍니다.

💡 즉, @Aspect는 스프링에게 “이 클래스는 AOP 기능을 포함하고 있다!”라고 알려주는 역할을 합니다.


📌 @Aspect 없이 Advice를 사용하면 어떻게 될까?

만약 @Aspect를 생략하면, 스프링은 이 클래스가 일반적인 빈(Component)이라고만 인식할 뿐, AOP 관련 동작을 하지 않습니다.

@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))") 
    public void beforeMethod() {
        System.out.println("[LOG] Method execution started...");
    }
}

👉 @Aspect를 생략하면 @Before가 동작하지 않습니다.
👉 AOP 기능을 활성화하려면 반드시 @Aspect를 선언해야 합니다.


📌 @Aspect를 사용할 때 @Component가 필요한 이유

@Aspect
@Component
public class LoggingAspect { ... }
  • @AspectAOP 기능을 정의하는 어노테이션이지만,
  • @Component해당 클래스를 스프링 빈으로 등록하는 역할을 합니다.

💡 @Component 없이 @Aspect만 사용하면 스프링이 빈으로 인식하지 않아 AOP가 적용되지 않습니다.
👉 @Aspect와 함께 @Component 또는 @Bean을 사용해야 합니다.

🛠 @Component 없이 AOP를 활성화하는 방법

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }
}

위처럼 @Bean을 사용해서 수동으로 등록할 수도 있습니다.


📌 결론

  • @AspectAOP 클래스임을 선언하는 어노테이션이다.
  • 스프링이 @Aspect가 붙은 클래스를 AOP 대상(Aspect)으로 인식하여, Pointcut 및 Advice를 적용한다.
  • @Aspect가 없으면 AOP 기능이 동작하지 않는다.
  • @Component 또는 @Bean을 사용하여 스프링 빈으로 등록해야 AOP가 정상적으로 실행된다.

🔹 순수 자바 기반 AOP 구현 예제

스프링 AOP 없이 순수 자바만으로 AOP 기능을 구현하려면 프록시 패턴(Proxy Pattern)을 사용해야 합니다.
Java의 java.lang.reflect.Proxy 를 활용하여 런타임에 동적으로 프록시를 생성하고 AOP를 적용할 수 있습니다.


1. 핵심 비즈니스 로직 클래스 (Target Class)

public interface SampleService {
    void execute();
}

public class SampleServiceImpl implements SampleService {
    @Override
    public void execute() {
        System.out.println("Business logic execution...");
    }
}
  • SampleServiceImpl 는 비즈니스 로직을 담당하는 클래스입니다.
  • 여기에 AOP를 적용하여 메서드 실행 전후에 로깅을 추가할 예정입니다.

2. InvocationHandler를 활용한 AOP 구현 (Proxy Class)

Java의 동적 프록시(Proxy.newProxyInstance) 를 사용하여 AOP 기능을 적용할 수 있습니다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LoggingProxy implements InvocationHandler {

    private final Object target; // 실제 실행할 객체 (핵심 비즈니스 로직)

    public LoggingProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[LOG] Method execution started: " + method.getName());

        long startTime = System.currentTimeMillis();
        Object result = method.invoke(target, args); // 📌 [Join Point] 실제 메서드 실행
        long endTime = System.currentTimeMillis();

        System.out.println("[LOG] Method executed successfully: " + method.getName());
        System.out.println("[LOG] Execution time: " + (endTime - startTime) + "ms");

        return result;
    }

    // 📌 프록시 객체를 생성하는 메서드
    public static Object createProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), 
                target.getClass().getInterfaces(), 
                new LoggingProxy(target)
        );
    }
}

3. AOP 적용 및 실행

public class Main {
    public static void main(String[] args) {
        // 원본 객체 생성
        SampleService originalService = new SampleServiceImpl();

        // 프록시 객체 생성 (AOP 적용)
        SampleService proxyService = (SampleService) LoggingProxy.createProxy(originalService);

        // 메서드 실행 (AOP가 적용된 프록시 객체를 통해 호출)
        proxyService.execute();
    }
}

출력 결과

[LOG] Method execution started: execute
Business logic execution...
[LOG] Method executed successfully: execute
[LOG] Execution time: 1ms

순수 자바 AOP의 동작 원리

  1. LoggingProxy.createProxy(originalService)를 호출하여 프록시 객체를 생성
  2. Proxy.newProxyInstance()를 사용하여 InvocationHandler(핸들러) 가 가로채도록 설정
  3. proxyService.execute(); 호출 시
    • invoke()가 먼저 실행됨 → [LOG] Method execution started
    • 실제 메서드 실행 (method.invoke(target, args))
    • 실행 후 [LOG] Method executed successfully 및 실행 시간 출력

정리

AOP 개념순수 자바 코드에서의 구현
AspectLoggingProxy 클래스
Join Pointmethod.invoke(target, args) (실제 메서드 실행)
Pointcut모든 메서드 (invoke()에서 자동 적용)
Adviceinvoke() 내의 System.out.println() (Before, After)
WeavingProxy.newProxyInstance()를 통해 런타임에 프록시 적용

🔥 결론

  • 스프링 없이도 프록시 패턴을 활용하면 AOP 기능을 적용할 수 있다.
  • 하지만 스프링 AOP처럼 @Aspect, @Pointcut 등을 제공하지 않으므로, 세부적인 컨트롤이 어렵고 코드가 길어질 수 있음.
  • 복잡한 AOP를 적용하려면 스프링 AOP 또는 AspectJ를 사용하는 것이 더 효율적!

🔹 AspectJ란?

AspectJJava에서 AOP(Aspect-Oriented Programming)를 구현하는 정식 프레임워크입니다.
스프링 AOP는 기본적으로 AspectJ의 문법을 차용하지만, 내부적으로는 프록시 기반으로 동작합니다.
반면, AspectJ는 컴파일 단계에서 직접 바이트코드를 조작하여 AOP를 적용할 수 있어 더 강력한 기능을 제공합니다. (참고, 문법은 같은데 의존성이 다르고 컴파일러 설치가 필요)


AspectJ vs Spring AOP 차이점

비교 항목AspectJSpring AOP
동작 방식컴파일 타임 (CTW) / 로드 타임 (LTW)런타임 프록시 기반
적용 범위모든 Join Point (메서드, 필드 접근, 객체 생성 등)메서드 실행(Join Point)만 지원
성능바이트코드 조작 → 높은 성능프록시 기반 → 상대적으로 성능 저하
구현 방식AspectJ 컴파일러(ajc) 필요스프링 컨테이너에서 동작
사용 방식@Aspect, XML, 컴파일러 설정 필요@Aspect 기반, 간단한 설정
복잡도상대적으로 설정이 많고 복잡스프링 컨텍스트만 있으면 쉬움

💡 즉, AspectJ는 더 강력하고 성능이 좋지만, Spring AOP는 설정이 간단하고 편리합니다.


AspectJ 예제 (Compile-time Weaving 방식)

1️⃣ 의존성 추가

AspectJ를 사용하려면 Spring AOP + AspectJ 런타임 라이브러리가 필요합니다.

🔹 Maven

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

2️⃣ AspectJ로 로깅 AOP 구현

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Aspect  // 📌 AspectJ AOP 기능 선언
@Component // 📌 스프링 빈으로 등록
public class LoggingAspect {

    // 📌 [Pointcut] - 특정 패키지의 모든 메서드 실행 시 적용
    @Before("execution(* com.example.service.*.*(..))")  
    public void beforeMethod() {
        System.out.println("[LOG] Method execution started...");
    }

    @After("execution(* com.example.service.*.*(..))")  
    public void afterMethod() {
        System.out.println("[LOG] Method execution ended...");
    }

    @Around("execution(* com.example.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 📌 실제 메서드 실행
        long end = System.currentTimeMillis();

        System.out.println("[LOG] Execution time: " + (end - start) + "ms");
        return result;
    }
}

3️⃣ AOP 적용 대상 클래스

import org.springframework.stereotype.Service;

@Service
public class SampleService {

    public void execute() {
        System.out.println("Business logic execution...");
    }
}

4️⃣ 테스트 실행

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class TestRunner implements CommandLineRunner {

    private final SampleService sampleService;

    public TestRunner(SampleService sampleService) {
        this.sampleService = sampleService;
    }

    @Override
    public void run(String... args) {
        sampleService.execute();
    }
}

출력 결과

[LOG] Method execution started...
Business logic execution...
[LOG] Execution time: 5ms
[LOG] Method execution ended...

AspectJ의 Weaving 방식

AspectJ는 AOP 적용 방식(Weaving 방식)에 따라 3가지 유형이 있습니다.

Weaving 방식설명
Compile-time Weaving (CTW)컴파일 단계에서 AOP 적용 (AspectJ 전용 컴파일러 ajc 필요)
Load-time Weaving (LTW)클래스가 JVM에 로드될 때 AOP 적용 (aspectjweaver 라이브러리 사용)
Runtime Weaving (Spring AOP)스프링 프록시 기반으로 런타임에 AOP 적용

💡 스프링에서 사용되는 방식은 Runtime Weaving(Spring AOP)입니다.
AspectJ를 완전하게 사용하려면 CTW 또는 LTW 설정이 필요합니다.


AspectJ를 CTW(Compile-time Weaving)로 적용하는 방법

만약 Spring AOP가 아닌 완전한 AspectJ를 적용하고 싶다면, AspectJ 전용 컴파일러(ajc) 를 사용해야 합니다.

1️⃣ ajc(AspectJ Compiler) 설치

brew install aspectj

2️⃣ ajc로 컴파일

ajc -cp . -aspectpath . SampleService.java LoggingAspect.java

💡 이 방식은 별도의 컴파일 과정이 필요하므로, 일반적인 Spring AOP보다 복잡합니다.


Spring AOP vs 완전한 AspectJ(CTW/LTW)

특징Spring AOPAspectJ (CTW/LTW)
설정 편의성쉬움설정이 복잡함
성능상대적으로 느림빠름 (바이트코드 직접 수정)
지원 범위메서드 실행만 AOP 적용 가능모든 Join Point 지원 (생성자, 필드 접근 등)
활용 사례대부분의 Spring 애플리케이션성능이 중요한 시스템 (예: 대용량 트랜잭션)

🔥 결론

  1. AspectJ는 Java에서 AOP를 구현하는 가장 강력한 프레임워크이다.
  2. Spring AOP는 AspectJ 문법을 차용했지만, 프록시 기반으로 동작한다.
  3. AspectJ는 컴파일 타임(CTW) 또는 로드 타임(LTW) 위빙이 가능하며, Spring AOP보다 강력하지만 설정이 복잡하다.
  4. 일반적인 Spring 프로젝트에서는 Spring AOP로도 충분하지만, 성능이 중요한 경우 AspectJ(CTW/LTW)를 고려할 수 있다.

🚀 Spring AOP를 기본으로 사용하고, 더 강력한 AOP 기능이 필요할 때 AspectJ를 고려!


© 2023 Lee. All rights reserved.