Spring AOP

2025. 4. 26. 12:08·BackEnd/Spring Framework

AOP는 코드의 중복을 획기적으로 줄이고 핵심 비즈니스 로직에 집중할 수 있게 해주는 강력한 기술입니다. 제가 이해한 AOP의 개념과 실제 활용법에 대해 공유하려 합니다.

AOP가 왜 필요할까

일반적인 서비스 클래스의 메서드를 살펴보면, 크게 두 부분으로 구성됩니다.

  1. 비즈니스 로직 - 실제로 우리가 수행하려는 핵심 작업 (데이터 생성, 조회 등)
  2. 부가 기능 - 핵심 작업을 수행하기 위해 필요한 보조 작업 (로깅, DB 접속, 트랜잭션 처리, 예외 처리 등)

문제는 이러한 부가 기능들이 여러 메서드에서 계속 반복된다는 점입니다. 예를 들어, 모든 DB 작업 전후로 로깅을 해야 한다면, 각 메서드마다 로깅 코드를 중복해서 작성해야 하죠. 이런 중복 코드는 유지보수를 어렵게 만들고, 핵심 비즈니스 로직을 파악하기 어렵게 합니다.

AOP는 이러한 '횡단 관심사(Cross-cutting Concerns)'를 분리하여 모듈화하는 프로그래밍 패러다임입니다. 즉, 여러 곳에 흩어진 부가 기능을 한 곳으로 모아 관리할 수 있게 해주는 것이죠.

AOP의 핵심 개념

AOP를 이해하기 위해서는 몇 가지 핵심 용어를 알아야 합니다.

1. Target

  • AOP가 적용될 객체입니다. 핵심 비즈니스 로직을 포함한 Bean 객체가 됩니다.

2. Aspect

  • 횡단 관심사를 모듈화한 단위입니다. 하나 이상의 Advice를 포함합니다.
  • Spring에서는 @Aspect 애노테이션을 사용하여 클래스를 Aspect로 지정합니다.

3. Advice

  • 특정 Join Point에서 실행되는 코드입니다. 실제 부가 기능을 정의합니다.
  • Spring에서는 @Before, @After, @Around 등의 애노테이션으로 Advice를 정의합니다.

4. Join Point

  • Advice가 적용될 수 있는 지점입니다.
  • Spring AOP에서는 메서드 실행 지점으로 제한됩니다.

5. Pointcut

  • Advice를 적용할 대상을 선택하는 표현식입니다.
  • 예: "com.example.service 패키지의 모든 메서드" 등

6. Weaving

  • Aspect 코드를 비즈니스 로직 코드에 적용하는 과정입니다.
  • Spring에서는 런타임에 Proxy를 통해 Weaving이 이루어집니다.

Spring AOP의 동작 원리 Proxy 패턴

Spring AOP는 Proxy 패턴을 사용하여 구현됩니다. 간단히 말해, 대상 객체를 감싸는 프록시 객체를 생성하고, 이 프록시가 요청을 가로채서 부가 기능을 수행한 뒤, 대상 객체에 요청을 전달하는 방식입니다.

Spring에서는 기본적으로 CGLIB(Code Generation Library)를 사용하여 클래스 기반 프록시를 생성합니다. 이 프록시는 원본 클래스를 상속받아 생성되므로, 원본 클래스의 모든 메서드를 오버라이드할 수 있습니다.

Spring AOP 프록시 생성 과정

  1. 빈 등록 및 초기화
    • Spring 컨테이너가 빈 객체를 생성하고 초기화합니다.
  2. BeanPostProcessor 작동
    • AOP를 위한 AbstractAutoProxyCreator(BeanPostProcessor의 구현체)가 동작합니다.
    • 이 단계에서 프록시 적용 대상인지 판단합니다(어노테이션, 설정 등 확인).
  3. 프록시 객체 생성
    • 대상 빈이 인터페이스를 구현했다면 → JDK 동적 프록시 사용
    • 인터페이스가 없는 클래스라면 → CGLIB 프록시(상속 기반) 사용
  4. 빈 교체
    • 원본 빈 대신 생성된 프록시 객체가 Spring 컨테이너에 등록됩니다.
    • 다른 빈에서 의존성 주입을 요청하면 프록시 객체가 제공됩니다.

