SpringStudy

Spring AOP

dding-shark 2025. 8. 19. 21:55
728x90

실습 코드 : https://github.com/DDINGJOO/Spring5PlayGround

Spring AOP 개요

AOP(Aspect-Oriented Programming)란?

관점 지향 프로그래밍(AOP)은 프로그램의 핵심 기능과 부가 기능을 분리하여 관점별로 모듈화하는 프로그래밍 패러다임입니다.

Spring AOP의 특징

  • 프록시 기반: 런타임에 프록시 객체를 생성하여 AOP 기능 제공
  • 메서드 레벨 적용: 메서드 호출 시점에서만 AOP 적용 가능
  • 스프링 컨테이너 연동: 스프링 IoC 컨테이너와 완전히 통합

핵심 개념

1. Aspect (관점)

횡단 관심사를 모듈화한 것으로, Advice와 Pointcut을 결합한 개념입니다.

2. Advice (어드바이스)

특정 조인포인트에서 실행되는 코드를 말합니다.

Advice 타입

  • Before Advice: 메서드 실행 전에 실행
  • After Returning Advice: 메서드가 정상적으로 반환된 후 실행
  • After Throwing Advice: 메서드에서 예외가 발생했을 때 실행
  • Around Advice: 메서드 실행 전후를 모두 제어

3. Pointcut (포인트컷)

Advice가 적용될 조인포인트를 선별하는 기준입니다.

4. Join Point (조인포인트)

프로그램 실행 중에 Advice가 적용될 수 있는 지점입니다.

5. Target Object (타겟 객체)

실제 비즈니스 로직을 수행하는 객체입니다.

6. Proxy (프록시)

AOP 프레임워크가 생성하는 객체로 Advice를 적용합니다.


실습 예제 분석

Ch5_03: 기본 AOP 데모 (SpringAopDemo)

개념

가장 기본적인 Spring AOP 사용법을 보여주는 예제입니다. ProxyFactory를 사용하여 프록시를 생성하고 Around Advice를 적용합니다.

코드 분석

Agent.java (타겟 객체)

public class Agent {
    public void speak() {
        System.out.println("Bond");
    }
}

AgentDecorator.java (Around Advice)

public class AgentDecorator implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("James");           // Before
        Object retVal = invocation.proceed();  // 원본 메서드 실행
        System.out.println("!");               // After
        return retVal;
    }
}

SpringAopDemo.java (메인 클래스)

public class SpringAopDemo {
    public static void main(String[] args) {
        Agent target = new Agent();

        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.addAdvice(new AgentDecorator());
        proxyFactory.setTarget(target);

        Agent proxy = (Agent) proxyFactory.getProxy();
        target.speak(); // "Bond"
        System.out.println(" ");
        proxy.speak();  // "James Bond !"
    }
}

실행 결과

Bond

James
Bond
!

학습 포인트

  • ProxyFactory를 사용한 프록시 생성 방법
  • MethodInterceptor를 통한 Around Advice 구현
  • 타겟 객체와 프록시 객체의 차이점

Ch5_05: Advice 데모 (SimpleBeforeAdvice, SimpleAfterAdvice)

개념

Before Advice와 After Returning Advice의 사용법을 보여주는 예제입니다.

Before Advice 분석

SimpleBeforeAdvice.java

public class SimpleBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("Before '" + method.getName() + "' turn guitar.");
    }

    public static void main(String[] args) {
        Guitarist JoneMayer = new Guitarist();

        ProxyFactory pf = new ProxyFactory();
        pf.addAdvice(new SimpleBeforeAdvice());
        pf.setTarget(JoneMayer);

        Guitarist proxy = (Guitarist) pf.getProxy();
        proxy.sing();
    }
}

After Returning Advice 분석

SimpleAfterAdvice.java

public class SimpleAfterAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("After '" + method.getName() + "' put down guitar.");
    }
}

학습 포인트

  • MethodBeforeAdvice 인터페이스 구현
  • AfterReturningAdvice 인터페이스 구현
  • 메서드 실행 전후의 부가 기능 추가

Ch5_07: BeforeAdvice 보안 예제 (SecurityDemo)

개념

Before Advice를 사용하여 메서드 실행 전 보안 검증을 수행하는 실무적인 예제입니다.

