AOP (관점 지향 프로그래밍)

Jerry·2025년 8월 19일

AOP (관점 지향 프로그래밍)

AOP는 Aspect-Oriented Programming의 약자로 관점 지향 프로그래밍을 의미합니다. 객체 지향 프로그래밍(OOP)을 보완하는 개념으로, 애플리케이션의 핵심 기능(Core Concern)과 부가 기능(횡단 관심사)을 분리하여 모듈화하는 것이 목표입니다. 간단히 말해, 여러 클래스에 공통으로 적용되는 보조 기능(예: 로깅, 보안, 트랜잭션 등)을 별도 모듈로 만들고, 필요할 때 결합하는 방식입니다. OOP에서 클래스가 모듈화의 단위라면, AOP에서는 Aspect(관점)가 모듈화의 단위가 됩니다.

AOP에서는 각 관심사에 따라 코드를 모듈화합니다. 애플리케이션을 개발하다 보면 여러 부분에서 반복되는 코드를 발견할 수 있는데, 이러한 반복적이고 공통적인 기능을 횡단 관심사(Cross-cutting Concern)라고 합니다. AOP를 활용하면 이러한 횡단 관심사를 분리하여 관리하고, 핵심 비즈니스 로직(핵심 관심사)과 깨끗하게 분리할 수 있습니다.

AOP의 주요 개념

  • Aspect (관점): 여러 클래스에 공통으로 적용되는 관심사(부가 기능)를 한 모듈로 만든 것입니다. 예를 들어 로깅, 보안, 트랜잭션 관리 등이 관점에 해당합니다. Spring AOP에서는 일반 클래스나 @Aspect 애노테이션이 붙은 클래스로 구현합니다
  • Core Concern (핵심 관심사): 애플리케이션의 핵심 비즈니스 로직을 담당하는 부분입니다. 예를 들어 전자상거래 애플리케이션의 주문 처리, 결제 로직 등이 핵심 관심사입니다. AOP에서는 핵심 관심사 코드를 변경하지 않으면서 부가 기능을 적용할 수 있도록 합니다.
  • Cross-cutting Concern (횡단 관심사): 여러 모듈이나 클래스에 공통적으로 나타나는 부가 기능입니다. 횡단 관심사는 핵심 관심사에 비해 부수적인 기능이지만, 여러 곳에서 반복 적용되어 공통 관심사라고도 부릅니다. 예로 로깅, 인증/인가, 트랜잭션, 캐싱 등이 있으며, 이러한 기능은 여러 클래스나 메서드에 걸쳐 공통적으로 사용됩니다.
  • Target Object (타겟 객체): 부가 기능(Aspect)이 적용되는 대상 객체를 말합니다. 쉽게 말해 어드바이스가 적용될 실제 객체입니다. Spring AOP에서는 런타임에 프록시(proxy) 객체를 만들어 타겟 객체를 감싸기 때문에, 최종적으로 타겟 객체는 프록시를 통해 제어됩니다.
  • Join Point (조인 포인트): 어드바이스(부가 기능 코드)가 삽입될 수 있는 프로그램 실행 상의 특정 지점입니다. 예를 들어 메서드 호출 시점, 예외 처리 시점, 필드 접근 시점 등이 조인 포인트가 될 수 있습니다. Spring AOP에서는 조인 포인트가 항상 메서드 실행 시점입니다. (필드 접근 같은 조인 포인트는 Spring AOP에서는 지원하지 않음)
  • Advice (어드바이스): 조인 포인트에서 실행되는 부가 기능 코드를 의미합니다. 어떤 시점에 실행되느냐에 따라 다양한 종류의 어드바이스가 있습니다:
    • Before Advice: 조인 포인트(메서드 실행) 전에 실행됩니다. (메서드 실행을 막을 순 없고, 예외를 던지는 경우만 실행을 중단시킬 수 있습니다.)
    • After Returning Advice: 조인 포인트가 정상 종료된 후(메서드가 예외 없이 값을 반환한 후) 실행됩니다.
    • After Throwing Advice: 조인 포인트 실행 중 예외가 발생한 후에 실행됩니다.
    • After (Finally) Advice: 조인 포인트가 종료되면 (정상이든 예외든 상관없이) 무조건 실행되는 어드바이스입니다.
    • Around Advice: 조인 포인트를 감싸서 전후에 실행되는 어드바이스입니다. 메서드 호출 자체를 대체하거나 그 실행 여부를 제어할 수 있는 가장 강력한 어드바이스입니다. (ProceedingJoinPoint.proceed()를 호출하여 원본 메서드를 실행할지 말지 결정할 수 있습니다.)
  • Pointcut (포인트컷): 특정 조인 포인트를 선별하는 표현식이나 규칙입니다. 어떤 조인 포인트에서 어드바이스를 실행할지 결정하는 필터 역할을 합니다. 어드바이스는 각각 포인트컷에 연결되어 있어, 포인트컷 조건과 일치하는 조인 포인트에서만 실행됩니다. Spring AOP에서는 AspectJ의 포인트컷 표현식 언어를 사용하여 조인 포인트를 지정합니다 (예: execution(* com.example.service.UserService.getUser(..)) 같은 형식으로 메서드 패턴을 지정).
  • Introduction (도입): 기존 객체에 새로운 메서드나 필드를 추가하는 기능입니다. Spring AOP에서는 특히 새로운 인터페이스를 구현하도록 만들어서 기능을 추가할 수 있습니다. 예를 들어, 캐싱 기능을 위해 어떤 객체에 IsModified라는 인터페이스를 구현하도록 도입하면, 해당 객체가 변경되었는지 여부를 추적하는 메서드를 추가할 수 있습니다. (Introduction은 AspectJ 용어로 인터타입 선언이라고도 합니다.)
  • AOP Proxy (프록시 객체): AOP 프레임워크가 타겟 객체에 부가 기능을 적용하기 위해 생성한 객체를 말합니다. 프록시 객체는 타겟 객체를 대신하여 요청을 가로채고, 필요한 부가 기능(어드바이스)을 수행한 뒤 원래의 메서드를 실행합니다. Spring에서는 JDK 표준 동적 프록시 또는 CGLIB 바이트코드 생성 라이브러리를 통해 프록시를 만듭니다. (일반적으로 타겟 객체가 구현한 인터페이스가 있을 경우 JDK 동적 프록시를, 인터페이스가 없을 경우 CGLIB을 사용하여 클래스의 서브클래스를 생성하는 방식을 사용합니다.)
  • Weaving (위빙): 정의한 Aspect(부가 기능 코드)를 타겟 객체에 적용하여 하나의 객체로 합치는 과정을 뜻합니다. 위빙이 일어나는 시점에 따라 컴파일 시(Compile-time), 클래스 로딩 시(Load-time), 런타임(Runtime) 위빙으로 분류합니다. Spring AOP는 다른 순수 Java AOP 프레임워크들과 마찬가지로 런타임 위빙 방식을 사용합니다. (아래에서 각 위빙 시점에 대해 자세히 설명합니다.)