프록시 객체 내부 구조

  1. 타겟 참조: 프록시는 원본 빈(타겟)에 대한 참조를 내부적으로 가지고 있습니다.
  2. Advice 참조: 공통 기능을 담당하는 Advice 객체에 대한 참조도 가지고 있습니다.
    • 중요: Advice 객체는 싱글톤으로 관리되어 여러 프록시가 공유합니다(로직 중복 방지)

런타임 실행 흐름

클라이언트 → 프록시 객체 → [전처리 Advice] → 원본 객체 → [후처리 Advice] → 프록시 객체 → 클라이언트

구체적인 예시(트랜잭션)

  1. 클라이언트가 서비스 메서드 호출 (실제로는 프록시를 호출)
  2. 프록시는 트랜잭션 시작 (begin transaction)
  3. 프록시가 원본 빈의 실제 비즈니스 메서드 호출
  4. 비즈니스 로직 실행 완료
  5. 프록시로 제어 반환
  6. 예외 없으면 트랜잭션 커밋, 예외 발생 시 롤백
  7. 결과를 클라이언트에게 반환

실제 AOP 코드 작성해보기

간단한 예제로 AOP를 적용해보겠습니다. DAO 메서드 호출 시 로깅을 자동으로 수행하는 AOP를 작성해보겠습니다.

먼저, Spring Boot 프로젝트에 AOP 의존성을 추가해야 합니다.

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

그리고 로깅을 담당할 Aspect 클래스를 작성합니다.

@Component // 스프링 빈으로 등록
@Aspect    // Aspect로 선언
@Slf4j     // Lombok의 로깅 기능 사용
public class LoggingAspect {
    
    // DAO 패키지의 모든 메서드 실행 전에 로깅
    @Before("execution(* com.example.dao..*(..))")
    public void logBeforeDao(JoinPoint jp) {
        log.info("DAO 메서드 호출: {} 파라미터: {}", 
                 jp.getSignature().toShortString(), 
                 Arrays.toString(jp.getArgs()));
    }
}

이제 com.example.dao 패키지의 모든 메서드가 실행되기 전에 로그가 자동으로 출력됩니다. 비즈니스 로직에는 로깅 코드가 없어도 됩니다.

Pointcut 표현식 작성법

Pointcut은 Advice를 적용할 대상을 선택하는 표현식입니다. Spring AOP에서는 다양한 Pointcut 지정자를 제공합니다.

주요 Pointcut 지정자

  1. execution: 메서드 시그니처를 기반으로 매칭
     
    execution(반환타입 패키지.클래스.메서드(파라미터))
  2. within: 특정 타입에 속한 모든 메서드 매칭
     
    within(com.example.service.*)
  3. bean: 특정 이름의 빈에 매칭
     
    bean(userService)
  4. @annotation: 특정 애노테이션이 적용된 메서드 매칭
     
    @annotation(org.springframework.transaction.annotation.Transactional)

execution 표현식 예제

execution 표현식은 가장 많이 사용되는 Pointcut 지정자입니다. 몇 가지 예제를 통해 알아보겠습니다.

  • execution(* com.example.service.*.*(..)): service 패키지의 모든 클래스의 모든 메서드
  • execution(* com.example.service..*(..)): service 패키지 및 하위 패키지의 모든 메서드
  • execution(* com.example.service.UserService.find*(..)): UserService의 find로 시작하는 모든 메서드
  • execution(* *..*Service.*(..)): 이름이 Service로 끝나는 모든 클래스의 모든 메서드
  • execution(void com.example.*.set*(..)): 리턴 타입이 void이고 이름이 set으로 시작하는 모든 메서드

Pointcut 표현식을 정교하게 작성하면 원하는 대상에만 정확히 AOP를 적용할 수 있습니다.

Advice 유형별 활용법

Spring AOP는 메서드 실행 시점에 따라 다양한 Advice 유형을 제공합니다.

@Before

타겟 메서드 실행 전에 동작합니다.

