AOP(Aspect-Oriented Programming) 란 애플리케이션의 공통된 관심사
(예: 로깅, 트랜잭션 관리, 권한 검사 등)를 모듈화하여
코드 중복을 줄이고 유지보수성을 높이는 데 사용되는 기술이다.
Spring AOP를 구현하려면 다음과 같은 구성 요소가 필요하다.
@Before
, @After
, @Around
등Spring AOP를 사용하려면 의존성을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-web'
starter-web 을 추가하면 자동적으로 spring-aop 가 딸려온다.
Aspect 클래스 선언
@Aspect
와 @Component
애너테이션을 사용하여 Aspect 클래스를 정의Advice 추가
@Before
, @After
, @Around
, @AfterThrowing
등을 사용하여 특정 시점에 실행하고 싶은 로직을 작성import org.aspectj.lang.annotation.*;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
// Pointcut 정의: 특정 패키지의 모든 메서드
@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
// Before Advice
@Before("serviceLayer()")
public void logBefore() {
System.out.println("메서드 실행 전 로깅");
}
// After Advice
@After("serviceLayer()")
public void logAfter() {
System.out.println("메서드 실행 후 로깅");
}
// Around Advice
@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전");
Object result = joinPoint.proceed(); // 원래 메서드 실행
System.out.println("메서드 실행 후");
return result;
}
}
Spring Boot에서는 @ComponentScan
기능을 통해
위와 같이 @Component
로 등록된 Aspect가 자동적으로 적용된다.
@Before
메서드 실행 이전에 실행
@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("메서드 이름: " + joinPoint.getSignature().getName());
System.out.println("메서드 실행 전 로깅");
}
@After
메서드 실행 이후에 실행
@After("execution(* com.example.service..*(..))")
public void logAfter() {
System.out.println("메서드 실행 후 로깅");
}
@AfterReturning
메서드가 정상적으로 종료된 후 실행
@AfterReturning(pointcut = "execution(* com.example.service..*(..))", returning = "result")
public void logAfterReturning(Object result) {
System.out.println("메서드 반환 값: " + result);
}
@AfterThrowing
메서드 실행 중 예외가 발생하면 실행
@AfterThrowing(pointcut = "execution(* com.example.service..*(..))", throwing = "exception")
public void logAfterThrowing(Exception exception) {
System.out.println("예외 발생: " + exception.getMessage());
}
@Around
메서드 실행 전후의 동작을 모두 처리
@Around("execution(* com.example.service..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전: " + joinPoint.getSignature());
Object result = joinPoint.proceed(); // 원래 메서드 실행
System.out.println("메서드 실행 후: " + joinPoint.getSignature());
return result;
}
Spring AOP의 Pointcut 표현식은 특정 메서드를 타겟팅할 때 사용된다.
주요 표현식은 다음과 같이 사용할 수 있다.
@Pointcut("execution(* com.example.service..*(..))")
execution
: 메서드 실행 시점을 타겟팅.com.example.service..*
: com.example.service
패키지와 하위 패키지의 모든 클래스.*(..)
: 모든 메서드와 매개변수를 타겟팅.@Pointcut("within(com.example.service.MyService)")
within
: 특정 클래스의 모든 메서드를 타겟팅.@Pointcut("@annotation(com.example.annotation.Loggable)")
@annotation
: 특정 애너테이션이 붙은 메서드를 타겟팅.살짝 봐도 둘은 하나의 쌍으로 붙어서 사용된다는 것을 알 수 있다.
어드바이스(Advice)와 포인트컷(Pointcut)은 보통 서로 결합되어
하나의 동작 단위처럼 움직이는 경우가 많다.
어드바이스는 특정 포인트컷에서 실행되기 때문에, 둘이 항상 짝을 이루어 동작한다.
즉
com.example.service
패키지의 모든 메서드, 특정 애노테이션이 붙은 메서드 등을 지정할 수 있다.Spring AOP에서 Advice를 작성할 때 Pointcut 표현식이 항상 뒤따르는 모습을 보았다.
@Around("execution(* com.example.service..*(..))")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전 로깅");
Object result = joinPoint.proceed();
System.out.println("메서드 실행 후 로깅");
return result;
}
@Around
는 언제 Advice가 실행될지를 정의 (여기서는 메서드 실행 전후)."execution(* com.example.service..*(..))"
는 어디에서 Advice를 실행할지를 정의따라서, Advice와 Pointcut은 하나의 동작 단위처럼 결합된다.
둘은 하나처럼 사용되지만 그렇다고 같은 것은 아니다.
이는 재사용성과 역할 분리를 위한 설계이다.
serviceLayer()
라는 Pointcut을 여러 Advice에서 재사용.@Pointcut("execution(* com.example.service..*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore() {
System.out.println("메서드 실행 전");
}
@After("serviceLayer()")
public void logAfter() {
System.out.println("메서드 실행 후");
}
@Aspect
@Component
public class AuthorizationAspect {
@Before("@annotation(com.example.annotation.RoleCheck)")
public void checkRole(JoinPoint joinPoint) {
System.out.println("권한 검사 로직 실행");
// 권한 체크 로직 구현
}
}
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service..*(..))")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 시작: " + joinPoint.getSignature());
Object result = joinPoint.proceed();
System.out.println("메서드 실행 종료: " + joinPoint.getSignature());
return result;
}
}
JoinPoint
와 ProceedingJoinPoint
는 둘 다 Spring AOP에서 메서드 호출을 가로챌 때 사용하는 객체지만, 기능과 사용 범위에서 차이가 있다.
JoinPoint
: 메서드 호출에 대한 정보만 제공하며, 메서드를 실행할 수는 없다.ProceedingJoinPoint
: JoinPoint
를 확장한 객체로, 메서드 실행(proceed()
) 기능을 제공한다.JoinPoint
JoinPoint
는 AOP의 특정 지점에 대한 정보를 제공한다.
메서드 호출, 예외 발생, 필드 접근 등의 정보를 얻는 데 사용된다.
@Before
, @After
, @AfterThrowing
등의 Advice에서 사용된다.getArgs()
:
[arg1, arg2, ...]
.getSignature()
:
getTarget()
:
getThis()
:
JoinPoint
사용 //이런 메서드가 존재할 때
@RoleCheck(allowRoles = {Role.MEMBER, Role.OWNER})
public WorkspaceUpdateResponseDto updateWorkspace(
Long workspaceId,
Long loginUserId,
WorkspaceUpdateRequestDto requestDto
)
@Before("@annotation(roleCheck)")
public void checkRole(JoinPoint joinPoint) {
// 메서드 정보
System.out.println("메서드 이름: " + joinPoint.getSignature().getName());
// 매개변수
Object[] args = joinPoint.getArgs();
System.out.println("매개변수: " + Arrays.toString(args));
// 타겟 객체
System.out.println("타겟 객체: " + joinPoint.getTarget());
}
메서드 이름: updateWorkspace
매개변수: [1, 2, "workspaceDto"]
타겟 객체: com.example.service.WorkspaceService@12345
ProceedingJoinPoint
ProceedingJoinPoint
는 JoinPoint
를 확장한 인터페이스로,
원래 메서드를 실행하거나 실행을 제어할 수 있는 기능을 제공한다.
@Around
Advice에서 사용proceed()
:
proceed(Object[] args)
:
ProceedingJoinPoint
사용@Around("execution(* com.example.service..*(..))")
public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 실행 전
System.out.println("메서드 실행 전: " + joinPoint.getSignature());
// 원래 메서드 실행
Object result = joinPoint.proceed();
// 메서드 실행 후
System.out.println("메서드 실행 후: " + joinPoint.getSignature());
return result; // 원래 메서드의 반환 값
}
메서드 실행 전: updateWorkspace
핵심 비즈니스 로직 실행 중...
메서드 실행 후: updateWorkspace
JoinPoint
와 ProceedingJoinPoint
의 차이점특징 | JoinPoint | ProceedingJoinPoint |
---|---|---|
Advice 타입 | @Before , @After , @AfterThrowing | @Around |
메서드 실행 제어 | 불가능 | 가능 (proceed() 사용) |
주요 역할 | 메서드 호출 정보 조회 | 메서드 호출 정보 조회 + 메서드 실행 전후 로직 추가 및 실행 제어 |
주요 메서드 | getArgs() , getSignature() , getTarget() | proceed() , proceed(Object[] args) |
JoinPoint
사용 (@Before
Advice)@Before("execution(* com.example.service..*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("메서드 호출: " + joinPoint.getSignature().getName());
System.out.println("매개변수: " + Arrays.toString(joinPoint.getArgs()));
}
ProceedingJoinPoint
사용 (@Around
Advice)@Around("execution(* com.example.service..*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전: " + joinPoint.getSignature().getName());
// 메서드 실행
Object result = joinPoint.proceed();
System.out.println("메서드 실행 후: " + joinPoint.getSignature().getName());
return result;
}
proceed()
를 통해 메서드 실행을 완전히 제어 가능.JoinPoint
:
ProceedingJoinPoint
:
@Around
Advice에서만 사용 가능.Spring AOP의 한계:
@EnableAspectJAutoProxy(exposeProxy = true)
를 설정하고, AopContext.currentProxy()
를 사용하여 자신을 호출.AOP 성능 고려:
Pointcut 정확성:
이제 실제로 적용해보러 가자!