spring

스프링 AOP

콩콩(๓° ˘ °๓)♡ 2025. 8. 6. 21:54

1. AOP란 무엇인가?

AOP (Aspect Oriented Programming) 는 공통 관심사를 분리하여 핵심 로직에 침투하지 않도록 도와주는 프로그래밍 기법입니다.

쉽게 말해, 비즈니스 로직과 상관없는 중복 코드(예: 로깅, 보안, 트랜잭션)를 따로 관리하는 방식입니다.


2. 왜 AOP가 필요한가?

예를 들어 모든 서비스 메서드에서 실행 시간을 측정하는 로직이 필요하다고 해봅시다.

public void doSomething() {
    long start = System.currentTimeMillis();
    // 핵심 로직
    long end = System.currentTimeMillis();
    System.out.println("수행 시간: " + (end - start));
}

이 코드를 매번 적는 건 비효율적이고 유지보수성도 떨어집니다. → AOP로 분리하면 해결됩니다!


3. 스프링 AOP 동작 원리

스프링은 프록시 기반 AOP를 제공합니다.

🔁 핵심 개념: 프록시

  • 실제 객체 대신 **프록시 객체(대리자)**가 스프링 빈으로 등록됨
  • 프록시가 메서드를 가로채서 부가 작업 수행 (Advice 실행)
  • 실제 핵심 로직은 이후 호출
  • AOP는 프록시 객체가 메서드를 "감싸는" 구조입니다.

4. AOP 용어 정리

용어설명
Aspect 공통 기능을 담은 모듈 (ex: LoggingAspect)
Advice 언제 실행할지에 대한 로직 (Before, After 등)
JoinPoint Advice가 적용될 수 있는 지점 (메서드 실행 등)
Pointcut Advice가 적용될 실제 조건 (ex: 특정 패키지 내 모든 메서드)
Weaving Pointcut + Advice를 실제 코드에 결합하는 행위
 

5. AOP 적용 예제

✅ 1) 의존성 추가 (Spring Boot 프로젝트)

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

✅ 2) 어노테이션 기반 AOP 정의

@Aspect
@Component
public class LogAspect {

    @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] " + joinPoint.getSignature() + " executed in " + (end - start) + "ms");

        return result;
    }
}

6. 프록시 방식: JDK 동적 프록시 vs CGLIB

구분 JDK Dynamic Proxy CGLIB
대상 인터페이스 기반 클래스 일반 클래스
구현 방식 java.lang.reflect.Proxy 바이트코드 조작
제약 사항 반드시 인터페이스 있어야 함 final 클래스/메서드 금지
기본 전략 스프링 AOP는 기본적으로 JDK Proxy 사용  
 

7. 실무에서의 활용 예

기능  적용방법
트랜잭션 관리 @Transactional
로깅 @Around, @Before, @After
인증/권한 검사 @PreAuthorize, @Secured (Spring Security)
캐싱 @Cacheable, @CacheEvict
 

AOP는 Spring Security, Spring Transaction, Spring Cache의 기반이 됩니다.

 

✅ 1. @Aspect – AOP 클래스 정의

AOP 기능을 적용할 클래스임을 선언합니다.

@Aspect
@Component
public class LogAspect {
    // Advice 정의
}
  • 반드시 @Component와 함께 사용해야 스프링 빈으로 등록되어 AOP가 동작함

✅ 2. @Around – 메서드 전후 모두 감싸는 Advice

핵심 로직 실행 전후로 커스텀 로직을 넣고 싶을 때 사용합니다.

@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("⏱ 실행시간: " + (end - start) + "ms");

    return result;
}
  • ProceedingJoinPoint로 핵심 메서드를 실행할 수 있음
  • @Around는 가장 강력하며, 나머지 모든 Advice 동작을 구현 가능

✅ 3. @Before – 메서드 실행 전 동작

핵심 비즈니스 로직 실행 이전에 로직을 실행합니다.

@Before("execution(* com.example.service..*(..))")
public void beforeMethod(JoinPoint joinPoint) {
    System.out.println("🔍 Before 실행: " + joinPoint.getSignature().toShortString());
}
  • 인증, 로깅, 파라미터 검증 등에 활용

