본문 바로가기

개발기술/Spring

Spring의 핵심 개념 (2) (AOP)

AOP 관점지향프로그래밍

  • AOP: Aspect Oriented Programming 
    • AOP는 관점 지향 프로그래밍으로,  메소드에 동일한 로직이 반복적으로 적용되는 핵심 로직과는 별개의 공통 관심 사항(Cross-Cutting Concern)을 분리하여 모듈화하는 방법이다.
      • Core Concern (핵심 관심 사항): 비즈니스 로직 그 자체 (예: 결제 처리, 회원 가입 등)
      • Cross-Cutting Concern (공통 관심 사항): 여러 로직에 걸쳐 공통으로 적용되는 기능 (예: 로그, 보안, 트랜잭션 등)
    • OOP(객체지향 프로그래밍)에서도 동일한 로직을 도입할 수 있지만, 상속이나 의존성 관리가 복잡해질 수 있수 있기에 이를 간편히 관리하는 방식

 

 AOP 개념

용어 설명
Aspect 공통 관심 사항을 모듈화한 클래스. 예: 로깅 Aspect, 트랜잭션 Aspect 등
Advice 실제로 수행될 AOP 기능 코드로 메소드. (ex: 메서드 실행 전 로그 출력), 메소드의 동작을 "조언(advising)"하는 역할
Join Point Advice를 적용할 수 있는 지점 (ex: 메서드 호출, 예외 발생 등)
Pointcut Join Point 중에서 Advice를 적용할 대상을 선별하는 조건식
Target Object Advice가 적용될 실제 객체
AOP Proxy Advice 적용을 위해 생성되는 프록시 객체. 호출을 가로채어 대상 객체를 감싼다 . 주로 CGLIB(Code Generation Library, 실행 중에 실시간으로 코드를 생성하는 라이브러리)를 사용하여 런타임에 프록시 객체를 생성한다.
Weaving Advice를 실제 코드 흐름에 결합하는 과정 (런타임, 컴파일타임, 클래스 로딩 시점 등)

 

AOP  작동 방식 요약

  • AOP Proxy :   공통관심사항을 작동하는 코드를 만들고 적용할 대상을 지정해주면된다. 그러면 AOP는 JavaBean으로 등록된 객체를 프록시로 생성하여 이에 대한 메소드 호출을 가로채어, 추가적인 코드동작들을 메소드 호출 전후에 주입하는 방식으로 작동한다.
  • AOP가 적용되려면 해당 객체가 반드시 스프링 빈이어야 합니다. 스프링 컨테이너가 프록시 빈(Proxy Bean) 을 대신 주입함으로써 AOP가 자연스럽게 동작합니다.
  • 스프링은 AOP를 적용할 때, "원본 객체" 대신 "프록시 객체"를 빈 컨테이너(Map)에 등록합니다. 그래서 @Transactional, @LogExecutionTime 등의 AOP가 동작하는 이유는
    ApplicationContext에서 가져오는 객체가 "원본"이 아니라 "프록시"이기 때문입니다.

 

  1. 스프링 컨테이너는 JavaBean으로 등록된 객체(= Target Object)에 대해 프록시를 생성한다.
  2. 이 프록시 객체가 메서드 호출을 가로채어(= Join Point),
  3. 지정된 Advice를 Pointcut 조건에 따라 실행 전후에 주입한다.
  4. 런타임에 프록시를 생성할 때는 CGLIB 또는 JDK 동적 프록시를 사용한다.
  5. 이러한 동작 과정을 통해 로직과는 분리된 공통 기능이 안전하고 일관되게 적용된다.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface Service { void doSomething(); }
class ServiceImpl implements Service {
    public void doSomething() {
        System.out.println("핵심 기능 실행");
    }
}

class Handler implements InvocationHandler {
    private final Service target;
    public Handler(Service target) { this.target = target; }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("공통로직(메소드 전)");
        Object result = method.invoke(target, args);
        System.out.println("공통로직(메소드 후)");
        return result;
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        Service realService = new ServiceImpl();
        Service proxy = (Service) Proxy.newProxyInstance(
                Service.class.getClassLoader(),
                new Class[] { Service.class },
                new Handler(realService));
        proxy.doSomething();
    }
}

 

 

 

