1. 프로젝트 생성 및 기본 설정
1.1. 스프링 부트 프로젝트 생성 방법
스프링 AOP를 학습하기 위한 프로젝트를 생성하는 과정이다. 스프링 부트 스타터 사이트(https://start.spring.io)를 사용하여 프로젝트를 생성한다.
필요한 설정값:
- Project: Gradle Project (빌드 도구 선택)
- Language: Java (프로그래밍 언어)
- Spring Boot: 2.5.x (스프링 부트 버전)
- Project Metadata:
- Group: hello (프로젝트 그룹 ID)
- Artifact: aop (프로젝트 이름)
- Name: aop (프로젝트 표시 이름)
- Package name: hello.aop (기본 패키지명)
- Packaging: Jar (패키징 방식)
- Java: 11 (자바 버전)
- Dependencies: Lombok (롬복 라이브러리 추가)
중요한 점:
- 이번 프로젝트에서는 스프링 웹 기술은 사용하지 않음
- 스프링 프레임워크의 핵심 모듈들은 별도의 설정 없이 자동으로 추가됨
- AOP 기능을 사용하기 위해 별도의 의존성을 추가해야 함
1.2. build.gradle 설정
plugins {
id 'org.springframework.boot' version '2.5.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-aop' // 직접 추가한 AOP 의존성
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
test {
useJUnitPlatform()
}
1.3. 프로젝트 설정 확인
프로젝트 생성 후 기본 메인 클래스(AopApplication.main())를 실행하여 스프링 부트 실행 로그가 출력되는지 확인한다. 스프링 웹 프로젝트를 추가하지 않았기 때문에 서버는 실행되지 않지만, 정상적으로 스프링 애플리케이션이 초기화되는지 확인한다.
2. 예제 프로젝트 만들기
2.1. OrderRepository 구현
AOP를 적용할 대상이 되는 리포지토리 클래스를 구현한다.
package hello.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
// 저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
클래스 설명:
- @Slf4j: 롬복의 로깅 애노테이션, SLF4J 로거 자동 생성
- @Repository: 스프링의 컴포넌트 스캔 대상으로 지정, DAO 역할의 빈으로 등록
- save() 메서드: 아이템 저장 기능을 수행하는 핵심 비즈니스 로직
- 특수한 경우("ex" 입력 시) 예외를 발생시켜 AOP의 예외 처리 기능을 테스트
2.2. OrderService 구현
비즈니스 로직을 처리하는 서비스 클래스를 구현한다.
package hello.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
클래스 설명:
- @Service: 스프링의 컴포넌트 스캔 대상으로 지정, 비즈니스 로직을 처리하는 서비스 빈으로 등록
- 생성자 주입을 통해 OrderRepository 의존성 주입
- orderItem() 메서드: 주문 처리의 핵심 비즈니스 로직
2.3. AopTest 테스트 클래스
AOP 적용 여부와 기능을 테스트하는 클래스를 구현한다.
package hello.aop;
import hello.aop.order.OrderRepository;
import hello.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
@SpringBootTest
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);
}
}
테스트 설명:
- aopInfo() 테스트:
- AopUtils.isAopProxy()를 사용하여 AOP 프록시가 적용되었는지 확인
- 현재는 AOP 관련 코드를 작성하지 않았으므로 false 반환
- success() 테스트:
- 정상적인 흐름으로 주문 처리
- "itemA"를 파라미터로 전달하여 정상적으로 실행되는지 확인
- exception() 테스트:
- 예외 발생 시나리오 테스트
- "ex"를 파라미터로 전달하여 예외가 발생하는지 확인
초기 실행 결과:
[orderService] 실행
[orderRepository] 실행