코드 분석

SecurityAdvice.java

public class SecurityAdvice implements MethodBeforeAdvice {
    private SecurityManager securityManager;

    public SecurityAdvice() {
        this.securityManager = new SecurityManager();
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        UserInfo userInfo = securityManager.getUserInfo();

        if(userInfo == null) {
            System.out.println("인증되지 않은 사용자 입니다.");
            throw new SecurityException(
                "메소드를 호출하려면 로그인이 필요합니다." + method.getName()
            );
        } else if ("John".equals(userInfo.getUserName())) {
            System.out.println("Jone 사용자로 로그인 했습니다. - OKEY!");
        } else {
            System.out.println(userInfo.getUserName() + "사용자로 로그인 했습니다." + "-NOT GOOD :(");
            throw new SecurityException(userInfo.getUserName()
                + " 사용자는 메서드에 대한 접근 권한이 없습니다. " + method.getName());
        }
    }
}

SecurityManager.java

public class SecurityManager {
    private static ThreadLocal<UserInfo> CONTEXT = new ThreadLocal<>();

    public void login(String userName, String password) {
        CONTEXT.set(new UserInfo(userName, password));
    }

    public void logout() {
        CONTEXT.set(null);
    }

    public UserInfo getUserInfo() {
        return CONTEXT.get();
    }
}

실행 흐름

  1. SecurityManager를 통해 사용자 인증 상태 확인
  2. 인증되지 않은 사용자의 경우 SecurityException 발생
  3. 권한이 없는 사용자의 경우 접근 거부
  4. 정상적인 사용자만 메서드 실행 허용

학습 포인트

  • Before Advice를 활용한 보안 검증
  • ThreadLocal을 사용한 사용자 컨텍스트 관리
  • 예외를 통한 접근 제어

Ch5_08: AfterAdvice 키 검증 예제 (WeekKeyCheckAdvice)

개념

After Returning Advice를 사용하여 메서드 실행 후 반환값을 검증하는 예제입니다.

코드 분석

WeekKeyCheckAdvice.java

public class WeekKeyCheckAdvice implements AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        if (returnValue instanceof Long && (Long) returnValue < 100000L) {
            throw new SecurityException("Week Key Generated");
        }
    }
}

KeyGenerator.java

public class KeyGenerator {
    public static final long WEEK_KEY = 0xFFFFL;
    public static final long STRONG_KEY = 0xFFFFFFL;

    public Long getKey() {
        int x = (int) (Math.random() * 3);
        return (x == 1) ? WEEK_KEY : STRONG_KEY;
    }
}

실행 흐름

  1. KeyGenerator.getKey() 메서드 실행
  2. 반환된 키 값이 100000보다 작으면 약한 키로 판단
  3. 약한 키의 경우 SecurityException 발생
  4. 강한 키의 경우 정상 처리

학습 포인트

  • After Returning Advice를 통한 반환값 검증
  • 비즈니스 로직과 검증 로직의 분리
  • 조건부 예외 처리

Ch5_09: AroundAdvice 프로파일링 예제 (ProfilingInterceptor)

개념

Around Advice를 사용하여 메서드 실행 시간을 측정하는 프로파일링 예제입니다.

코드 분석

ProfilingInterceptor.java

public class ProfilingInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        StopWatch sw = new StopWatch();
        sw.start();

        Object retVal = invocation.proceed();

        sw.stop();

        System.out.println("Method " + invocation.getMethod().getName()
            + " took " + sw.getTotalTimeMillis() + " ms"
            + " Target: " + invocation.getThis().getClass().getName());

        return retVal;
    }
}

WorkerBean.java

public class WorkerBean {
    public void doSomeWork(int noOfTimes) {
        for (int x = 0; x < noOfTimes; x++) {
            work();
        }
    }

    private void work() {
        int x = Math.random() < 0.5 ? 1000 : 2000;
        System.out.println("Working hard for " + x + " ms");

        try {
            Thread.sleep(x);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
        }
    }
}

실행 결과 예시

Working hard for 1000 ms
Working hard for 2000 ms
Method doSomeWork took 3005 ms Target: WorkerBean