AOP 주요 어노테이션 및 개념 설명

  • @Aspect : 해당 클래스가 AOP의 핵심 단위인 Aspect임을 나타냅니다.
  • @Component : Aspect 클래스를 Spring Bean으로 등록합니다.
  • Advice 어노테이션
    • 부가기능(Advice)을 적용할 시점을 지정하는 AOP 어노테이션으로 PointCut 표현식도 함께 지정합니다.
    • @Around  : 메서드 실행 전/후 또는 예외 상황에서 실행될 코드를 정의합니다. 
  • ProceedingJoinPoint.proceed() : 원래 대상 메서드를 실행하는 코드입니다. @Around에서 반드시 호출해야 합니다.
@Component
@Aspect
public class TimeTraceAop {

    // Conditional property to enable or disable the tracing
    @Value("${aop.trace.enabled:true}") // This can be set in application.properties
    private boolean isTraceEnabled;

    @Around("execution(* hello.hellospring..*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {

        // Check if tracing is enabled
        if (!isTraceEnabled) {
            return joinPoint.proceed(); // Skip timing and proceed if tracing is disabled
        }

        // Proceed with tracing if enabled
        long start = System.currentTimeMillis();
        System.out.println("START: " + joinPoint.toString());

        try {
            return joinPoint.proceed(); // Proceed with the method execution
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END: " + joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

 

@RequiredArgsConstructor
@Aspect
@Component
@Slf4j
public class LockAopAspect {

    private final LockService lockService;

    @Around("@annotation(com.zerobase.travel.redis.PostLock)")
    public Object aroundMethod(
        ProceedingJoinPoint pjp) throws Throwable {

        long postId;
        try {
            postId = (long) pjp.getArgs()[0];
        } catch (Exception e) {
            throw new BizException(ParticipationErrorCode.LOCK_PARAM_PROCESSING_ERROR);
        }

        // lock 취득시도
        log.info("Asepct interception triggered");
        lockService.lock(postId);

        try {
            return pjp.proceed();
        } finally {
            // lock 해체
            lockService.unlock(postId);
        }
    }

}

 

 

PointCut 표현식 요약

Spring AOP에서는 PointCut 표현식을 통해 어떤 메서드에 Advice를 적용할지 지정합니다.

  • execution(...)  : 특정 메서드 시그니처에 매칭되는 실행 지점을 선택합니다. 예: execution(* com.example.service..*(..))
@Aspect
@Component
public class SimpleLoggingAspect {

    @Before("execution(* com.example.service..*(..))")
    public void beforeMethod() {
        System.out.println("메서드 실행 전");
    }
}
@Aspect
@Component
public class ReusableLoggingAspect {

    @Pointcut("execution(* com.example.service..*(..))")
    private void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeMethod() {
        System.out.println("서비스 메서드 실행 전");
    }

    @AfterReturning("serviceMethods()")
    public void afterMethod() {
        System.out.println("서비스 메서드 실행 후");
    }
}
  • args(...) : 메서드의 인자 타입을 기준으로 조인 포인트를 지정합니다.
  • @annotation(...) : 특정 어노테이션이 붙은 메서드를 대상으로 Advice를 적용합니다.
@Around("@annotation(com.zerobase.travel.redis.PostLock)")
public Object aroundMethod(

 

 

Advice 메서드 내부에서는:

  • 메서드 실행 전 원하는 코드를 작성하고,
  • joinPoint.proceed()를 호출하여 원래 비즈니스 로직을 실행하며,
  • finally 블록에서 실행 후 처리할 로직을 작성할 수 있습니다.

'개발기술 > Spring' 카테고리의 다른 글

배치처리  (1) 2025.04.21
스프링 Bean 컨테이너와 생명주기  (0) 2025.02.11
스프링 환경설정 ; 스프링 부트  (0) 2025.01.25
비동기 Web Flux  (0) 2024.11.19
Spring 스케쥴러  (1) 2024.11.15