AOP 핵심 개념
- 관점(Aspect): 로깅이나 권한 검사와 같은 공통 관심사를 모듈화한 구성요소로,
@Aspect어노테이션을 통해 클래스를 관점으로 선언한다. - 조인포인트(Join Point): 메서드 호출이나 예외 발생과 같은 실행 시점의 특정 위치이며, 스프링 AOP는 메서드 수준만 지원한다.
- 어드바이스(Advice): 조인포인트에서 수행되는 동작으로 다섯 가지 유형이 있다:
@Before: 메서드 실행 전에 동작@After: 성공 여부와 관계없이 메서드 실행 후 동작@AfterReturning: 정상 반환 후 동작@AfterThrowing: 예외 발생 시 동작@Around: 메서드 전체를 감싸며 실행 흐름 제어 가능
- 포인트컷(Pointcut): 어떤 메서드에 어드바이스를 적용할지를 결정하는 표현식으로, AspectJ 문법을 사용하여 정의한다.
포인트컷 표현식 예제
AspectJ 기반 포인트컷 표현식의 기본 구조는 다음과 같다:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
실제 사용 예시:
- 서비스 계층 모든 메서드 대상
@Pointcut("execution(* com.example.demo.service.*.*(..))")
*: 임의의 반환 타입com.example.demo.service.*: service 패키지 내 모든 클래스* *(..): 임의의 메서드명 및 파라미터
- 특정 어노테이션이 붙은 메서드
@Pointcut("@annotation(com.example.demo.annotation.Log)")
@Log어노테이션이 적용된 메서드만 선택
- 복합 조건식
@Pointcut("execution(* com.example.demo.controller.*.*(..)) && @annotation(com.example.demo.annotation.Auth)")
- 컨트롤러 패키지 중
@Auth어노테이션이 있는 메서드 대상
전체 구현 예시
요구사항: 컨트롤러 메서드 호출 시 메서드 이름, 입력값, 소요 시간, 결과 값을 로그로 기록한다.
- 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String desc() default "";
}
- 관점 클래스 생성
@Aspect
@Component
public class LoggingAspect {
// @Log 어노테이션이 붙은 메서드를 위한 포인트컷
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void logPointcut() {}
// 메서드 실행 전 로그 출력
@Before("logPointcut()")
public void beforeLog(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
Object[] params = joinPoint.getArgs();
System.out.println("[LOG-BEFORE] Method: " + method + ", Args: " + Arrays.toString(params));
}
// 메서드 실행 시간 측정
@Around("logPointcut()")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = pjp.proceed(); // 실제 메서드 실행
long endTime = System.currentTimeMillis();
System.out.println("[LOG-AROUND] Duration: " + (endTime - startTime) + "ms");
return result;
}
// 메서드 리턴 후 로그 출력
@AfterReturning(pointcut = "logPointcut()", returning = "output")
public void afterReturnLog(Object output) {
System.out.println("[LOG-AFTER] Result: " + output);
}
}
- 컨트롤러 메서드에 어노테이션 적용
@RestController
@RequestMapping("/api/user")
public class UserController {
@Log(desc = "사용자 정보 조회")
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
동작 원리
- 프록시 메커니즘: 스프링 AOP는 JDK 동적 프록시 또는 CGLIB를 이용해 런타임에 대상 객체의 프록시를 생성하고, 메서드 호출 시점에 관점 로직을 삽입한다.
- 실행 순서 (
@Around중심):- 프록시가 메서드 호출 가로챔
@Before어드바이스 실행ProceedingJoinPoint.proceed()로 실제 메서드 실행@AfterReturning/@AfterThrowing어드바이스 실행@After어드바이스 실행
권장 사항
- 관점 범위 조절:
- 복잡한 비즈니스 로직은 관점 외부로 분리
- 커스텀 어노테이션을 활용하여 정확한 포인트컷 설정
- 성능 최적화:
@Around내부에서 무거운 연산 피하기- 고빈도 호출 메서드에는 AOP 적용 신중하게 고려
- 예외 처리:
@Around에서는 원본 예외 누락 방지@AfterThrowing을 통해 통합된 오류 로깅
- 응용 시나리오:
- 로깅 + 성능 모니터링:
@Around로 시간 측정 및 로깅 - 권한 체크 + 트랜잭션 관리:
@Before에서 권한 확인,@Around에서 트랜잭션 제어
- 로깅 + 성능 모니터링:
AspectJ 포인트컷 심화 설명
AspectJ 포인트컷 표현식은 AOP의 핵심으로, 어떤 메서드에 어드바이스를 적용할지를 결정한다. 스프링 부트에서는 AspectJ의 문법을 차용하여 사용한다.
기본 표현식 형식
execution(<접근제한자>? <반환타입> <클래스경로>.<메서드명>(<파라미터>) <예외>?)
주요 와일드카드:
*: 임의의 문자열 또는 타입..: 임의 개수의 파라미터 또는 하위 패키지 경로+: 해당 클래스 및 서브클래스
표현식 예제
- 메서드 실행 대상
- 모든 public 메서드:
execution(public * *(..)) - 특정 패키지 내 모든 메서드:
execution(* com.example.service.*.*(..)) - 특정 메서드 이름:
execution(* com.example.service.*.add*(..)) - 특정 파라미터:
execution(* com.example.service.*.*(String, int))
- 모든 public 메서드:
- 어노테이션 기반 매칭
- 특정 어노테이션이 붙은 메서드:
@annotation(com.example.annotation.Log) - 특정 어노테이션이 붙은 클래스 내 메서드:
@within(com.example.annotation.Service)
- 특정 어노테이션이 붙은 메서드:
- 복합 조건
- AND 조건:
execution(...) && @annotation(...) - OR 조건:
execution(...) || execution(...)
- AND 조건:
고급 표현식
this(UserService): 프록시 객체 타입 매칭target(UserService): 실제 대상 객체 타입 매칭get(* com.model.User.name): 필드 접근 시점execution(com.model.User.new(..)): 생성자 호출 시점
최선의 활용 방법
- 과도한 매칭 방지:
*(..)보다는 구체적인 메서드 명세 사용 - 커스텀 어노테이션 활용:
@annotation과 함께 유연하게 제어 - 복합 표현식 조합: 가독성 향상을 위해 AND/OR 연산자 적극 활용
- 성능 모니터링 + 로깅:
@Around로 메서드 실행 시간 측정
종합 예제
- 관점 정의
@Aspect
@Component
public class PerformanceLogger {
// 컨트롤러 public 메서드 포인트컷
@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
public void controllerMethods() {}
// @Log 어노테이션이 붙은 메서드 포인트컷
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void annotatedWithLog() {}
// 두 조건 모두 만족하는 경우
@Before("controllerMethods() && annotatedWithLog()")
public void preLog(JoinPoint jp) {
System.out.println("Executing method: " + jp.getSignature().getName());
}
// 실행 시간 측정
@Around("controllerMethods()")
public Object timeCheck(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
System.out.println("Elapsed Time: " + (System.currentTimeMillis() - start) + " ms");
return result;
}
}
- 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
- 컨트롤러 적용
@RestController
@RequestMapping("/api/user")
public class UserController {
@Log("사용자 정보 가져오기")
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
Java 포인트컷 지시자(PCD)
Java AOP에서 주요 포인트컷 지시자는 다음과 같다:
1. execution
메서드 실행 지점을 나타내며 가장 일반적으로 사용됨:
// service 패키지 내 모든 메서드
execution(* com.example.service.*.*(..))
// service 패키지 및 하위 패키지 내 모든 메서드
execution(* com.example.service..*.*(..))
// 특정 메서드명 패턴
execution(public * *(..))
execution(* set*(..))
2. 어노테이션 관련
@within: 특정 어노테이션이 붙은 클래스 내 모든 메서드@target: 대상 객체가 특정 어노테이션이 붙은 클래스@annotation: 메서드에 특정 어노테이션이 붙은 경우@args: 메서드 파라미터에 특정 어노테이션이 붙은 경우
3. 파라미터 관련
args: 파라미터 타입이나 개수에 따라 매칭
4. 기타
within: 특정 패키지나 클래스 내 연결점this / target: 프록시 또는 실제 객체 타입 매칭bean: 스프링 빈 이름으로 매칭(Spring 전용)
결합 연산자
&&: AND||: OR!: NOT
예시
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Pointcut("within(com.example.dao..*)")
public void daoLayer() {}
@Pointcut("serviceLayer() || daoLayer()")
public void allLayers() {}
@Before("allLayers() && @annotation(com.example.Auditable)")
public void audit(JoinPoint jp) {
// 감사 로직
}
JoinPoint와 ProceedingJoinPoint 비교
1. JoinPoint
- 역할: 메서드 호출 시점의 정보 제공
- 사용 대상:
@Before,@After,@AfterReturning,@AfterThrowing - 주요 메서드:
getArgs(): 파라미터 배열 반환getSignature(): 메서드 시그니처 정보getTarget(): 실제 대상 객체getThis(): 프록시 객체
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
String method = jp.getSignature().getName();
Object[] args = jp.getArgs();
System.out.println("Calling method: " + method + ", Args: " + Arrays.toString(args));
}
2. ProceedingJoinPoint
- 역할: 메서드 실행 흐름 제어 (
JoinPoint확장) - 사용 대상:
@Around전용 - 핵심 메서드:
proceed(): 실제 메서드 실행proceed(Object[] args): 새로운 파라미터로 메서드 실행
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Before method: " + pjp.getSignature().getName());
Object result = pjp.proceed(); // 또는 pjp.proceed(newArgs);
System.out.println("After method, Result: " + result);
return result;
}
차이 요약
| 속성 | JoinPoint | ProceedingJoinPoint |
|---|---|---|
| 용도 | 정보 취득 | 실행 흐름 제어 |
| 적용 어드바이스 | @Before, @After 등 |
@Around 전용 |
| 핵심 메서드 | getArgs(), getSignature() |
proceed(), proceed(Object[] args) |
| 실행 흐름 변경 | 불가능 | 가능 |
권장 사항
- 필요한 정보만 얻을 때는
JoinPoint사용 @Around에서는 반드시proceed()호출 필요- 고성능 요구 시
@Around사용 자제
파라미터 바인딩
AOP에서 파라미터 바인딩은 포인트컷 표현식을 통해 어드바이스 메서드의 파라미터에 실제 메서드 파라미터 값을 전달하는 기술이다.
1. 바인딩 메커니즘
- args() 사용: 파라미터 이름 또는 타입으로 매칭
@Before("execution(* com.example.service.*.*(..)) && args(param)")
public void logParam(JoinPoint jp, Object param) {
System.out.println("Parameter value: " + param);
}
- 여러 파라미터 바인딩:
@Before("execution(* com.example.service.*.*(..)) && args(id, name)")
public void logMultiple(Long id, String name) {
System.out.println("ID: " + id + ", Name: " + name);
}
2. 활용 시나리오
- 로깅:
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println("Calling method: " + pjp.getSignature().getName() + ", Args: " + Arrays.toString(args));
return pjp.proceed();
}
- 파라미터 유효성 검사:
@Before("execution(* com.example.service.*.*(..)) && args(param)")
public void validate(Object param) {
if (param == null) {
throw new IllegalArgumentException("Parameter cannot be null");
}
}
- 권한 체크:
@Before("execution(* com.example.service.*.*(..)) && args(userId)")
public void checkPermission(Long userId) {
if (!permissionService.hasAccess(userId)) {
throw new SecurityException("Access denied");
}
}
3. 고급 기법
- 커스텀 어노테이션 결합
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
boolean logParams() default true;
}
@Aspect
@Component
public class LoggingAspect {
@Before("@annotation(loggable)")
public void handleLoggable(JoinPoint jp, Loggable loggable) {
System.out.println("Annotation value: " + loggable.value());
}
}
- 파라미터 수정
@Around("execution(* com.example.service.*.*(..))")
public Object modifyArg(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
if (args.length > 0 && args[0] instanceof String) {
args[0] = "modified value"; // 첫 번째 파라미터 수정
}
return pjp.proceed(args); // 새 파라미터로 실행
}
- 제네릭 처리
@Before("execution(* com.example.service.*.*(..)) && args(list)")
public void logList(List<String> list) {
System.out.println("List size: " + list.size());
}
4. 문제 해결 팁
- 바인딩 실패: 파라미터 이름 또는 타입 불일치 확인
- 성능 이슈: 고빈도 호출 메서드에서는 바인딩 로직 최소화
- 내부 호출 무시: 같은 클래스 내 메서드 호출은 프록시를 우회하므로 직접 프록시 접근 필요