학습 포인트

  • Around Advice를 통한 메서드 실행 시간 측정
  • StopWatch 클래스 활용
  • 성능 모니터링과 비즈니스 로직의 분리

Ch5_10: ThrowsAdvice 예외 처리 예제 (SimpleThrowsAdvice)

개념

Throws Advice를 사용하여 메서드에서 발생하는 예외를 처리하는 예제입니다.

코드 분석

SimpleThrowsAdvice.java

public class SimpleThrowsAdvice implements ThrowsAdvice {
    public static void main(String[] args) {
        ErrorBean errorBean = new ErrorBean();

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(errorBean);
        pf.addAdvice(new SimpleThrowsAdvice());

        ErrorBean proxy = (ErrorBean) pf.getProxy();

        try {
            proxy.errorProneMethod();
        } catch (Exception ignored) {}

        try {
            proxy.otherErrorProneMethod();
        } catch (Exception ignored) {}
    }

    public void afterThrowing(Exception ex) {
        System.out.println("Caught :" + ex.getClass().getName());
    }

    public void afterThrowing(Method method, Object[] args, Object target, IllegalArgumentException ex) {
        System.out.println("Caught :" + ex.getClass().getName());
        System.out.println("Method :" + method.getName());
    }
}

ErrorBean.java

public class ErrorBean {
    public void errorProneMethod() throws Exception {
        throw new Exception("Generic Exception");
    }

    public void otherErrorProneMethod() throws IllegalArgumentException {
        throw new IllegalArgumentException("Illegal Argument");
    }
}

학습 포인트

  • ThrowsAdvice를 통한 예외 처리
  • 예외 타입별 차별화된 처리
  • afterThrowing 메서드의 다양한 시그니처

Ch5_11: 정적 메서드 매처 포인트컷 (SimpleStaticPointcut)

개념

StaticMethodMatcherPointcut을 상속하여 특정 메서드에만 Advice를 적용하는 예제입니다.

코드 분석

SimpleStaticPointcut.java

public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        return "sing".equals(method.getName());
    }

    @Override
    public ClassFilter getClassFilter() {
        return cls -> cls == (GoodGuitarist.class);
    }
}

StaticPointcutDemo.java

public class StaticPointcutDemo {
    public static void main(String[] args) {
        GoodGuitarist target = new GoodGuitarist();

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(new DefaultPointcutAdvisor(new SimpleStaticPointcut(), new SimpleAdvice()));

        GoodGuitarist proxy = (GoodGuitarist) pf.getProxy();

        proxy.sing();
        proxy.rest();
    }
}

실행 흐름

  1. GoodGuitarist 클래스의 "sing" 메서드에만 Advice 적용
  2. "rest" 메서드는 Pointcut 조건에 맞지 않아 Advice 적용 안됨
  3. 선택적인 Advice 적용으로 성능 향상

학습 포인트

  • StaticMethodMatcherPointcut 상속
  • matches() 메서드를 통한 메서드 필터링
  • ClassFilter를 통한 클래스 필터링
  • DefaultPointcutAdvisor 사용법

Ch5_12: 동적 메서드 매처 포인트컷 (SimpleDynamicPointcut)

개념

DynamicMethodMatcherPointcut을 상속하여 런타임에 메서드 인수를 검사하여 Advice 적용 여부를 결정하는 예제입니다.

코드 분석

SimpleDynamicPointcut.java

public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        System.out.println("Static check for " + method.getName());
        return "foo".equals(method.getName());
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        System.out.println("Dynamic check for " + method.getName());
        int x = (Integer) args[0];
        return (x != 100);
    }

    @Override
    public ClassFilter getClassFilter() {
        return cls -> cls == (SampleBean.class);
    }
}

SampleBean.java

public class SampleBean {
    public void foo(int x) {
        System.out.println("Invoked foo() with: " + x);
    }

    public void bar() {
        System.out.println("Invoked bar()");
    }
}

실행 흐름

  1. 정적 검사: "foo" 메서드명 확인
  2. 동적 검사: 런타임 인수값이 100이 아닌지 확인
  3. 조건을 모두 만족하는 경우에만 Advice 적용