3. 스프링 AOP 구현1 - 시작
3.1. 가장 기본적인 AOP 구현
@Aspect 애노테이션을 사용한 가장 기본적인 AOP 구현 방법을 살펴본다.
AspectV1 클래스:
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@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()); // join point 시그니처
return joinPoint.proceed();
}
}
3.2. 주요 구성 요소 상세 설명
3.2.1. @Aspect 애노테이션
- 이 클래스가 AOP를 처리하는 Aspect임을 나타냄
- AspectJ 프레임워크에서 제공하는 애노테이션
- 스프링 AOP에서도 동일한 애노테이션을 사용
3.2.2. @Around 애노테이션
- 역할: 메서드 실행 전후에 실행되는 어드바이스(Advice)
- 특징:
- 가장 강력한 어드바이스 타입
- 메서드 실행 전후 모두 제어 가능
- 반환값 변경 가능
- 예외 처리 가능
3.2.3. 포인트컷 표현식
"execution(* hello.aop.order..*(..))"
| 구성 요소 | 내용 | 상세 설명 |
| execution | 선언부 | 메서드 실행 조인 포인트를 매칭하는 포인트컷 지시자(PCD) |
| * (첫 번째) | 반환 타입 | 모든 반환 타입을 허용 (public, protected 등 접근 제어자는 생략됨) |
| hello.aop.order.. | 패키지 범위 | hello.aop.order 패키지와 그 하위 패키지(..)를 포함 |
| * (두 번째) | 메서드 이름 | 해당 패키지 내 모든 클래스의 모든 메서드 이름에 매칭 |
| (..) | 파라미터 | 파라미터의 개수와 타입에 제한이 없음을 의미 |
3.2.4. ProceedingJoinPoint
- 역할: 어드바이스에서 대상 메서드를 실행할 수 있는 객체
- 주요 메서드:
- getSignature(): 대상 메서드의 정보(시그니처)를 반환
- proceed(): 대상 메서드를 실행하고 결과를 반환
3.3. AOP 적용하기
@Aspect 클래스는 스프링 빈으로 등록해야 AOP가 적용된다. 등록 방법에는 여러 가지가 있다.
테스트 클래스에 @Import로 등록:
package hello.aop;
import hello.aop.order.OrderRepository;
import hello.aop.order.OrderService;
import hello.aop.order.aop.AspectV1;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
@Import(AspectV1.class) // AspectV1을 스프링 빈으로 등록
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
// ... 테스트 메서드들
}
스프링 빈 등록 방법 비교:
| 방법 | 설명 | 사용 시나리오 |
| @Import | 설정 파일을 임포트할 때 사용, 빈 등록 가능 | 테스트 환경, 설정 분리 |
| @Component | 컴포넌트 스캔 대상으로 등록 | 일반적인 애플리케이션 |
| @Bean | 설정 클래스에서 직접 빈 정의 | 조건부 빈 등록, 복잡한 초기화 |
3.4. AOP 적용 결과
적용 후 테스트 실행 결과:
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
AOP 동작 방식 이해:
- AOP 적용 전:
클라이언트 → OrderService.orderItem() → OrderRepository.save() - AOP 적용 후:
클라이언트 → [AOP 프록시] → [AspectV1.doLog()] → OrderService.orderItem() → [AspectV1.doLog()] → OrderRepository.save()
3.5. 스프링 AOP와 AspectJ의 관계
중요한 개념 구분:
- 스프링 AOP는 AspectJ의 문법을 차용:
- @Aspect, @Around, 포인트컷 표현식 등은 AspectJ에서 가져옴
- 하지만 구현 방식은 다름
- 구현 방식 차이:
- AspectJ: 컴파일 타임, 로드 타임 위빙을 사용
- 스프링 AOP: 프록시 패턴을 사용한 런타임 위빙
- 의존성 관계:
- aspectjweaver.jar 라이브러리 필요
- spring-boot-starter-aop에 이미 포함됨
- 스프링은 AspectJ의 애노테이션과 인터페이스만 사용
4. 스프링 AOP 구현2 - 포인트컷 분리
4.1. 포인트컷을 별도로 분리하는 이유
기존의 AspectV1에서는 포인트컷 표현식을 @Around 애노테이션에 직접 작성했다. 이 방식에는 다음과 같은 한계가 있다:
- 재사용 불가능: 같은 포인트컷을 여러 어드바이스에서 사용할 수 없음
- 가독성 저하: 복잡한 포인트컷 표현식이 코드에 직접 포함되어 가독성이 떨어짐
- 유지보수 어려움: 포인트컷 변경 시 모든 어드바이스를 수정해야 함
4.2. AspectV2: 포인트컷 분리 구현
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV2 {
// hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") // pointcut expression
private void allOrder() {} // pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
4.3. @Pointcut 애노테이션 상세 설명
4.3.1. 기본 문법
@Pointcut("포인트컷_표현식")
접근제어자 반환타입 포인트컷_시그니처() {}
4.3.2. 주요 규칙
- 반환 타입: 반드시 void이어야 함
- 메서드 내용: 비워둠 (구현부가 없음)
- 포인트컷 시그니처: 메서드 이름과 파라미터를 합친 것
- 접근 제어자:
- 내부에서만 사용: private
- 외부에서 참조: public
4.3.3. 포인트컷 시그니처
- allOrder(): 포인트컷을 식별하는 이름
- 직관적인 이름을 사용하여 가독성을 높임
- "주문과 관련된 모든 기능"이라는 의미를 담음
4.4. 어드바이스에서 포인트컷 사용 방법
// 방법 1: 포인트컷 시그니처 직접 사용
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
// ...
}
// 방법 2: 포인트컷 표현식 직접 사용 (AspectV1 방식)
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
// ...
}
4.5. 포인트컷 분리의 장점
4.5.1. 재사용성 향상
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) { /* 로깅 */ }
@Around("allOrder()")
public Object doTransaction(ProceedingJoinPoint joinPoint) { /* 트랜잭션 */ }
- 하나의 포인트컷을 여러 어드바이스에서 사용 가능
4.5.2. 가독성 향상
- 복잡한 표현식이 메서드 시그니처로 대체됨
- 코드의 의도가 명확해짐
4.5.3. 유지 보수성 향상
- 포인트컷 변경 시 한 곳만 수정하면 됨
- 오류 가능성 감소
4.5.4. 외부 참조 가능
- 다른 클래스의 포인트컷도 참조 가능
4.6. 테스트 적용
AopTest 수정:
// @Import(AspectV1.class) // 주석 처리
@Import(AspectV2.class) // AspectV2 적용
@SpringBootTest
public class AopTest {
// ... 테스트 코드
}
실행 결과:
AspectV1과 동일한 결과를 보이지만, 내부적으로는 포인트컷이 분리되어 관리되고 있음
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행