Spring AOP

스프링 프레임워크의 핵심은 IoC 컨테이너를 통한 객체 관리이지만, AOP를 활용하면 이를 보완하여 애플리케이션의 중복 코드를 제거하고 구조를 개선할 수 있습니다. Spring에서 AOP는 선택 사항이며, IoC/DI를 사용하는 데 필수적이지는 않습니다. 그러나 AOP를 결합하면 스프링 기반 미들웨어 솔루션을 더욱 강력하고 깔끔하게 만들 수 있습니다. 예를 들어, 보일러플레이트(반복되는 상투적인 코드)를 Aspect로 분리함으로써 핵심 비즈니스 로직에만 집중할 수 있습니다.

Spring AOP 구현 방식

Spring AOP에서는 개발자가 직접 커스텀 Aspect를 정의할 수 있도록 두 가지 방식을 지원합니다:

  • 스키마 기반 설정: XML 설정 파일에 <aop:config> 등의 태그를 사용하여 Aspect와 어드바이스를 정의하는 방식입니다. (Spring 2.x 시대부터 제공되던 전통적인 방법)
  • @AspectJ 애노테이션 기반: 일반 클래스에 @Aspect 애노테이션을 붙이고, 어드바이스 메서드에 @Before, @AfterReturning 등의 애노테이션을 붙여서 Aspect를 정의하는 현대적인 방식입니다.

