자바 백엔드 입문 24편 — @Aspect로 첫 AOP 박기

2026-05-16자바 백엔드 입문

자바 백엔드 입문 24편. @Aspect·@Before·@After·@Around로 첫 AOP를 작성하는 실전 코드. 포인트컷 표현식과 ProceedingJoinPoint까지 보안 직원 비유로 풀어쓴 Phase 3 마무리 글.

📚 자바 백엔드 입문 · 24편 — @Aspect로 첫 AOP 박기

이 글은 자바 백엔드 입문 시리즈 59편 중 24편이에요. 23편에서 AOP 발상과 5대 용어를 잡았다면, 이번 24편은 실제 @Aspect 클래스 한 개를 작성하는 실전 글입니다. Phase 3 SpEL·AOP 마지막 편이고, 끝까지 따라하면 직접 짠 Aspect가 메서드를 가로채는 첫 경험을 하게 돼요.

@Aspect 코드가 어렵게 들리는 이유

17편에서 AOP 발상은 그림으로 잡았지만 코드로 옮기려면 — @Aspect·@Before·@After·@Around 어노테이션이 많고, execution(* com.example..*.*(..)) 같은 포인트컷 표현식이 마치 정규식처럼 생겨서 처음 보면 막막해요.

이 글에서는 보안 직원 비유로 풀어요. 회사 사옥에 보안 직원 한 명이 "임원실에 들어오는 사람을 사진 찍고, 시간 기록" 하는 그림. 임원실 = 메서드, 보안 직원 = Aspect, 사진 찍기 = Advice, "임원실" 이라는 지정 = Pointcut. 끝까지 따라오시면 5종 어드바이스 + 포인트컷 표현식이 한 번에 박혀요.

Spring Boot 프로젝트에 AOP 활성화

먼저 build.gradle 에 의존성 한 줄 추가.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

Spring Boot 의존성을 추가하면 — @EnableAspectJAutoProxy 가 자동 활성화돼요. 우리가 별도 설정 안 해도 @Aspect 박힌 클래스가 자동으로 동작 준비. 이게 Spring Boot의 마법.

첫 Aspect — @LogExecution 어노테이션과 짝궁

기능 한 가지 — "이 어노테이션 박힌 메서드의 실행 시간을 로깅". 두 파일이 필요해요.

1. 마커 어노테이션 정의

package com.example.myshop.aop;

import java.lang.annotation.*;

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

4편 어노테이션에서 다룬 패턴. "메서드에 박을 수 있고 런타임까지 살아있는" 사용자 정의 어노테이션.

2. Aspect 클래스 작성

package com.example.myshop.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("@annotation(com.example.myshop.aop.LogExecution)")
    public Object logExecution(ProceedingJoinPoint pjp) throws Throwable {
        String method = pjp.getSignature().toShortString();
        long start = System.currentTimeMillis();
        log.info("→ {} 시작", method);

        try {
            Object result = pjp.proceed();   // 진짜 메서드 실행
            log.info("← {} 성공 — {}ms", method, System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            log.error("✗ {} 실패 — {}", method, e.getMessage());
            throw e;
        }
    }
}

3. 사용하기

@Service
public class OrderService {

    @LogExecution                              // ← 이거 한 줄
    public void placeOrder(Order order) {
        // 비즈니스 로직
    }
}

@LogExecution 박은 메서드를 호출하면 — LoggingAspect 가 자동으로 가로채서 "시작 로그 → 메서드 실행 → 성공/실패 로그" 를 처리해요. 우리는 비즈니스 로직만 짜면 끝.

5종 Advice — Spring이 메서드를 가로채는 5가지 시점

@Around 외에도 어드바이스가 네 개 더 있어요. "메서드의 어느 지점에 끼어들지" 가 다른 거예요.

어드바이스호출 시점사용 예
@Before메서드 실행 직전권한 검증·전처리
@After메서드 실행 직후 (성공·실패 무관)자원 정리
@AfterReturning메서드 정상 반환 후반환값 가공·로깅
@AfterThrowing메서드 예외 발생 후예외 로깅·알림
@Around메서드 실행 앞뒤 모두 + 결과 가로채기시간 측정·재시도·캐싱

@Around 가 가장 강력해요. 메서드 실행 자체를 우리가 결정 가능(pjp.proceed() 호출 여부). 다른 4개는 어드바이스 안에서 메서드 실행을 막거나 결과를 바꿀 수 없어요.

@Before("@annotation(com.example.myshop.aop.LogExecution)")
public void before(JoinPoint jp) {
    log.info("메서드 실행 직전 — {}", jp.getSignature());
}

@AfterReturning(pointcut = "@annotation(com.example.myshop.aop.LogExecution)", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
    log.info("정상 반환 — {} → {}", jp.getSignature(), result);
}

@AfterThrowing(pointcut = "@annotation(com.example.myshop.aop.LogExecution)", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
    log.error("예외 발생 — {}", jp.getSignature(), ex);
}

포인트컷 표현식 — 어느 메서드에 적용할지

Aspect를 어디에 적용할지 지정하는 게 포인트컷(Pointcut) 표현식. 위에서 본 @annotation(...) 외에도 여러 방식이 있어요.

1. 어노테이션 기반 — 가장 직관적

@Around("@annotation(com.example.myshop.aop.LogExecution)")

마커 어노테이션 박힌 메서드에만 적용. 가장 흔한 패턴.

2. execution — 메서드 시그니처 기반

@Around("execution(* com.example.myshop.service.*.*(..))")

"com.example.myshop.service 패키지의 모든 클래스의 모든 메서드" 에 적용. execution() 안의 와일드카드 풀이:

