Spring AOP, AspectJ 비교

방지환·2026년 1월 27일

Java

목록 보기
19/19

AOP(Aspect-Oriented Programming)란?

AOP(관점 지향 프로그래밍)는 프로그램의 핵심 비즈니스 로직과 부가 기능을 분리하여 모듈화하는 프로그래밍 기법입니다. 로깅, 트랜잭션 관리, 보안 등과 같이 여러 곳에서 반복적으로 사용되는 공통 관심사를 별도의 모듈로 분리하여 코드의 재사용성과 유지보수성을 높입니다.

간단한 비유

식당에서 음식을 만든다고 생각해보세요.

  • 핵심 기능: 요리하기 (비즈니스 로직)
  • 부가 기능: 손 씻기, 앞치마 입기, 주방 정리하기 (공통 관심사)

AOP는 이 부가 기능들을 따로 빼서 자동으로 실행되게 만드는 것입니다.

기본 용어 (쉽게 이해하기)

1. Aspect (애스펙트) = "부가 기능 모음"

여러 곳에서 쓰이는 공통 기능을 모아놓은 클래스

  • 예: 로깅 기능, 보안 검사, 실행 시간 측정

2. Join Point (조인 포인트) = "적용 가능한 시점"

부가 기능을 끼워넣을 수 있는 지점

  • 예: 메서드 실행 전, 메서드 실행 후, 예외 발생 시

3. Advice (어드바이스) = "실제 실행되는 코드"

언제, 무엇을 할지 정의한 것

  • @Before: 메서드 실행 전에 실행
  • @After: 메서드 실행 후에 실행
  • @Around: 메서드 실행 전후를 모두 제어

4. Pointcut (포인트컷) = "어디에 적용할지"

어떤 메서드에 부가 기능을 적용할지 선택하는 표현식

  • 예: "UserService의 모든 메서드", "save로 시작하는 모든 메서드"

Part 1: Spring AOP (초보자 추천)

Spring AOP란?

Spring Framework에 내장된 AOP 기능으로, 별도 설정 없이 간단하게 사용할 수 있습니다.

특징

설정이 쉬움 - Spring Boot에서 바로 사용 가능
메서드에만 적용 - 메서드 실행 전후에만 동작
프록시 방식 - 원본 객체를 감싸는 방식
성능 부담 적음 - 런타임에 동작

언제 사용하나요?

  • 메서드 실행 전후에 로그를 남기고 싶을 때
  • 메서드 실행 시간을 측정하고 싶을 때
  • 메서드에 권한 체크를 추가하고 싶을 때
  • 트랜잭션 관리를 자동화하고 싶을 때

1단계: 의존성 추가 (Maven)

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

2단계: Aspect 클래스 만들기

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);
    }
}

3단계: 일반 서비스 클래스 작성

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에는 로그 코드가 없는데도 자동으로 로그가 찍힙니다!

실전 예제 1: 실행 시간 측정

@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;
    }
}

실전 예제 2: 커스텀 어노테이션으로 간단하게

// 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);
    }
}

Pointcut 표현식 쉽게 이해하기

// 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 * *(..))")

패턴 설명:

  • * = 모든 것
  • .. = 모든 하위 패키지 또는 모든 파라미터
  • *(..) = 이름이 무엇이든, 파라미터가 무엇이든

Spring AOP의 제약사항

❌ 동작하지 않는 경우

@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 정리

장점

  • 설정이 매우 간단함
  • Spring과 완벽하게 통합됨
  • 대부분의 경우 충분히 사용 가능

단점

  • 메서드 실행에만 적용 가능
  • private, static, final 메서드는 안됨
  • 같은 클래스 내부 호출은 안됨

결론: 일반적인 웹 애플리케이션에서는 Spring AOP로 충분합니다!


Part 2: AspectJ (고급 사용자용)

AspectJ란?

가장 강력한 AOP 프레임워크로, Spring AOP보다 훨씬 많은 기능을 제공합니다.

특징

모든 곳에 적용 가능 - 메서드뿐만 아니라 필드, 생성자 등
더 강력함 - private 메서드, static 메서드도 가능
바이트코드 조작 - 컴파일 시점에 코드를 직접 수정
⚠️ 설정이 복잡함 - 추가 도구와 설정 필요