학습 포인트

  • DynamicMethodMatcherPointcut 상속
  • 정적 검사와 동적 검사의 차이점
  • 런타임 인수를 활용한 조건부 Advice 적용
  • 성능 고려사항 (동적 검사는 비용이 높음)

Ch5_13: 네임 매치 메서드 포인트컷 (NameMatchMethodPointcut)

개념

메서드 이름 패턴을 사용하여 Pointcut을 정의하는 예제입니다.

코드 분석

NamePointcutDemo.java

public class NamePointcutDemo {
    public static void main(String[] args) {
        GrammyGuitarist target = new GrammyGuitarist();

        ProxyFactory pf = new ProxyFactory();

        NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
        pc.addMethodName("sing");
        pc.addMethodName("rest");

        pf.setTarget(target);
        pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));

        GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();

        proxy.sing();
        proxy.sing2();
        proxy.rest();
    }
}

실행 결과

Before method: sing
Invoked sing()
After method: sing

Invoked sing2() (Advice 적용 안됨)

Before method: rest
Invoked rest()
After method: rest

학습 포인트

  • NameMatchMethodPointcut 사용법
  • addMethodName()을 통한 여러 메서드 이름 등록
  • 메서드 이름 기반의 간편한 Pointcut 정의

Ch5_14: JDK 정규식 메서드 포인트컷 (JdkRegexpMethodPointcut)

개념

정규식을 사용하여 메서드 이름 패턴을 매칭하는 Pointcut 예제입니다.

코드 분석

RegexpPointcutDemo.java

public class RegexpPointcutDemo {
    public static void main(String[] args) {
        Guitarist target = new Guitarist();

        ProxyFactory pf = new ProxyFactory();

        JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
        pc.setPattern(".*sing.*");

        pf.setTarget(target);
        pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));

        Guitarist proxy = (Guitarist) pf.getProxy();

        proxy.sing();
        proxy.sing2();
        proxy.rest();
    }
}

정규식 패턴 설명

  • .*sing.*: "sing"이 포함된 모든 메서드 매칭
  • sing(), sing2() 메서드는 매칭됨
  • rest() 메서드는 매칭되지 않음

학습 포인트

  • JdkRegexpMethodPointcut 사용법
  • 정규식을 활용한 유연한 메서드 매칭
  • 패턴 기반의 Pointcut 정의

Ch5_15: AspectJ 표현식 포인트컷 (AspectJExpressionPointcut)

개념

AspectJ의 강력한 포인트컷 표현식을 Spring AOP에서 사용하는 예제입니다.

코드 분석

AspectjexpPointcutDemo.java

public class AspectjexpPointcutDemo {
    private static AspectJExpressionPointcut pc;

    public static void main(String[] args) {
        Guitarist proxyOne;
        Guitarist proxyTwo;

        pc = new AspectJExpressionPointcut();
        pc.setExpression("execution(* sing*(..))");

        // 첫 번째 프록시
        proxyOne = getProxy();
        proxyOne.sing();
        proxyOne.sing2();
        proxyOne.rest();

        System.out.println("");

        // 두 번째 프록시
        pc.setExpression("execution(* *.sing2(..))");
        proxyTwo = getProxy();
        proxyTwo.sing();
        proxyTwo.sing2();
        proxyTwo.rest();
    }

    private static Guitarist getProxy() {
        Guitarist target = new Guitarist();
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));
        return (Guitarist) pf.getProxy();
    }
}

AspectJ 표현식 설명

  • execution(* sing*(..)): 반환타입 상관없이 sing으로 시작하는 모든 메서드
  • execution(* *.sing2(..)): 모든 클래스의 sing2 메서드

학습 포인트

  • AspectJExpressionPointcut 사용법
  • execution 표현식 문법
  • 동적인 포인트컷 표현식 변경

Ch5_16: 어노테이션 매칭 포인트컷 (AnnotationMatchingPointcut)

개념

메서드에 특정 어노테이션이 붙은 경우에만 Advice를 적용하는 예제입니다.

코드 분석

AdviceRequired.java (커스텀 어노테이션)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdviceRequired {
}

Guitarist.java

public class Guitarist implements Singer {
    @AdviceRequired
    public void sing() {
        System.out.println("sing() called");
    }

    @AdviceRequired
    public void sing2() {
        System.out.println("sing2() called");
    }