5. 스프링 AOP 구현3 - 어드바이스 추가
5.1. 복잡한 AOP 시나리오 구현
실제 프로젝트에서는 단순한 로깅 외에도 다양한 횡단 관심사를 처리해야 한다. 트랜잭션 관리를 예로 들어 여러 개의 어드바이스를 조합하는 방법을 알아본다.
5.2. AspectV3: 트랜잭션 관리 추가
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV3 {
// 포인트컷 1: hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
// 포인트컷 2: 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService() {}
// 어드바이스 1: 로깅 (모든 주문 관련 메서드에 적용)
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// 어드바이스 2: 트랜잭션 (주문 관련 + 서비스 클래스에 적용)
@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());
}
}
}
5.3. 포인트컷 표현식 상세 분석
5.3.1. allOrder() 포인트컷
- execution(* hello.aop.order..*(..))
- 적용 대상: hello.aop.order 패키지와 모든 하위 패키지의 모든 메서드
- 예시 적용 클래스: OrderService, OrderRepository
5.3.2. allService() 포인트컷
- execution(* *..*Service.*(..))
- 구성 요소 분석:
- : 모든 반환 타입
- ..*Service: 모든 패키지에서 이름이 Service로 끝나는 클래스
- .*(..): 모든 메서드
- 예시 적용 클래스: OrderService
- 비적용 클래스: OrderRepository
5.3.3. 포인트컷 조합 연산자
| 연산자 | 설명 | 예시 |
| && (AND) | 두 포인트컷 모두 만족 | allOrder() && allService() |
| ` | ` (OR) | |
| ! (NOT) | 포인트컷을 만족하지 않음 | !allService() |
5.4. 트랜잭션 어드바이스 동작 방식
트랜잭션의 일반적인 흐름:
- 트랜잭션 시작
- 비즈니스 로직 실행
- 성공 시: 트랜잭션 커밋
- 실패 시: 트랜잭션 롤백
- 항상 실행: 리소스 정리
어드바이스 구현 패턴:
try {
// 트랜잭션 시작
Object result = joinPoint.proceed(); // 비즈니스 로직 실행
// 트랜잭션 커밋
return result;
} catch (Exception e) {
// 트랜잭션 롤백
throw e;
} finally {
// 리소스 정리
}
5.5. AOP 적용 대상 분석
클래스 doLog() 적용 doTransaction() 적용 이유
| OrderService | ✅ | ✅ | allOrder() && allService() 만족 |
| OrderRepository | ✅ | ❌ | allOrder()는 만족 but allService()는 만족하지 않음 |
5.6. 테스트 실행 결과
AopTest 수정:
// @Import(AspectV1.class)
// @Import(AspectV2.class)
@Import(AspectV3.class) // AspectV3 적용
@SpringBootTest
public class AopTest {
// ... 테스트 코드
}
정상 실행 결과 (success() 테스트):