✅ 4. @After – 메서드 실행 후 (성공/실패 상관없이)

메서드의 성공/실패 여부와 관계없이 무조건 실행됩니다.

@After("execution(* com.example.service..*(..))")
public void afterMethod(JoinPoint joinPoint) {
    System.out.println("📌 After 실행: " + joinPoint.getSignature().getName());
}
  • 주로 리소스 정리, 로그 마무리 작업에 사용

✅ 5. @AfterReturning – 메서드 정상 종료 후 동작

예외 없이 정상적으로 리턴될 경우에만 실행됩니다.

@AfterReturning(
    pointcut = "execution(* com.example.service..*(..))",
    returning = "result"
)
public void afterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("✅ 정상 리턴: " + result);
}
  • 반환 값이 필요한 후처리 작업에 유용
  • returning 이름은 매개변수 이름과 일치해야 함

✅ 6. @AfterThrowing – 예외 발생 시 동작

핵심 메서드 실행 중 예외가 발생했을 때만 실행됩니다.

@AfterThrowing(
    pointcut = "execution(* com.example.service..*(..))",
    throwing = "ex"
)
public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
    System.out.println("❌ 예외 발생: " + ex.getMessage());
}
  • 예외 로깅, 트랜잭션 롤백 알림 등 예외 처리 로직에 활용

 

✨ 정리: 언제 어떤 어노테이션을 써야 할까?

어노테이션 실행 시점 사용 목적
@Before 메서드 실행 파라미터 검증, 로깅
@Around 메서드 실행 전/후 전체 실행 시간 측정, 트랜잭션 관리
@After 메서드 실행 후(무조건) 로그 마무리, 리소스 정리
@AfterReturning 정상 종료 후 리턴 값 후처리
@AfterThrowing 예외 발생 시 예외 로깅, 모니터링

 


 

8. 마무리 요약

스프링 AOP는 반복적이고 공통된 부가 기능을 프록시 객체를 통해 핵심 로직과 분리해줍니다.

🧠요약

  • AOP = 공통 관심사 분리
  • Aspect는 부가기능 로직의 모듈
  • Advice는 언제 실행할지
  • Proxy 객체가 핵심 로직을 감싼다
  • DI로 프록시 객체가 주입되어야 AOP 동작함
  • JDK 동적 프록시(인터페이스) / CGLIB(클래스 상속)

 

 

📌 1. Spring AOP의 한계점

1.1. 프록시 기반이기 때문에, 자기 자신 호출에는 적용되지 않음

@Component
public class MyService {

    @Transactional
    public void outer() {
        inner(); // ❌ AOP 미적용 (프록시를 우회)
    }

    public void inner() {
        // 트랜잭션이 적용되지 않음
    }
}
  • outer()는 프록시를 거치지만, 내부에서 직접 호출한 inner()는 프록시를 거치지 않음
  • → **자기 호출(Self Invocation)**에는 AOP 미적용

🔧 해결 방법:

  • 내부 메서드도 외부에서 호출되도록 분리하거나
  • AOP 프레임워크를 AspectJ로 전환

1.2. final 메서드 또는 클래스에는 적용되지 않음 (CGLIB 한계)

  • CGLIB는 클래스를 상속해서 프록시를 만듦
  • 하지만 final 클래스나 final 메서드는 상속/오버라이딩이 불가능
@Component
public final class FinalService {
    @Transactional
    public void doWork() { } // ❌ 적용 안 됨
}

✅ 해결 방법:

  • final 키워드를 제거하거나
  • 인터페이스 기반으로 재구성

1.3. 생성자에는 적용 불가

AOP는 메서드 단위로 프록시를 감싸므로, 객체가 생성되는 순간에는 개입할 수 없습니다.

@Component
public class MyService {

    public MyService() {
        System.out.println("Constructor"); // AOP 적용 ❌
    }
}

1.4. 비 Spring Bean에는 AOP가 적용되지 않음

  • AOP는 스프링 컨테이너에 등록된 빈에만 프록시를 적용
  • new로 직접 생성한 객체에는 절대 적용되지 않음
MyService svc = new MyService(); // AOP ❌