    public void rest() {
        System.out.println("rest() called");
    }
}

AnnotationPointcutDemo.java

public class AnnotationPointcutDemo {
    public static void main(String[] args) {
        Guitarist guitarist = new Guitarist();

        AnnotationMatchingPointcut pc = AnnotationMatchingPointcut.forMethodAnnotation(AdviceRequired.class);

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(guitarist);
        pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));

        Guitarist proxy = (Guitarist) pf.getProxy();

        proxy.sing();   // Advice 적용
        proxy.sing2();  // Advice 적용
        proxy.rest();   // Advice 적용 안됨
    }
}

학습 포인트

  • 커스텀 어노테이션 정의
  • AnnotationMatchingPointcut 사용법
  • 어노테이션 기반의 선언적 AOP
  • @Target, @Retention 어노테이션 활용

Ch5_17: 편리한 포인트컷 구현 (NamePointcutUsingAdvisor)

개념

NameMatchMethodPointcutAdvisor를 사용하여 Pointcut과 Advice를 하나로 결합하는 편리한 방법을 보여주는 예제입니다.

코드 분석

NamePointcutUsingAdvisor.java

public class NamePointcutUsingAdvisor {
    public static void main(String[] args) {
        GrammyGuitarist guitarist = new GrammyGuitarist();

        NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(new SimpleAdvice());
        advisor.addMethodName("sing");
        advisor.addMethodName("rest");

        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(guitarist);
        pf.addAdvisor(advisor);

        GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();

        proxy.sing();
        proxy.sing2();
        proxy.rest();
    }
}

장점

  • Pointcut과 Advice를 별도로 생성할 필요 없음
  • 코드 간소화
  • 일반적인 사용 사례에 최적화

학습 포인트

  • NameMatchMethodPointcutAdvisor 사용법
  • Advisor 패턴의 편리함
  • 코드 간소화 방법

AOP 실행 흐름

전체 AOP 실행 흐름도

graph TD
    A[클라이언트 코드] --> B[프록시 객체]
    B --> C{포인트컷 매칭}
    C -->|매칭됨| D[어드바이스 실행]
    C -->|매칭 안됨| E[타겟 메서드 직접 호출]
    D --> F{어드바이스 타입}
    F -->|Before| G[Before Advice 실행]
    F -->|After| H[After Advice 실행]
    F -->|Around| I[Around Advice 실행]
    F -->|Throws| J[Exception 처리]
    G --> K[타겟 메서드 호출]
    K --> L[결과 반환]
    H --> L
    I --> M{proceed() 호출}
    M --> K
    J --> N[예외 처리 후 반환]
    L --> O[클라이언트에게 결과 반환]
    N --> O
    E --> O

Before Advice 실행 흐름

sequenceDiagram
    participant C as 클라이언트
    participant P as 프록시
    participant BA as BeforeAdvice
    participant T as 타겟객체

    C->>P: 메서드 호출
    P->>P: 포인트컷 매칭 확인
    P->>BA: before() 메서드 호출
    Note over BA: 전처리 로직 실행
    BA->>P: 완료
    P->>T: 실제 메서드 호출
    T->>P: 결과 반환
    P->>C: 최종 결과 반환

Around Advice 실행 흐름

sequenceDiagram
    participant C as 클라이언트
    participant P as 프록시
    participant AA as AroundAdvice
    participant T as 타겟객체

    C->>P: 메서드 호출
    P->>P: 포인트컷 매칭 확인
    P->>AA: invoke() 메서드 호출
    Note over AA: 전처리 로직
    AA->>AA: invocation.proceed() 호출
    AA->>T: 실제 메서드 호출
    T->>AA: 결과 반환
    Note over AA: 후처리 로직
    AA->>P: 최종 결과 반환
    P->>C: 결과 전달

프록시 생성 과정

graph LR
    A[ProxyFactory 생성] --> B[타겟 객체 설정]
    B --> C[Advice/Advisor 추가]
    C --> D[getProxy() 호출]
    D --> E{인터페이스 존재?}
    E -->|Yes| F[JDK Dynamic Proxy]
    E -->|No| G[CGLIB Proxy]
    F --> H[프록시 객체 생성 완료]
    G --> H

