8-9주차 자료의 모든 토픽을 두 주에 걸쳐 정리한 학습 경로.
1) 8주차 — 프록시의 진화 (AOP가 필요한 이유, 디자인 패턴, 동적 프록시, ProxyFactory)
2) 9주차 — Spring AOP 실전 (자동 프록시, @Aspect, AOP 용어, @Transactional 함정, 트랜잭션 전파)7주차에서 @Transactional의 프록시 패턴을 맛봤다면, 8-9주차는 그 프록시가 어떻게 만들어지고 적용되는지 의 모든 메커니즘을 파헤친다.
김영한의 스프링 핵심 원리 - 고급편 전체 흐름이 압축되어 있다. F-lab 자바 커리큘럼의 하이라이트 이자 가장 분량이 많은 주차다.
[Part A — 8주차: 프록시의 진화]
[Phase 1] AOP 입문과 동기
↓
[Phase 2] 디자인 패턴의 진화 — 템플릿 메서드 → 전략 패턴
↓
[Phase 3] 콜백과 프록시의 만남 — 템플릿 콜백 → 프록시 개념
↓
[Phase 4] 프록시 패턴 vs 데코레이터 패턴
↓
[Phase 5] 동적 프록시 기술 — Reflection / JDK / CGLIB
↓
[Phase 6] ProxyFactory — 스프링의 통합 추상화 ◄ 8주차 정점
[Part B — 9주차: Spring AOP 실전]
[Phase 7] 빈 후처리기와 자동 프록시 생성기
↓
[Phase 8] @Aspect와 AOP 용어 완전 정리
↓
[Phase 9] Spring AOP 실전 구현 패턴
↓
[Phase 10] @Transactional의 함정과 트랜잭션 전파 ◄ 9주차 정점
총 10 Phase × 35 Unit (5·6주차의 합과 비슷한 분량)
| 주차 | 주제 | 핵심 변화 |
|---|---|---|
| 1주차 | OOP·JVM·GC·컬렉션·I/O 개론 | 자바 큰 그림 |
| 2주차 | JVM 내부·바이트코드·G1 GC | "어떻게 돌아가나" |
| 3주차 | 컬렉션·제네릭·함수형 | 자바 표현력 |
| 4주차 | 멀티스레딩·동시성·Executor | 동시성 정복 |
| 5주차 | Atomic + Spring IoC/DI 입문 | 자바 → Spring 다리 |
| 6주차 | 테스트 + 웹 인프라 + DB 접근 진화 | Spring 실전 환경 |
| 7주차 | JPA/ORM + 트랜잭션 추상화 | DB 추상화의 정점 |
| 8주차 (지금) | 프록시의 진화 | AOP 메커니즘 이해 |
| 9주차 (지금) | Spring AOP 실전 + 트랜잭션 전파 | AOP 실전 활용 |
| Day | Phase | 학습 목표 |
|---|---|---|
| Week 1 | ||
| 1일차 | Phase 1 | AOP 동기, 로그 추적기, ThreadLocal |
| 2일차 | Phase 2 | 템플릿 메서드 + 전략 패턴 |
| 3일차 | Phase 3 | 템플릿 콜백 패턴, 프록시 개념 |
| 4일차 | Phase 4 | 프록시 vs 데코레이터 |
| 5일차 | Phase 5 | Reflection + JDK 동적 프록시 + CGLIB |
| 6-7일차 | Phase 6 | ProxyFactory + Advice + Pointcut + Advisor (★ 8주차 정점) |
| Week 2 | ||
| 8일차 | Phase 7 | 빈 후처리기 + 자동 프록시 생성기 |
| 9일차 | Phase 8 | @Aspect + AOP 용어 |
| 10일차 | Phase 9 | Spring AOP 실전 패턴 |
| 11-12일차 | Phase 10 | @Transactional 함정 + 트랜잭션 전파 (★ 9주차 정점) |
| 13-14일차 | 종합 자기 점검 + 실습 | 전체 정리 |
여유 일정 (21일): 각 정점 Phase에 +2일씩, 그리고 9-섹션 마스터 프롬프트로 핵심 Unit을 깊이 파는 시간 확보.
목표: "왜 AOP가 필요한가"를 코드의 고통으로 직접 이해한다. 7주차에서 본 @Transactional의 정체를 더 깊이 알기 위한 출발점.
선수 지식: 7주차 Phase 7 (@Transactional)
핵심 개념
AOP (Aspect Oriented Programming) = 관점 지향 프로그래밍
핵심 통찰:
핵심 vs 부가:
흩어진 관심사 (Crosscutting Concerns):
자기 점검
선수 지식: Unit 1.1
핵심 시나리오
로그 추적기란?:
예시 출력:
정상 요청
[796bccd9] OrderController.request()
[796bccd9] |-->OrderService.orderItem()
[796bccd9] | |-->OrderRepository.save()
[796bccd9] | |<--OrderRepository.save() time=1004ms
[796bccd9] |<--OrderService.orderItem() time=1014ms
[796bccd9] OrderController.request() time=1016ms
예외 발생
[b7119f27] OrderController.request()
[b7119f27] |-->OrderService.orderItem()
[b7119f27] | |<X-OrderService.orderItem() time=10ms ex=...
문제:
자기 점검
선수 지식: 4주차 Phase 4 (synchronized), 5주차 Phase 8 (싱글톤 빈)
핵심 개념
문제 상황:
ThreadLocal의 해결:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Thread-1 데이터"); // 이 스레드만 보임
String value = threadLocal.get(); // 다른 스레드는 다른 값
⚠️ 치명적 함정 — remove() 필수:
remove()try {
threadLocal.set(data);
// 작업
} finally {
threadLocal.remove(); // ✅ 필수!
}
자기 점검
목표: "변하는 것과 변하지 않는 것을 분리"라는 좋은 설계의 원칙을 두 가지 패턴(템플릿 메서드 → 전략)을 통해 코드로 익힌다.
선수 지식: Phase 1, 5주차 Phase 5
핵심 원칙
"좋은 설계는 변하는 것과 변하지 않는 것을 분리하는 것이다"
예시 — 시간 측정 로직:
private void logic1() {
long startTime = System.currentTimeMillis(); // ← 변하지 않음
log.info("비즈니스 로직1 실행"); // ← 변함
long endTime = System.currentTimeMillis(); // ← 변하지 않음
log.info("resultTime={}", endTime - startTime); // ← 변하지 않음
}
private void logic2() {
long startTime = System.currentTimeMillis(); // ← 변하지 않음
log.info("비즈니스 로직2 실행"); // ← 변함
long endTime = System.currentTimeMillis(); // ← 변하지 않음
log.info("resultTime={}", endTime - startTime); // ← 변하지 않음
}
→ 시간 측정은 같은데 비즈니스 로직만 다름
자기 점검
선수 지식: Unit 2.1, 5주차 Unit 5.1
핵심 개념
"슈퍼클래스에 변하지 않는 템플릿을 두고, 변하는 부분을 자식 클래스에 두는 패턴"
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
call(); // ← 변하는 부분 (자식이 구현)
long endTime = System.currentTimeMillis();
log.info("resultTime={}", endTime - startTime);
}
protected abstract void call();
}
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
익명 내부 클래스 활용:
AbstractTemplate template = new AbstractTemplate() {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
};
template.execute();
자기 점검
선수 지식: Unit 2.2
핵심 한계
상속의 강한 결합:
extends AbstractTemplate → 부모에 강하게 의존복잡함:
해결의 방향:
extends) 대신 합성(composition)자기 점검
선수 지식: Unit 2.3, 5주차 Unit 6.3
핵심 개념
"변하지 않는 부분을 Context에, 변하는 부분을 Strategy 인터페이스로"
상속 대신 위임:
public interface Strategy {
void call();
}
public class StrategyLogic1 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
public class ContextV1 {
private Strategy strategy;
public ContextV1(Strategy strategy) {
this.strategy = strategy; // 위임
}
public void execute() {
long startTime = System.currentTimeMillis();
strategy.call(); // ← 위임
long endTime = System.currentTimeMillis();
log.info("resultTime={}", endTime - startTime);
}
}
람다로 더 간결하게:
ContextV1 context = new ContextV1(() -> log.info("비즈니스 로직1"));
context.execute();
핵심 통찰:
"Context는 Strategy 인터페이스에만 의존" → Spring의 DI와 같은 사상
자기 점검
목표: 전략 패턴의 한계를 콜백 패턴으로 극복하고, 그래도 남는 한계를 프록시로 해결하기 위한 출발점에 선다.
선수 지식: Unit 2.4
핵심 개념
Context V1의 한계:
Context V2 — 파라미터로 받기:
public class ContextV2 {
public void execute(Strategy strategy) { // ← 매번 다른 전략
long startTime = System.currentTimeMillis();
strategy.call();
long endTime = System.currentTimeMillis();
log.info("resultTime={}", endTime - startTime);
}
}
// 사용
ContextV2 context = new ContextV2();
context.execute(() -> log.info("비즈니스 로직1"));
context.execute(() -> log.info("비즈니스 로직2"));
선 조립 후 실행 vs 실행 시점 전달:
자기 점검
선수 지식: Unit 3.1
핵심 개념
용어 매핑:
Context → TemplateStrategy → Callback템플릿 콜백 패턴은 GOF가 아닌 스프링 전용 용어:
Spring의 XxxTemplate 시리즈 ⭐ :
JdbcTemplate (6주차)RestTemplateTransactionTemplateRedisTemplate→ 이름에 "Template"이 붙으면 이 패턴
콜백(Callback)의 정의:
"다른 코드의 인수로서 넘겨주는 실행 가능한 코드"
"코드가 호출(call)되는데, 코드를 넘겨준 곳의 뒤(back)에서 실행됨"
template.execute(() -> log.info("비즈니스 로직1"));
// ↑ 이 람다가 콜백
// template 안에서 "나중에" 실행됨
자기 점검
선수 지식: Phase 3
핵심 한계 직시
지금까지의 모든 방법(템플릿 메서드, 전략 패턴, 템플릿 콜백)의 공통 한계:
"결국 원본 코드를 수정해야 한다"
100개 메서드면 100군데 수정. 이걸 안 하려면?
프록시(Proxy)의 등장:
프록시가 되기 위한 조건:
[Client] → [Server] ──변경──> [Client] → [Proxy] → [Server]
(DI 활용) (코드 변경 X)
프록시의 주요 기능:
자기 점검
목표: 같은 모양의 두 패턴을 의도 로 구분하는 법을 익힌다.
선수 지식: Unit 3.3
핵심 개념
프록시 패턴 = 접근 제어 목적
캐시 예제:
public class CacheProxy implements Subject {
private Subject target;
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
if (cacheValue == null) {
cacheValue = target.operation(); // 처음만 실제 호출
}
return cacheValue; // 두 번째부터 캐시 반환
}
}
효과:
자기 점검
선수 지식: Unit 4.1
핵심 개념
데코레이터 패턴 = 부가 기능 추가 목적
예시 — 메시지 꾸미기 + 시간 측정:
public class MessageDecorator implements Component {
private Component component;
@Override
public String operation() {
String result = component.operation();
return "*****" + result + "*****"; // 꾸며줌
}
}
public class TimeDecorator implements Component {
private Component component;
@Override
public String operation() {
long start = System.currentTimeMillis();
String result = component.operation();
log.info("time={}ms", System.currentTimeMillis() - start);
return result;
}
}
중첩 사용:
Component real = new RealComponent();
Component message = new MessageDecorator(real);
Component time = new TimeDecorator(message);
client.execute(time);
// 호출 흐름: time → message → real → "data"
// → "*****data*****" → 시간 로그
자기 점검
선수 지식: Unit 4.1, 4.2
핵심 통찰
모양은 같다, 의도가 다르다:
| 프록시 패턴 | 데코레이터 패턴 | |
|---|---|---|
| 의도 | 접근 제어 | 기능 추가 |
| 사례 | 캐시, 권한, 지연 로딩 | 로깅, 메시지 꾸미기, 시간 측정 |
| 클라이언트 인지 | 보통 모름 | 알 수도 있음 |
| 중첩 사용 | 드물음 | 흔함 |
구분 기준:
"프록시가 접근 제어 가 목적이면 프록시 패턴, 새 기능 추가 가 목적이면 데코레이터 패턴"
자기 점검
목표: "프록시 클래스를 100개 만들어야 하는 문제"를 자바의 동적 기술로 해결한다.
선수 지식: Phase 4
핵심 문제
수동 프록시의 한계:
해결의 출발점 — 리플렉션:
리플렉션 사용 전:
target.callA(); // 메서드 이름이 코드에 박혀있음
target.callB(); // 다른 메서드는 다른 호출 코드
리플렉션 사용 후:
Method methodA = classHello.getMethod("callA");
Method methodB = classHello.getMethod("callB");
dynamicCall(methodA, target);
dynamicCall(methodB, target);
private void dynamicCall(Method method, Object target) {
method.invoke(target); // 어떤 메서드든 호출 가능
}
리플렉션의 장단점:
일반 코드에서 쓰지 말 것 — 프레임워크 개발용
자기 점검
선수 지식: Unit 5.1
핵심 개념
"프록시 클래스를 런타임에 자동 생성"
전제 조건: 인터페이스 필수
InvocationHandler 인터페이스:
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
구현 예시 — 시간 측정 프록시:
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args); // 실제 호출
log.info("time={}ms", System.currentTimeMillis() - start);
return result;
}
}
프록시 생성:
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(
AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
handler
);
proxy.call(); // 동적 프록시가 가로챔
// 출력: proxyClass=class com.sun.proxy.$Proxy1
핵심 통찰:
$Proxy1, $Proxy2...)자기 점검
선수 지식: Unit 5.2
핵심 개념
JDK 동적 프록시의 한계:
CGLIB (Code Generator Library):
비교:
| JDK 동적 프록시 | CGLIB | |
|---|---|---|
| 전제 | 인터페이스 필요 | 구체 클래스만으로 OK |
| 방식 | 인터페이스 구현 | 클래스 상속 |
| 핸들러 | InvocationHandler | MethodInterceptor |
| 라이브러리 | JDK 표준 | 외부 (Spring 포함) |
실무에서 직접 사용?:
자기 점검
목표: JDK 동적 프록시와 CGLIB의 분기를 Spring이 어떻게 통합했는지, 그리고 그 위에 어떻게 Pointcut/Advice/Advisor 추상화를 쌓았는지를 본다.
선수 지식: Phase 5
핵심 개념
문제:
ProxyFactory의 해결:
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// → 자동으로 JDK 동적 프록시 또는 CGLIB 선택
자동 선택 규칙:
proxyTargetClass=true → 강제 CGLIB)Spring Boot의 기본 설정:
Spring Boot는 AOP 적용 시 기본적으로
proxyTargetClass=true→ 항상 CGLIB
자기 점검
선수 지식: Unit 6.1
핵심 개념
문제:
InvocationHandler, CGLIB는 MethodInterceptorAdvice의 등장:
// org.aopalliance.intercept.MethodInterceptor 구현
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 실제 호출
log.info("time={}ms", System.currentTimeMillis() - start);
return result;
}
}
⚠️ 패키지 주의:
org.aopalliance.intercept.MethodInterceptor (Advice용 ✅)org.springframework.cglib.proxy.MethodInterceptor (CGLIB 직접 사용용)상속 구조:
Advice ← Interceptor ← MethodInterceptor (org.aopalliance)
자기 점검
선수 지식: Unit 6.2
핵심 개념
3대 개념 정리 ⭐ :
| 용어 | 의미 |
|---|---|
| Pointcut | "어디에" 적용할지 (필터링) |
| Advice | "어떤 로직"을 적용할지 (부가 기능) |
| Advisor | "Pointcut + Advice" (어디에 + 어떤 로직) |
Pointcut 사용 예시:
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save"); // save 메서드만 매칭
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save(); // ✅ Advice 적용됨
proxy.find(); // ❌ Advice 안 됨
스프링 기본 Pointcut 종류:
NameMatchMethodPointcut: 메서드 이름 매칭JdkRegexpMethodPointcut: 정규표현식TruePointcut: 항상 참AnnotationMatchingPointcut: 어노테이션 매칭AspectJExpressionPointcut: AspectJ 표현식 ⭐ (실무 표준)자기 점검
선수 지식: Unit 6.3
핵심 개념
여러 부가 기능을 한 객체에:
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvisor(advisor2); // 먼저 등록
proxyFactory.addAdvisor(advisor1); // 나중 등록
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
// 호출 흐름: proxy → advisor2 → advisor1 → target
중요 — Spring AOP 최적화 ⭐ :
"스프링 AOP는 target 마다 하나의 프록시만 생성한다"
여러 AOP가 동시 적용되어도 → 1개 프록시 + N개 어드바이저
의미:
자기 점검
목표: 컴포넌트 스캔된 빈에도 프록시를 적용하는 메커니즘을 이해한다.
선수 지식: Phase 6
두 가지 큰 문제
문제 1 — 너무 많은 설정:
문제 2 — 컴포넌트 스캔:
@Service, @Repository 등으로 자동 등록된 빈→ 빈 등록을 가로채서 프록시로 바꿔치기 해야 한다
자기 점검
@Component 컴포넌트 스캔 메커니즘과 충돌하는가?선수 지식: Unit 7.1
핵심 개념
BeanPostProcessor 인터페이스:
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName);
Object postProcessAfterInitialization(Object bean, String beanName);
}
예시 — A를 B로 바꿔치기:
public class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof A) {
return new B(); // A 대신 B 반환
}
return bean;
}
}
// 결과
B b = applicationContext.getBean("beanA", B.class); // beanA 이름으로 B 반환!
의의:
자기 점검
선수 지식: Unit 7.2
핵심 개념
스프링이 제공하는 자동 프록시 생성기:
spring-boot-starter-aop동작:
1. 스프링 빈으로 등록된 모든 Advisor 들을 자동으로 찾음
2. 각 빈에 대해 Advisor의 Pointcut으로 매칭 검사
3. 매칭되면 해당 빈을 프록시로 교체
@Bean
public Advisor advisor3(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
이름의 의미:
@Aspect)자기 점검
목표: 실무에서 가장 많이 쓰는 @Aspect 어노테이션 방식을 마스터하고, AOP 용어를 정확히 잡는다.
선수 지식: Phase 7
핵심 개념
@Aspect 의 역할:
예시:
@Slf4j
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Around("execution(* hello.proxy.app..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
TraceStatus status = null;
try {
String message = joinPoint.getSignature().toShortString();
status = logTrace.begin(message);
Object result = joinPoint.proceed(); // 실제 호출
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
구성 요소:
@Aspect: "이 클래스는 어드바이저 변환 대상"@Around: 어드바이스 + 포인트컷ProceedingJoinPoint: MethodInvocation의 AOP 버전joinPoint.proceed(): target 호출⚠️ @Aspect는 자동 빈 등록이 아님:
@Bean, @Component, @Import자기 점검
선수 지식: Unit 8.1, Phase 1
핵심 통찰
OOP의 한계:
AOP의 답:
"부가 기능 + 부가 기능을 어디에 적용할지 의 선택을 합쳐서 하나의 모듈로 만든다"
그게 Aspect = 관점
중요한 명제:
AOP는 OOP를 대체하는 것이 아니다.
OOP의 부족한 부분(횡단 관심사)을 보조한다.
자기 점검
선수 지식: Unit 8.2
핵심 비교
AspectJ 프레임워크:
Spring AOP:
AOP 적용 시점 3가지:
| 시점 | 방식 | 누가 사용? |
|---|---|---|
| 컴파일 시점 | 실제 코드에 부가 기능 코드 삽입 | AspectJ 직접 |
| 클래스 로딩 시점 | 클래스 로딩 시 코드 변경 | AspectJ 직접 |
| 런타임 시점 | 프록시로 부가 기능 적용 | Spring AOP ⭐ |
실무 결론:
"Spring AOP만으로 대부분 해결 가능. AspectJ 직접 사용은 안 해도 됨"
자기 점검
선수 지식: Phase 7~8
핵심 용어 7가지 ⭐ :
| 용어 | 의미 | 예시 |
|---|---|---|
| 조인 포인트(Join point) | 부가 기능을 적용할 수 있는 모든 지점 | 메서드 호출, 생성자, 필드 접근 |
| 포인트컷(Pointcut) | 조인 포인트 중 실제 적용할 곳을 선택 | execution(* hello..*Service.*(..)) |
| 어드바이스(Advice) | 적용할 부가 기능 코드 | @Around 메서드 |
| 애스펙트(Aspect) | 포인트컷 + 어드바이스의 모듈 | @Aspect 클래스 |
| 타겟(Target) | 부가 기능이 적용되는 실제 객체 | OrderService 인스턴스 |
| 위빙(Weaving) | 포인트컷을 통해 어드바이스를 결합 | 프록시 생성 시 |
| AOP 프록시 | AOP 기능을 구현한 프록시 | JDK 동적 프록시 또는 CGLIB |
Spring AOP의 한계 — 메서드 조인 포인트만:
자기 점검
목표: 실무에서 매일 쓰는 패턴들을 손에 익힌다.
선수 지식: Phase 8
핵심 개념
@Around — 가장 강력한 어드바이스:
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed(); // 실제 호출
}
}
ProceedingJoinPoint의 주요 메서드:
proceed(): target 메서드 호출getSignature(): 호출된 메서드의 시그니처getArgs(): 전달 인자 배열getTarget(): 실제 대상 객체자기 점검
선수 지식: Unit 9.1
핵심 개념
문제: 같은 포인트컷을 여러 어드바이스에서 반복 작성 → 중복
해결 — @Pointcut으로 시그니처 분리:
@Aspect
public class AspectV2 {
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {} // 시그니처만 (body 없음)
@Around("allOrder()") // 재사용
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
별도 클래스로 분리 (실무 표준):
public class Pointcuts {
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
@Pointcut("allOrder() && allService()") // 조합
public void orderAndService() {}
}
@Aspect
public class AspectV4Pointcut {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { ... }
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable { ... }
}
포인트컷 표현식의 조합:
&&, ||, !자기 점검
선수 지식: Unit 9.2
핵심 개념
| 어드바이스 | 시점 | 용도 |
|---|---|---|
@Around | 메서드 전후 | 가장 강력, 모든 것 가능 |
@Before | 메서드 실행 전 | 단순 사전 작업 |
@AfterReturning | 정상 완료 후 | 결과 처리 |
@AfterThrowing | 예외 발생 시 | 예외 로깅 |
@After | 정상/예외 무관 (finally) | 자원 해제 |
선택 가이드:
@Around@Before / @AfterReturning@After@Around의 강력함:
실무 결론:
"고민되면 @Around 써라. 다른 건 단순 케이스에서만"
자기 점검
목표: 7주차에서 다룬 @Transactional의 5가지 함정 중 internal call 문제 를 깊이 파고, 트랜잭션 전파의 4가지 시나리오를 마스터한다.
선수 지식: 7주차 Phase 7, Phase 6 (ProxyFactory)
핵심 정리
@Transactional은 트랜잭션 AOP 다.
동작 흐름:
1. 빈 등록 시 빈 후처리기가 가로챔
2. @Transactional 메서드가 있으면 → 트랜잭션 프록시로 교체
3. DI 시 실제 객체 대신 프록시 주입
4. 메서드 호출 → 프록시가 가로채서 트랜잭션 시작/커밋/롤백
@Service
public class CallService {
@Transactional
public void internal() {
// 트랜잭션 적용됨
}
}
// CallService 빈은 실제로는 프록시 객체
자기 점검
선수 지식: Unit 10.1
핵심 시나리오
@Service
public class CallService {
public void external() {
log.info("call external");
internal(); // ⚠️ 같은 클래스의 메서드 호출
}
@Transactional
public void internal() {
log.info("call internal");
}
}
문제:
callService.external() 호출internal() 호출 → 사실 this.internal()this는 프록시가 아닌 target!문제의 본질:
"프록시는 외부 호출을 가로챌 수 있지만, 내부 호출은 가로채지 못한다"
원인 — 자바의 동작:
this자기 점검
선수 지식: Unit 10.2
핵심 해결
원리:
@Service
public class CallService {
private final InternalService internalService; // 다른 빈 주입
public void external() {
log.info("call external");
internalService.internal(); // ✅ 프록시 거침
}
}
@Service
public class InternalService {
@Transactional
public void internal() {
log.info("call internal");
}
}
호출 흐름:
test → CallService(실제 객체) → InternalService(프록시) → InternalService(실제) → DB
↑
여기서 트랜잭션 시작
다른 해결책들 (참고):
자기 점검
선수 지식: Unit 10.3, 7주차 Phase 7
핵심 개념
트랜잭션 전파(Propagation):
"이미 진행 중인 트랜잭션이 있을 때, 새 트랜잭션 요청을 어떻게 처리할까?"
REQUIRED (기본 옵션):
예시 — 둘 다 커밋:
@Test
void inner_commit() {
log.info("외부 트랜잭션 시작");
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("outer.isNewTransaction()={}", outer.isNewTransaction()); // true
log.info("내부 트랜잭션 시작");
TransactionStatus inner = txManager.getTransaction(new DefaultTransactionAttribute());
log.info("inner.isNewTransaction()={}", inner.isNewTransaction()); // false (참여)
txManager.commit(inner); // 실제로는 안 일어남
txManager.commit(outer); // 여기서 진짜 commit
}
핵심 통찰:
자기 점검
선수 지식: Unit 10.4
핵심 시나리오
@Test
void inner_rollback() {
TransactionStatus outer = txManager.getTransaction(...);
TransactionStatus inner = txManager.getTransaction(...);
txManager.rollback(inner); // 내부 롤백
assertThatThrownBy(() -> txManager.commit(outer)) // 외부 커밋 시도
.isInstanceOf(UnexpectedRollbackException.class); // ⚠️ 예외!
}
동작 분석:
내부 롤백 시:
1. 신규 트랜잭션이 아니므로 실제 롤백 안 함
2. 트랜잭션 동기화 매니저에 rollbackOnly=true 표시
외부 커밋 시:
1. 신규 트랜잭션이므로 실제 커밋 시도
2. rollbackOnly 표시 발견
3. 커밋 대신 롤백 실행
4. 개발자에게 UnexpectedRollbackException 던짐
중요한 통찰 ⭐ :
"내부든 외부든 논리 트랜잭션이 하나라도 롤백되면 물리 트랜잭션은 롤백"
왜 예외를 던지는가:
자기 점검
선수 지식: Unit 10.5
핵심 개념
REQUIRES_NEW:
예시:
TransactionStatus outer = txManager.getTransaction(new DefaultTransactionAttribute());
DefaultTransactionAttribute def = new DefaultTransactionAttribute();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus inner = txManager.getTransaction(def); // ⭐ 새 물리 트랜잭션
txManager.rollback(inner); // 내부만 롤백
txManager.commit(outer); // 외부는 정상 커밋 ✅
효과:
⚠️ 주의사항:
전파 옵션 7가지:
| 옵션 | 설명 |
|---|---|
| REQUIRED | 기본. 있으면 참여, 없으면 시작 |
| REQUIRES_NEW | 항상 새 트랜잭션 |
| SUPPORT | 있으면 참여, 없으면 트랜잭션 없이 |
| NOT_SUPPORT | 있으면 보류, 트랜잭션 없이 진행 |
| MANDATORY | 반드시 있어야 함, 없으면 예외 |
| NEVER | 트랜잭션 있으면 예외 |
| NESTED | 중첩 트랜잭션 (Savepoint) |
실무에서 가장 많이 쓰는 것: REQUIRED, REQUIRES_NEW
자기 점검
이번 주차는 분량이 많은 만큼 반드시 깊이 파야 할 Unit 을 명확히 표시:
★★★ 면접·실무 단골 (반드시):
★★ 매우 권장:
[ Part A — 8주차 ]
[ ] Phase 1 — AOP 입문과 동기 (Unit 1.1~1.3)
[ ] Phase 2 — 디자인 패턴의 진화 (Unit 2.1~2.4)
[ ] Phase 3 — 콜백과 프록시의 만남 (Unit 3.1~3.3)
[ ] Phase 4 — 프록시 vs 데코레이터 (Unit 4.1~4.3)
[ ] Phase 5 — 동적 프록시 기술 (Unit 5.1~5.3)
[ ] Phase 6 — ProxyFactory 통합 추상화 (Unit 6.1~6.4) ★ 8주차 정점
[ Part B — 9주차 ]
[ ] Phase 7 — 빈 후처리기와 자동 프록시 (Unit 7.1~7.3)
[ ] Phase 8 — @Aspect와 AOP 용어 (Unit 8.1~8.4)
[ ] Phase 9 — Spring AOP 실전 패턴 (Unit 9.1~9.3)
[ ] Phase 10 — @Transactional + 트랜잭션 전파 (Unit 10.1~10.6) ★ 9주차 정점
[ ] 종합 자기 점검 32문항 통과
8주차 정점 — Phase 6 (ProxyFactory):
9주차 정점 — Phase 10 (트랜잭션 전파):
8-9주차는 1~7주차의 학습이 모두 결합 되는 지점:
| 출처 주차 | 8-9주차에서의 역할 |
|---|---|
| 1주차 (OOP, 인터페이스) | 프록시·전략 패턴의 기반 |
| 3주차 (람다, 함수형) | Advice/Callback의 람다 활용 |
| 4주차 (멀티스레드, ThreadLocal) | 로그 추적기의 동시성 해결 |
| 5주차 (디자인 패턴, OCP) | 템플릿 메서드 → 전략 패턴 진화 |
| 5주차 (싱글톤 빈) | 자동 프록시의 빈 교체 메커니즘 |
| 6주차 (JdbcTemplate) | 템플릿 콜백 패턴의 실제 사례 |
| 7주차 (@Transactional) | 8-9주차 학습의 동기 + 적용 사례 |
→ 8-9주차는 자바·Spring 학습의 클라이맥스