[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 { ... }
@Aspect
는 AOP 기능을 정의하는 어노테이션이지만,@Component
는 해당 클래스를 스프링 빈으로 등록하는 역할을 합니다.
💡 @Component
없이 @Aspect
만 사용하면 스프링이 빈으로 인식하지 않아 AOP가 적용되지 않습니다.
👉 @Aspect
와 함께 @Component
또는 @Bean
을 사용해야 합니다.
🛠 @Component
없이 AOP를 활성화하는 방법
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
@Bean
public LoggingAspect loggingAspect() {
return new LoggingAspect();
}
}
위처럼 @Bean
을 사용해서 수동으로 등록할 수도 있습니다.
📌 결론
@Aspect
는 AOP 클래스임을 선언하는 어노테이션이다.- 스프링이
@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의 동작 원리
LoggingProxy.createProxy(originalService)
를 호출하여 프록시 객체를 생성Proxy.newProxyInstance()
를 사용하여 InvocationHandler(핸들러) 가 가로채도록 설정proxyService.execute();
호출 시invoke()
가 먼저 실행됨 →[LOG] Method execution started
- 실제 메서드 실행 (
method.invoke(target, args)
) - 실행 후
[LOG] Method executed successfully
및 실행 시간 출력
✅ 정리
AOP 개념 | 순수 자바 코드에서의 구현 |
---|---|
Aspect | LoggingProxy 클래스 |
Join Point | method.invoke(target, args) (실제 메서드 실행) |
Pointcut | 모든 메서드 (invoke() 에서 자동 적용) |
Advice | invoke() 내의 System.out.println() (Before, After) |
Weaving | Proxy.newProxyInstance() 를 통해 런타임에 프록시 적용 |
🔥 결론
- 스프링 없이도 프록시 패턴을 활용하면 AOP 기능을 적용할 수 있다.
- 하지만 스프링 AOP처럼
@Aspect
,@Pointcut
등을 제공하지 않으므로, 세부적인 컨트롤이 어렵고 코드가 길어질 수 있음. - 복잡한 AOP를 적용하려면 스프링 AOP 또는 AspectJ를 사용하는 것이 더 효율적!
🔹 AspectJ란?
AspectJ는 Java에서 AOP(Aspect-Oriented Programming)를 구현하는 정식 프레임워크입니다.
스프링 AOP는 기본적으로 AspectJ의 문법을 차용하지만, 내부적으로는 프록시 기반으로 동작합니다.
반면, AspectJ는 컴파일 단계에서 직접 바이트코드를 조작하여 AOP를 적용할 수 있어 더 강력한 기능을 제공합니다. (참고, 문법은 같은데 의존성이 다르고 컴파일러 설치가 필요)
✅ AspectJ vs Spring AOP 차이점
비교 항목 | AspectJ | Spring 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 AOP | AspectJ (CTW/LTW) |
---|---|---|
설정 편의성 | 쉬움 | 설정이 복잡함 |
성능 | 상대적으로 느림 | 빠름 (바이트코드 직접 수정) |
지원 범위 | 메서드 실행만 AOP 적용 가능 | 모든 Join Point 지원 (생성자, 필드 접근 등) |
활용 사례 | 대부분의 Spring 애플리케이션 | 성능이 중요한 시스템 (예: 대용량 트랜잭션) |
🔥 결론
- AspectJ는 Java에서 AOP를 구현하는 가장 강력한 프레임워크이다.
- Spring AOP는 AspectJ 문법을 차용했지만, 프록시 기반으로 동작한다.
- AspectJ는 컴파일 타임(CTW) 또는 로드 타임(LTW) 위빙이 가능하며, Spring AOP보다 강력하지만 설정이 복잡하다.
- 일반적인 Spring 프로젝트에서는 Spring AOP로도 충분하지만, 성능이 중요한 경우 AspectJ(CTW/LTW)를 고려할 수 있다.
🚀 Spring AOP를 기본으로 사용하고, 더 강력한 AOP 기능이 필요할 때 AspectJ를 고려!