포인트컷 평가 과정

graph TD
    A[메서드 호출] --> B{클래스 필터}
    B -->|통과| C{메서드 매처}
    B -->|실패| D[Advice 적용 안함]
    C -->|Static 매칭| E{정적 조건 확인}
    C -->|Dynamic 매칭| F{동적 조건 확인}
    E -->|통과| G[Advice 적용]
    E -->|실패| D
    F -->|통과| G
    F -->|실패| D

심화 학습

Spring AOP vs AspectJ 비교

특성 Spring AOP AspectJ
구현 방식 프록시 기반 바이트코드 조작/위빙
적용 범위 메서드 레벨만 필드, 생성자, 메서드 등
성능 런타임 오버헤드 컴파일 타임 최적화
학습 곡선 상대적으로 쉬움 복잡함
Spring 통합 완전 통합 별도 설정 필요
사용 시점 스프링 빈에만 적용 모든 Java 객체

프록시 타입별 특징

JDK Dynamic Proxy

  • 조건: 타겟이 인터페이스를 구현해야 함
  • 장점: 표준 Java 기능 사용
  • 단점: 인터페이스 기반으로만 동작
// JDK Proxy 강제 사용 예시
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.setProxyTargetClass(false); // JDK Proxy 강제

CGLIB Proxy

  • 조건: 클래스 기반 프록시 생성
  • 장점: 인터페이스 없이도 동작
  • 단점: final 클래스/메서드는 프록시 불가
// CGLIB 강제 사용 예시
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.setProxyTargetClass(true); // CGLIB 강제

성능 최적화 고려사항

1. 포인트컷 최적화

// 비효율적 - 모든 메서드 검사
public boolean matches(Method method, Class<?> targetClass) {
    return method.getName().startsWith("get") || 
           method.getName().startsWith("set");
}

// 효율적 - 빠른 실패
public boolean matches(Method method, Class<?> targetClass) {
    String name = method.getName();
    return name.length() > 3 && 
           (name.startsWith("get") || name.startsWith("set"));
}

2. 정적 vs 동적 포인트컷

  • 정적 포인트컷: 성능이 우수하나 유연성 제한
  • 동적 포인트컷: 유연하나 런타임 오버헤드 존재

3. Advice 체인 최적화

// 효율적인 Advice 체인 구성
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SecurityAdvice());      // 가장 먼저 실행되어야 함
pf.addAdvice(new PerformanceAdvice());   // 성능 측정
pf.addAdvice(new LoggingAdvice());       // 마지막에 로깅

실무 활용 패턴

1. 트랜잭션 관리

public class TransactionAdvice implements MethodInterceptor {
    private PlatformTransactionManager txManager;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object result = invocation.proceed();
            txManager.commit(status);
            return result;
        } catch (Exception ex) {
            txManager.rollback(status);
            throw ex;
        }
    }
}

2. 캐싱 구현

public class CachingAdvice implements MethodInterceptor {
    private Map<String, Object> cache = new ConcurrentHashMap<>();

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String key = generateKey(invocation);

        Object cached = cache.get(key);
        if (cached != null) {
            return cached;
        }

        Object result = invocation.proceed();
        cache.put(key, result);
        return result;
    }

    private String generateKey(MethodInvocation invocation) {
        return invocation.getMethod().getName() + 
               Arrays.toString(invocation.getArguments());
    }
}

3. 감사 로깅

public class AuditAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
    private Logger logger = LoggerFactory.getLogger(AuditAdvice.class);

    @Override
    public void before(Method method, Object[] args, Object target) {
        logger.info("Method {} called with args: {}", 
                   method.getName(), Arrays.toString(args));
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) {
        logger.info("Method {} completed successfully", method.getName());
    }
}

4. 재시도 메커니즘

public class RetryAdvice implements MethodInterceptor {
    private int maxAttempts = 3;
    private long delay = 1000L;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Exception lastException = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                return invocation.proceed();
            } catch (Exception ex) {
                lastException = ex;
                if (attempt < maxAttempts) {
                    Thread.sleep(delay * attempt);
                }
            }
        }
        throw lastException;
    }
}

디버깅 및 테스팅

1. AOP 디버깅 팁