@Before("execution(* com.example.service.UserService.saveUser(..)) && args(user)")
public void beforeSaveUser(JoinPoint jp, User user) {
    // 사용자 저장 전 비밀번호 암호화
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    log.info("사용자 저장 전 비밀번호 암호화 완료");
}

@AfterReturning

타겟 메서드가 정상적으로 결과를 반환한 후 동작합니다.

@AfterReturning(
    value = "execution(* com.example.service.UserService.getUser(*))",
    returning = "result"
)
public void afterReturnGetUser(JoinPoint jp, User result) {
    // 개인정보 마스킹 처리
    if (result != null) {
        result.setPhoneNumber(maskPhoneNumber(result.getPhoneNumber()));
    }
}

@AfterThrowing

타겟 메서드가 예외를 던질 때 동작합니다.

@AfterThrowing(
    value = "execution(* com.example.service.*.*(..))",
    throwing = "ex"
)
public void handleException(JoinPoint jp, Exception ex) {
    log.error("메서드 {} 실행 중 예외 발생: {}", 
              jp.getSignature().toShortString(), 
              ex.getMessage());
    // 관리자에게 알림 발송 등의 처리
}

@After

예외 발생 여부와 상관없이 타겟 메서드 종료 후 항상 동작합니다.

@After("execution(* com.example.service.ResourceService.*(..))")
public void releaseResources(JoinPoint jp) {
    log.info("리소스 정리 작업 수행");
    // 리소스 정리 로직
}

@Around

가장 강력한 Advice 유형으로, 타겟 메서드 호출을 완전히 제어할 수 있습니다.

@Around("execution(* com.example.service.ProductService.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    long startTime = System.currentTimeMillis();
    
    // 타겟 메서드 호출
    Object result;
    try {
        result = pjp.proceed();
    } catch (Exception e) {
        log.error("메서드 실행 중 예외 발생", e);
        throw e;
    }
    
    long endTime = System.currentTimeMillis();
    log.info("메서드 {} 실행 시간: {}ms", 
             pjp.getSignature().toShortString(), 
             (endTime - startTime));
    
    return result;
}

@Around는 다른 모든 Advice 유형을 대체할 수 있는 가장 유연한 방식이지만, 복잡도가 높아질 수 있으므로 필요한 경우에만 사용하는 것이 좋습니다.

실전 활용 사례

AOP는 다양한 실제 상황에서 활용할 수 있습니다. 몇 가지 대표적인 사례를 소개합니다.

메서드 실행 시간 측정

애플리케이션의 성능을 모니터링하기 위해 메서드 실행 시간을 측정할 수 있습니다.

@Aspect
@Component
@Slf4j
public class PerformanceAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long executionTime = System.currentTimeMillis() - start;
        log.info("{} 실행 시간: {}ms", joinPoint.getSignature(), executionTime);
        
        return result;
    }
}

API 요청/응답 로깅

RESTful API의 요청과 응답을 로깅하여 디버깅에 활용할 수 있습니다.

@Aspect
@Component
@Slf4j
public class ApiLoggingAspect {
    
    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object logApiCalls(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        
        log.info("API 요청: {} {}", request.getMethod(), request.getRequestURI());
        log.info("요청 파라미터: {}", Arrays.toString(joinPoint.getArgs()));
        
        Object result = joinPoint.proceed();
        
        log.info("API 응답: {}", result);
        
        return result;
    }
}

사용자 권한 검사

특정 메서드 호출 시 사용자 권한을 검사하는 로직을 AOP로 구현할 수 있습니다.

@Aspect
@Component
public class SecurityAspect {
    
    @Autowired
    private SecurityService securityService;
    
    @Before("@annotation(com.example.annotation.RequireAdmin)")
    public void checkAdminAccess(JoinPoint joinPoint) {
        if (!securityService.isCurrentUserAdmin()) {
            throw new AccessDeniedException("관리자 권한이 필요합니다.");
        }
    }
}

이 경우 @RequireAdmin 애노테이션을 정의하고, 관리자 권한이 필요한 메서드에 적용하면 됩니다.

Spring 내부의 AOP 활용

Spring 프레임워크 자체도 내부적으로 AOP를 활용하여 다양한 기능을 제공합니다.

@Configuration

