실습 코드 : https://github.com/DDINGJOO/Spring5PlayGround
Spring AOP 개요
AOP(Aspect-Oriented Programming)란?
관점 지향 프로그래밍(AOP)은 프로그램의 핵심 기능과 부가 기능을 분리하여 관점별로 모듈화하는 프로그래밍 패러다임입니다.
Spring AOP의 특징
- 프록시 기반: 런타임에 프록시 객체를 생성하여 AOP 기능 제공
- 메서드 레벨 적용: 메서드 호출 시점에서만 AOP 적용 가능
- 스프링 컨테이너 연동: 스프링 IoC 컨테이너와 완전히 통합
핵심 개념
1. Aspect (관점)
횡단 관심사를 모듈화한 것으로, Advice와 Pointcut을 결합한 개념입니다.
2. Advice (어드바이스)
특정 조인포인트에서 실행되는 코드를 말합니다.
Advice 타입
- Before Advice: 메서드 실행 전에 실행
- After Returning Advice: 메서드가 정상적으로 반환된 후 실행
- After Throwing Advice: 메서드에서 예외가 발생했을 때 실행
- Around Advice: 메서드 실행 전후를 모두 제어
3. Pointcut (포인트컷)
Advice가 적용될 조인포인트를 선별하는 기준입니다.
4. Join Point (조인포인트)
프로그램 실행 중에 Advice가 적용될 수 있는 지점입니다.
5. Target Object (타겟 객체)
실제 비즈니스 로직을 수행하는 객체입니다.
6. Proxy (프록시)
AOP 프레임워크가 생성하는 객체로 Advice를 적용합니다.
실습 예제 분석
Ch5_03: 기본 AOP 데모 (SpringAopDemo)
개념
가장 기본적인 Spring AOP 사용법을 보여주는 예제입니다. ProxyFactory를 사용하여 프록시를 생성하고 Around Advice를 적용합니다.
코드 분석
Agent.java (타겟 객체)
public class Agent {
public void speak() {
System.out.println("Bond");
}
}
AgentDecorator.java (Around Advice)
public class AgentDecorator implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("James"); // Before
Object retVal = invocation.proceed(); // 원본 메서드 실행
System.out.println("!"); // After
return retVal;
}
}
SpringAopDemo.java (메인 클래스)
public class SpringAopDemo {
public static void main(String[] args) {
Agent target = new Agent();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvice(new AgentDecorator());
proxyFactory.setTarget(target);
Agent proxy = (Agent) proxyFactory.getProxy();
target.speak(); // "Bond"
System.out.println(" ");
proxy.speak(); // "James Bond !"
}
}
실행 결과
Bond
James
Bond
!
학습 포인트
- ProxyFactory를 사용한 프록시 생성 방법
- MethodInterceptor를 통한 Around Advice 구현
- 타겟 객체와 프록시 객체의 차이점
Ch5_05: Advice 데모 (SimpleBeforeAdvice, SimpleAfterAdvice)
개념
Before Advice와 After Returning Advice의 사용법을 보여주는 예제입니다.
Before Advice 분석
SimpleBeforeAdvice.java
public class SimpleBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("Before '" + method.getName() + "' turn guitar.");
}
public static void main(String[] args) {
Guitarist JoneMayer = new Guitarist();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SimpleBeforeAdvice());
pf.setTarget(JoneMayer);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
}
}
After Returning Advice 분석
SimpleAfterAdvice.java
public class SimpleAfterAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("After '" + method.getName() + "' put down guitar.");
}
}
학습 포인트
- MethodBeforeAdvice 인터페이스 구현
- AfterReturningAdvice 인터페이스 구현
- 메서드 실행 전후의 부가 기능 추가
Ch5_07: BeforeAdvice 보안 예제 (SecurityDemo)
개념
Before Advice를 사용하여 메서드 실행 전 보안 검증을 수행하는 실무적인 예제입니다.
코드 분석
SecurityAdvice.java
public class SecurityAdvice implements MethodBeforeAdvice {
private SecurityManager securityManager;
public SecurityAdvice() {
this.securityManager = new SecurityManager();
}
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
UserInfo userInfo = securityManager.getUserInfo();
if(userInfo == null) {
System.out.println("인증되지 않은 사용자 입니다.");
throw new SecurityException(
"메소드를 호출하려면 로그인이 필요합니다." + method.getName()
);
} else if ("John".equals(userInfo.getUserName())) {
System.out.println("Jone 사용자로 로그인 했습니다. - OKEY!");
} else {
System.out.println(userInfo.getUserName() + "사용자로 로그인 했습니다." + "-NOT GOOD :(");
throw new SecurityException(userInfo.getUserName()
+ " 사용자는 메서드에 대한 접근 권한이 없습니다. " + method.getName());
}
}
}
SecurityManager.java
public class SecurityManager {
private static ThreadLocal<UserInfo> CONTEXT = new ThreadLocal<>();
public void login(String userName, String password) {
CONTEXT.set(new UserInfo(userName, password));
}
public void logout() {
CONTEXT.set(null);
}
public UserInfo getUserInfo() {
return CONTEXT.get();
}
}
실행 흐름
- SecurityManager를 통해 사용자 인증 상태 확인
- 인증되지 않은 사용자의 경우 SecurityException 발생
- 권한이 없는 사용자의 경우 접근 거부
- 정상적인 사용자만 메서드 실행 허용
학습 포인트
- Before Advice를 활용한 보안 검증
- ThreadLocal을 사용한 사용자 컨텍스트 관리
- 예외를 통한 접근 제어
Ch5_08: AfterAdvice 키 검증 예제 (WeekKeyCheckAdvice)
개념
After Returning Advice를 사용하여 메서드 실행 후 반환값을 검증하는 예제입니다.
코드 분석
WeekKeyCheckAdvice.java
public class WeekKeyCheckAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
if (returnValue instanceof Long && (Long) returnValue < 100000L) {
throw new SecurityException("Week Key Generated");
}
}
}
KeyGenerator.java
public class KeyGenerator {
public static final long WEEK_KEY = 0xFFFFL;
public static final long STRONG_KEY = 0xFFFFFFL;
public Long getKey() {
int x = (int) (Math.random() * 3);
return (x == 1) ? WEEK_KEY : STRONG_KEY;
}
}
실행 흐름
- KeyGenerator.getKey() 메서드 실행
- 반환된 키 값이 100000보다 작으면 약한 키로 판단
- 약한 키의 경우 SecurityException 발생
- 강한 키의 경우 정상 처리
학습 포인트
- After Returning Advice를 통한 반환값 검증
- 비즈니스 로직과 검증 로직의 분리
- 조건부 예외 처리
Ch5_09: AroundAdvice 프로파일링 예제 (ProfilingInterceptor)
개념
Around Advice를 사용하여 메서드 실행 시간을 측정하는 프로파일링 예제입니다.
코드 분석
ProfilingInterceptor.java
public class ProfilingInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
StopWatch sw = new StopWatch();
sw.start();
Object retVal = invocation.proceed();
sw.stop();
System.out.println("Method " + invocation.getMethod().getName()
+ " took " + sw.getTotalTimeMillis() + " ms"
+ " Target: " + invocation.getThis().getClass().getName());
return retVal;
}
}
WorkerBean.java
public class WorkerBean {
public void doSomeWork(int noOfTimes) {
for (int x = 0; x < noOfTimes; x++) {
work();
}
}
private void work() {
int x = Math.random() < 0.5 ? 1000 : 2000;
System.out.println("Working hard for " + x + " ms");
try {
Thread.sleep(x);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
실행 결과 예시
Working hard for 1000 ms
Working hard for 2000 ms
Method doSomeWork took 3005 ms Target: WorkerBean
학습 포인트
- Around Advice를 통한 메서드 실행 시간 측정
- StopWatch 클래스 활용
- 성능 모니터링과 비즈니스 로직의 분리
Ch5_10: ThrowsAdvice 예외 처리 예제 (SimpleThrowsAdvice)
개념
Throws Advice를 사용하여 메서드에서 발생하는 예외를 처리하는 예제입니다.
코드 분석
SimpleThrowsAdvice.java
public class SimpleThrowsAdvice implements ThrowsAdvice {
public static void main(String[] args) {
ErrorBean errorBean = new ErrorBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(errorBean);
pf.addAdvice(new SimpleThrowsAdvice());
ErrorBean proxy = (ErrorBean) pf.getProxy();
try {
proxy.errorProneMethod();
} catch (Exception ignored) {}
try {
proxy.otherErrorProneMethod();
} catch (Exception ignored) {}
}
public void afterThrowing(Exception ex) {
System.out.println("Caught :" + ex.getClass().getName());
}
public void afterThrowing(Method method, Object[] args, Object target, IllegalArgumentException ex) {
System.out.println("Caught :" + ex.getClass().getName());
System.out.println("Method :" + method.getName());
}
}
ErrorBean.java
public class ErrorBean {
public void errorProneMethod() throws Exception {
throw new Exception("Generic Exception");
}
public void otherErrorProneMethod() throws IllegalArgumentException {
throw new IllegalArgumentException("Illegal Argument");
}
}
학습 포인트
- ThrowsAdvice를 통한 예외 처리
- 예외 타입별 차별화된 처리
- afterThrowing 메서드의 다양한 시그니처
Ch5_11: 정적 메서드 매처 포인트컷 (SimpleStaticPointcut)
개념
StaticMethodMatcherPointcut을 상속하여 특정 메서드에만 Advice를 적용하는 예제입니다.
코드 분석
SimpleStaticPointcut.java
public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return "sing".equals(method.getName());
}
@Override
public ClassFilter getClassFilter() {
return cls -> cls == (GoodGuitarist.class);
}
}
StaticPointcutDemo.java
public class StaticPointcutDemo {
public static void main(String[] args) {
GoodGuitarist target = new GoodGuitarist();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(new DefaultPointcutAdvisor(new SimpleStaticPointcut(), new SimpleAdvice()));
GoodGuitarist proxy = (GoodGuitarist) pf.getProxy();
proxy.sing();
proxy.rest();
}
}
실행 흐름
- GoodGuitarist 클래스의 "sing" 메서드에만 Advice 적용
- "rest" 메서드는 Pointcut 조건에 맞지 않아 Advice 적용 안됨
- 선택적인 Advice 적용으로 성능 향상
학습 포인트
- StaticMethodMatcherPointcut 상속
- matches() 메서드를 통한 메서드 필터링
- ClassFilter를 통한 클래스 필터링
- DefaultPointcutAdvisor 사용법
Ch5_12: 동적 메서드 매처 포인트컷 (SimpleDynamicPointcut)
개념
DynamicMethodMatcherPointcut을 상속하여 런타임에 메서드 인수를 검사하여 Advice 적용 여부를 결정하는 예제입니다.
코드 분석
SimpleDynamicPointcut.java
public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
System.out.println("Static check for " + method.getName());
return "foo".equals(method.getName());
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
System.out.println("Dynamic check for " + method.getName());
int x = (Integer) args[0];
return (x != 100);
}
@Override
public ClassFilter getClassFilter() {
return cls -> cls == (SampleBean.class);
}
}
SampleBean.java
public class SampleBean {
public void foo(int x) {
System.out.println("Invoked foo() with: " + x);
}
public void bar() {
System.out.println("Invoked bar()");
}
}
실행 흐름
- 정적 검사: "foo" 메서드명 확인
- 동적 검사: 런타임 인수값이 100이 아닌지 확인
- 조건을 모두 만족하는 경우에만 Advice 적용
학습 포인트
- DynamicMethodMatcherPointcut 상속
- 정적 검사와 동적 검사의 차이점
- 런타임 인수를 활용한 조건부 Advice 적용
- 성능 고려사항 (동적 검사는 비용이 높음)
Ch5_13: 네임 매치 메서드 포인트컷 (NameMatchMethodPointcut)
개념
메서드 이름 패턴을 사용하여 Pointcut을 정의하는 예제입니다.
코드 분석
NamePointcutDemo.java
public class NamePointcutDemo {
public static void main(String[] args) {
GrammyGuitarist target = new GrammyGuitarist();
ProxyFactory pf = new ProxyFactory();
NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
pc.addMethodName("sing");
pc.addMethodName("rest");
pf.setTarget(target);
pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));
GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}
실행 결과
Before method: sing
Invoked sing()
After method: sing
Invoked sing2() (Advice 적용 안됨)
Before method: rest
Invoked rest()
After method: rest
학습 포인트
- NameMatchMethodPointcut 사용법
- addMethodName()을 통한 여러 메서드 이름 등록
- 메서드 이름 기반의 간편한 Pointcut 정의
Ch5_14: JDK 정규식 메서드 포인트컷 (JdkRegexpMethodPointcut)
개념
정규식을 사용하여 메서드 이름 패턴을 매칭하는 Pointcut 예제입니다.
코드 분석
RegexpPointcutDemo.java
public class RegexpPointcutDemo {
public static void main(String[] args) {
Guitarist target = new Guitarist();
ProxyFactory pf = new ProxyFactory();
JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
pc.setPattern(".*sing.*");
pf.setTarget(target);
pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}
정규식 패턴 설명
.*sing.*: "sing"이 포함된 모든 메서드 매칭sing(),sing2()메서드는 매칭됨rest()메서드는 매칭되지 않음
학습 포인트
- JdkRegexpMethodPointcut 사용법
- 정규식을 활용한 유연한 메서드 매칭
- 패턴 기반의 Pointcut 정의
Ch5_15: AspectJ 표현식 포인트컷 (AspectJExpressionPointcut)
개념
AspectJ의 강력한 포인트컷 표현식을 Spring AOP에서 사용하는 예제입니다.
코드 분석
AspectjexpPointcutDemo.java
public class AspectjexpPointcutDemo {
private static AspectJExpressionPointcut pc;
public static void main(String[] args) {
Guitarist proxyOne;
Guitarist proxyTwo;
pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* sing*(..))");
// 첫 번째 프록시
proxyOne = getProxy();
proxyOne.sing();
proxyOne.sing2();
proxyOne.rest();
System.out.println("");
// 두 번째 프록시
pc.setExpression("execution(* *.sing2(..))");
proxyTwo = getProxy();
proxyTwo.sing();
proxyTwo.sing2();
proxyTwo.rest();
}
private static Guitarist getProxy() {
Guitarist target = new Guitarist();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));
return (Guitarist) pf.getProxy();
}
}
AspectJ 표현식 설명
execution(* sing*(..)): 반환타입 상관없이 sing으로 시작하는 모든 메서드execution(* *.sing2(..)): 모든 클래스의 sing2 메서드
학습 포인트
- AspectJExpressionPointcut 사용법
- execution 표현식 문법
- 동적인 포인트컷 표현식 변경
Ch5_16: 어노테이션 매칭 포인트컷 (AnnotationMatchingPointcut)
개념
메서드에 특정 어노테이션이 붙은 경우에만 Advice를 적용하는 예제입니다.
코드 분석
AdviceRequired.java (커스텀 어노테이션)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdviceRequired {
}
Guitarist.java
public class Guitarist implements Singer {
@AdviceRequired
public void sing() {
System.out.println("sing() called");
}
@AdviceRequired
public void sing2() {
System.out.println("sing2() called");
}
public void rest() {
System.out.println("rest() called");
}
}
AnnotationPointcutDemo.java
public class AnnotationPointcutDemo {
public static void main(String[] args) {
Guitarist guitarist = new Guitarist();
AnnotationMatchingPointcut pc = AnnotationMatchingPointcut.forMethodAnnotation(AdviceRequired.class);
ProxyFactory pf = new ProxyFactory();
pf.setTarget(guitarist);
pf.addAdvisor(new DefaultPointcutAdvisor(pc, new SimpleAdvice()));
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing(); // Advice 적용
proxy.sing2(); // Advice 적용
proxy.rest(); // Advice 적용 안됨
}
}
학습 포인트
- 커스텀 어노테이션 정의
- AnnotationMatchingPointcut 사용법
- 어노테이션 기반의 선언적 AOP
- @Target, @Retention 어노테이션 활용
Ch5_17: 편리한 포인트컷 구현 (NamePointcutUsingAdvisor)
개념
NameMatchMethodPointcutAdvisor를 사용하여 Pointcut과 Advice를 하나로 결합하는 편리한 방법을 보여주는 예제입니다.
코드 분석
NamePointcutUsingAdvisor.java
public class NamePointcutUsingAdvisor {
public static void main(String[] args) {
GrammyGuitarist guitarist = new GrammyGuitarist();
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(new SimpleAdvice());
advisor.addMethodName("sing");
advisor.addMethodName("rest");
ProxyFactory pf = new ProxyFactory();
pf.setTarget(guitarist);
pf.addAdvisor(advisor);
GrammyGuitarist proxy = (GrammyGuitarist) pf.getProxy();
proxy.sing();
proxy.sing2();
proxy.rest();
}
}
장점
- Pointcut과 Advice를 별도로 생성할 필요 없음
- 코드 간소화
- 일반적인 사용 사례에 최적화
학습 포인트
- NameMatchMethodPointcutAdvisor 사용법
- Advisor 패턴의 편리함
- 코드 간소화 방법
AOP 실행 흐름
전체 AOP 실행 흐름도
graph TD
A[클라이언트 코드] --> B[프록시 객체]
B --> C{포인트컷 매칭}
C -->|매칭됨| D[어드바이스 실행]
C -->|매칭 안됨| E[타겟 메서드 직접 호출]
D --> F{어드바이스 타입}
F -->|Before| G[Before Advice 실행]
F -->|After| H[After Advice 실행]
F -->|Around| I[Around Advice 실행]
F -->|Throws| J[Exception 처리]
G --> K[타겟 메서드 호출]
K --> L[결과 반환]
H --> L
I --> M{proceed() 호출}
M --> K
J --> N[예외 처리 후 반환]
L --> O[클라이언트에게 결과 반환]
N --> O
E --> O
Before Advice 실행 흐름
sequenceDiagram
participant C as 클라이언트
participant P as 프록시
participant BA as BeforeAdvice
participant T as 타겟객체
C->>P: 메서드 호출
P->>P: 포인트컷 매칭 확인
P->>BA: before() 메서드 호출
Note over BA: 전처리 로직 실행
BA->>P: 완료
P->>T: 실제 메서드 호출
T->>P: 결과 반환
P->>C: 최종 결과 반환
Around Advice 실행 흐름
sequenceDiagram
participant C as 클라이언트
participant P as 프록시
participant AA as AroundAdvice
participant T as 타겟객체
C->>P: 메서드 호출
P->>P: 포인트컷 매칭 확인
P->>AA: invoke() 메서드 호출
Note over AA: 전처리 로직
AA->>AA: invocation.proceed() 호출
AA->>T: 실제 메서드 호출
T->>AA: 결과 반환
Note over AA: 후처리 로직
AA->>P: 최종 결과 반환
P->>C: 결과 전달
프록시 생성 과정
graph LR
A[ProxyFactory 생성] --> B[타겟 객체 설정]
B --> C[Advice/Advisor 추가]
C --> D[getProxy() 호출]
D --> E{인터페이스 존재?}
E -->|Yes| F[JDK Dynamic Proxy]
E -->|No| G[CGLIB Proxy]
F --> H[프록시 객체 생성 완료]
G --> H
포인트컷 평가 과정
graph TD
A[메서드 호출] --> B{클래스 필터}
B -->|통과| C{메서드 매처}
B -->|실패| D[Advice 적용 안함]
C -->|Static 매칭| E{정적 조건 확인}
C -->|Dynamic 매칭| F{동적 조건 확인}
E -->|통과| G[Advice 적용]
E -->|실패| D
F -->|통과| G
F -->|실패| D
심화 학습
Spring AOP vs AspectJ 비교
| 특성 | Spring AOP | AspectJ |
|---|---|---|
| 구현 방식 | 프록시 기반 | 바이트코드 조작/위빙 |
| 적용 범위 | 메서드 레벨만 | 필드, 생성자, 메서드 등 |
| 성능 | 런타임 오버헤드 | 컴파일 타임 최적화 |
| 학습 곡선 | 상대적으로 쉬움 | 복잡함 |
| Spring 통합 | 완전 통합 | 별도 설정 필요 |
| 사용 시점 | 스프링 빈에만 적용 | 모든 Java 객체 |
프록시 타입별 특징
JDK Dynamic Proxy
- 조건: 타겟이 인터페이스를 구현해야 함
- 장점: 표준 Java 기능 사용
- 단점: 인터페이스 기반으로만 동작
// JDK Proxy 강제 사용 예시
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.setProxyTargetClass(false); // JDK Proxy 강제
CGLIB Proxy
- 조건: 클래스 기반 프록시 생성
- 장점: 인터페이스 없이도 동작
- 단점: final 클래스/메서드는 프록시 불가
// CGLIB 강제 사용 예시
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.setProxyTargetClass(true); // CGLIB 강제
성능 최적화 고려사항
1. 포인트컷 최적화
// 비효율적 - 모든 메서드 검사
public boolean matches(Method method, Class<?> targetClass) {
return method.getName().startsWith("get") ||
method.getName().startsWith("set");
}
// 효율적 - 빠른 실패
public boolean matches(Method method, Class<?> targetClass) {
String name = method.getName();
return name.length() > 3 &&
(name.startsWith("get") || name.startsWith("set"));
}
2. 정적 vs 동적 포인트컷
- 정적 포인트컷: 성능이 우수하나 유연성 제한
- 동적 포인트컷: 유연하나 런타임 오버헤드 존재
3. Advice 체인 최적화
// 효율적인 Advice 체인 구성
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new SecurityAdvice()); // 가장 먼저 실행되어야 함
pf.addAdvice(new PerformanceAdvice()); // 성능 측정
pf.addAdvice(new LoggingAdvice()); // 마지막에 로깅
실무 활용 패턴
1. 트랜잭션 관리
public class TransactionAdvice implements MethodInterceptor {
private PlatformTransactionManager txManager;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
try {
Object result = invocation.proceed();
txManager.commit(status);
return result;
} catch (Exception ex) {
txManager.rollback(status);
throw ex;
}
}
}
2. 캐싱 구현
public class CachingAdvice implements MethodInterceptor {
private Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String key = generateKey(invocation);
Object cached = cache.get(key);
if (cached != null) {
return cached;
}
Object result = invocation.proceed();
cache.put(key, result);
return result;
}
private String generateKey(MethodInvocation invocation) {
return invocation.getMethod().getName() +
Arrays.toString(invocation.getArguments());
}
}
3. 감사 로깅
public class AuditAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
private Logger logger = LoggerFactory.getLogger(AuditAdvice.class);
@Override
public void before(Method method, Object[] args, Object target) {
logger.info("Method {} called with args: {}",
method.getName(), Arrays.toString(args));
}
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) {
logger.info("Method {} completed successfully", method.getName());
}
}
4. 재시도 메커니즘
public class RetryAdvice implements MethodInterceptor {
private int maxAttempts = 3;
private long delay = 1000L;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Exception lastException = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return invocation.proceed();
} catch (Exception ex) {
lastException = ex;
if (attempt < maxAttempts) {
Thread.sleep(delay * attempt);
}
}
}
throw lastException;
}
}
디버깅 및 테스팅
1. AOP 디버깅 팁
public class DebuggingAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("=== AOP Debug Info ===");
System.out.println("Target: " + invocation.getThis().getClass().getName());
System.out.println("Method: " + invocation.getMethod().getName());
System.out.println("Args: " + Arrays.toString(invocation.getArguments()));
Object result = invocation.proceed();
System.out.println("Return: " + result);
System.out.println("=== End Debug Info ===");
return result;
}
}
2. 단위 테스트 작성
@Test
public void testSecurityAdvice() {
// Given
SecureBean target = new SecureBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvice(new SecurityAdvice());
SecureBean proxy = (SecureBean) pf.getProxy();
// When & Then
assertThrows(SecurityException.class, () -> {
proxy.secureMethod();
});
}
학습 정리
Spring AOP 핵심 개념 요약
1. AOP의 필요성
- 횡단 관심사 분리: 로깅, 보안, 트랜잭션 등의 공통 기능을 비즈니스 로직과 분리
- 코드 중복 제거: 동일한 부가 기능을 여러 곳에서 재사용
- 유지보수성 향상: 관심사별로 코드가 모듈화되어 변경이 용이
- 테스트 용이성: 핵심 비즈니스 로직과 부가 기능을 독립적으로 테스트 가능
2. Spring AOP 동작 원리
Spring AOP는 프록시 패턴을 기반으로 동작합니다:
- 프록시 생성: ProxyFactory가 타겟 객체의 프록시를 생성
- 메서드 호출 가로채기: 클라이언트의 메서드 호출을 프록시가 가로챔
- 포인트컷 평가: 현재 메서드 호출이 어드바이스 적용 조건에 맞는지 확인
- 어드바이스 실행: 조건에 맞으면 해당 어드바이스를 실행
- 타겟 메서드 호출: 실제 비즈니스 로직 실행
- 결과 반환: 최종 결과를 클라이언트에게 반환
3. Advice 타입별 활용 시나리오
| Advice 타입 | 주요 사용 사례 | 예제 |
|---|---|---|
| Before | 입력 검증, 보안 검사, 로깅 | 사용자 권한 확인, 매개변수 유효성 검사 |
| After Returning | 결과 검증, 성공 로깅, 캐시 저장 | 반환값 검증, 성공 감사 로그 |
| After Throwing | 예외 처리, 오류 로깅, 알림 | 예외 로깅, 관리자 알림, 복구 작업 |
| Around | 성능 측정, 트랜잭션, 재시도 | 실행 시간 측정, DB 트랜잭션 관리 |
4. 포인트컷 선택 가이드
| 포인트컷 타입 | 사용 시점 | 장점 | 단점 |
|---|---|---|---|
| 정적 (Static) | 메서드명 기반 필터링 | 성능 우수 | 런타임 조건 불가 |
| 동적 (Dynamic) | 매개변수 기반 필터링 | 유연함 | 성능 오버헤드 |
| 이름 매칭 | 간단한 메서드명 패턴 | 사용 간편 | 제한적 표현력 |
| 정규식 | 복잡한 패턴 매칭 | 강력한 표현력 | 복잡성 증가 |
| AspectJ 표현식 | 복잡한 조건 | 매우 강력함 | 학습 비용 |
| 어노테이션 | 선언적 방식 | 직관적 | 어노테이션 필요 |
실습 예제별 핵심 학습 내용
Ch5_03 ~ Ch5_05: 기초 개념
- ProxyFactory 사용법: Spring AOP의 기본 프록시 생성 방법
- 기본 Advice 타입들: Before, After, Around의 기본적인 구현 방법
- 프록시와 타겟의 차이점: 프록시 객체와 원본 객체의 동작 차이 이해
Ch5_07 ~ Ch5_10: 실무 활용
- 보안 검증: Before Advice를 활용한 접근 권한 검사
- 결과 검증: After Returning Advice를 통한 반환값 유효성 확인
- 성능 모니터링: Around Advice를 사용한 실행 시간 측정
- 예외 처리: Throws Advice를 통한 중앙화된 예외 처리
Ch5_11 ~ Ch5_17: 고급 포인트컷
- 선택적 적용: 특정 조건에서만 Advice를 적용하는 방법
- 성능 최적화: 정적 vs 동적 포인트컷의 성능 차이
- 유연한 매칭: 다양한 패턴 매칭 방법 (이름, 정규식, AspectJ 표현식)
- 어노테이션 기반: 선언적이고 직관적인 AOP 구현 방법
모범 사례 (Best Practices)
1. 설계 원칙
- 단일 책임: 하나의 Advice는 하나의 관심사만 처리
- 최소 침입: 비즈니스 로직에 최소한의 영향만 미치도록 설계
- 성능 고려: 불필요한 동적 포인트컷 사용 자제
- 테스트 가능: Advice 로직을 독립적으로 테스트할 수 있도록 설계
2. 코드 구조화
// 권장: 관심사별 분리
public class SecurityAspect {
// 보안 관련 로직만 포함
}
public class PerformanceAspect {
// 성능 관련 로직만 포함
}
// 비권장: 여러 관심사 혼재
public class MixedAspect {
// 보안 + 성능 + 로깅이 섞여있음
}
3. 예외 처리
public class SafeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
// Advice 로직
return invocation.proceed();
} catch (Exception ex) {
// Advice에서 발생한 예외는 적절히 처리
logger.error("Advice execution failed", ex);
throw ex; // 원본 예외는 그대로 전파
}
}
}
트러블슈팅 가이드
1. 일반적인 문제들
문제: Advice가 적용되지 않음
- 원인: 포인트컷 조건 불일치
- 해결: 포인트컷 조건을 디버깅하여 확인
문제: 성능 저하 발생
- 원인: 동적 포인트컷 남용
- 해결: 정적 포인트컷으로 변경 검토
문제: 예외 발생 시 타겟 메서드 미실행
- 원인: Before Advice에서 예외 발생
- 해결: Advice 내 예외 처리 로직 추가
2. 디버깅 방법
// 프록시 정보 확인
if (AopUtils.isAopProxy(proxy)) {
System.out.println("프록시 타입: " +
(AopUtils.isJdkDynamicProxy(proxy) ? "JDK" : "CGLIB"));
}
// Advisor 정보 확인
if (proxy instanceof Advised) {
Advised advised = (Advised) proxy;
for (Advisor advisor : advised.getAdvisors()) {
System.out.println("Advisor: " + advisor);
}
}
미래 발전 방향
1. @AspectJ 어노테이션
현재 실습한 프로그래매틱 방식에서 더 나아가 어노테이션 기반의 선언적 AOP 학습:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
// Before advice 로직
}
@Around("@annotation(com.example.annotation.Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// Around advice 로직
}
}
2. Spring Boot 통합
Spring Boot의 자동 설정을 활용한 더 간편한 AOP 설정:
@EnableAspectJAutoProxy
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
결론
이번 Spring AOP 실습을 통해 다음과 같은 핵심 개념들을 학습했습니다:
- AOP의 기본 개념: Aspect, Advice, Pointcut, Join Point 등의 핵심 용어와 개념
- 프록시 기반 구현: ProxyFactory를 사용한 프록시 생성과 동작 원리
- 다양한 Advice 타입: Before, After, Around, Throws Advice의 특징과 활용법
- 포인트컷 구현: 정적/동적 포인트컷, 이름 매칭, 정규식, AspectJ 표현식, 어노테이션 기반 등
- 실무 활용 패턴: 보안, 성능 모니터링, 예외 처리, 검증 등의 실제 사용 사례
- 성능 최적화: 효율적인 포인트컷 작성과 프록시 선택 방법
'SpringStudy' 카테고리의 다른 글
| Spring Hibernate (7) | 2025.08.26 |
|---|---|
| JDBC 최종 (4) | 2025.08.21 |
| JDBC 학습 정리(1) (0) | 2025.08.21 |
| Pointcut 문법 정리 (0) | 2025.08.20 |
| Spring AOP (1) | 2025.08.19 |