public class DebuggingAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("=== AOP Debug Info ===");
        System.out.println("Target: " + invocation.getThis().getClass().getName());
        System.out.println("Method: " + invocation.getMethod().getName());
        System.out.println("Args: " + Arrays.toString(invocation.getArguments()));

        Object result = invocation.proceed();

        System.out.println("Return: " + result);
        System.out.println("=== End Debug Info ===");

        return result;
    }
}

2. 단위 테스트 작성

@Test
public void testSecurityAdvice() {
    // Given
    SecureBean target = new SecureBean();
    ProxyFactory pf = new ProxyFactory();
    pf.setTarget(target);
    pf.addAdvice(new SecurityAdvice());

    SecureBean proxy = (SecureBean) pf.getProxy();

    // When & Then
    assertThrows(SecurityException.class, () -> {
        proxy.secureMethod();
    });
}

학습 정리

Spring AOP 핵심 개념 요약

1. AOP의 필요성

  • 횡단 관심사 분리: 로깅, 보안, 트랜잭션 등의 공통 기능을 비즈니스 로직과 분리
  • 코드 중복 제거: 동일한 부가 기능을 여러 곳에서 재사용
  • 유지보수성 향상: 관심사별로 코드가 모듈화되어 변경이 용이
  • 테스트 용이성: 핵심 비즈니스 로직과 부가 기능을 독립적으로 테스트 가능

2. Spring AOP 동작 원리

Spring AOP는 프록시 패턴을 기반으로 동작합니다:

  1. 프록시 생성: ProxyFactory가 타겟 객체의 프록시를 생성
  2. 메서드 호출 가로채기: 클라이언트의 메서드 호출을 프록시가 가로챔
  3. 포인트컷 평가: 현재 메서드 호출이 어드바이스 적용 조건에 맞는지 확인
  4. 어드바이스 실행: 조건에 맞으면 해당 어드바이스를 실행
  5. 타겟 메서드 호출: 실제 비즈니스 로직 실행
  6. 결과 반환: 최종 결과를 클라이언트에게 반환

3. Advice 타입별 활용 시나리오

Advice 타입 주요 사용 사례 예제
Before 입력 검증, 보안 검사, 로깅 사용자 권한 확인, 매개변수 유효성 검사
After Returning 결과 검증, 성공 로깅, 캐시 저장 반환값 검증, 성공 감사 로그
After Throwing 예외 처리, 오류 로깅, 알림 예외 로깅, 관리자 알림, 복구 작업
Around 성능 측정, 트랜잭션, 재시도 실행 시간 측정, DB 트랜잭션 관리

4. 포인트컷 선택 가이드

포인트컷 타입 사용 시점 장점 단점
정적 (Static) 메서드명 기반 필터링 성능 우수 런타임 조건 불가
동적 (Dynamic) 매개변수 기반 필터링 유연함 성능 오버헤드
이름 매칭 간단한 메서드명 패턴 사용 간편 제한적 표현력
정규식 복잡한 패턴 매칭 강력한 표현력 복잡성 증가
AspectJ 표현식 복잡한 조건 매우 강력함 학습 비용
어노테이션 선언적 방식 직관적 어노테이션 필요

실습 예제별 핵심 학습 내용

Ch5_03 ~ Ch5_05: 기초 개념

  • ProxyFactory 사용법: Spring AOP의 기본 프록시 생성 방법
  • 기본 Advice 타입들: Before, After, Around의 기본적인 구현 방법
  • 프록시와 타겟의 차이점: 프록시 객체와 원본 객체의 동작 차이 이해

Ch5_07 ~ Ch5_10: 실무 활용

  • 보안 검증: Before Advice를 활용한 접근 권한 검사
  • 결과 검증: After Returning Advice를 통한 반환값 유효성 확인
  • 성능 모니터링: Around Advice를 사용한 실행 시간 측정
  • 예외 처리: Throws Advice를 통한 중앙화된 예외 처리

Ch5_11 ~ Ch5_17: 고급 포인트컷

  • 선택적 적용: 특정 조건에서만 Advice를 적용하는 방법
  • 성능 최적화: 정적 vs 동적 포인트컷의 성능 차이
  • 유연한 매칭: 다양한 패턴 매칭 방법 (이름, 정규식, AspectJ 표현식)
  • 어노테이션 기반: 선언적이고 직관적인 AOP 구현 방법