Spring에서 @Configuration 애노테이션은 하나 이상의 Bean을 포함하고 Bean의 싱글톤을 보장합니다. 이는 CGLIB 기반의 프록시로 처리되기 때문에, @Bean 메서드가 호출될 때 이미 생성된 Bean이 있다면 그것을 반환합니다.

싱글톤을 보장하는 방법: @Component와의 비교

  • @Configuration
    • CGLIB 프록시를 사용하여 Bean 메서드를 감싸고 관리합니다.
    • Bean 메서드가 여러 번 호출되더라도 Spring 컨테이너에서 이미 생성된 동일한 인스턴스를 반환합니다.
  • @Component
    • 프록시 처리가 되지 않습니다.
    • Bean 메서드가 호출될 때마다 실제 메서드 로직이 그대로 실행되어 새로운 인스턴스가 생성됩니다.
// @Configuration 사용 시
@Configuration
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(serviceB()); // 여기서 serviceB()는 이미 생성된 빈을 반환
    }
    
    @Bean
    public ServiceB serviceB() {
        System.out.println("Creating a new ServiceB instance");
        return new ServiceB();
    }
}

// @Component 사용 시
@Component
public class AppConfig {
    @Bean
    public ServiceA serviceA() {
        return new ServiceA(serviceB()); // 여기서 serviceB()는 새로운 인스턴스 생성
    }
    
    @Bean
    public ServiceB serviceB() {
        System.out.println("Creating a new ServiceB instance");
        return new ServiceB();
    }
}

@Transactional

가장 대표적인 예로, @Transactional 애노테이션은 AOP를 통해 트랜잭션 관리를 자동화합니다.

@Service
public class OrderService {
    
    @Transactional
    public void placeOrder(Order order) {
        // 주문 처리 로직
        // 이 메서드 실행 전후로 트랜잭션 시작/종료가 자동으로 처리됨
    }
}

@Async

비동기 메서드 실행도 AOP를 통해 구현됩니다. 별도의 Thread나 ThreadPool을 직접 생성하지 않고도 비동기 처리가 가능합니다.

@Service
public class EmailService {
    
    @Async
    public void sendNotificationEmail(String email, String message) {
        // 이메일 전송 로직
        // 이 메서드는 별도의 스레드에서 비동기적으로 실행됨
    }
}

@Cacheable

메서드 결과를 캐싱하는 기능도 AOP로 구현되어 있습니다.

@Service
public class ProductService {
    
    @Cacheable("products")
    public Product getProductById(Long id) {
        // 상품 조회 로직 (DB 조회 등)
        // 결과가 자동으로 캐시되어 동일한 id로 요청 시 DB 조회를 생략
    }
}

마무리

AOP는 개념을 이해하고 나면 코드의 중복을 획기적으로 줄이고 비즈니스 로직의 가독성을 높일 수 있는 강력한 도구입니다. 특히 로깅, 보안, 성능 측정과 같은 공통 관심사를 처리할 때 AOP의 진가가 발휘됩니다.

'BackEnd > Spring Framework' 카테고리의 다른 글

Interceptor | ErrorPage | FileUpload  (0) 2025.04.30
Spring MVC  (0) 2025.04.27
SpringBoot  (3) 2025.04.24
SLF4J와 JUnit  (2) 2025.04.24
Spring Framework 개요  (0) 2025.04.23
'BackEnd/Spring Framework' 카테고리의 다른 글
  • Interceptor | ErrorPage | FileUpload
  • Spring MVC
  • SpringBoot
  • SLF4J와 JUnit
leve68
leve68
leve68 님의 블로그 입니다.
  • leve68
    leve68
    leve68
  • 전체
    오늘
    어제
    • 분류 전체보기
      • BackEnd
        • Spring Framework
        • Database
      • FrontEnd
        • JavaScript
        • Vue
      • Infra
        • Docker
        • CI CD
      • CS
        • Algorithm
      • Project
        • Web
  • 인기 글

  • 태그

    docker
    sql
    MySQL
    SSAFY
    DATABASE
    Spring
    spring security
    MyBatis
    springboot
    compose
  • 링크

    • github
    • portfolio
  • 최근 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
leve68
Spring AOP
상단으로

티스토리툴바