AOP(관점 지향 프로그래밍)는 프로그램의 핵심 비즈니스 로직과 부가 기능을 분리하여 모듈화하는 프로그래밍 기법입니다. 로깅, 트랜잭션 관리, 보안 등과 같이 여러 곳에서 반복적으로 사용되는 공통 관심사를 별도의 모듈로 분리하여 코드의 재사용성과 유지보수성을 높입니다.
식당에서 음식을 만든다고 생각해보세요.
AOP는 이 부가 기능들을 따로 빼서 자동으로 실행되게 만드는 것입니다.
여러 곳에서 쓰이는 공통 기능을 모아놓은 클래스
부가 기능을 끼워넣을 수 있는 지점
언제, 무엇을 할지 정의한 것
어떤 메서드에 부가 기능을 적용할지 선택하는 표현식
Spring Framework에 내장된 AOP 기능으로, 별도 설정 없이 간단하게 사용할 수 있습니다.
✅ 설정이 쉬움 - Spring Boot에서 바로 사용 가능
✅ 메서드에만 적용 - 메서드 실행 전후에만 동작
✅ 프록시 방식 - 원본 객체를 감싸는 방식
✅ 성능 부담 적음 - 런타임에 동작
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect // 이 클래스가 AOP 기능을 담당한다고 선언
@Component // Spring Bean으로 등록
public class SimpleLoggingAspect {
// 어디에 적용할지 정의 (UserService의 모든 메서드)
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 메서드 실행 전에 로그 출력
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info(">>> 메서드 시작: {}", methodName);
}
// 메서드 실행 후에 로그 출력
@After("userServiceMethods()")
public void logAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("<<< 메서드 종료: {}", methodName);
}
}
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUser(Long id) {
System.out.println("사용자 조회 중...");
return "홍길동";
}
public void saveUser(String name) {
System.out.println("사용자 저장 중: " + name);
}
}
>>> 메서드 시작: getUser
사용자 조회 중...
<<< 메서드 종료: getUser
핵심: UserService에는 로그 코드가 없는데도 자동으로 로그가 찍힙니다!
@Aspect
@Component
@Slf4j
public class PerformanceAspect {
// UserService의 모든 메서드 실행 시간 측정
@Around("execution(* com.example.service.UserService.*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 시작 시간
long start = System.currentTimeMillis();
// 실제 메서드 실행
Object result = joinPoint.proceed();
// 종료 시간
long end = System.currentTimeMillis();
log.info("{} 실행 시간: {}ms",
joinPoint.getSignature().getName(),
(end - start));
return result;
}
}
// 1. 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
// 2. Aspect 작성
@Aspect
@Component
@Slf4j
public class CustomAnnotationAspect {
@Around("@annotation(LogExecutionTime)")
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
log.info("{} 실행 시간: {}ms",
joinPoint.getSignature().getName(), duration);
return result;
}
}
// 3. 사용하고 싶은 메서드에만 적용
@Service
public class ProductService {
@LogExecutionTime // 이 어노테이션만 붙이면 끝!
public List<Product> searchProducts(String keyword) {
// 상품 검색 로직
return productRepository.search(keyword);
}
public Product getProduct(Long id) {
// 이 메서드는 시간 측정 안 함
return productRepository.findById(id);
}
}
// 1. 특정 클래스의 모든 메서드
@Pointcut("execution(* com.example.service.UserService.*(..))")
// 2. 특정 패키지의 모든 클래스, 모든 메서드
@Pointcut("execution(* com.example.service..*(..))")
// 3. 이름이 "save"로 시작하는 모든 메서드
@Pointcut("execution(* save*(..))")
// 4. 특정 어노테이션이 붙은 메서드만
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
// 5. public 메서드만
@Pointcut("execution(public * *(..))")
패턴 설명:
* = 모든 것.. = 모든 하위 패키지 또는 모든 파라미터*(..) = 이름이 무엇이든, 파라미터가 무엇이든@Service
public class UserService {
@LogExecutionTime
public void method1() {
// 같은 클래스 내부에서 호출
this.method2(); // ❌ AOP 적용 안됨!
}
@LogExecutionTime
private void method2() { // ❌ private 메서드는 AOP 적용 안됨!
System.out.println("실행");
}
}
@Service
public class UserService {
// 다른 서비스를 주입받아서 호출
@Autowired
private AnotherService anotherService;
@LogExecutionTime
public void method1() {
anotherService.method2(); // ✅ AOP 적용됨!
}
}
@Service
public class AnotherService {
@LogExecutionTime
public void method2() { // ✅ public이고 다른 클래스에서 호출
System.out.println("실행");
}
}
장점
단점
결론: 일반적인 웹 애플리케이션에서는 Spring AOP로 충분합니다!
가장 강력한 AOP 프레임워크로, Spring AOP보다 훨씬 많은 기능을 제공합니다.
✅ 모든 곳에 적용 가능 - 메서드뿐만 아니라 필드, 생성자 등
✅ 더 강력함 - private 메서드, static 메서드도 가능
✅ 바이트코드 조작 - 컴파일 시점에 코드를 직접 수정
⚠️ 설정이 복잡함 - 추가 도구와 설정 필요
| 구분 | Spring AOP | AspectJ |
|---|---|---|
| 난이도 | 쉬움 ⭐ | 어려움 ⭐⭐⭐ |
| 적용 범위 | 메서드만 | 메서드, 필드, 생성자 등 모두 |
| 동작 시점 | 런타임 | 컴파일 시점 또는 클래스 로드 시점 |
| private 메서드 | ❌ 불가능 | ✅ 가능 |
| static 메서드 | ❌ 불가능 | ✅ 가능 |
| 같은 클래스 내부 호출 | ❌ 불가능 | ✅ 가능 |
| 설정 | 간단 | 복잡 |
| 성능 | 약간 느림 | 빠름 |
| 권장 대상 | 대부분의 경우 | 특수한 경우만 |
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
@Aspect
public class AdvancedAspect {
// 필드 접근 감시 (Spring AOP 불가능)
@Before("get(private String com.example.model.User.password)")
public void beforePasswordAccess() {
log.warn("비밀번호 필드에 접근!");
}
// 생성자 호출 감시 (Spring AOP 불가능)
@Before("execution(com.example.model.User.new(..))")
public void beforeUserCreation() {
log.info("User 객체 생성됨");
}
// static 메서드에 적용 (Spring AOP 불가능)
@Around("execution(public static * com.example.utils..*(..))")
public Object aroundStaticMethod(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("static 메서드 실행");
return joinPoint.proceed();
}
}
컴파일 타임 위빙 (CTW)
포스트 컴파일 타임 위빙
로드 타임 위빙 (LTW)
-javaagent 옵션 필요장점
단점
결론: 특별한 이유가 없다면 Spring AOP를 사용하세요!
@Aspect
@Component
@Slf4j
public class LoggingAspect {
// 모든 Controller에 적용
@Around("execution(* com.example.controller..*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info(">>> [{}] {} 호출 / 파라미터: {}", className, methodName, args);
try {
Object result = joinPoint.proceed();
log.info("<<< [{}] {} 완료 / 결과: {}", className, methodName, result);
return result;
} catch (Exception e) {
log.error("!!! [{}] {} 실패 / 예외: {}", className, methodName, e.getMessage());
throw e;
}
}
}
// 커스텀 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAdmin {
}
// Aspect
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(RequireAdmin)")
public void checkAdmin() {
// 현재 사용자가 관리자인지 확인
boolean isAdmin = SecurityContextHolder.getContext()
.getAuthentication()
.getAuthorities()
.stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
if (!isAdmin) {
throw new SecurityException("관리자 권한이 필요합니다!");
}
}
}
// 사용
@RestController
public class AdminController {
@RequireAdmin // 이 어노테이션으로 간단하게 권한 체크
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
}
@Aspect
@Component
@Slf4j
public class ApiMonitoringAspect {
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping)")
public Object monitorApi(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String apiName = joinPoint.getSignature().toShortString();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
// 느린 API 경고 (1초 이상)
if (duration > 1000) {
log.warn("⚠️ 느린 API: {} ({}ms)", apiName, duration);
} else {
log.info("✅ API: {} ({}ms)", apiName, duration);
}
return result;
} catch (Exception e) {
long duration = System.currentTimeMillis() - start;
log.error("❌ API 실패: {} ({}ms) - {}", apiName, duration, e.getMessage());
throw e;
}
}
}
// 재시도 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int maxAttempts() default 3;
long delay() default 1000; // 밀리초
}
// Aspect
@Aspect
@Component
@Slf4j
public class RetryAspect {
@Around("@annotation(retry)")
public Object retry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
int maxAttempts = retry.maxAttempts();
long delay = retry.delay();
for (int i = 1; i <= maxAttempts; i++) {
try {
return joinPoint.proceed();
} catch (Exception e) {
if (i == maxAttempts) {
log.error("재시도 {}번 모두 실패", maxAttempts);
throw e;
}
log.warn("시도 {}/{} 실패, {}ms 후 재시도", i, maxAttempts, delay);
Thread.sleep(delay);
}
}
return null;
}
}
// 사용
@Service
public class ExternalApiService {
@Retry(maxAttempts = 3, delay = 2000)
public String callExternalApi() {
// 외부 API 호출 (실패할 수 있음)
return restTemplate.getForObject("https://api.example.com/data", String.class);
}
}
@Service
public class UserService {
@LogExecutionTime
public void method1() {
this.method2(); // ❌ AOP 적용 안됨!
}
@LogExecutionTime
public void method2() {
System.out.println("실행");
}
}
✅ 해결: 다른 Bean으로 분리하거나 자기 자신을 주입
@Service
public class UserService {
@Autowired
private UserService self; // 자기 자신 주입
@LogExecutionTime
public void method1() {
self.method2(); // ✅ AOP 적용됨!
}
@LogExecutionTime
public void method2() {
System.out.println("실행");
}
}
@Service
public class UserService {
@LogExecutionTime
private void privateMethod() { // ❌ Spring AOP는 private 안됨!
System.out.println("실행");
}
}
✅ 해결: public으로 변경하거나 AspectJ 사용
@Service
public class UserService {
@LogExecutionTime
public void publicMethod() { // ✅ public으로 변경
System.out.println("실행");
}
}
@Pointcut("execution(* com.example.service.UserService.*(..))") // ✅ 올바름
@Pointcut("execution(* com.example.service.UserService**(..))") // ❌ * 하나 빠짐
@Pointcut("execution(* com.example..service.UserService.*(..))") // ❌ .. 위치 잘못됨
// 패턴 1: 실행 시간 측정
@Around("execution(* com.example.service..*(..))")
public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
log.info("실행 시간: {}ms", System.currentTimeMillis() - start);
return result;
}
// 패턴 2: 로그 남기기
@Before("execution(* com.example.controller..*(..))")
public void logBefore(JoinPoint joinPoint) {
log.info("메서드 실행: {}", joinPoint.getSignature().getName());
}
// 패턴 3: 예외 처리
@AfterThrowing(pointcut = "execution(* com.example.service..*(..))", throwing = "ex")
public void handleException(JoinPoint joinPoint, Exception ex) {
log.error("예외 발생: {} - {}", joinPoint.getSignature().getName(), ex.getMessage());
}