[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)
예외 발생 결과 (exception() 테스트):
[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)
5.7. AOP 적용 전후 비교
AOP 적용 전:
클라이언트 → orderService.orderItem() → orderRepository.save()
AOP 적용 후:
클라이언트 → [doLog() → doTransaction()] → orderService.orderItem() → [doLog()] → orderRepository.save()
실행 흐름 분석:
- OrderService.orderItem() 호출 시:
- doLog() 실행 (포인트컷: allOrder())
- doTransaction() 실행 (포인트컷: allOrder() && allService())
- 실제 orderItem() 메서드 실행
- orderRepository.save() 호출
- OrderRepository.save() 호출 시:
- doLog() 실행 (포인트컷: allOrder())
- 실제 save() 메서드 실행
- doTransaction()는 적용되지 않음 (allService() 조건 불만족)
5.8. 어드바이스 실행 순서 문제
현재 구현에서는 로깅 어드바이스가 트랜잭션 어드바이스보다 먼저 실행된다. 이 순서는 AOP 프록시 생성 시점이나 JVM 환경에 따라 달라질 수 있다. 일관된 순서를 보장하려면 명시적인 순서 지정이 필요하다.
6. 스프링 AOP 구현4 - 포인트컷 참조
6.1. 포인트컷의 재사용성 확장
여러 Aspect 클래스에서 동일한 포인트컷을 사용해야 하는 경우가 많다. 이때 포인트컷을 별도의 클래스로 분리하여 공용으로 사용할 수 있다.
6.2. Pointcuts 클래스: 포인트컷 집합
package hello.aop.order.aop;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
// 포인트컷 1: hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
// 포인트컷 2: 타입 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
// 포인트컷 3: allOrder와 allService의 조합
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
6.3. 외부 포인트컷 클래스 설계 원칙
- 공용 포인트컷 집합:
- 관련된 포인트컷들을 한 곳에 모아 관리
- 이름 규칙을 통일하여 일관성 유지
- 접근 제어자:
- 다른 클래스에서 참조해야 하므로 public으로 선언
- 내부에서만 사용하는 포인트컷은 private으로 선언 가능
- 포인트컷 조합:
- 기본 포인트컷을 조합하여 새로운 포인트컷 생성
- orderAndService()는 allOrder()와 allService()의 AND 조합
6.4. AspectV4Pointcut: 외부 포인트컷 사용
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@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 {
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());
}
}
}
6.5. 외부 포인트컷 참조 문법
완전한 참조 형식:
@Around("패키지명.클래스명.포인트컷시그니처()")
예시:
// 정확한 패키지 경로 지정
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
// 같은 패키지 내에 있을 경우 클래스명만으로 참조 가능
@Around("Pointcuts.allOrder()")
6.6. 외부 포인트컷 사용의 장점
- 중앙화된 관리:
- 모든 포인트컷을 한 곳에서 관리
- 변경이 필요할 때 한 곳만 수정하면 됨
- 일관성 유지:
- 프로젝트 전체에서 동일한 포인트컷 정의 사용
- 다른 개발자도 일관된 방식으로 포인트컷 참조
- 재사용성 극대화:
- 여러 Aspect 클래스에서 동일한 포인트컷 공유
- 중복 코드 제거
- 가독성 향상:
- Aspect 클래스는 비즈니스 로직(어드바이스)에 집중
- 포인트컷 정의는 별도 클래스로 분리
6.7. 테스트 적용
AopTest 수정:
// @Import(AspectV1.class)
// @Import(AspectV2.class)
// @Import(AspectV3.class)
@Import(AspectV4Pointcut.class) // AspectV4Pointcut 적용
@SpringBootTest
public class AopTest {
// ... 테스트 코드
}
실행 결과:
AspectV3와 동일한 결과를 보이지만, 포인트컷은 외부 클래스에서 관리됨
[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)
6.8. 실무 적용 팁
6.8.1. 포인트컷 분류 체계:
// 도메인별 포인트컷 분리
public class OrderPointcuts { /* 주문 도메인 포인트컷 */ }
public class MemberPointcuts { /* 회원 도메인 포인트컷 */ }
public class CommonPointcuts { /* 공통 포인트컷 */ }
6.8.2. 포인트컷 네이밍 규칙
- 도메인명 + 대상: allOrder(), orderServiceMethods()
- 조합 포인트컷: orderAndService(), orderButNotRepository()
6.8.3. 포인트컷 문서화
/**
* 주문 도메인의 모든 메서드를 대상으로 하는 포인트컷
* - 적용 대상: hello.aop.order 패키지와 하위 패키지
* - 예외: none
*/
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
7. 스프링 AOP 구현5 - 어드바이스 순서
7.1. 어드바이스 순서 제어의 필요성
AOP에서 여러 어드바이스가 적용될 때 실행 순서가 중요한 경우가 많다. 예를 들어:
- 트랜잭션 vs 로깅: 트랜잭션 내부의 실행 시간만 측정하고 싶다면 트랜잭션 시작 후 로깅이 실행되어야 함
- 보안 vs 로깅: 보안 검증 실패 시 로깅이 먼저 실행되어야 디버깅에 도움됨
- 캐싱 vs 검증: 캐싱 전에 입력값 검증이 먼저 실행되어야 함
7.2. 순서 제어의 문제점
기본적으로 스프링 AOP는 어드바이스 실행 순서를 보장하지 않는다. 순서는 다음과 같은 요인에 의해 결정될 수 있다:
- AOP 프록시 생성 시점
- 빈 등록 순서
- JVM 실행 환경
- AspectJ 버전
7.3. @Order 애노테이션을 이용한 순서 제어
스프링은 @Order 애노테이션을 통해 어드바이스 실행 순서를 제어할 수 있다. 중요한 점은 @Order가 클래스 수준에서 적용된다는 것이다.
@Order 애노테이션 규칙:
- 숫자가 작을수록 높은 우선순위 (먼저 실행)
- 동일한 @Aspect 클래스 내의 어드바이스는 순서를 지정할 수 없음
- 순서를 지정하려면 Aspect를 별도의 클래스로 분리해야 함
7.4. AspectV5Order: 순서 제어 구현
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Slf4j
public class AspectV5Order {
// Aspect 1: 로깅 (낮은 우선순위)
@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 2: 트랜잭션 (높은 우선순위)
@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());
}
}
}
}
7.5. 정적 내부 클래스 사용 이유
- 단일 파일 관리: 관련 Aspect들을 한 파일에서 관리
- 네임스페이스 격리: LogAspect, TxAspect로 명확히 구분
- 의존성 명시: 두 Aspect가 밀접하게 관련되어 있음을 표현
7.6. 순서 제어 메커니즘
실행 순서 결정 과정:
- 스프링은 모든 @Aspect 빈을 수집
- @Order 값으로 정렬
- 숫자가 작은 순서대로 어드바이스 체인에 추가
- 어드바이스 체인 순서대로 실행
예상 실행 흐름:
TxAspect.doTransaction() 시작 (Order 1)
→ LogAspect.doLog() 시작 (Order 2)
→ 실제 비즈니스 메서드 실행
← LogAspect.doLog() 종료
← TxAspect.doTransaction() 종료
7.7. 테스트 적용
AopTest 수정:
// @Import(AspectV4Pointcut.class)
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@SpringBootTest
public class AopTest {
// ... 테스트 코드
}
중요: 두 개의 Aspect를 모두 import해야 함
실행 결과:

[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[log] 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)
순서 변화 관찰:
- 이전: [log] → [트랜잭션 시작]
- 이후: [트랜잭션 시작] → [log]
7.8. 실무 적용 시 고려사항
7.8.1. 순서 값 관리 전략
public interface AspectOrder {
int TRANSACTION = 1;
int LOGGING = 2;
int SECURITY = 3;
int CACHING = 4;
}
@Aspect
@Order(AspectOrder.TRANSACTION)
public class TransactionAspect { /* ... */ }
7.8.2. 복잡한 순서 제어
- 3개 이상의 Aspect 간 순서 제어
- 조건부 순서 변경
- 동일 순위 Aspect 처리
7.8.3. 순서 관련 문제 해결
- 순서가 보장되지 않는 경우 디버깅 방법
- @Order 무시되는 경우 확인 사항
7.9. 주의사항
7.9.1. 동일 Aspect 내 순서 제어 불가
@Aspect
@Order(1) // 클래스 수준에서만 적용됨
public class MyAspect {
@Around("pointcut1()") // 순서 지정 불가
public Object advice1() { /* ... */ }
@Around("pointcut2()") // 순서 지정 불가
public Object advice2() { /* ... */ }
}
7.9.2. 빈 등록 방식 영향
- @Component vs @Bean vs @Import
- 빈 등록 방식에 따라 순서가 달라질 수 있음
7.9.3. 프록시 타입 영향
- JDK 동적 프록시 vs CGLIB 프록시
- 프록시 생성 방식에 따라 순서 처리 방식이 다름
8. 스프링 AOP 구현6 - 어드바이스 종류
8.1. 다양한 어드바이스 타입 소개
스프링 AOP는 다양한 상황에 맞춰 사용할 수 있는 5가지 어드바이스 타입을 제공한다. 각 어드바이스는 특정 시점에 실행되며, 목적에 맞게 선택하여 사용할 수 있다.
어드바이스 종류 요약:
| 어드바이스 | 실행 시점 | 주요 특징 |
| @Around | 메서드 호출 전후 | 가장 강력함, 모든 제어 가능 |
| @Before | 메서드 호출 전 | 간단한 사전 처리 |
| @AfterReturning | 메서드 정상 반환 후 | 반환값 활용 가능 |
| @AfterThrowing | 메서드 예외 발생 시 | 예외 정보 활용 가능 |
| @After | 메서드 종료 후 (finally) | 성공/실패 관계없이 실행 |
8.2. AspectV6Advice: 모든 어드바이스 타입 구현
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
public class AspectV6Advice {
// 1. @Around: 가장 강력한 어드바이스
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// @Before에 해당
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
// @AfterReturning에 해당
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
// @AfterThrowing에 해당
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
// @After에 해당
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
// 2. @Before: 메서드 실행 전
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
// 3. @AfterReturning: 메서드 정상 반환 후
@AfterReturning(
value = "hello.aop.order.aop.Pointcuts.orderAndService()",
returning = "result"
)
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
// 4. @AfterThrowing: 메서드 예외 발생 시
@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());
}
// 5. @After: 메서드 종료 후 (finally 블록과 유사)
@After("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
8.3. JoinPoint 인터페이스 상세
모든 어드바이스는 JoinPoint(또는 ProceedingJoinPoint)를 첫 번째 파라미터로 받을 수 있다.
JoinPoint 주요 메서드:
| 메서드 | 반환 타입 | 설명 |
| getArgs() | Object[] | 메서드 인수 배열 |
| getThis() | Object | 프록시 객체 |
| getTarget() | Object | 대상 객체 (실제 비즈니스 객체) |
| getSignature() | Signature | 조인 포인트 시그니처 (메서드 정보) |
| toString() | String | 조인 포인트 설명 문자열 |
ProceedingJoinPoint 추가 메서드:
- proceed(): 다음 어드바이스나 타겟 메서드 호출
- proceed(Object[] args): 인수를 변경하여 호출
8.4. 각 어드바이스 타입 상세 설명
8.4.1. @Around
- 가장 강력한 어드바이스
- 특징:
- 메서드 실행 전후 모두 제어 가능
- 조인 포인트 실행 여부 결정 가능 (proceed() 호출 여부)
- 반환값 변경 가능
- 예외 변환 가능
- 트랜잭션처럼 복잡한 흐름 제어 가능
- 주의사항:
- 반드시 ProceedingJoinPoint를 파라미터로 받아야 함
- proceed()를 호출하지 않으면 대상 메서드가 실행되지 않음
- 여러 번 proceed() 호출 가능 (재시도 로직 구현 시 유용)
8.4.2. @Before
- 메서드 실행 전에 실행
- 특징:
- 간단한 사전 검증, 로깅, 인증 등에 적합
- JoinPoint만 파라미터로 받음
- 대상 메서드 실행을 막을 수 없음
- 예외 발생 시 대상 메서드 실행되지 않음
- 사용 예시:
@Before("execution(* com.example.service.*.*(..))") public void validateArguments(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); // 인수 검증 로직 }
8.4.3. @AfterReturning
- 메서드가 정상적으로 반환된 후 실행
- 특징:
- returning 속성으로 반환값 받을 수 있음
- 반환값은 조작할 수 없지만 확인은 가능
- 파라미터 이름과 returning 값이 일치해야 함
- 사용 예시:
@AfterReturning( pointcut = "execution(* com.example.service.*.*(..))", returning = "result" ) public void logReturnValue(JoinPoint joinPoint, Object result) { // 반환값 로깅 }
8.4.4. @AfterThrowing
- 메서드가 예외를 던질 때 실행
- 특징:
- throwing 속성으로 예외 객체 받을 수 있음
- 특정 예외 타입만 처리하도록 제한 가능
- 예외 처리 후 다시 예외를 던질 수 있음
- 사용 예시:
@AfterThrowing( pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex" ) public void handleException(JoinPoint joinPoint, RuntimeException ex) { // 예외 처리 로직 }
8.4.5. @After
- 메서드 종료 후 항상 실행 (finally 블록과 유사)
- 특징:
- 성공/실패 여부와 관계없이 실행
- 리소스 정리, 로깅 등에 적합
- JoinPoint만 파라미터로 받음
- 사용 예시:
@After("execution(* com.example.service.*.*(..))") public void cleanupResources(JoinPoint joinPoint) { // 리소스 정리 로직 }
8.5. 어드바이스 실행 순서 규칙
스프링 5.2.7 버전부터 동일한 @Aspect 내에서의 어드바이스 실행 순서가 명시적으로 정의되었다.
실행 순서:
- @Around (진입)
- @Before
- 실제 메서드 실행
- @After
- @AfterReturning 또는 @AfterThrowing
- @Around (종료)
중요: 같은 종류의 어드바이스가 2개 이상 있으면 순서가 보장되지 않음. 이 경우 Aspect를 분리하고 @Order를 사용해야 함.
8.6. 테스트 적용
AopTest 수정:
// @Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@Import(AspectV6Advice.class)
@SpringBootTest
public class AopTest {
// ... 테스트 코드
}
실행 결과:

[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)
8.7. @Around 외 어드바이스가 존재하는 이유
@Around 하나로 모든 기능을 구현할 수 있음에도 다른 어드바이스 타입들이 존재하는 이유는 다음과 같다:
8.7.1. 실수 방지
// 잘못된 @Around 사용 예시
@Around("pointcut()")
public void doBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
// proceed() 호출을 잊어버림 → 대상 메서드 실행되지 않음!
}
// 올바른 @Before 사용
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
// proceed() 호출 필요 없음 → 실수 방지
}
8.7.2. 의도 명확화
- @Before: "이 코드는 메서드 실행 전에만 작동한다"
- @AfterReturning: "이 코드는 정상 반환 시에만 작동한다"
- 코드만 보고도 개발자의 의도를 파악할 수 있음
8.7.3. 제약을 통한 안전성
- 제약은 실수를 미연에 방지하는 가이드 역할
- 적절한 제약은 코드의 안전성을 높임
8.7.4. 최적의 도구 선택 원칙
- @Around: 복잡한 제어 흐름이 필요할 때
- @Before/@After: 단순한 사전/사후 처리
- @AfterReturning: 반환값 확인이 필요할 때
- @AfterThrowing: 예외 처리 로직이 필요할 때
8.8. 실무 적용 가이드라인
- 어드바이스 선택 기준:
- 가능한 한 특화된 어드바이스 사용 권장
- @Around는 정말 필요할 때만 사용
- 코드 의도를 명확히 표현하는 어드바이스 선택
- 에러 처리 패턴:
@Aspect public class ErrorHandlingAspect { @AfterThrowing( pointcut = "execution(* com.example..*(..))", throwing = "ex" ) public void handleServiceException(JoinPoint joinPoint, ServiceException ex) { // 서비스 예외 처리 } @AfterThrowing( pointcut = "execution(* com.example..*(..))", throwing = "ex" ) public void handleSystemException(JoinPoint joinPoint, SystemException ex) { // 시스템 예외 처리 } } - 성능 모니터링 패턴:
@Aspect public class PerformanceAspect { @Around("execution(* com.example.service.*.*(..))") public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long endTime = System.currentTimeMillis(); log.info("{} 실행 시간: {}ms", joinPoint.getSignature(), endTime - startTime ); } } }
9. 정리
9.1. 스프링 AOP 구현 핵심 포인트
- @Aspect 기반 구현: 스프링 AOP의 표준 구현 방식
- 포인트컷 분리: @Pointcut을 이용한 재사용성 향상
- 어드바이스 다양성: 목적에 맞는 적절한 어드바이스 선택
- 순서 제어: @Order를 통한 실행 순서 관리
- 외부 참조: 공용 포인트컷 클래스를 통한 중앙화 관리
9.2. 각 구현 단계별 학습 내용
| 구현 단계 | 주요 학습 내용 | 실무 적용 포인트 |
| AspectV1 | 기본 AOP 구현, @Around 사용 | 단순 로깅, 기본 AOP 패턴 |
| AspectV2 | 포인트컷 분리, @Pointcut 사용 | 코드 재사용성, 가독성 향상 |
| AspectV3 | 다중 어드바이스, 포인트컷 조합 | 트랜잭션, 보안 등 복합 AOP |
| AspectV4 | 외부 포인트컷 참조 | 중앙화된 포인트컷 관리 |
| AspectV5 | 어드바이스 순서 제어 | 실행 순서가 중요한 AOP |
| AspectV6 | 다양한 어드바이스 타입 | 목적에 맞는 어드바이스 선택 |
9.3. 실무 적용 시 권장 사항
- 코드 가독성 우선:
- 명확한 포인트컷 시그니처 사용
- 적절한 어드바이스 타입 선택
- 과도한 @Around 사용 지양
- 재사용성 고려:
- 공용 포인트컷 클래스 활용
- 도메인별 Aspect 분리
- 설정을 통한 AOP 활성화 제어
- 성능 영향 최소화:
- 불필요한 포인트컷 지양
- 어드바이스 내 무거운 작업 지양
- 필요한 경우에만 AOP 적용
- 테스트 용이성:
- 단위 테스트에서 AOP 비활성화 옵션 제공
- AOP 동작을 검증하는 테스트 작성
- 모의 객체(Mock)를 이용한 테스트
9.4. 다음 단계 학습 주제
- AOP 고급 기능:
- 어드바이스 파라미터 바인딩
- @DeclareParents를 이용한 인터페이스 도입
- @Aspect 상속과 재사용
- 성능 최적화:
- AOP 프록시 생성 비용 최적화
- 런타임 성능 영향 분석
- 컴파일 타임 위빙 고려
- 실무 패턴:
- 트랜잭션 관리 AOP 패턴
- 캐싱 AOP 패턴
- 재시도(retry) AOP 패턴
- 회로 차단기(circuit breaker) AOP 패턴
- 모니터링과 디버깅:
- AOP 적용 여부 확인 방법
- 실행 순서 디버깅
- 성능 영향 모니터링
9.5. 마무리
스프링 AOP는 애플리케이션의 다양한 횡단 관심사를 효과적으로 처리할 수 있는 강력한 도구이다. 적절히 활용하면 코드의 모듈성을 높이고, 유지보수성을 향상시키며, 반복적인 코드를 제거할 수 있다. 그러나 과도한 사용은 성능 저하와 디버깅 어려움을 초래할 수 있으므로, 항상 필요한 경우에만 적절한 수준으로 적용하는 것이 중요하다.
이번 학습을 통해 스프링 AOP의 기본 개념부터 실무 적용 패턴까지 체계적으로 이해할 수 있었을 것이다. 실제 프로젝트에 적용할 때는 단계적으로 도입하고, 효과를 측정하며, 팀의 코딩 표준과 일치시키는 것이 성공적인 AOP 적용의 핵심이다.
'Spring > Core' 카테고리의 다른 글
| [Advanced-12] 스프링 AOP - 실전 예제 (0) | 2026.01.02 |
|---|---|
| [Advanced-11] 스프링 AOP - 포인트 컷 (0) | 2026.01.02 |
| [Advanced-9] 스프링 AOP 개념 (0) | 2026.01.02 |
| [Advanced-8] @Aspect AOP (0) | 2026.01.02 |
| [Advanced-7] 빈 후처리기 (0) | 2026.01.02 |