Spring AOP 주요 활용 사례

  • 선언적 엔터프라이즈 서비스: 트랜잭션 관리, 보안, 캐싱, 지연 로딩 등 반복적으로 사용되는 기능을 선언적으로 적용할 수 있습니다. 예를 들어 @Transactional 애노테이션만 붙이면 메서드 앞뒤로 트랜잭션 시작과 종료를 자동 수행하거나, @Cacheable만 붙이면 캐싱 로직을 알아서 적용해주는 등, 부가 기능을 일일이 코드로 작성하지 않고 선언적으로 적용할 수 있습니다. 이러한 기능들은 대부분 Spring AOP를 기반으로 구현되어 있습니다.
  • 사용자 정의 Aspect 구현: 개발자가 직접 공통 로직을 Aspect로 구현하여 적용할 수 있습니다. 기존의 OOP 코드에 AOP를 결합함으로써 여러 모듈에 걸쳐 나타나는 공통 기능(로깅, 감사 로그, 성능 모니터링 등)을 한 곳에 모듈화하고, 필요한 대상에 쉽게 적용할 수 있습니다. 이를 통해 중복 코드를 줄이고 코드 간결성과 유지보수성을 높일 수 있습니다.

    참고: 트랜잭션이나 보안 같은 기본 제공 기능만 사용한다면, Spring AOP를 직접 다룰 필요는 없습니다. Spring Boot에서 관련 starter를 추가하고 애노테이션을 선언하기만 해도 내부적으로 AOP 프록시가 설정되어 동작합니다. 하지만 AOP의 동작 방식이나 개념을 이해하면 이러한 기능들의 내부 동작을 파악하고 문제 발생 시 대응하는 데 도움이 됩니다.

Spring AOP의 특징

  • 프록시 기반 AOP 구현: Spring AOP는 프록시 패턴을 이용하여 AOP를 구현합니다. 스프링 IoC 컨테이너가 애플리케이션 구동 시점에 대상 객체 대신 프록시 객체를 생성하고, 클라이언트는 이 프록시를 통해서만 대상 빈에 접근합니다. 프록시가 메서드 호출을 가로채 적절한 어드바이스를 수행한 후 실제 메서드를 호출함으로써, 개발자가 직접 부가 기능을 끼워넣지 않아도 관심사 분리가 실현됩니다.
  • Spring 빈에만 적용: Spring AOP의 부가 기능은 스프링 컨테이너가 관리하는 빈 객체에만 적용됩니다. 즉, 스프링이 생성하고 관리하지 않는 객체(New 키워드로 생성한 객체 등)는 Spring AOP 프록시의 관리하에 있지 않으므로, 해당 객체의 메서드에는 어드바이스가 적용되지 않습니다.
  • 메서드 실행 조인 포인트만 지원: 앞서 언급했듯이, Spring AOP는 메서드 실행에만 어드바이스를 적용할 수 있습니다. 필드 접근이나 객체 생성 시점 등 보다 미세한 조인 포인트는 지원하지 않습니다. 이러한 제한은 프록시 기반 동작이라는 점에서 기인하며, 일반적인 애플리케이션의 부가 기능 요구 사항(로그, 트랜잭션 등)은 대부분 메서드 단위로 해결되기에 큰 문제가 되지 않습니다. 만약 필드 접근까지 가로채야 하는 등 정교한 AOP가 필요하다면 AspectJ 같은 풀스펙 AOP 프레임워크를 함께 사용하는 것을 고려해야 합니다.
  • 경량 & 단순 통합: Spring AOP는 가장 완벽한 AOP 기능을 제공하는 것이 목적이 아니라, Spring IoC와 긴밀하게 통합하여 엔터프라이즈 애플리케이션의 공통 문제(중복 코드, 부적절한 클래스 설계 등)를 쉽고 효율적으로 해결하는 데 초점을 맞추고 있습니다. 예를 들어, Spring AOP를 통해 트랜잭션 경계설정, 보안 검사 등을 투명하게 처리함으로써, 개발자는 핵심 로직 개발에 집중할 수 있습니다. 아주 세밀한 객체(예: 도메인 객체의 내부 호출)에 부가 기능을 적용하는 것은 Spring AOP로는 어렵지만, 그런 경우에는 AspectJ와의 통합을 통해 해결할 수 있습니다. 실제로 Spring은 AspectJ와도 잘 연동되며, 두 AOP 방식을 경쟁 관계가 아닌 보완 관계로 보고 있습니다.

Spring AOP 포인트컷 표현식의 활용

앞서 언급한 AspectJ 포인트컷 표현식을 사용하면 메서드 패턴(execution), 타입 패턴(within), 객체 이름(bean), 어노테이션(@annotation) 등 다양한 기준으로 어드바이스 적용 대상을 정밀하게 지정할 수 있습니다. 일반적으로 가장 많이 쓰는 것은 execution() 지시자를 통한 메서드 명시이지만, 그 밖에도 유용한 지시자가 많습니다. 예를 들어:

  • bean(beanName): 특정 스프링 빈의 이름으로 대상 지정이 가능합니다. 예를 들어 @Before("bean(userService)")와 같이 하면, 스프링 컨테이너에 등록된 이름이 userService인 빈의 모든 메서드 실행 전에 어드바이스를 적용합니다.
  • @annotation(AnnotationClass): 메서드에 특정 어노테이션이 붙어있는 경우에만 어드바이스를 적용할 수 있습니다. 예를 들어 @Before("@annotation(com.example.annotation.Logged)")와 같이 포인트컷을 정의하면, @Logged 애노테이션이 붙은 메서드가 실행될 때만 해당 어드바이스가 수행됩니다.

