AOP(Aspect Oriented Programming)
💡 AOP란 관점 지향 프로그래밍이란 의미로 프록시를 사용하여 핵심 비즈니스 로직과 공통 부가 기능을 분리하는 것
↳ 핵심 로직(주문, 회원가입 등) / 부가 기능(시간 측정, 트랜잭션 관리 등)
🎈 OOP(객체 지향 프로그래밍) - 객체를 기준으로 기능을 모듈화했다면,
AOP(관점 지향 프로그래밍) - 여러 객체에 공통적으로 적용되는 기능(횡단 관심사)을 기준으로 모듈화 한 것,
↳ 즉 여러 곳에 흩어져 있는 공통 코드를 한 곳으로 모아서 관리하는 방식
*횡단 관심사(Cross-cutting Concern) : 여러 클래스에서 반복적으로 나타나는 공통 기능(로깅, 트랜잭션 처리, 성능 측정, 보안 체크 등)
프록시(Proxy)
💡 클라이언트가 실제 객체를 직접 호출하지 않고, 중간에 있는 대리 객체(Proxy)를 통해 호출하는 방식
↳ 클라이언트 -> 프록시(대리인) -> 실제 객체
🎈 프록시는 실제 메서드 실행 전후에 개입 하여 공통 기능을 수행한다(로깅, 권한 검사, 시간 측정, 보안 체크 등)
스프링 AOP의 3요소
| 용어 | 정의 | 역할 |
| 포인트컷(Pointcut) | 적용 위치(어디에?) | 부가 기능을 적용할 대상 메서드를 선정 ex) hello.aop.order 패키지의 모든 메서드 |
| 어드바이스(Advice) | 적용 로직(무엇을?) | 실직적으로 수행할 부가 기능 코드 ex) 로그 출력, 실행 시간 측정 등 |
| 어드바이저(Advisor) | 포인트컷 + 어드바이스 결합체 | 스프링 AO의 가장 기본적인 형태로, 스프링 빈 등록 필요 |
🎈 어드바이저를 스프링 빈으로 등록하면, 자동 프록시 생성기가 알아서 처리
자동 프록시 생성기 동작 방식
✔어드바이저 등록 -> 스프링 내부의 자동 프록시 생성기 동작
1. 조회 : 등록된 모드 어드바이저를 탐색
2. 검사 : 생성되는 모든 스프링 빈이 포인트컷 조건에 맞는지 확인
3. 교체 : 포인트컷이 매칭 되면, 원본 대신 프록시 객체 자동 생성 후 사용, 매칭 안되면 원본 객체 사용
4. 실행 : 사용자가 빈을 호출하면, 원본이 아닌 프록시 객체가 호출되어 공통 기능 먼저 수행
@Aspect - 어드바이저 생성
💡 @Aspect는 해당 클래스는 어드바이저라고 스프링에게 알려주는 역할, 애노테이션 기반 프록시 적용할 때 필요
↳ 컴포넌트 스캔이 되지 않으며, 스프링 빈으로 직접 등록 필요
@Aspect //✔ 어드바이저
public class AspectV1 {
//✔ 포인트컷, hello.aop order 패키지와 그 하위 패키지의 모든 메서드에 적용
@Around("execution(* hello.aop.order..*(..))")
//✔ 어드바이스, 실제로 실행할 공통 기능(로그 출력)
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed(); // ✔ 실제 메서드 실행(원래 메서드)
}
}
포인트컷 분리
💡 포인트컷 분리란, 포인트컷 표현식을 별도의 메서드로 추출하여 이름을 붙여주는 것
↳ 프로젝트가 커지면 같은 범위에 여러 기능을 넣고 싶을 때 사용
@Slf4j
@Aspect
public class AspectV2 {
//✔ pointcut signature, hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
✔ @Pointcut에 포인트컷 표현식 사용
✔ 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라고 함
✔ 메서드 반환 타입은 void여야 한다.
✔ 코드 내용은 비워둔다(선언만 하는 용도)
✔ 내부에서만 사용하면 private, 다른 애스팩트에서 참고하려면 public
🎈 포인트컷 분리를 하면 하나의 포인트컷 표현식을 여러 어드바이스 함께 사용하기에 재사용할 수 있으며, 가독성이 좋아 의도를 파악하기 쉽습니다.
포인트컷 조합(Logic Operators)
💡 AOP에서 중요한 건 단순히 공통 기능을 넣는 것이 아닌, 어디에 적용할지(포인트컷) 언제 실행할지(어드바이스), 여러 개가 있을 대 어떤 순서로 실행될 지 이 세가지를 정확하게 제어하는 것이다.
트랜잭션(Transaction) 기능을 추가해보겠습니다. 핵심은 여러 개의 어드바이스가 어떤 기준으로, 어떤 순서로 동작하는가?를 이해하는 것입니다.
트랜잭션(Transaction) 기능 추가
💡 트랜잭션(Transaction)이란 하나의 작업을 전부 성공시키거나, 전부 실패시키는 것을 보장하는 기능
✔ 트랜잭션(Transaction) 시뮬레이션
1. 시작 : 핵심 로직 실행 직전
2. 커밋 : 로직이 성공적으로 끝나면 확정
3. 롤백 : 중간에 예외(Exception)가 터지면 취소
4. 릴리즈 : 성공/실패 상관 없이 자원 반납
🎈 트랜잭션은 "하나의 어드바이스"가 아니라 여러 어드바이스가 조합된 구조
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService() {}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//트랜잭션 처리
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e ) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
💡 포인트컷은 자바 논리 연산자(&&, ||, !)와 동일하게 조합 가능, 논리 연산자를 통해 더 세밀한 타겟팅 가능
1. doLog() 실행 (로그 시작)
2. doTransaction() 실행 (트랜잭션 시작)
3. 실제 비즈니스 로직 (orderItem()) 실행
4. doTransaction() 종료 (트랜잭션 커밋 -> 리소스 릴리즈)
5. doLog() 종료 (로그 끝)
어드바이스의 실행 순서
💡 어드바이스는 기본적으로 순서를 보장하지 않습니다. 순서를 보장하고 싶으면, @Order는 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있습니다. 그래서 하나의 애스팩트에 여러 어드바이스가 있으면 순서를 보장받을 수 없습니다.
🎈이를 해결하기 위해서는 어드바이스를 별도의 클래스로 분리하고, 클래스 단위에 @Order 애노테이션을 사용합니다.
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.PointCuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e ) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
}
@Slf4j
@SpringBootTest
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
포인트 컷 참조(공용 사용)
💡 포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 됨.
↳ 외부에서 호출할때는 포인트컷의 접근 제어자를 public으로 변경 필요
public class PointCuts {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
@Slf4j
@Aspect
public class AspectV4 {
@Around("hello.aop.order.aop.PointCuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e ) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
조인 포인트(Join Point)와 프로시딩 조인 포인트(ProceedingJoinPoint)
조인 포인트(Join Point)
💡 조인 포인트(Join Point)란 어드바이스가 적용될 수 있는 모든 지점을 의미합니다.
↳ 스프링 AOP는 '프록시' 방식을 사용하기에, 메서드 호출 시점이 곧 조인 포인트이며 조인 포인트 파라미터 같은 객체에는 호출 정보가 담겨있습니다.
주요 기능
| 메서드 | 의미 | 예시 |
| getArgs() | 파라미터 정보 추출 (사용자가 메서드에 전달한 파라미터 값을 배열로 가져옴) |
orderItem("itemA") 호출시 ["itemA] |
| getSignature() | 호출된 메서드 정보 확인 (메서드의 상세 선언 정보(이름, 반환 타입, 파라미터 타입 등) |
로그에 어떤 메서드가 실행중인지 확인 |
| getTarget() | 대상 객체 참조 (프록시가 아닌, 실제 비즈니스 로직을 수행하는 원본 객체 반환) |
원본 클래스의 필드 값 참조 및 다른 메서드와의 연동이 필요할 때 사용 |
프록시는 포인트컷에 걸려 있는 메서드나, 인터페이스에 정의된 메서드만 대행, 원본의 멤버 변수 값을 직접 갖고 있지 않음.
프로시딩 조인 포인트(Proceeding Join Point)
💡JoinPoint의 모든 기능을 가지고 있으면서, 딱 한 개의 기능(proceed())이 추가된 자식 객체입니다.
🎈 proceed() : 다음 로직을 실행하라는 신호로 @Around에서만 사용되며 조인 포인트 실행 여부 결정이 가능합니다.
↳ @Around에만 사용되는 이유는 메서드 실행 전과 후를 모두 직접 제어해야 하기 때문이며, proceed()를 호출하지 않으면 실제 로직이 아예 실행되지 않게 막을 수 있습니다.
어드바이스 종류
💡 어드바이스는 프록시가 호출되는 시점에 따라 5가지로 나뉩니다.
| 종류 | 의미 | 특징 |
| @Around (전, 후 처리) |
메서드 실행 전/후 + 예외 까지 모든 흐름을 제어하는 가장 강력한 메서드 | - ProceedingJoinPoint 사용(유일) - proceed() 호출 시 실제 메서드 실행, 호출 안 하면 비즈니스 로직 실행 x - 실행 여부 제어, 여러번 실행(재시도 가능) *성능 측정, 트랜잭션, 공통 처리에 많이 사용 |
| @Before (사전 처리) |
메서드 실행 직전에 동작 | - 메서드 종료되면 자동으로 다음 타겟 호출 * 권한 체크, 입력 값 로그 기록 등 실행 전 검증 로직에 적합 |
| @AfterReturning (정상 종료 후 처리) |
메서드가 정상적으로 실행된 경우만 실행 | - returning 속성과 매개변수 일치해야 함. - 지정한 타입 또는 그 부모 타입일때만 동작 - 반환 객체를 다른 객체로 교환 불가(내부 값 변경만 가능) * 결과 로그 기록, 반환 데이터 가공에 적합 |
| @AfterThrwoing (예외 발생 처리) |
메서드 실행 중 예외 발생했을 때만 실행 | - throwing 속성에 지정된 이름과 매개변수명 일치해야 함 - 지정된 예외 타입 발생했을때만 동작 *예외 발생시 공통 로깅, 알림발송, 롤백 등에 적합 |
| @ After (항상 처리) |
성공, 실패 상관 없이 메서드 종료 시 무조건 실행 (자바 finally와 유사) |
*리소스 정리 및 DB 연결 해제에 적합 |
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e ) {
//@AFterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("hello.aop.order.aop.PointCuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.PointCuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return = {}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.PointCuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint jpoinPoint, Exception ex) {
log.info("[ex] {} message = {}", ex);
}
@After(value = "hello.aop.order.aop.PointCuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
'Spring' 카테고리의 다른 글
| [Spring Boot] 중복 로그인 차단 정리(스케일업, 스케일 아웃) (0) | 2026.05.13 |
|---|---|
| [Spring Boot] 테스트의 이해와 @Transactional 동작 원리 (0) | 2026.05.12 |
| [Spring] - BCryptPasswordEncoder를 이용한 비밀번호 암호화와 보안 원리 (1) | 2026.04.16 |
| [Spring] 싱글톤 패턴과 싱글톤 컨테이너 정리 (0) | 2026.03.23 |
| [Spring] 스프링 프레임워크의 기초 이해(2) - 스프링 애플리케이션 구조와 스프링 컨테이너, 빈(Bean) (0) | 2026.03.15 |