자바 백엔드 입문 24편. @Aspect·@Before·@After·@Around로 첫 AOP를 작성하는 실전 코드. 포인트컷 표현식과 ProceedingJoinPoint까지 보안 직원 비유로 풀어쓴 Phase 3 마무리 글.
이 글은 자바 백엔드 입문 시리즈 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가 얼마나 강력한지 한눈에 보여요.
대부분의 횡단 관심사는 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()호출 안 하면 진짜 메서드 실행 Xpjp.getArgs()로 매개변수 가공 후pjp.proceed(args)가능pjp.getSignature()= 메서드 시그니처 정보- 같은 클래스 안 메서드 자기 호출 시 AOP 안 먹힘 (17편 함정)
@Order어노테이션으로 여러 Aspect 우선순위 지정 가능- 마커 어노테이션 정의 시
@Target(METHOD)+@Retention(RUNTIME)필수 - 실무에서 자체 Aspect 쓰는 일 = 정책 로깅 / 메트릭 / 감사 로그
- 대부분의 횡단 관심사는 Spring 기본 어노테이션(
@Transactional등)으로 처리 - 자체 Aspect는 "기본 어노테이션으로 안 되는" 시나리오에서만
- AOP는 단위 테스트가 까다로워 —
@SpringBootTest같은 통합 테스트 환경 필요
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 19편 — @Configuration @Bean Java Config
- 20편 — Bean Scope Singleton과 Prototype
- 21편 — Bean 생명주기 PostConstruct PreDestroy
- 22편 — SpEL 표현식 언어
- 23편 — AOP 횡단 관심사가 뭔가
다음 글: