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 ❌