Spring/문법

AOP (Aspect-Oriented Programming)

가지코딩 2025. 6. 11. 20:10

AOP란?

  • 핵심 비즈니스 로직과 부가적인 관심사(로깅, 트랜잭션, 보안 등)를 분리하여 모듈화하는 프로그래밍 기법
  • 관심사의 분리(Separation of Concerns, SoC)를 통해 코드의 재사용성, 유지보수성을 높인다

AOP의 필요성

  • 로깅, 예외 처리, 트랜잭션 관리 등은 여러 클래스에서 반복적으로 등장한다.
  • 이를 각 클래스에 흩뿌려 두면 코드가 중복되고, 수정이 어려워진다.
  • AOP를 사용하면 공통 기능을 한 곳에 모아 관리할 수 있다.

AOP 핵심 용어

  • Aspect: 공통 기능(로깅, 트랜잭션 등)을 모듈화한 단위
  • Join Point: Advice가 적용될 수 있는 지점(메서드 실행 등)
  • Pointcut: Join Point 중 실제 Advice가 적용될 지점을 지정
  • Advice: 언제(언제 실행되는지: before/after 등) 어떤 기능을 실행할지 정의
  • Weaving: Advice를 실제 대상 코드에 적용하는 과정
@Aspect
@Component
public class LoggingAspect {

    // Pointcut: MyService 클래스의 모든 메서드 실행 시점 (Join Point 대상)
    @Pointcut("execution(* com.example.service.MyService.*(..))")
    public void myServiceMethods() {}

    // Advice: Join Point 실행 '전'에 실행될 코드
    @Before("myServiceMethods()")
    public void beforeAdvice(JoinPoint joinPoint) {
        // Join Point: 핵심 비즈니스 메서드가 호출되는 시점(joinPoint.getSignature())
        System.out.println("[Before] " + joinPoint.getSignature().getName() + " 메서드 실행 전");
    }

    // Advice: Join Point 실행 '후'에 실행될 코드
    @AfterReturning(pointcut = "myServiceMethods()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("[AfterReturning] " + joinPoint.getSignature().getName() + " 메서드 실행 후, 반환값: " + result);
    }
}
// 핵심 비즈니스 클래스
public class MyService {
    public String doSomething() {
        System.out.println("핵심 비즈니스 로직 실행");
        return "결과값";
    }
}

 

 

아래는 실제 구현 코드 X, 어떻게 동작하는지 표현한 것

  • @SpringBootApplication 어노테이션을 사용하면
  • 스프링 부트가 자동으로 ApplicationContext를 생성하고, 빈(Bean) 등록, 의존성 주입, AOP 프록시 생성(Weaving) 등도 모두 자동으로 처리한다.
// 스프링 애플리케이션 실행 코드
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        // Weaving(위빙)된 프록시 객체를 받아온다.
        // 실제 MyService 객체가 아니라 프록시 객체다.
        MyService myService = context.getBean(MyService.class);
        
        // 아래 호출 시점이 Join Point
        // 프록시가 Advice 코드를 끼워 넣어 Before, AfterReturning이 실행된다.
        String result = myService.doSomething();
        
        System.out.println("최종 반환값: " + result);
    }
}

Pointcut 지정하는 법

 

execution 표현식

  • 형식: execution([접근제어자] [반환타입] [패키지.클래스.메서드](매개변수))
    • public → 접근제어자
    • * → 모든 반환 타입
    • com.example.service.* → service 패키지 내 모든 클래스
    • .*(..) → 모든 메서드 이름, 모든 매개변수
@Aspect
@Component
public class LoggingAspect {

    // com.example.service 패키지 내 모든 public 메서드 대상
    @Pointcut("execution(public * com.example.service.*.*(..))")
    public void allPublicServiceMethods() {}

    @Before("allPublicServiceMethods()")
    public void beforeServiceMethods(JoinPoint joinPoint) {
        System.out.println("[Before] 호출 메서드: " + joinPoint.getSignature());
    }
}

 

 

@annotation 표현식

  • 형식: @annotation(어노테이션클래스)
  • 타겟 메서드에 특정 노테이션이 붙어 있어야 한다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
@Aspect
@Component
public class ExecutionTimeAspect {

    // @LogExecutionTime 붙은 메서드 대상
    @Pointcut("@annotation(com.example.LogExecutionTime)")
    public void annotatedWithLogExecutionTime() {}

    @Around("annotatedWithLogExecutionTime()")
    public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("[ExecutionTime] " + pjp.getSignature() + " 수행 시간: " + (end - start) + "ms");
        return retVal;
    }
}
@Service
public class UserService {

    @LogExecutionTime
    public void createUser() {
        // 로직 수행
    }
}

Advice 종류

  • @Before: 대상 메서드 실행 전에 수행
  • @After: 대상 메서드 실행 후 (성공/예외와 무관하게) 수행
  • @AfterReturning: 대상 메서드가 정상적으로 실행된 후 수행
  • @AfterThrowing: 예외가 발생한 경우 수행
  • @Around: 대상 메서드 실행 전/후를 모두 감싸서 제어

 

@Before

  • 용도: 메서드 실행 전에 수행할 작업 (예: 로그 기록, 권한 체크)
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice(JoinPoint jp) {
    System.out.println("[Before] 메서드 호출 전: " + jp.getSignature());
}

 

 

@After

  • 용도: 메서드 실행 후 무조건 실행 (성공, 예외 구분 없음)
@After("execution(* com.example.service.*.*(..))")
public void afterAdvice(JoinPoint jp) {
    System.out.println("[After] 메서드 호출 후: " + jp.getSignature());
}

 

 

@AfterReturning

  • 용도: 정상적으로 메서드가 리턴된 후 실행 (예외 발생 시 실행 안 됨)
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint jp, Object result) {
    System.out.println("[AfterReturning] 메서드 정상 리턴: " + jp.getSignature() + ", 반환값: " + result);
}

 

 

@AfterThrowing

  • 용도: 메서드가 예외를 던진 후 실행
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint jp, Throwable ex) {
    System.out.println("[AfterThrowing] 예외 발생: " + jp.getSignature() + ", 예외: " + ex);
}

 

 

@Around

  • 용도: 메서드 실행 전후 모두 제어 가능, 실행 흐름 직접 조작 가능
@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("[Around] 메서드 실행 전: " + pjp.getSignature());
    Object result = pjp.proceed();  // 타겟 메서드 호출
    System.out.println("[Around] 메서드 실행 후: " + pjp.getSignature());
    return result;
}

실무 적용 사례

  • 서비스 로깅: 모든 서비스 메서드 호출 로그 남기기
  • 트랜잭션 처리: @Transactional이 내부적으로 AOP 활용
  • 보안 처리: 메서드 접근 권한 체크
  • 성능 측정: 실행 시간 측정

주의할 점

  • 프록시 기반의 한계: 내부 메서드 호출은 AOP가 적용되지 않는다.
  • private 메서드에는 적용되지 않음
  • 순환 참조 주의: Aspect가 빈 간의 의존성을 만들 때 주의