위치 의미
* (첫 번째) 모든 반환 타입
com.example.myshop.service.* 그 패키지의 모든 클래스
* (메서드 이름) 모든 메서드 이름
(..) 모든 매개변수 (개수·타입 무관)

조금 더 좁히는 예:

@Around("execution(* com.example.myshop.service.OrderService.placeOrder(..))")
// OrderService 클래스의 placeOrder 메서드만

3. within — 클래스·패키지 범위

@Around("within(com.example.myshop.service..*)")
// service 패키지와 하위 패키지 모든 클래스

4. bean — Bean 이름

@Around("bean(orderService)")
// 이름이 orderService인 Bean의 모든 메서드

5. 조합 — AND·OR·NOT

@Around("execution(* com.example..*.*(..)) && @annotation(LogExecution)")
// 둘 다 만족

ProceedingJoinPoint — @Around의 손잡이

@Around 어드바이스에만 등장하는 객체 ProceedingJoinPoint. 이 손잡이로 "실제 메서드 실행" 을 제어할 수 있어요.

@Around("@annotation(LogExecution)")
public Object aroundExample(ProceedingJoinPoint pjp) throws Throwable {
    String methodName = pjp.getSignature().getName();     // 메서드 이름
    Object[] args = pjp.getArgs();                         // 매개변수 배열
    Class<?> targetClass = pjp.getTarget().getClass();    // 실제 객체의 클래스

    // 매개변수 가공 후 메서드 실행
    Object result = pjp.proceed(args);

    // 반환값 가공도 가능
    return result;
}

pjp.proceed() 를 호출하지 않으면 — 진짜 메서드가 아예 실행 안 됨. 캐싱 어드바이스가 "캐시에 있으면 진짜 메서드 안 부르고 캐시값 반환" 같은 패턴이 가능한 이유.

실전 예 — @Cacheable 비슷한 단순 캐시

@Cacheable 의 원리를 짧게 구현해볼게요.

@Aspect
@Component
public class SimpleCacheAspect {
    private final Map<String, Object> cache = new ConcurrentHashMap<>();

    @Around("@annotation(MyCacheable)")
    public Object cache(ProceedingJoinPoint pjp) throws Throwable {
        String key = pjp.getSignature() + Arrays.toString(pjp.getArgs());

        if (cache.containsKey(key)) {
            return cache.get(key);   // 캐시 히트 — 메서드 실행 X
        }

        Object result = pjp.proceed();   // 캐시 미스 — 메서드 실행
        cache.put(key, result);
        return result;
    }
}

15줄로 "메서드 호출 결과 자동 캐싱" 이 구현돼요. 진짜 Spring @Cacheable 은 이보다 훨씬 정교하지만 — 핵심 발상은 위 코드 그대로. AOP가 얼마나 강력한지 한눈에 보여요.

🎯 실무에서 직접 Aspect 쓰는 일

대부분의 횡단 관심사는 Spring이 이미 만들어둔 어노테이션(@Transactional·@PreAuthorize·@Cacheable·@Retryable·@Timed)으로 처리돼요. 직접 @Aspect 짤 일은 회사 정책 로깅·메트릭·감사 로그 같은 영역에서 가끔 생깁니다. "AOP 이해 → 기존 어노테이션 활용 → 필요시 자체 Aspect" 순서.

한 줄 정리 — @Aspect 클래스에 @Before·@After·@Around 어드바이스 박고, 포인트컷 표현식으로 적용 대상 지정. Spring AOP가 자동으로 모든 메서드에 끼워 넣어줌. Phase 3 마무리.

시험 직전 한 번 더 — @Aspect 입문자가 매번 헷갈리는 것

  • AOP 활성화 = spring-boot-starter-aop 의존성 한 줄
  • Spring Boot는 @EnableAspectJAutoProxy 자동 활성화
  • @Aspect + @Component 두 어노테이션 박아야 Bean 등록 + AOP 인식
  • 5종 어드바이스 = @Before / @After / @AfterReturning / @AfterThrowing / @Around
  • @Around 가 가장 강력 — 메서드 실행 자체를 제어 가능
  • 다른 4개는 메서드 실행을 막거나 결과 가공 불가
  • 포인트컷 표현식 = @annotation(...) / execution(...) / within(...) / bean(...)
  • @annotation 패턴이 가장 직관적 — 마커 어노테이션 정의해서 적용 대상 표시
  • execution() 와일드카드 = * (모든 토큰) / .. (하위 전체) / (..) (모든 매개변수)
  • ProceedingJoinPoint = @Around 전용. 실제 메서드 실행 제어
  • pjp.proceed() 호출 안 하면 진짜 메서드 실행 X
  • pjp.getArgs() 로 매개변수 가공 후 pjp.proceed(args) 가능
  • pjp.getSignature() = 메서드 시그니처 정보
  • 같은 클래스 안 메서드 자기 호출 시 AOP 안 먹힘 (17편 함정)
  • @Order 어노테이션으로 여러 Aspect 우선순위 지정 가능
  • 마커 어노테이션 정의 시 @Target(METHOD) + @Retention(RUNTIME) 필수
  • 실무에서 자체 Aspect 쓰는 일 = 정책 로깅 / 메트릭 / 감사 로그
  • 대부분의 횡단 관심사는 Spring 기본 어노테이션(@Transactional 등)으로 처리
  • 자체 Aspect는 "기본 어노테이션으로 안 되는" 시나리오에서만
  • AOP는 단위 테스트가 까다로워 — @SpringBootTest 같은 통합 테스트 환경 필요

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!