[Spring] AOP

배창민·2025년 10월 14일
post-thumbnail

스프링 AOP

관점 지향 프로그래밍으로 중복되는 공통 로직을 분리하고, 메소드 실행 전·후·예외 시점에 주입해 코드 중복을 줄이는 기술


1) 핵심 개념

  • Aspect: 횡단 관심사(로깅, 트랜잭션 등)의 묶음
  • Advice: 적용 시점별 실행 로직(Before/After/Around 등)
  • Join point: 어드바이스가 붙을 수 있는 지점(스프링 AOP는 메소드 실행만 대상)
  • Pointcut: Join point 중 실제로 적용할 지점 선별 표현식
  • Weaving: 핵심 로직에 어드바이스를 적용하는 행위

2) Advice 종류 한눈표

종류시점
Before대상 메소드 실행 전
After-returning대상 메소드 정상 종료 후
After-throwing대상 메소드 예외 발생 시
After대상 메소드 종료 직후(정상/예외 불문)
Around전·후 모두(진행 시점 직접 제어)

3) Spring AOP 특징

  • 프록시 기반: 대상 객체를 감싸는 프록시를 런타임에 생성
  • 메소드 조인 포인트만 제공: 실행 시점 메소드 호출에만 부가기능 적용

4) 구현 절차(스텝별 스니펫)

4-1) 의존성 추가

// build.gradle(.kts)
dependencies {
    implementation("org.aspectj:aspectjweaver:1.9.19")
    implementation("org.aspectj:aspectjrt:1.9.19")
}

4-2) AOP 활성화

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true) // CGLIB 프록시 사용
public class ContextConfiguration {}

4-3) Aspect 정의

@Aspect
@Component
public class LoggingAspect {

    // pointcut: *Service.*(..) 메소드 전부
    @Pointcut("execution(* com.example..*Service.*(..))")
    public void serviceMethods(){}

    @Before("serviceMethods()")
    public void logBefore(JoinPoint jp) {
        System.out.println("[Before] " + jp.getSignature());
        if (jp.getArgs().length > 0) System.out.println("arg0=" + jp.getArgs()[0]);
    }

    @After("serviceMethods()")
    public void logAfter(JoinPoint jp) {
        System.out.println("[After] " + jp.getSignature());
    }

    @AfterReturning(pointcut="serviceMethods()", returning="result")
    public void logAfterReturning(JoinPoint jp, Object result) {
        System.out.println("[AfterReturning] result=" + result);
        // 필요 시 반환값 가공 가능(타입 체크 후 수정)
    }

    @AfterThrowing(pointcut="serviceMethods()", throwing="ex")
    public void logAfterThrowing(Throwable ex) {
        System.out.println("[AfterThrowing] ex=" + ex);
    }

    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Around-Before] " + pjp.getSignature().getName());
        Object ret = pjp.proceed();                 // ★ 반드시 호출해 진행
        System.out.println("[Around-After] " + pjp.getSignature().getName());
        return ret;
    }
}

5) Pointcut 표현식 치트시트

  • 기본형:
    execution([접근자] [리턴타입] [패키지.클래스] [메소드명]([파라미터]))

  • 예시

    • execution(* com.example..*Service.*(..))
      패키지 하위의 모든 Service 메소드
    • execution(void com.example.*.*.get*(..))
      세 가지 깊이 패키지에서 void 반환, get* 메소드
    • execution(* com.example..set*(java.lang.String))
      String 하나 받는 set* 메소드

6) Around 사용 시 유의점

  • ProceedingJoinPoint진행 시점 직접 제어
  • pjp.proceed() 호출 누락 주의
  • 가능한 가장 약한 어드바이스로 해결하고, 불가할 때만 Around 사용

7) Reflection 요약(스프링 내부 활용 포인트)

  • Class/Field/Constructor/Method 메타정보 접근 및 호출
  • 런타임에 빈/메소드 정보를 분석하고 사용할 수 있게 지원

간단 스니펫:

Class<?> clazz = Account.class;
Field[] fields = clazz.getDeclaredFields();
Constructor<?>[] ctors = clazz.getConstructors();
Method m = clazz.getMethod("getBalance");
Object obj = ctors[0].newInstance("20","110-...","1234",10000);
System.out.println(m.invoke(obj));

8) 프록시 방식 비교

방식전제특징/성능
JDK Dynamic Proxy인터페이스 필요InvocationHandler 기반, 호출마다 검증 비용
CGLIB클래스 상속 기반바이트코드 조작, 인터페이스 불필요, 재사용으로 빠름

스프링 4.3+부터 CGLIB이 코어에 포함되어 기본적으로 원활하게 사용 가능.


9) 초간단 프록시 예시

9-1) JDK Dynamic Proxy

public interface Student { void study(int hours); }

public class OhgiraffersStudent implements Student {
    public void study(int h){ System.out.println(h + "시간 공부"); }
}

public class Handler implements InvocationHandler {
    private final Student target;
    public Handler(Student target){ this.target = target; }
    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        System.out.println("프록시-전"); m.invoke(target, args); System.out.println("프록시-후");
        return null;
    }
}

// 사용
Student proxy = (Student) Proxy.newProxyInstance(
        Student.class.getClassLoader(),
        new Class[]{Student.class},
        new Handler(new OhgiraffersStudent()));
proxy.study(2);

9-2) CGLIB(간단 개념)

OhgiraffersStudent proxy =
    (OhgiraffersStudent) Enhancer.create(OhgiraffersStudent.class,
        (InvocationHandler) (p, m, args) -> {
            System.out.println("프록시-전"); Object r = m.invoke(new OhgiraffersStudent(), args);
            System.out.println("프록시-후"); return r;
        });
proxy.study(2);

10) 체크리스트

  • 대상 범위 최소화: Pointcut을 좁게 설계
  • 빠른 실패: 예외 로깅은 AfterThrowing에서 처리
  • 반환값 가공은 AfterReturning에서 타입 안전하게
  • 성능 민감 구간은 Around에 타임로그 측정
  • 프록시 방식: 클래스 기반은 CGLIB, 인터페이스만 있으면 JDK 프록시도 OK
  • AOP 활성화 옵션: @EnableAspectJAutoProxy(proxyTargetClass = true) 검토
profile
개발자 희망자

0개의 댓글