모범 사례 (Best Practices)

1. 설계 원칙

  • 단일 책임: 하나의 Advice는 하나의 관심사만 처리
  • 최소 침입: 비즈니스 로직에 최소한의 영향만 미치도록 설계
  • 성능 고려: 불필요한 동적 포인트컷 사용 자제
  • 테스트 가능: Advice 로직을 독립적으로 테스트할 수 있도록 설계

2. 코드 구조화

// 권장: 관심사별 분리
public class SecurityAspect {
    // 보안 관련 로직만 포함
}

public class PerformanceAspect {
    // 성능 관련 로직만 포함
}

// 비권장: 여러 관심사 혼재
public class MixedAspect {
    // 보안 + 성능 + 로깅이 섞여있음
}

3. 예외 처리

public class SafeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            // Advice 로직
            return invocation.proceed();
        } catch (Exception ex) {
            // Advice에서 발생한 예외는 적절히 처리
            logger.error("Advice execution failed", ex);
            throw ex; // 원본 예외는 그대로 전파
        }
    }
}

트러블슈팅 가이드

1. 일반적인 문제들

문제: Advice가 적용되지 않음

  • 원인: 포인트컷 조건 불일치
  • 해결: 포인트컷 조건을 디버깅하여 확인

문제: 성능 저하 발생

  • 원인: 동적 포인트컷 남용
  • 해결: 정적 포인트컷으로 변경 검토

문제: 예외 발생 시 타겟 메서드 미실행

  • 원인: Before Advice에서 예외 발생
  • 해결: Advice 내 예외 처리 로직 추가

2. 디버깅 방법

// 프록시 정보 확인
if (AopUtils.isAopProxy(proxy)) {
    System.out.println("프록시 타입: " + 
        (AopUtils.isJdkDynamicProxy(proxy) ? "JDK" : "CGLIB"));
}

// Advisor 정보 확인  
if (proxy instanceof Advised) {
    Advised advised = (Advised) proxy;
    for (Advisor advisor : advised.getAdvisors()) {
        System.out.println("Advisor: " + advisor);
    }
}

미래 발전 방향

1. @AspectJ 어노테이션

현재 실습한 프로그래매틱 방식에서 더 나아가 어노테이션 기반의 선언적 AOP 학습:

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        // Before advice 로직
    }

    @Around("@annotation(com.example.annotation.Loggable)")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // Around advice 로직
    }
}

2. Spring Boot 통합

Spring Boot의 자동 설정을 활용한 더 간편한 AOP 설정:

@EnableAspectJAutoProxy
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

결론

이번 Spring AOP 실습을 통해 다음과 같은 핵심 개념들을 학습했습니다:

  1. AOP의 기본 개념: Aspect, Advice, Pointcut, Join Point 등의 핵심 용어와 개념
  2. 프록시 기반 구현: ProxyFactory를 사용한 프록시 생성과 동작 원리
  3. 다양한 Advice 타입: Before, After, Around, Throws Advice의 특징과 활용법
  4. 포인트컷 구현: 정적/동적 포인트컷, 이름 매칭, 정규식, AspectJ 표현식, 어노테이션 기반 등
  5. 실무 활용 패턴: 보안, 성능 모니터링, 예외 처리, 검증 등의 실제 사용 사례
  6. 성능 최적화: 효율적인 포인트컷 작성과 프록시 선택 방법

Spring AOP는 엔터프라이즈 애플리케이션 개발에서 횡단 관심사를 효과적으로 분리하는 강력한 도구입니다. 이번 실습을 통해 익힌 개념들을 바탕으로, 실제 프로젝트에서 로깅, 보안, 트랜잭션 관리 등의 공통 기능을 우아하게 처리할 수 있을 것입니다.

728x90

'SpringStudy' 카테고리의 다른 글

Spring Hibernate  (7) 2025.08.26
JDBC 최종  (4) 2025.08.21
JDBC 학습 정리(1)  (0) 2025.08.21
Pointcut 문법 정리  (0) 2025.08.20
SpringAop -final  (0) 2025.08.20