이처럼 Spring AOP는 다양한 포인트컷 지시자를 통해 유연하게 대상을 선별할 수 있습니다. 포인트컷 표현식 문법은 AspectJ와 동일하므로, 필요에 따라 args(), this, target, within 등도 활용할 수 있습니다.

Spring AOP 예시

Logging

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.UserService.getUser(..))")
    public void logBeforeUserGet() {
        System.out.println("Getting user...");
    }
}

Transaction

@Aspect
@Component
public class TransactionAspect {

    @AfterReturning("execution(* com.example.service.ProductService.*(..))")
    public void commitTransaction() {
        System.out.println("Committing transaction...");
    }

    @AfterThrowing("execution(* com.example.service.ProductService.*(..))")
    public void rollbackTransaction() {
        System.out.println("Rolling back transaction due to exception...");
    }
}

Security

@Aspect
@Component
public class SecurityAspect {

    @Before("execution(* com.example.controller.AdminController.*(..))")
    public void checkAdminPermission() {
        System.out.println("Checking admin permission...");
    }
}

Caching

@Aspect
@Component
public class CachingAspect {

    @AfterReturning(pointcut = "execution(* com.example.service.CacheService.*(..))", returning = "result")
    public void cacheMethodResult(Object result) {
        // Cache the result...
        System.out.println("Caching method result...");
    }
}

Exception Handling

@Aspect
@Component
public class ExceptionLoggingAspect {

    @AfterThrowing(pointcut = "execution(* com.example.service.PaymentService.*(..))", throwing = "exception")
    public void logException(Exception exception) {
        System.out.println("Exception caught: " + exception.getMessage());
    }
}

Performance Monitoring

@Aspect
@Component
public class PerformanceMonitoringAspect {

    @Around("execution(* com.example.service.AnalyticsService.*(..))")
    public Object measureExecutionTime(org.aspectj.lang.ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        System.out.println("Method execution time: " + (endTime - startTime) + "ms");
        return result;
    }
}

Spring이 내부적으로 AOP를 쓰는 대표적인 경우

  • @Transactional: 메서드에 이 애노테이션을 붙이면, 스프링이 해당 빈에 대해 프록시를 생성하여 트랜잭션 시작과 종료를 관리합니다. 프록시는 메서드 실행 전에 트랜잭션을 시작하고, 정상 종료하면 커밋(commit)하거나 예외 발생 시 롤백(rollback)합니다. 개발자는 단순히 애노테이션 선언만으로 트랜잭션 처리를 얻게 되는데, 이것이 가능한 이유가 바로 Spring AOP 프록시 덕분입니다.
  • @Async: 이 애노테이션이 붙은 메서드는 별도의 스레드 풀에서 비동기로 실행됩니다. Spring은 프록시를 통해 해당 메서드 호출을 가로채고, 즉시 리턴시키면서 백그라운드 스레드에서 실제 메서드를 수행합니다. 이 역시 AOP 프록시가 없으면 구현하기 어려운 기능을 편리하게 제공하는 사례입니다.
  • @Cacheable/@CacheEvict: 캐싱 관련 애노테이션들도 AOP로 동작합니다. @Cacheable의 경우 프록시가 대상 메서드를 호출하기 전에 캐시에 결과가 있는지 조회하고, 있으면 아예 대상 메서드를 실행하지 않고 캐시 값을 반환합니다. 없으면 메서드를 실행한 후 반환값을 캐시에 저장합니다. @CacheEvict는 메서드 실행 후에 캐시를 제거하는 로직을 프로키시가 수행합니다. 이러한 캐싱 로직은 모두 부가적인 관심사이며, AOP를 통해 핵심 로직과 분리되어 투명하게 처리됩니다.
  • 보안 설정 (예: @PreAuthorize, @Secured): Spring Security에서는 메서드에 대한 사전 권한 체크도 AOP를 통해 구현됩니다. 해당 애노테이션이 붙은 메서드를 가진 빈은 프록시로 생성되며, 메서드가 호출될 때 프록시가 먼저 SecurityContext를 확인하여 올바른 권한이 있는지 검사합니다. 권한이 없으면 예외를 던지고, 권한이 있으면 실제 메서드를 호출합니다.

Reference

profile
Backend engineer

0개의 댓글