스프링 AOP 구현
- 김영한님의 스프링 원리 - 고급편 강의를 바탕으로 스프링 AOP의 구현 방법과 단계별 진화 과정, 그리고 다양한 어드바이스 종류를 정리함
프로젝트 설정
build.gradle
1
2
3
4
5
implementation 'org.springframework.boot:spring-boot-starter-aspectj'
// 테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
- 스프링 부트를 사용하면
@EnableAspectJAutoProxy는 자동으로 추가됨
예제 프로젝트 구조

OrderRepository
1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
OrderService
1
2
3
4
5
6
7
8
9
10
@Slf4j
@Service
public class OrderService {
private final OrderRepository orderRepository;
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
AopTest (기본)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@SpringBootTest
public class AopTest {
@Autowired OrderService orderService;
@Autowired OrderRepository orderRepository;
@Test
void aopInfo() {
// AOP 프록시 적용 여부 확인 (현재는 false)
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);
}
}
시작 (AspectV1)
-
@Aspect와@Around를 사용한 가장 기본적인 AOP 구현 방식임1 2 3 4 5 6 7 8 9 10 11
@Slf4j @Aspect public class AspectV1 { // 포인트컷: hello.aop.order 패키지와 하위 패키지 전체 @Around("execution(* hello.aop.order..*(..))") public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { log.info("[log] {}", joinPoint.getSignature()); // 조인 포인트 시그니처 출력 return joinPoint.proceed(); // 실제 타겟 호출 } }
테스트 등록
1
2
3
@Import(AspectV1.class) // @Aspect는 컴포넌트 스캔 대상이 아님 → 직접 빈 등록 필요
@SpringBootTest
public class AopTest { ... }
@Aspect는 빈으로 등록되어야 동작하며@Bean,@Component,@Import중 선택하여 등록할 수 있음
실행 결과
1
2
3
4
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
포인트컷 분리 (AspectV2)
-
@Pointcut으로 포인트컷 표현식을 메서드로 분리하여 재사용할 수 있음1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Slf4j @Aspect public class AspectV2 { // 반환 타입은 반드시 void, 내용은 비워둠 @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 표현식 |
execution(* hello.aop.order..*(..)) |
| 포인트컷 시그니처 | allOrder() (메서드 이름 + 파라미터) |
| 접근 제어자 | private 내부에서만 참조, public 외부 애스펙트에서도 참조 가능 |
어드바이스 추가 (AspectV3)
-
포인트컷을 조합하고 어드바이스를 추가하여 트랜잭션 흉내를 내는 예제임
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
@Slf4j @Aspect public class AspectV3 { @Pointcut("execution(* hello.aop.order..*(..))") public void allOrder() {} // order 패키지 + 하위 패키지 @Pointcut("execution(* *..*Service.*(..))") private void allService() {} // *Service 클래스 @Around("allOrder()") public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { log.info("[log] {}", joinPoint.getSignature()); return joinPoint.proceed(); } // order 패키지 && *Service 클래스 (OrderService에만 적용) @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()); } } }
포인트컷 조합 연산자
| 연산자 | 의미 | 예시 |
|---|---|---|
&& |
AND | allOrder() && allService() |
\|\| |
OR | allOrder() \|\| allService() |
! |
NOT | !allService() |
어드바이스 적용 결과

1
2
3
4
5
6
7
[log] void hello.aop.order.OrderService.orderItem(String)
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
포인트컷 참조 (AspectV4)
- 포인트컷을 별도 클래스로 분리하여 여러 애스펙트에서 공유할 수 있음
Pointcuts (공용 포인트컷 모음)
1
2
3
4
5
6
7
8
9
10
11
public class Pointcuts {
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
AspectV4Pointcut (외부 포인트컷 참조)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Aspect
public class AspectV4Pointcut {
// 패키지명 포함 전체 경로로 참조
@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 {
}
}
어드바이스 순서 (AspectV5)
- 어드바이스는 기본적으로 순서를 보장하지 않음
- 순서를 지정하려면
@Order를 사용하되 클래스(애스펙트) 단위로만 적용이 가능함 -
하나의
@Aspect안에 여러 어드바이스가 있으면 순서 보장이 불가능하므로 클래스로 분리해야 함1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
public class AspectV5Order { @Aspect @Order(2) // 숫자가 작을수록 먼저 실행 public static class LogAspect { @Around("hello.aop.order.aop.Pointcuts.allOrder()") public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { } } @Aspect @Order(1) // TxAspect가 먼저 실행됨 public static class TxAspect { @Around("hello.aop.order.aop.Pointcuts.orderAndService()") public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable { } } }
테스트 등록
1
2
3
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@SpringBootTest
public class AopTest { ... }
@Order 적용 후 실행 흐름

1
2
3
4
5
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
어드바이스 종류 (AspectV6)
어드바이스 종류 한눈에 보기

실행 순서 (동일 Aspect 내)
@Around→@Before→@After→@AfterReturning→@AfterThrowing순으로 실행됨- 호출 순서와 리턴 순서는 반대임에 유의해야 함
AspectV6Advice 전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// proceed() 호출 필수
}
@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 joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
어드바이스 종류 비교
| 어드바이스 | 실행 시점 | proceed() | 반환값 변경 | 예외 처리 |
|---|---|---|---|---|
@Around |
전후 모두 | 직접 호출 필요 | 가능 | 가능 |
@Before |
실행 전 | 불필요 (자동) | 불가 | 불가 |
@AfterReturning |
정상 반환 후 | 불필요 | 불가 (조작만 가능) | 불가 |
@AfterThrowing |
예외 발생 후 | 불필요 | 불가 | 예외 확인 가능 |
@After |
항상 (finally) | 불필요 | 불가 | 불가 |
JoinPoint / ProceedingJoinPoint 주요 메서드
1
2
3
4
5
6
7
8
9
// JoinPoint - 모든 어드바이스에서 사용 가능함
joinPoint.getArgs() // 메서드 인수 반환
joinPoint.getThis() // 프록시 객체 반환
joinPoint.getTarget() // 실제 타겟 객체 반환
joinPoint.getSignature() // 메서드 시그니처 반환
// ProceedingJoinPoint - @Around 전용 (JoinPoint의 하위 타입임)
joinPoint.proceed() // 다음 어드바이스 또는 타겟 호출
joinPoint.proceed(args[]) // 변경된 인수로 호출
실행 결과
1
2
3
4
5
6
7
8
[around][트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[before] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[orderRepository] 실행
[return] void hello.aop.order.OrderService.orderItem(String) return=null
[after] void hello.aop.order.OrderService.orderItem(String)
[around][트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[around][리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
@Around 외에 다른 어드바이스가 필요한 이유
잘못된 @Around 사용
1
2
3
4
5
6
// 잘못된 @Around 사용 - proceed()를 호출하지 않아 타겟이 실행되지 않는 치명적 버그가 발생함
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
// joinPoint.proceed() 누락 시 타겟 호출이 되지 않음
}
1
2
3
4
5
// 올바른 방법 - @Before 사용 시 proceed() 호출 고민 자체가 불필요함
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- 넓은 기능을 제공하는
@Around- 가장 강력하지만, 개발자가
proceed()호출을 누락하면 타겟 자체가 실행되지 않는 치명적인 버그가 발생할 위험이 있음
- 가장 강력하지만, 개발자가
- 좁은 기능을 보장하는
@Before,@After등- 기능은 제한적이지만 어드바이스 흐름을 프레임워크가 제어하므로 위와 같은 치명적인 실수가 원천 차단됨
- 코드를 읽는 순간 타겟 실행 전(또는 후)에만 동작한다는 의도를 즉시 파악할 수 있어 훨씬 안전함
연습 문제
-
AOP에서 어드바이스가 적용될 지점(Join Point)을 지정하는 규칙 또는 표현식을 무엇이라고 할까요?
a. 포인트컷
- 포인트컷은 어드바이스가 적용될 조인 포인트를 선택하는 기준임
- 어드바이스는 실행될 부가 기능 코드이며, 애스펙트는 부가 기능을 모듈화한 것임
-
스프링 AOP가 런타임에 핵심 로직(대상 객체)에 부가 기능(애스펙트)을 적용하는 주요 기술 방식은 무엇일까요?
a. 프록시 기반
- 스프링 AOP는 대상 빈 객체 대신 프록시 객체를 만들어 호출을 가로챔
- 컴파일/로드 타임 위빙은 주로 AspectJ가 사용하는 방식임
-
@Pointcut 애노테이션을 사용하여 포인트컷 표현식을 별도의 메서드로 분리했을 때 가장 큰 장점은 무엇일까요?
a. 포인트컷 재사용성 및 가독성 증가
- 포인트컷을 분리하면 여러 어드바이스에서 동일한 포인트컷을 참조할 수 있어 코드 중복을 줄이고 가독성을 높일 수 있음
- 이는 성능이나 어드바이스 유형과는 무관함
-
조인 포인트 실행 전/후 처리를 모두 제어하고, 필요에 따라 대상 메서드 실행을 막거나 결과/예외를 조작할 수 있는 가장 강력한 어드바이스 유형은 무엇일까요?
a. @Around
- Around 어드바이스는 ProceedingJoinPoint를 사용하여 대상 호출 전후의 흐름을 완벽하게 제어할 수 있음
- 다른 어드바이스 유형들은 특정 시점에만 실행된다는 차이가 있음
-
동일한 조인 포인트에 여러 애스펙트가 적용될 때, 각 애스펙트의 실행 순서를 명시적으로 지정하기 위해 사용하는 스프링 애노테이션은 무엇일까요?
a. @Order
- @Order는 애스펙트 클래스 단위로 실행 순서를 제어하며, 설정된 숫자가 낮을수록 먼저 실행됨
- @Aspect나 @Pointcut 등 다른 애노테이션은 역할이 다름
요약 정리
- 스프링 AOP 구현은
@Aspect를 사용하여 애노테이션 기반으로 편리하게 개발할 수 있음 @Pointcut을 사용하면 포인트컷을 분리하고 모듈화하여 여러 곳에서 재사용할 수 있음- 어드바이스의 실행 순서는 클래스 단위로
@Order를 적용하여 제어해야 함 - 상황에 맞춰
@Around외에도@Before,@AfterReturning등의 어드바이스를 적절히 사용하면 제약을 통해 실수 가능성을 줄이고 의도를 명확하게 전달할 수 있음