F-LAB JAVA · 7주차 · Phase 7 · @Transactional
★ 깊이 파기 — @Transactional 의 핵심 기반, Phase 7 시작
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
프록시 패턴은 실제 객체의 대리인 (Proxy) 을 만들어 호출을 가로채고 부가 기능 (트랜잭션·로깅·보안) 을 자동 적용하는 5주차 디자인 패턴이며, 자바에서는 JDK 동적 프록시 (인터페이스 기반, java.lang.reflect.Proxy) 와 CGLIB 프록시 (구체 클래스의 자식 클래스 동적 생성) 두 종류가 있고 Spring Boot 2.x+ 는 기본 CGLIB 를 사용하며, 이 프록시가 @Transactional 자동화와 JPA Lazy Loading 모두의 직접적 기반이다.
프록시 패턴 (Proxy Pattern) 은 5주차 디자인 패턴의 하나로 — 실제 객체 (Real Subject) 의 대리인 (Proxy) 을 만들어 클라이언트가 직접 객체에 접근하지 않고 프록시를 통해 접근 하게 한다.
자바의 두 종류 프록시 — (1) JDK 동적 프록시 (java.lang.reflect.Proxy, 인터페이스 기반, 인터페이스의 구현 클래스를 런타임 생성, InvocationHandler 로 호출 가로채기), (2) CGLIB 프록시 (구체 클래스 기반, 대상 클래스의 자식 클래스를 바이트코드 조작으로 동적 생성, MethodInterceptor 로 가로채기).
Spring 의 선택 — Spring Boot 2.x+ 부터 기본 CGLIB (이전엔 인터페이스 있으면 JDK, 없으면 CGLIB), 이유는 인터페이스 없는 클래스도 프록시 가능 + 일관성, 설정으로 변경 가능 (spring.aop.proxy-target-class=false).
프록시의 역할 4가지 — (1) 호출 가로채기 (Interception), (2) 부가 기능 (Cross-cutting Concern, 트랜잭션·로깅·보안·캐시), (3) 진짜 객체에 위임, (4) 결과 후처리.
이 프록시 패턴이 @Transactional 자동화의 직접 기반 (다음 Unit 7.2 ★★★) + JPA 의 Lazy Loading (@ManyToOne(fetch = LAZY)시 프록시 객체) + 5주차 학습의 정점 이다.
프록시 패턴 = 비서:
상황:
- CEO (실제 객체)
- 비서 (프록시)
- 손님 (클라이언트)
비서의 역할:
1. 호출 가로채기:
- 모든 손님 = 비서 통과
- CEO 직접 X
2. 부가 기능:
- 손님 등록 (보안)
- 회의록 작성 (로깅)
- 일정 조율 (트랜잭션)
3. CEO 에게 위임:
- 검증 통과 시
- CEO 호출
4. 결과 후처리:
- 회의 결과 정리
- 다음 회의 준비
두 가지 비서 유형:
[1] 인터페이스 비서 (JDK 동적):
- 회사 표준 양식 (인터페이스)
- CEO 와 같은 양식 따름
- 표준화
[2] CGLIB 비서 (구체):
- CEO 의 직접 자녀 (상속)
- CEO 모든 메서드 오버라이드
- 양식 불필요
Spring 의 선택:
- Spring Boot 2.x+ = CGLIB 기본
- 일관성 (인터페이스 없어도 OK)
활용:
- @Transactional (다음 Unit ★★★)
- JPA Lazy Loading
- 5주차 + 7주차 응축
ILIC:
- 1020 @Transactional 모두 프록시
- 매 메서드 호출 = 프록시 통과
→ 프록시 = 대리인, 호출 가로채기 + 부가 기능, JDK/CGLIB, @Transactional 기반.
1. 프록시 패턴 정의
2. 프록시의 역할 4가지
3. JDK 동적 프록시
4. CGLIB 프록시
5. Spring 의 선택
6. AOP 와 프록시
7. @Transactional 의 기반
8. JPA Lazy Loading 연결
9. Phase 7.2 ★★★ 예고
프록시 패턴 (Proxy Pattern):
실제 객체의 대리인:
- Proxy (대리인)
- Real Subject (실제 객체)
- 클라이언트는 Proxy 와만 통신
GoF 디자인 패턴
→ 5주차 학습
프록시 구조:
Subject (인터페이스 또는 추상)
↑
├── RealSubject (실제 객체)
└── Proxy (대리인)
- RealSubject 참조
- 같은 인터페이스 구현
- 호출 가로채기
Client → Proxy → RealSubject
// 1. Subject (인터페이스)
interface ShipmentService {
void process(Long id);
}
// 2. RealSubject (실제)
class ShipmentServiceImpl implements ShipmentService {
@Override
public void process(Long id) {
// 진짜 비즈니스 로직
}
}
// 3. Proxy (대리인)
class ShipmentServiceProxy implements ShipmentService {
private final ShipmentService real; // 진짜 참조
public ShipmentServiceProxy(ShipmentService real) {
this.real = real;
}
@Override
public void process(Long id) {
// 1. 호출 전 (부가 기능)
System.out.println("Before: " + id);
// 2. 진짜 호출
real.process(id);
// 3. 호출 후 (부가 기능)
System.out.println("After: " + id);
}
}
// 사용
ShipmentService service = new ShipmentServiceProxy(
new ShipmentServiceImpl()
);
service.process(1L);
// 출력:
// Before: 1
// (진짜 로직)
// After: 1
정적 vs 동적 프록시:
정적 프록시 (위 코드):
- 컴파일 시 프록시 클래스 작성
- 메서드마다 직접 위임 코드
- 코드 ↑ (보일러플레이트)
동적 프록시:
- 런타임에 자동 생성
- JDK 동적 프록시 (인터페이스)
- CGLIB (구체 클래스)
- Spring 이 사용
5주차 학습:
GoF 디자인 패턴:
- 생성 / 구조 / 행동
- Proxy = 구조 패턴
관련 패턴:
- Decorator (장식, 비슷)
- Adapter (변환)
- Facade (외부 간결화)
→ 다 비슷한 정신
ILIC 의 프록시 활용
ILIC 의 코드:
@Service
public class ShipmentService {
@Transactional
public void process(Long id) {
// 비즈니스
}
}
Spring 의 동작 (자동):
1. Spring Boot 시작 시
2. @Service 감지 → 빈 등록
3. @Transactional 감지 → 프록시 생성
- ShipmentService 의 프록시 (자식 클래스)
- 호출 가로채기
4. 프록시를 빈으로 등록 (진짜 X)
5. @Autowired 받는 곳에 프록시 주입
매 호출:
- service.process(1L)
- 프록시.process(1L)
- 트랜잭션 시작
- 진짜 process 호출
- 트랜잭션 commit / rollback
→ 1020 메서드 모두 프록시
프록시 패턴의 정의는?
답:
1. 프록시:
구조:
5주차:
종류:
역할 1 — 호출 가로채기 (Interception):
클라이언트 → Proxy:
- Proxy 가 메서드 호출 받음
- 진짜 객체 즉시 호출 X
- 가로채서 부가 작업
핵심:
"메서드 호출 = 가로챌 수 있는 지점"
역할 2 — 부가 기능 (Cross-cutting Concern):
비즈니스 로직 외:
- 트랜잭션
- 로깅
- 보안 (인증/인가)
- 캐시
- 성능 측정
- 예외 처리
횡단 관심사:
- 여러 클래스에 공통
- "옆으로 가로지르는"
- AOP 의 개념
역할 3 — 진짜에 위임 (Delegation):
부가 기능 완료 후:
- Proxy.process()
- real.process() ← 진짜 호출
- 반환값 받음
의미:
- Proxy 가 비즈니스 X
- 진짜 객체가 비즈니스
- Proxy = 양념
역할 4 — 결과 후처리:
진짜 호출 후:
- 정상 반환 → commit / 로그 / 캐시
- 예외 발생 → rollback / 알림
완전한 흐름:
1. 호출 가로채기
2. 부가 작업
3. 진짜 위임
4. 결과 후처리
→ 4 단계
// 4 단계 코드
public class ShipmentServiceProxy implements ShipmentService {
private final ShipmentService real;
@Override
public void process(Long id) {
// ① 호출 가로채기 (지금 여기)
long start = System.currentTimeMillis();
log.info("Calling process({})", id);
// ② 부가 작업
tm.begin(); // 트랜잭션 시작
try {
// ③ 진짜에 위임
real.process(id);
// ④ 결과 후처리 (정상)
tm.commit();
log.info("Success: {}ms", System.currentTimeMillis() - start);
} catch (Exception e) {
// ④ 결과 후처리 (예외)
tm.rollback();
log.error("Failed", e);
throw e;
}
}
}
// → 4 단계 모두
class ShipmentService { void process(Long id) {} }
ShipmentService real;
class TM { void begin() {} void commit() {} void rollback() {} }
TM tm;
org.slf4j.Logger log;
횡단 관심사 시각화:
┌─ ShipmentService.process() ─┐
│ ┌──────────────────────┐ │
│ │ 트랜잭션 시작 (프록시) │ │ ← 횡단
│ │ 로깅 (프록시) │ │ ← 횡단
│ │ ┌──────────────────┐ │ │
│ │ │ 비즈니스 로직 │ │ │ ← 핵심
│ │ └──────────────────┘ │ │
│ │ 트랜잭션 commit │ │ ← 횡단
│ │ 로깅 후처리 │ │ ← 횡단
│ └──────────────────────┘ │
└─────────────────────────────┘
- 핵심 = 비즈니스
- 횡단 = 프록시 자동
- 분리 = SoC
ILIC 의 프록시 역할
ILIC 의 ShipmentService.process(1L) 호출:
① 호출 가로채기:
- 프록시가 받음
- 시작 시각 기록
② 부가 기능:
- 트랜잭션 시작 (@Transactional)
- 로깅 (메서드 진입)
- 메트릭 시작 (관측성)
③ 진짜 위임:
- 진짜 ShipmentService.process()
- 비즈니스 로직
④ 결과 후처리:
- 정상 → commit + 메트릭 종료
- 예외 → rollback + 알림
→ 비즈니스 코드 깔끔
→ 모든 부가 기능 자동
프록시의 역할 4가지는?
답:
1. 호출 가로채기:
부가 기능:
진짜에 위임:
결과 후처리:
JDK 동적 프록시:
자바 표준 동적 프록시:
- java.lang.reflect.Proxy
- 인터페이스 기반
- 런타임 클래스 생성
요구사항:
- 인터페이스 필수
- 인터페이스 없으면 X
// InvocationHandler 구현
public class ShipmentHandler implements InvocationHandler {
private final ShipmentService real;
public ShipmentHandler(ShipmentService real) {
this.real = real;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 호출 가로채기 (모든 메서드)
System.out.println("Before: " + method.getName());
// 진짜에 위임
Object result = method.invoke(real, args);
// 후처리
System.out.println("After");
return result;
}
}
class ShipmentService {}
ShipmentService real;
class Method {
String getName() { return null; }
Object invoke(Object o, Object[] args) throws Throwable { return null; }
}
class InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; }
}
// JDK 동적 프록시 생성
ShipmentService real = new ShipmentServiceImpl();
ShipmentService proxy = (ShipmentService) Proxy.newProxyInstance(
real.getClass().getClassLoader(),
new Class[] { ShipmentService.class }, // 구현할 인터페이스
new ShipmentHandler(real) // 호출 처리
);
// 사용
proxy.process(1L);
// 출력:
// Before: process
// (진짜 호출)
// After
class Proxy {
static Object newProxyInstance(ClassLoader cl, Class<?>[] interfaces, Object handler) { return null; }
}
class ShipmentService { void process(Long id) {} }
class ShipmentServiceImpl extends ShipmentService {}
class ShipmentHandler {
ShipmentHandler(ShipmentService s) {}
}
동작 원리:
1. Proxy.newProxyInstance() 호출
2. JVM 이 런타임에:
- 인터페이스 (ShipmentService) 의 구현 클래스 생성
- 모든 메서드는 InvocationHandler.invoke() 호출
3. proxy.process(1L) 호출 시:
- 동적 생성된 클래스의 process() 실행
- 내부적으로 InvocationHandler.invoke() 호출
- method = process, args = [1L]
4. InvocationHandler 안:
- 부가 기능
- method.invoke(real, args)
- 후처리
JDK 동적 프록시:
장점:
- 자바 표준 (별도 라이브러리 X)
- 가벼움
- 빠름
단점:
- 인터페이스 필수
- 구체 클래스 만 있으면 X
- 인터페이스 X 메서드 가로채기 X
// 한계 — 인터페이스 없으면 X
public class ShipmentServiceNoInterface {
public void process(Long id) {
// 비즈니스
}
}
// JDK 동적 프록시?
// → ❌ 안 됨!
// → 인터페이스 없음
// CGLIB 가 필요
class ShipmentServiceNoInterface { void process(Long id) {} }
// ILIC 의 JDK 동적 프록시 케이스 (가정)
// 만약 ILIC 가 인터페이스 사용:
public interface ShipmentService {
void process(Long id);
}
@Service
public class ShipmentServiceImpl implements ShipmentService {
@Transactional
@Override
public void process(Long id) {
// 비즈니스
}
}
// Spring (Spring Boot 1.x 또는 spring.aop.proxy-target-class=false):
// → JDK 동적 프록시 사용
// 빈 등록:
// - 진짜 빈: ShipmentServiceImpl
// - 프록시: ShipmentService 의 구현 (JDK 동적)
// - 주입은 ShipmentService 타입으로
// 현재 Spring Boot 2.x+:
// → CGLIB 기본 (다음 섹션)
@interface Service {}
@interface Transactional {}
JDK 동적 프록시의 동작은?
답:
1. JDK 동적:
요구:
InvocationHandler:
한계:
CGLIB (Code Generator Library):
바이트코드 조작 라이브러리:
- 자바 표준 X (외부)
- 구체 클래스 자식 동적 생성
- 인터페이스 없어도 OK
Spring 이 내장 (Spring Boot 2+)
자식 클래스 생성:
원본:
class ShipmentService {
public void process(Long id) { ... }
}
CGLIB:
class ShipmentService$$EnhancerByCGLIB extends ShipmentService {
@Override
public void process(Long id) {
// 가로채기
interceptor.intercept(this, "process", new Object[]{id});
}
}
→ 자식 클래스 (extends)
→ 메서드 오버라이드
// MethodInterceptor 구현
public class ShipmentInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
throws Throwable {
// 호출 가로채기
System.out.println("Before: " + method.getName());
// 진짜 호출 (부모 메서드)
Object result = proxy.invokeSuper(obj, args);
// 후처리
System.out.println("After");
return result;
}
}
class MethodInterceptor {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return null; }
}
class MethodProxy {
Object invokeSuper(Object obj, Object[] args) throws Throwable { return null; }
}
class Method {
String getName() { return null; }
}
// CGLIB 프록시 생성
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ShipmentService.class); // 부모 (구체 클래스)
enhancer.setCallback(new ShipmentInterceptor());
ShipmentService proxy = (ShipmentService) enhancer.create();
// 사용
proxy.process(1L);
// 출력:
// Before: process
// (진짜 호출)
// After
class Enhancer {
void setSuperclass(Class<?> c) {}
void setCallback(Object o) {}
Object create() { return null; }
}
class ShipmentService { void process(Long id) {} }
class ShipmentInterceptor {}
동작 원리:
1. CGLIB.Enhancer 사용
2. 바이트코드 조작:
- 원본 클래스 (ShipmentService) 분석
- 자식 클래스 동적 생성
- 메서드 오버라이드
3. proxy.process(1L) 호출:
- 자식 클래스의 process() 실행
- MethodInterceptor.intercept() 호출
- 부가 기능 + 부모 메서드 호출
CGLIB:
장점:
- 인터페이스 불필요
- 구체 클래스 OK
- 일관성 (모든 클래스 가능)
단점:
- 외부 라이브러리 (Spring 내장)
- final 클래스 X (상속 불가)
- final 메서드 가로채기 X
- private 메서드 가로채기 X (다음 Unit 함정)
- 생성자 호출 (1번)
// final 의 문제 (Unit 4.1 도 다룸)
// ❌ final 클래스
public final class ShipmentService {
@Transactional
public void process(Long id) { }
}
// CGLIB:
// → 자식 클래스 만들기 (상속) X
// → 에러!
// ❌ final 메서드
public class ShipmentService {
@Transactional
public final void process(Long id) { }
}
// CGLIB:
// → 자식 클래스 OK
// → process 오버라이드 X (final)
// → 프록시 동작 X
// → @Transactional 무시
// 해결:
// - final 제거
// - 또는 JDK 동적 프록시 사용
@interface Transactional {}
// ILIC 의 CGLIB 케이스 (현재 표준)
@Service // 인터페이스 X
public class ShipmentService { // 보통 인터페이스 없음
@Autowired ShipmentRepository repo;
@Transactional
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
}
}
// Spring Boot 2.x+:
// → CGLIB 사용
// → ShipmentService$$EnhancerByCGLIB 자동 생성
// → 자식 클래스 (ShipmentService extends)
// → process 오버라이드 + 트랜잭션
// final 사용 금지:
// public final class ShipmentService ← ❌
// public final void process ← ❌
// → ILIC 의 1020 메서드 모두 CGLIB 프록시
class Shipment { void markAsShipped() {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}
CGLIB 프록시의 동작은?
답:
1. CGLIB:
자식 클래스:
인터페이스 X:
final 금지:
Spring 의 선택 기준:
Spring Boot 1.x ~ 2.0 초기:
- 인터페이스 있음 → JDK 동적 프록시
- 인터페이스 없음 → CGLIB
Spring Boot 2.x+ (2.0 이후):
- 기본 CGLIB (모든 경우)
- proxyTargetClass = true (기본)
변경 가능:
spring.aop.proxy-target-class=false
→ JDK 동적 (인터페이스만)
왜 CGLIB 기본?:
1. 일관성:
- 인터페이스 유무 무관
- 모든 빈 같은 방식
2. 인터페이스 안 만들어도 OK:
- 보일러플레이트 ↓
- 단순
3. 캐스팅 안전:
- 구체 클래스 타입 사용 가능
- JDK 동적은 인터페이스 타입만
→ Spring Boot 의 결정
| 항목 | JDK 동적 프록시 | CGLIB |
|---|---|---|
| 표준 | 자바 표준 | 외부 라이브러리 |
| 요구사항 | 인터페이스 필수 | 구체 클래스 (final X) |
| 동작 방식 | 인터페이스 구현 | 자식 클래스 (extends) |
| 핸들러 | InvocationHandler | MethodInterceptor |
| Spring Boot 2+ | 옵션 | 기본 |
| 성능 | 빠름 | 약간 느림 (바이트코드) |
| 캐스팅 | 인터페이스 타입만 | 구체 타입 가능 |
# application.yml (JDK 동적 프록시로 변경)
spring:
aop:
proxy-target-class: false # JDK 동적
# 기본 (Spring Boot 2.x+)
# spring.aop.proxy-target-class: true # CGLIB
// 또는 @EnableTransactionManagement
@Configuration
@EnableTransactionManagement(proxyTargetClass = false) // JDK 동적
public class AppConfig {
}
어떤 걸 선택?:
기본 (CGLIB):
- 거의 모든 신규 프로젝트
- 일관성
JDK 동적:
- 인터페이스 강제 (DI / 테스트)
- 옛 시스템 호환
실무:
- 거의 모두 CGLIB 기본
- 명시적 변경 X
// ILIC = Spring Boot 3 = CGLIB 기본
// 코드:
@Service
public class ShipmentService { // 인터페이스 X
@Transactional
public void process(Long id) { }
}
// Spring 동작:
// 1. @Service 감지 → 빈 등록 시도
// 2. @Transactional 감지 → 프록시 필요
// 3. CGLIB 프록시 생성
// - ShipmentService$$EnhancerByCGLIB 동적 생성
// - extends ShipmentService
// - process 오버라이드
// 4. 프록시를 빈 등록 (진짜 X)
// 5. @Autowired 시 프록시 주입
// 사용:
@Autowired ShipmentService service; // 프록시
service.process(1L); // 프록시 호출
// → 트랜잭션 시작 → 진짜 호출 → 트랜잭션 commit
class ShipmentService { void process(Long id) {} }
ShipmentService service;
@interface Service {}
@interface Transactional {}
@interface Autowired {}
Spring 의 프록시 선택 기준은?
답:
1. Spring Boot 2.x+:
이유:
변경:
실무:
AOP (Aspect-Oriented Programming):
관점 지향 프로그래밍:
- 횡단 관심사 분리
- 비즈니스 로직 + 부가 기능 분리
- 모듈성 ↑
핵심 개념:
- Aspect (관점)
- Advice (조치)
- Pointcut (적용 지점)
- Join Point (실행 지점)
AOP 구현 방식:
1. 프록시 기반 (Spring AOP):
- 메서드 호출 시점
- JDK 동적 / CGLIB
- 가볍 / 제한적
2. 바이트코드 위빙 (AspectJ):
- 컴파일 시 또는 로드 시
- 모든 지점 (필드 접근 등)
- 강력 / 무거움
Spring 의 선택:
- Spring AOP (프록시 기반)
- 일반 케이스 충분
- @AspectJ 어노테이션 활용
AOP 용어:
Aspect (관점):
- 횡단 관심사의 모듈화
- @Aspect 클래스
Advice (조치):
- 실제 동작
- @Before / @After / @Around 등
Pointcut (절단점):
- 어디에 적용?
- 메서드 패턴
Join Point (결합점):
- 실제 실행 지점
- 메서드 호출 등
Weaving (위빙):
- Aspect 를 코드에 적용
- 프록시 생성 = 위빙
// @Aspect 예시
@Aspect
@Component
public class TransactionAspect {
@Around("@annotation(transactional)") // Pointcut
public Object handleTransaction(
ProceedingJoinPoint pjp,
Transactional transactional) throws Throwable {
// Before
TransactionStatus status = tm.getTransaction(...);
try {
// Around (진짜 메서드)
Object result = pjp.proceed();
// After (정상)
tm.commit(status);
return result;
} catch (Exception e) {
// After (예외)
tm.rollback(status);
throw e;
}
}
}
// → Spring 의 @Transactional 의 동작 (개념)
// → 다음 Unit 7.2 ★★★ 상세
class TransactionStatus {}
class TransactionManager {
TransactionStatus getTransaction(Object o) { return null; }
void commit(TransactionStatus s) {}
void rollback(TransactionStatus s) {}
}
TransactionManager tm;
class ProceedingJoinPoint { Object proceed() throws Throwable { return null; } }
@interface Aspect {}
@interface Component {}
@interface Around { String value(); }
@interface Transactional {}
AOP 와 프록시:
Spring AOP:
- 프록시 패턴 활용
- 어노테이션 (@Transactional) 감지
- 프록시 자동 생성
프록시 = AOP 의 구현 기반:
- 호출 가로채기 = Join Point
- 부가 기능 = Advice
- 프록시 생성 = Weaving
→ 프록시 = AOP 의 엔진
ILIC 의 AOP
ILIC = Spring AOP (프록시 기반):
- @Transactional 의 자동화
- @Cacheable 의 캐시
- @PreAuthorize 의 보안
- @Async 의 비동기
모두 AOP + 프록시:
1. 어노테이션 감지
2. 프록시 생성 (CGLIB)
3. 호출 가로채기
4. 부가 기능
ILIC 의 1020 메서드 + @Transactional:
- 모두 프록시
- AOP 의 결과
- 자동화
→ 5주차 + 7주차 응축
AOP 와 프록시의 관계는?
답:
1. AOP:
구현:
용어:
관계:
@Transactional 의 동작 (개요):
1. Spring Boot 시작:
- Bean Post Processor 가
- @Transactional 감지
2. 프록시 생성:
- CGLIB (기본)
- 자식 클래스 동적 생성
3. 빈 등록:
- 프록시를 빈으로 (진짜 X)
- @Autowired 시 프록시 주입
4. 호출 시:
- 프록시.process()
- 트랜잭션 시작
- 진짜.process()
- 트랜잭션 commit / rollback
→ 다음 Unit 7.2 ★★★ 상세
어노테이션 감지:
@EnableTransactionManagement:
- Spring Boot 자동 (대부분)
- TransactionInterceptor 등록
Bean Post Processor:
- 빈 생성 후 후처리
- @Transactional 감지
- 프록시로 감쌈
TransactionInterceptor:
Spring 의 핵심 클래스:
- MethodInterceptor 구현
- 모든 @Transactional 메서드 가로채기
- PlatformTransactionManager 와 연동
동작:
1. 메서드 호출 가로채기
2. @Transactional 옵션 확인
3. PlatformTransactionManager 호출
4. 진짜 메서드 호출
5. 결과 처리
흐름 시각화:
Application Code:
@Autowired ShipmentService service;
service.process(1L);
↓
CGLIB 프록시 (ShipmentService$$EnhancerByCGLIB):
- process() 메서드 (오버라이드)
- TransactionInterceptor 호출
↓
TransactionInterceptor:
- @Transactional 옵션 읽기
- PlatformTransactionManager.getTransaction()
↓
PlatformTransactionManager (JpaTransactionManager):
- EntityManager 생성
- tx.begin()
↓
진짜 ShipmentService.process():
- 비즈니스 로직
- Dirty Checking (JPA)
↓ (정상 반환)
TransactionInterceptor:
- PlatformTransactionManager.commit()
↓
PlatformTransactionManager:
- em.flush()
- tx.commit()
- em.close()
↓
반환
5+6+7주차 응축:
5주차:
- 디자인 패턴 (프록시, DI, 템플릿+전략)
- SoC
6주차:
- DataSource
- ACID
- JdbcTemplate
7주차:
- JPA (Phase 3-4)
- PlatformTransactionManager (Phase 6)
- 프록시 (Phase 7)
@Transactional:
- 모든 응축
- 어노테이션 1줄
- 모든 기술 활용
다음 Unit 7.2 ★★★:
자세한 동작 원리:
- Bean Post Processor
- InfrastructureAdvisorAutoProxyCreator
- 프록시 생성 과정
- TransactionInterceptor
- PlatformTransactionManager
- 호출 흐름 전체
면접 정점:
- "@Transactional 어떻게 동작?"
- 위 흐름 설명
Phase 7 의 핵심
ILIC 의 @Transactional 동작
ILIC 의 service.process(1L) 호출 시:
1. 진짜 ShipmentService 가 아니라
ShipmentService$$EnhancerByCGLIB (프록시)
2. 프록시의 process() 메서드 실행:
→ TransactionInterceptor.invoke()
3. TransactionInterceptor:
→ JpaTransactionManager.getTransaction()
→ EntityManager 생성
→ tx.begin()
4. 진짜 ShipmentService.process() 호출:
→ repo.findById(id)
→ s.markAsShipped()
→ Dirty Checking 표시
5. 정상 반환 후:
→ JpaTransactionManager.commit()
→ em.flush() (UPDATE SQL)
→ tx.commit()
→ em.close()
→ 1020 메서드 모두 이 흐름
→ ILIC 의 표준
@Transactional 의 기반으로서 프록시는?
답:
1. 프록시:
TransactionInterceptor:
PlatformTransactionManager:
5+6+7주차 응축:
JPA 의 또 다른 프록시:
@ManyToOne(fetch = LAZY):
- 연관 객체 즉시 로딩 X
- 프록시 객체 먼저
접근 시:
- 진짜 객체 로딩 (SQL)
- 또는 이미 로딩된 객체
→ 프록시 패턴의 또 다른 활용
@Entity
public class Shipment {
@Id Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
}
// 사용
Shipment s = repo.findById(1L).orElseThrow();
// 이때 customer 는?
//
// s.customer 는 프록시 객체:
// Customer$HibernateProxy$xxx
// - extends Customer (CGLIB 와 비슷)
// - 모든 메서드 오버라이드
// - 호출 시 SQL 로딩
s.getCustomer().getName();
// → 이때 SQL 실행!
// → "SELECT * FROM customers WHERE id = ?"
// → 진짜 Customer 객체 반환
class Shipment {
Long id;
Customer customer;
Customer getCustomer() { return null; }
}
class Customer { String getName() { return null; } }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
final 의 문제 (Unit 4.1 다룸):
@Entity
public final class Customer { ... }
↑ final 금지!
이유:
- Hibernate 가 자식 클래스 만들어야 (CGLIB 같은 패턴)
- final 은 상속 불가
- 프록시 생성 X
→ JPA 의 final 금지 = 프록시 때문
N+1 문제와 프록시:
List<Shipment> all = repo.findAll();
// 1 쿼리: SELECT * FROM shipments
for (Shipment s : all) {
s.getCustomer().getName();
// 매번 프록시 → SQL
// N 쿼리
// 총: 1 + N
}
해결:
- JOIN FETCH
- @EntityGraph
- default_batch_fetch_size
| 항목 | @Transactional 프록시 | JPA Lazy 프록시 |
|---|---|---|
| 목적 | 트랜잭션 관리 | 지연 로딩 |
| 생성 | Spring AOP | Hibernate |
| 라이브러리 | CGLIB | 비슷 (CGLIB-like) |
| 호출 가로채기 | 트랜잭션 + 메서드 | SQL 로딩 |
| 5주차 | 프록시 패턴 | 프록시 패턴 |
| 활용 | 부가 기능 | 성능 최적화 |
// ILIC 의 두 프록시
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
@Transactional // ← 프록시 1 (Spring AOP)
public Customer getCustomer(Long id) {
Shipment s = repo.findById(id).orElseThrow();
return s.getCustomer(); // ← 프록시 2 (JPA Lazy)
// 이때 SQL 실행
}
}
// 호출 시 동작:
// 1. CGLIB 프록시 (Spring)
// → 트랜잭션 시작
// 2. 진짜 메서드 호출
// → s.getCustomer() = Hibernate 프록시
// → SQL 실행 (Customer 로딩)
// 3. 트랜잭션 commit
// 4. Customer 반환
// → 5주차 디자인 패턴 곳곳에 활용
class Customer {}
class Shipment { Customer getCustomer() { return null; } }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}
JPA Lazy Loading 의 프록시 연결은?
답:
1. JPA:
Lazy Loading:
final 금지:
두 프록시:
✨ Phase 7 — @Transactional
Unit 7.1 — 프록시 패턴 ★ ← 여기
- 프록시 정의 / 역할
- JDK 동적 / CGLIB
- Spring 의 선택
- AOP 와 프록시
Unit 7.2 — @Transactional 동작 원리 ★★★:
- Bean Post Processor
- InfrastructureAdvisorAutoProxyCreator
- 프록시 생성 과정 상세
- TransactionInterceptor
- 호출 흐름 전체
- 면접 정점
Unit 7.3 — 5가지 함정 ★:
- private 메서드
- self-invocation
- checked exception
- propagation
- readOnly
Unit 7.2 가 ★★★ 인 이유:
1. 면접 단골:
- "@Transactional 동작 원리?"
- 가장 자주 묻는 주제
- 시니어 시험
2. 실무 디버깅:
- 함정 회피
- 트러블슈팅
- 성능 분석
3. 5+6+7주차 응축:
- 디자인 패턴 (5)
- DB 접근 (6)
- JPA + 트랜잭션 (7)
4. 깊은 이해:
- 추상화의 정점
- 자바 진영 정수
다음 Unit 의 흐름:
Unit 7.2 — @Transactional 동작 원리 ★★★:
1. Spring Boot 시작 흐름
2. Bean Post Processor 의 역할
3. @Transactional 감지 (어노테이션 스캐닝)
4. AutoProxyCreator
5. 프록시 생성 (CGLIB)
6. TransactionInterceptor 의 동작
7. 호출 흐름 (메서드 호출 시)
8. PlatformTransactionManager 와 연동
9. 5+6+7주차 응축 종합
🗂️ Part A (완주)
✅ Phase 1-4 (16)
🔄 Part B — 트랜잭션 추상화의 진화
✅ Phase 5 (2)
✅ Phase 6 (3)
✨ Phase 7 — @Transactional (1/3) — 모두 ★깊이
✅ Unit 7.1 프록시 패턴 ★ ← 여기
⏭ Unit 7.2 @Transactional 동작 원리 ★★★
⏭ Unit 7.3 @Transactional 5가지 함정 ★
총: 22/24 Unit (92%)
| Q | 핵심 답변 |
|---|---|
| 프록시 패턴? | 대리인 / GoF |
| 역할? | 가로채기 + 부가 + 위임 + 후처리 |
| JDK 동적? | 인터페이스 기반 |
| CGLIB? | 자식 클래스 |
| Spring 선택? | CGLIB 기본 (2.x+) |
| final 금지? | 상속 불가 |
| AOP? | 횡단 관심사 |
| @Transactional 기반? | 프록시 |
| JPA Lazy? | 같은 프록시 정신 |
| Unit 7.2? | ★★★ 정점 |
답:
답:
답:
답:
답:
1. 프록시 패턴 = 대리인 + 호출 가로채기
2. 두 종류 프록시
3. @Transactional + JPA Lazy 의 공통 기반
이번 Unit에서 프록시를 봤다면, 다음은 @Transactional 동작 원리 (★★★ 정점, 면접 단골).
✨ Phase 7 — @Transactional (모두 ★ 깊이)
✅ Unit 7.1 프록시 패턴 ★ ← 여기
⏭ Unit 7.2 @Transactional 동작 원리 ★★★
⏭ Unit 7.3 @Transactional 5가지 함정 ★
🗂️ Part A (완주)
✅ Phase 1-4 (16)
🔄 Part B — 트랜잭션 추상화의 진화
✅ Phase 5 (2)
✅ Phase 6 (3)
✨ Phase 7 (1/3) — 모두 ★깊이
총: 22/24 Unit (92%)
★ 깊이 파기 — 프록시 패턴 완료, Phase 7 시작