Spring AOP vs AspectJ 비교

구분Spring AOPAspectJ
난이도쉬움 ⭐어려움 ⭐⭐⭐
적용 범위메서드만메서드, 필드, 생성자 등 모두
동작 시점런타임컴파일 시점 또는 클래스 로드 시점
private 메서드❌ 불가능✅ 가능
static 메서드❌ 불가능✅ 가능
같은 클래스 내부 호출❌ 불가능✅ 가능
설정간단복잡
성능약간 느림빠름
권장 대상대부분의 경우특수한 경우만

언제 AspectJ를 사용하나요?

  1. private 메서드에도 AOP를 적용해야 할 때
  2. 필드 접근을 감시해야 할 때
  3. 생성자 호출을 가로채야 할 때
  4. static 메서드에 AOP를 적용해야 할 때
  5. Spring을 사용하지 않는 프로젝트일 때

AspectJ 설정 (간단히만 소개)

1. 의존성 추가

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

2. AspectJ로만 가능한 기능 예시

@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();
    }
}

AspectJ 위빙 방식

  1. 컴파일 타임 위빙 (CTW)

    • 컴파일할 때 바이트코드를 직접 수정
    • 가장 빠름
    • AspectJ 컴파일러 필요
  2. 포스트 컴파일 타임 위빙

    • 이미 컴파일된 .class 파일을 수정
    • jar 파일에도 적용 가능
  3. 로드 타임 위빙 (LTW)

    • 클래스를 메모리에 로드할 때 수정
    • Spring에서 가장 많이 사용
    • -javaagent 옵션 필요

AspectJ 정리

장점

  • 모든 Join Point 지원 (필드, 생성자 등)
  • private, static 메서드 지원
  • 같은 클래스 내부 호출 가능
  • 성능이 더 좋음

단점

  • 설정이 복잡함
  • 학습 곡선이 높음
  • 빌드 과정이 복잡해짐

결론: 특별한 이유가 없다면 Spring AOP를 사용하세요!


어떤 것을 선택해야 할까?

🎯 Spring AOP를 선택하세요 (90% 케이스)

  • 일반적인 웹 애플리케이션
  • 메서드 실행 전후에 뭔가 하고 싶을 때
  • 로깅, 트랜잭션, 권한 체크 등
  • Spring Boot 사용 중

🎯 AspectJ를 선택하세요 (10% 케이스)

  • private 메서드에도 적용해야 할 때
  • 필드 접근을 감시해야 할 때
  • Spring 없이 순수 Java 프로젝트일 때
  • 매우 복잡한 AOP 요구사항이 있을 때

실전 활용 가이드

1. 로깅 Aspect (가장 많이 사용)

@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;
        }
    }
}

2. 권한 체크 Aspect

// 커스텀 어노테이션
@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);
    }
}

3. API 응답 시간 모니터링

@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;
        }
    }
}

4. 메서드 재시도 (Retry)

// 재시도 어노테이션
@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);
    }
}

자주 하는 실수와 해결법

❌ 실수 1: 같은 클래스에서 메서드 호출

@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("실행");
    }
}

❌ 실수 2: private 메서드에 적용

@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("실행");
    }
}

❌ 실수 3: Pointcut 표현식 오타

@Pointcut("execution(* com.example.service.UserService.*(..))")  // ✅ 올바름
@Pointcut("execution(* com.example.service.UserService**(..))")   // ❌ * 하나 빠짐
@Pointcut("execution(* com.example..service.UserService.*(..))")  // ❌ .. 위치 잘못됨

마무리

Spring AOP 핵심 정리

  1. @Aspect + @Component로 Aspect 클래스 만들기
  2. @Before, @After, @Around로 실행 시점 선택
  3. Pointcut 표현식으로 어디에 적용할지 정함
  4. 커스텀 어노테이션으로 더 간단하게 사용

가장 많이 쓰는 패턴

// 패턴 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());
}

다음 단계

  1. 직접 간단한 로깅 Aspect 만들어보기
  2. 실행 시간 측정 Aspect 적용해보기
  3. 커스텀 어노테이션 만들어서 사용해보기
  4. 실제 프로젝트에 점진적으로 적용하기

참고 자료

0개의 댓글