F-LAB JAVA · 7주차 · Phase 7 · @Transactional
★★★ 깊이 파기 — 7주차의 정점, 면접 정점, 5+6+7주차 응축의 결정체
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
@Transactional 의 동작 원리는 Spring Boot 시작 시 Bean Post Processor (InfrastructureAdvisorAutoProxyCreator) 가 빈을 스캔하면서 @Transactional 어노테이션을 감지하고 CGLIB 으로 자식 클래스를 동적 생성해 프록시 빈으로 등록하며, 호출 시점에는 (1) 클라이언트가 프록시 호출 (2) 프록시가 TransactionInterceptor 호출 (3) Interceptor 가 PlatformTransactionManager.getTransaction() 으로 EntityManager 생성 + tx.begin (4) 진짜 메서드 호출 (5) 정상이면 commit (em.flush 포함) / 예외면 rollback 의 7 단계 흐름이 모든 @Transactional 메서드에서 일어나는데, 이게 7주차 정점이자 5+6+7주차 (디자인 패턴 + DataSource + JPA) 응축의 결정체다.
@Transactional 동작 원리 는 Spring 백엔드의 핵심이자 면접의 정점 이다.
시작 단계 — Spring Boot 시작 시@EnableTransactionManagement(Spring Boot 자동) 가 TransactionInterceptor + InfrastructureAdvisorAutoProxyCreator (Bean Post Processor) 를 등록 → 모든 빈 스캔하며 @Transactional 어노테이션 감지 → CGLIB 으로 자식 클래스 동적 생성 (ShipmentService$$EnhancerByCGLIB) → 프록시를 빈으로 등록 (진짜 X).
호출 단계 7 단계 — (1) 클라이언트service.process(1L)호출 → (2) CGLIB 프록시 의 오버라이드된 메서드 실행 → (3) TransactionInterceptor 가@Transactional옵션 (propagation/isolation/timeout/readOnly/rollbackFor) 읽음 → (4) PlatformTransactionManager (JpaTransactionManager).getTransaction()호출 → EntityManager 생성 +tx.begin()+ ThreadLocal 바인딩 → (5) 진짜 ShipmentService.process() 호출 (MethodProxy.invokeSuper또는joinPoint.proceed()) → 비즈니스 로직 실행 → (6) 정상 반환 시 TransactionInterceptor 가commit()호출 →em.flush()(Dirty Checking → UPDATE SQL) +tx.commit()+em.close()+ ThreadLocal 정리 → (7) 예외 발생 시rollback()호출 →tx.rollback()+em.close()+ 예외 throw.
이 모든 것이 — 5주차 (프록시 + AOP + DI + 템플릿+전략) + 6주차 (DataSource + ACID) + 7주차 (JPA + PlatformTM) 의 완벽한 응축으로, ILIC 의 1020 메서드가 모두 이 7 단계 흐름으로 동작하며 자바 백엔드의 정수다.
@Transactional = 자율주행 자동차:
운전자 (개발자):
- "@Transactional" 어노테이션 1줄 = "목적지 입력"
- 비즈니스 로직 = "기본 운전 의도"
- 나머지 모든 동작 자동
시작 (Spring Boot 시작):
1. 자동차 시동 (Application 시작)
2. 시스템 점검 (Bean Post Processor):
- 모든 빈 스캔
- @Transactional 검사
- 검사 통과 빈 = 자율주행 가능
3. 자율주행 모듈 설치 (CGLIB 프록시):
- 자식 클래스 동적 생성
- 운전대 가로채기 모듈
4. 운행 준비
운행 (메서드 호출):
1. 운전자 출발 명령 (service.process)
↓
2. 자율주행 모듈 작동 (CGLIB 프록시):
↓
3. 안전 점검 (TransactionInterceptor):
- 어노테이션 옵션 확인
- "어떤 모드?" (propagation, isolation)
↓
4. 시동 + 출발 (PlatformTM.getTransaction):
- EntityManager 생성
- tx.begin()
↓
5. 실제 주행 (진짜 메서드):
- 비즈니스 로직
- Dirty Checking 표시
↓
6 또는 7. 목적지 도착 / 사고:
6. 정상 → commit:
- em.flush() (UPDATE SQL)
- tx.commit()
- em.close()
7. 예외 → rollback:
- tx.rollback()
- em.close()
- 예외 전파
5+6+7주차 응축:
- 5주차: 자율주행 알고리즘 (디자인 패턴)
- 6주차: 도로 인프라 (DataSource, ACID)
- 7주차: GPS + 네비 (JPA + PlatformTM)
- 모두 자동
ILIC:
- 1020 메서드 = 1020 자율주행
- 박승제 = 목적지만 입력
- 모든 운전 자동
→ Spring 의 마법, 7 단계 흐름, 5+6+7주차 응축, 자바 백엔드 정점.
1. @Transactional 동작 원리 개요
2. Spring Boot 시작 흐름
3. Bean Post Processor 의 역할
4. 어노테이션 감지 + AutoProxyCreator
5. 프록시 생성 (CGLIB)
6. TransactionInterceptor 의 동작
7. 메서드 호출 전체 흐름 (7 단계)
8. 5+6+7주차 응축 종합
9. Phase 7.3 예고 (5가지 함정)
두 단계 흐름:
① 시작 단계 (Spring Boot 시작 시):
- 빈 스캔
- @Transactional 감지
- 프록시 생성 (CGLIB)
- 프록시를 빈으로 등록
② 호출 단계 (런타임):
- 메서드 호출 시 7 단계
- 프록시 → Interceptor → TM → 진짜
핵심 컴포넌트 5개:
1. Bean Post Processor:
- 빈 생성 후 후처리
- @Transactional 감지
2. InfrastructureAdvisorAutoProxyCreator:
- 특수 Bean Post Processor
- 프록시 자동 생성
3. TransactionInterceptor:
- 메서드 호출 가로채기
- 트랜잭션 관리
4. PlatformTransactionManager:
- 실제 트랜잭션 수행
- JpaTM / DataSourceTM 등
5. EntityManager / Connection:
- 실제 DB 작업
- JPA / JDBC
시각화:
[Spring Boot 시작]
↓
Bean Post Processor 등록
↓
빈 스캔 + @Transactional 감지
↓
CGLIB 프록시 생성
↓
프록시를 빈으로 등록
[런타임 - 메서드 호출]
Client
↓
CGLIB Proxy (Bean)
↓
TransactionInterceptor
↓
PlatformTransactionManager (JpaTM)
↓
EntityManager
↓
Real Service (비즈니스 로직)
↓ (정상)
PlatformTransactionManager.commit
↓
em.flush() + tx.commit() + em.close()
ILIC 의 @Transactional 동작
ILIC 의 1020 메서드:
- 모두 @Transactional 어노테이션
- 모두 위 두 단계 흐름
시작 시:
- Spring Boot 시작
- 102 ShipmentService (등 모든 @Service) 빈 등록
- 각 빈 검사
- @Transactional 감지
- 프록시 1020개 생성
런타임:
- 매 메서드 호출 = 위 7 단계
- 박승제 의 코드 = 비즈니스만
- Spring 이 모두 자동
@Transactional 동작 원리 전체 개요는?
답:
1. 두 단계:
5 컴포넌트:
시작:
호출:
Spring Boot 시작 시점:
main():
SpringApplication.run(App.class, args);
↓
ApplicationContext 생성
↓
Auto Configuration 실행 (@EnableAutoConfiguration)
↓
TransactionAutoConfiguration 활성화
↓
@EnableTransactionManagement 자동
↓
TransactionInterceptor + AutoProxyCreator 등록
↓
빈 스캔 + 프록시 생성
↓
Application 시작 완료
@EnableTransactionManagement:
Spring Boot 가 자동 활성화 (Auto Configuration):
- 명시 불필요
- spring-boot-starter-data-jpa 의존성으로 자동
내부 동작:
1. TransactionInterceptor 빈 등록
2. InfrastructureAdvisorAutoProxyCreator 빈 등록
3. 트랜잭션 어노테이션 처리 준비
옵션:
- proxyTargetClass (true = CGLIB)
- mode (PROXY / ASPECTJ)
// TransactionAutoConfiguration (Spring Boot 내부)
@AutoConfiguration
@ConditionalOnClass(PlatformTransactionManager.class)
public class TransactionAutoConfiguration {
@Configuration
@ConditionalOnBean(TransactionManager.class)
@ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
public static class EnableTransactionManagementConfiguration {
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
public static class CglibAutoProxyConfiguration { }
}
}
// → Spring Boot 가 자동
// → 우리는 @EnableTransactionManagement 명시 불필요
class PlatformTransactionManager {}
class TransactionManager {}
class AbstractTransactionManagementConfiguration {}
@interface AutoConfiguration {}
@interface ConditionalOnClass { Class<?> value(); }
@interface Configuration {}
@interface ConditionalOnBean { Class<?> value(); }
@interface ConditionalOnMissingBean { Class<?> value(); }
@interface EnableTransactionManagement { boolean proxyTargetClass(); }
빈 생성 흐름:
① @ComponentScan (Spring Boot 자동):
- @Service / @Repository / @Component 스캔
- BeanDefinition 등록
② AbstractApplicationContext.refresh():
- 빈 생성 시작
③ 각 빈 생성:
- new Instance()
- 의존성 주입 (DI)
④ Bean Post Processor 호출:
- postProcessBeforeInitialization()
- postProcessAfterInitialization()
- ← 여기서 프록시 생성!
⑤ 빈 등록 (DI 컨테이너):
- 진짜 빈 또는 프록시
컴포넌트 등록 순서:
1. Infrastructure 빈 (먼저):
- PlatformTransactionManager
- InfrastructureAdvisorAutoProxyCreator
- TransactionInterceptor
2. 일반 빈 (@Service 등):
- 위 인프라가 준비된 후
- 프록시 생성 가능
→ 순서 중요!
ILIC 의 Spring Boot 시작
1. main() 호출
2. ApplicationContext 생성
3. Auto Configuration:
- DataSource (HikariCP)
- EntityManagerFactory
- JpaTransactionManager (PlatformTM)
- TransactionInterceptor
- InfrastructureAdvisorAutoProxyCreator
4. 빈 스캔:
- @Service 102 개 (ShipmentService, CustomerService, ...)
- @Repository
- @Controller
5. 각 @Service 빈 생성 중:
- Bean Post Processor 호출
- @Transactional 감지
- CGLIB 프록시 생성
6. 1020 프록시 등록
7. ApplicationContext 시작 완료
→ 모두 자동
→ application.yml 만으로
Spring Boot 시작 흐름은?
답:
1. main():
Auto Configuration:
빈 스캔:
Bean Post Processor:
Bean Post Processor:
Spring 의 핵심 확장 메커니즘:
- 빈 생성 후 (또는 초기화 전후) 후처리
- "빈을 가로채서 변경 가능"
- 프록시 생성의 기반
인터페이스:
BeanPostProcessor:
- postProcessBeforeInitialization (초기화 전)
- postProcessAfterInitialization (초기화 후, 여기서 프록시!)
package org.springframework.beans.factory.config;
public interface BeanPostProcessor {
// 초기화 전 (@PostConstruct 전)
default Object postProcessBeforeInitialization(
Object bean, String beanName) {
return bean;
}
// 초기화 후 (@PostConstruct 후)
default Object postProcessAfterInitialization(
Object bean, String beanName) {
return bean; // ← 여기서 프록시로 감쌀 수 있음!
}
}
동작 흐름:
① 빈 객체 생성:
ShipmentService bean = new ShipmentService();
② BeanPostProcessor.postProcessBeforeInitialization():
return bean; // 변경 X
③ 초기화 (@PostConstruct):
bean.init();
④ BeanPostProcessor.postProcessAfterInitialization():
// ★ 여기서 프록시 생성!
if (bean 에 @Transactional 메서드 있음) {
return createProxy(bean); // 프록시로 교체
}
return bean;
⑤ DI 컨테이너에 등록 (프록시 또는 진짜)
핵심 Bean Post Processor:
InfrastructureAdvisorAutoProxyCreator:
- extends AbstractAutoProxyCreator
- extends ProxyProcessorSupport
- implements BeanPostProcessor
역할:
- 빈 후처리
- @Transactional 감지
- 프록시 생성
→ @Transactional 의 핵심 엔진
// @EnableTransactionManagement 가 내부적으로 등록
// (사용자 코드 X)
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class TransactionManagementConfiguration {
@Bean
public InfrastructureAdvisorAutoProxyCreator autoProxyCreator() {
return new InfrastructureAdvisorAutoProxyCreator();
}
@Bean
public TransactionInterceptor transactionInterceptor(
PlatformTransactionManager tm) {
return new TransactionInterceptor(tm, ...);
}
@Bean
public BeanFactoryTransactionAttributeSourceAdvisor advisor(
TransactionInterceptor interceptor) {
return new BeanFactoryTransactionAttributeSourceAdvisor(
interceptor, ...);
}
}
// → 3 핵심 빈:
// 1. AutoProxyCreator (BPP)
// 2. Interceptor (가로채기)
// 3. Advisor (Pointcut + Advice 묶음)
class InfrastructureAdvisorAutoProxyCreator {}
class TransactionInterceptor { TransactionInterceptor(PlatformTransactionManager tm, Object... rest) {} }
class PlatformTransactionManager {}
class BeanFactoryTransactionAttributeSourceAdvisor {
BeanFactoryTransactionAttributeSourceAdvisor(TransactionInterceptor i, Object... rest) {}
}
class BeanDefinition { static int ROLE_INFRASTRUCTURE = 2; }
@interface Configuration {}
@interface Role { int value(); }
@interface Bean {}
BPP 의 일반 활용:
Spring 의 다양한 어노테이션:
- @Transactional (TransactionInterceptor)
- @Async (AsyncAnnotationBeanPostProcessor)
- @Cacheable (CacheInterceptor)
- @PreAuthorize (MethodSecurityInterceptor)
모두 BPP 기반:
- 어노테이션 감지
- 프록시 생성
- 가로채기
→ Spring 의 확장 메커니즘
ILIC 의 BPP 동작
ILIC 의 Spring Boot 시작 시:
1. InfrastructureAdvisorAutoProxyCreator (BPP) 등록
2. ShipmentService 빈 생성:
ShipmentService bean = new ShipmentService();
3. BPP.postProcessAfterInitialization(bean):
- bean 에 @Transactional 있나?
- YES (process, get, findAll 등)
- createProxy(bean) → CGLIB 프록시
4. 프록시 등록:
- 빈 이름 "shipmentService"
- 진짜가 아니라 프록시!
5. @Autowired 시:
- ShipmentService service = ... // 프록시 주입
- service.process() = 프록시.process() = 트랜잭션 자동
→ 102 @Service × 평균 10 메서드 = 1020 프록시
Bean Post Processor 의 역할은?
답:
1. BPP:
두 메서드:
AutoProxyCreator:
Spring 확장:
InfrastructureAdvisorAutoProxyCreator:
AbstractAutoProxyCreator 의 자손:
- BPP 구현
- Advisor 들을 자동 적용
- "Infrastructure" Advisor 만
역할:
1. 모든 빈 검사
2. Advisor 매칭 (Pointcut)
3. 매칭되면 프록시 생성
Advisor:
AOP 의 핵심:
- Advisor = Pointcut + Advice
- Pointcut: 어디에 적용?
- Advice: 무엇을 할까?
TransactionAdvisor:
- Pointcut: @Transactional 있는 메서드
- Advice: TransactionInterceptor
→ "@Transactional 메서드를 TransactionInterceptor 가 가로채라"
매칭 알고리즘:
AutoProxyCreator 의 postProcessAfterInitialization():
for (each bean) {
for (each Advisor) {
if (Advisor.matches(bean)) {
// 프록시 생성
proxy = createProxy(bean, advisors);
return proxy;
}
}
return bean; // 매칭 X = 진짜 빈
}
→ @Transactional 있는 메서드 = 매칭 = 프록시
→ 없는 = 진짜 빈
// TransactionAttributeSourcePointcut (간략)
public class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
TransactionAttributeSource tas = getTransactionAttributeSource();
// @Transactional 어노테이션이 있는가?
TransactionAttribute attr = tas.getTransactionAttribute(method, targetClass);
return attr != null;
}
}
// 동작:
// - 모든 메서드 검사
// - @Transactional 있으면 true
// - true 면 → 프록시 적용
class Method {}
class StaticMethodMatcherPointcut { boolean matches(Method m, Class<?> c) { return false; } }
class TransactionAttributeSource { TransactionAttribute getTransactionAttribute(Method m, Class<?> c) { return null; } }
class TransactionAttribute {}
TransactionAttributeSource getTransactionAttributeSource() { return null; }
어노테이션 스캐닝:
Spring 의 검사 위치:
1. 메서드 위 @Transactional
2. 클래스 위 @Transactional (모든 public 메서드)
3. 부모 인터페이스의 @Transactional (proxyTargetClass=false 시)
예:
@Transactional ← 클래스
public class ShipmentService {
public void method1() { } // ← 적용
public void method2() { } // ← 적용
@Transactional(readOnly = true) ← 메서드 (오버라이드)
public void method3() { }
}
어떤 메서드 프록시?:
✅ public 메서드:
- 프록시 적용
❌ private 메서드:
- CGLIB / JDK 동적 모두 X
- 함정 1 (Unit 7.3)
❌ protected 메서드:
- CGLIB 만 가능 (이론)
- 보통 안 함
❌ package-private:
- CGLIB 만 가능 (이론)
- 보통 안 함
❌ final 메서드:
- 상속 X → 오버라이드 X
- 무시
// ILIC 의 어노테이션 감지
@Service
public class ShipmentService {
@Transactional // ← 감지!
public void process(Long id) { } // ✅ 프록시 적용
@Transactional(readOnly = true) // ← 감지!
public Shipment get(Long id) { } // ✅ 프록시 적용
public void publicMethod() { } // ❌ 감지 X (어노테이션 X)
@Transactional
private void privateMethod() { } // ❌ private (함정 1)
@Transactional
public final void finalMethod() { } // ❌ final
}
// Spring 의 동작:
// 1. ShipmentService 빈 생성
// 2. BPP 가 검사:
// - process: @Transactional + public → 적용
// - get: @Transactional + public → 적용
// - publicMethod: 어노테이션 X → 적용 X
// - privateMethod: private → 적용 X (함정!)
// - finalMethod: final → 적용 X
// 3. 위 결과로 프록시 생성:
// - 적용 메서드 = 트랜잭션 자동
// - 적용 X = 트랜잭션 X
class Shipment {}
@interface Service {}
@interface Transactional { boolean readOnly() default false; }
어노테이션 감지 + InfrastructureAdvisorAutoProxyCreator 의 동작은?
답:
1. AutoProxyCreator:
Advisor:
매칭:
public 메서드:
프록시 생성 시점:
Bean Post Processor 의 후처리 중:
BPP.postProcessAfterInitialization() {
if (@Transactional 메서드 있음) {
// ★ 여기서 프록시 생성
return createProxy(bean);
}
return bean;
}
// 프록시 생성 코드 (개념)
private Object createProxy(Object bean, List<Advisor> advisors) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(bean); // 진짜 객체
proxyFactory.addAdvisors(advisors); // Advisor (Pointcut + Interceptor)
proxyFactory.setProxyTargetClass(true); // CGLIB
return proxyFactory.getProxy(); // 프록시 객체
}
class ProxyFactory {
void setTarget(Object o) {}
void addAdvisors(java.util.List<Advisor> a) {}
void setProxyTargetClass(boolean b) {}
Object getProxy() { return null; }
}
class Advisor {}
CGLIB 의 자식 클래스 생성:
원본:
class ShipmentService {
@Transactional
public void process(Long id) {
// 비즈니스
}
}
CGLIB 가 동적 생성:
class ShipmentService$$EnhancerByCGLIB$xyz extends ShipmentService {
private MethodInterceptor[] interceptors;
@Override
public void process(Long id) {
// 가로채기 → TransactionInterceptor 호출
MethodInvocation invocation = new ReflectiveMethodInvocation(
this, // 프록시
target, // 진짜 객체
method, // process Method
new Object[]{id}, // 인자
targetClass, // ShipmentService.class
interceptors // [TransactionInterceptor]
);
invocation.proceed(); // 가로채기 시작
}
// 진짜 메서드도 호출 가능 (super)
// MethodProxy.invokeSuper(this, args)
}
→ 바이트코드 동적 생성
→ 클래스명: ShipmentService$$EnhancerByCGLIB$xyz
프록시 객체의 특징:
타입:
- ShipmentService 의 자식
- instanceof ShipmentService = true
메모리:
- 진짜 객체 + 프록시 객체 = 2개
- 진짜는 프록시 내부에 참조
메서드:
- 모든 public 메서드 오버라이드
- 각 메서드 = 가로채기
identity:
- proxy != target
- proxy.equals(target) = false (보통)
빈 등록:
BPP.postProcessAfterInitialization() 반환값:
- 진짜 빈 또는 프록시
- 반환된 객체가 빈으로 등록
결과:
- "shipmentService" 빈 = 프록시
- 진짜는 프록시 내부
@Autowired:
- 프록시 주입
- 사용자는 모름 (구분 X)
// 디버깅 시 프록시 확인
@Autowired ShipmentService service;
System.out.println(service.getClass().getName());
// 출력 예:
// com.ilic.ShipmentService$$EnhancerByCGLIB$$abc123
// 또는 Spring Boot Actuator
// /actuator/beans
class ShipmentService {}
ShipmentService service;
// ILIC 의 프록시 생성
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
@Transactional
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
}
}
// Spring 시작 시:
// 1. ShipmentService bean = new ShipmentService();
// 2. repo 의존성 주입
// 3. BPP 가 검사 → @Transactional 발견
// 4. CGLIB 프록시 생성:
// - ShipmentService$$EnhancerByCGLIB$$xyz extends ShipmentService
// - process(Long id) 오버라이드
// → TransactionInterceptor 호출
// 5. 빈 "shipmentService" = 프록시
// 6. 진짜 ShipmentService 는 프록시 내부에 참조
// 사용 시:
// @Autowired ShipmentService service; ← 프록시
// service.process(1L); ← 가로채기 시작
class Shipment { void markAsShipped() {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}
프록시 생성 (CGLIB 의 자식 클래스 동적 생성) 은?
답:
1. 시점:
ProxyFactory:
자식 클래스:
클래스명:
TransactionInterceptor:
Spring 트랜잭션의 핵심 클래스:
- MethodInterceptor 구현
- invoke() 메서드가 핵심
- PlatformTransactionManager 와 연동
역할:
1. 메서드 호출 가로채기
2. @Transactional 옵션 읽기
3. PlatformTM 호출
4. 진짜 메서드 호출
5. 결과 처리 (commit / rollback)
// TransactionInterceptor.invoke (개념)
public Object invoke(MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis().getClass();
Method method = invocation.getMethod();
// 1. @Transactional 옵션 읽기
TransactionAttribute txAttr =
getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
// 2. PlatformTransactionManager 결정
PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 3. 트랜잭션 시작
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, ...);
Object retVal;
try {
// 4. 진짜 메서드 호출 (proceed)
retVal = invocation.proceed();
} catch (Throwable ex) {
// 5. 예외 → rollback
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
// 정리
cleanupTransactionInfo(txInfo);
}
// 6. 정상 → commit
commitTransactionAfterReturning(txInfo);
return retVal;
}
class MethodInvocation {
Object getThis() { return null; }
Method getMethod() { return null; }
Object proceed() throws Throwable { return null; }
}
class Method {}
class TransactionAttribute {}
class TransactionAttributeSource { TransactionAttribute getTransactionAttribute(Method m, Class<?> c) { return null; } }
TransactionAttributeSource getTransactionAttributeSource() { return null; }
class PlatformTransactionManager {}
PlatformTransactionManager determineTransactionManager(TransactionAttribute t) { return null; }
class TransactionInfo {}
TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute t, Object... rest) { return null; }
void completeTransactionAfterThrowing(TransactionInfo i, Throwable t) {}
void cleanupTransactionInfo(TransactionInfo i) {}
void commitTransactionAfterReturning(TransactionInfo i) {}
핵심 호출 분석:
invocation.proceed():
- 다음 Advisor 호출 또는
- 진짜 메서드 호출
- CGLIB 의 MethodProxy.invokeSuper(target, args)
createTransactionIfNecessary():
- txAttr 의 propagation 따라
- tm.getTransaction(txDefinition) 호출
- TransactionInfo 반환
completeTransactionAfterThrowing():
- rollbackOn 검사
- tm.rollback() 호출
commitTransactionAfterReturning():
- tm.commit() 호출
// propagation 따른 분기 (간략)
TransactionInfo createTransactionIfNecessary(...) {
// txAttr.getPropagationBehavior()
switch (propagation) {
case REQUIRED:
// 기존 있으면 참여, 없으면 새로
if (기존 트랜잭션) participate();
else newTransaction();
break;
case REQUIRES_NEW:
// 항상 새 트랜잭션 (기존 보류)
suspend(기존);
newTransaction();
break;
case NESTED:
// 기존 있으면 SAVEPOINT
if (기존) createSavepoint();
else newTransaction();
break;
case SUPPORTS:
// 기존 있으면 사용, 없으면 X
break;
case MANDATORY:
// 기존 필수 (없으면 예외)
break;
case NOT_SUPPORTED:
// 기존 보류, 트랜잭션 없이
break;
case NEVER:
// 기존 없어야 (있으면 예외)
break;
}
}
// → 7 가지 propagation
class TransactionInfo {}
void participate() {}
void newTransaction() {}
void suspend(Object o) {}
void createSavepoint() {}
int propagation;
rollback 결정:
기본 (rollback 안 함):
- Checked Exception (Exception 상속)
- 트랜잭션 commit!
기본 (rollback 함):
- RuntimeException + Error
- 또는 그 자손
명시:
@Transactional(rollbackFor = {Exception.class})
- Checked 도 rollback
@Transactional(noRollbackFor = {SomeException.class})
- 특정만 commit
// ILIC 의 TransactionInterceptor 동작
@Transactional(rollbackFor = Exception.class)
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
if (s.getWeight().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("Invalid weight");
// Checked → rollbackFor 명시 → rollback
}
}
// TransactionInterceptor 의 동작:
// 1. @Transactional 옵션 읽기:
// - rollbackFor = Exception.class
// 2. PlatformTransactionManager (JpaTM) 결정
// 3. createTransactionIfNecessary:
// - propagation REQUIRED (기본)
// - JpaTM.getTransaction() 호출
// - EntityManager 생성, tx.begin
// 4. invocation.proceed():
// - 진짜 process() 호출
// 5. BusinessException 발생:
// - completeTransactionAfterThrowing:
// - rollbackOn 검사 → Exception → rollback OK
// - JpaTM.rollback() 호출
// 6. 예외 전파 (throw)
class Shipment {
void markAsShipped() {}
java.math.BigDecimal getWeight() { return null; }
}
class BusinessException extends Exception { BusinessException(String s) {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Transactional { Class<? extends Throwable>[] rollbackFor() default {}; }
TransactionInterceptor 의 동작은?
답:
1. TransactionInterceptor:
invoke:
proceed:
propagation:
전체 흐름 (7 단계):
① 클라이언트 → 프록시 호출
② 프록시 → TransactionInterceptor
③ TransactionInterceptor → PlatformTM.getTransaction()
④ PlatformTM → EntityManager 생성, tx.begin
⑤ 진짜 메서드 호출 (proceed)
⑥ 정상 반환 → PlatformTM.commit
⑦ 예외 → PlatformTM.rollback
단계 1 — 클라이언트 호출:
@Service
public class OrderService {
@Autowired ShipmentService shipmentService; // 프록시!
public void createOrder() {
shipmentService.process(1L); // 프록시 메서드 호출
}
}
- shipmentService 는 진짜가 아니라 프록시
- process() 호출 = 프록시의 process()
- 가로채기 시작
단계 2 — 프록시 → Interceptor:
ShipmentService$$EnhancerByCGLIB$$xyz.process(1L):
// CGLIB 가 생성한 메서드
// 1. MethodInvocation 생성
MethodInvocation invocation = new ReflectiveMethodInvocation(
this, // proxy
realTarget, // 진짜 객체
Method.process, // 메서드 정보
new Object[]{1L}, // 인자
ShipmentService.class, // 타겟 클래스
interceptors // [TransactionInterceptor]
);
// 2. invocation.proceed()
// → 첫 Interceptor.invoke() 호출
// → TransactionInterceptor.invoke(invocation)
return invocation.proceed();
단계 3 — Interceptor → PlatformTM:
TransactionInterceptor.invoke(invocation):
// @Transactional 옵션 읽기
TransactionAttribute txAttr = ... (@Transactional 의 옵션);
// PlatformTransactionManager 결정
PlatformTransactionManager tm = (JpaTransactionManager);
// 트랜잭션 시작
TransactionStatus status = tm.getTransaction(txAttr);
// ↓ 다음 단계 (PlatformTM 내부)
단계 4 — PlatformTM → EntityManager 생성:
JpaTransactionManager.doBegin(transaction, definition):
// 1. EntityManager 생성
EntityManager em = entityManagerFactory.createEntityManager();
// 2. tx.begin() 호출
em.getTransaction().begin();
// 3. ThreadLocal 에 바인딩
TransactionSynchronizationManager.bindResource(
entityManagerFactory,
new EntityManagerHolder(em)
);
// (옵션: DataSource 도 바인딩 → JdbcTemplate 혼용 가능)
// 4. 트랜잭션 시작 완료
단계 5 — 진짜 메서드 호출:
TransactionInterceptor.invoke() 안:
// invocation.proceed() 호출
Object result = invocation.proceed();
// 내부적으로:
// - 다른 Advisor 없으면
// - MethodProxy.invokeSuper(target, args)
// - CGLIB 가 진짜 메서드 호출
// 진짜 ShipmentService.process(1L):
// Shipment s = repo.findById(1L).orElseThrow();
// s.markAsShipped();
// // Dirty Checking 표시
// 반환
단계 6 — 정상 반환 (commit):
TransactionInterceptor:
// 진짜 메서드 정상 반환
commitTransactionAfterReturning(txInfo):
tm.commit(status);
↓
JpaTransactionManager.doCommit(status):
EntityManager em = ThreadLocal 에서 가져옴
// 1. Dirty Checking → UPDATE SQL
em.flush();
// 2. 트랜잭션 commit
em.getTransaction().commit();
// 3. EntityManager 정리
em.close();
// 4. ThreadLocal 정리
TransactionSynchronizationManager.unbindResource(emf);
return result; // 반환
단계 7 — 예외 (rollback):
TransactionInterceptor:
// invocation.proceed() 에서 예외
completeTransactionAfterThrowing(txInfo, exception):
// rollback 해야?
if (txAttr.rollbackOn(exception)) {
tm.rollback(status);
↓
JpaTransactionManager.doRollback(status):
EntityManager em = ThreadLocal 에서
// 1. 트랜잭션 rollback
em.getTransaction().rollback();
// 2. EntityManager 정리
em.close();
// 3. ThreadLocal 정리
// 예외 다시 throw
throw exception;
시각화 (전체 흐름):
Client.createOrder():
shipmentService.process(1L)
↓
┌───────────────────────────────────────┐
│ CGLIB 프록시: │
│ ShipmentService$$EnhancerByCGLIB │
│ .process(1L) │
│ ↓ │
│ invocation.proceed() │
└───────────────────────────────────────┘
↓
┌───────────────────────────────────────┐
│ TransactionInterceptor.invoke(): │
│ 1. @Transactional 옵션 읽기 │
│ 2. PlatformTM (JpaTM) 결정 │
│ 3. tm.getTransaction(txAttr) │
└───────────────────────────────────────┘
↓
┌───────────────────────────────────────┐
│ JpaTransactionManager.doBegin(): │
│ - em = emf.createEntityManager() │
│ - em.getTransaction().begin() │
│ - ThreadLocal bind │
└───────────────────────────────────────┘
↓
┌───────────────────────────────────────┐
│ 진짜 ShipmentService.process(1L): │
│ - Shipment s = repo.findById(1L) │
│ - s.markAsShipped() │
│ - // Dirty Checking 표시 │
└───────────────────────────────────────┘
↓
↙ ↘
[정상] [예외]
↓ ↓
doCommit: doRollback:
- em.flush() - tx.rollback()
- tx.commit() - em.close()
- em.close() - throw e
// ILIC 의 흐름
// 클라이언트 코드
@RestController
public class ShipmentController {
@Autowired ShipmentService shipmentService; // ← 프록시
@PostMapping("/shipments/{id}/ship")
public void ship(@PathVariable Long id) {
shipmentService.process(id); // ← 7 단계 흐름 시작
}
}
// 서비스 코드
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
@Transactional
public void process(Long id) {
// 진짜 메서드 (단계 5)
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
// Dirty Checking → 트랜잭션 commit 시 UPDATE
}
}
// 7 단계 전체:
// 1. Controller → service.process(1L) (프록시)
// 2. CGLIB 프록시 → invocation.proceed()
// 3. TransactionInterceptor.invoke()
// 4. JpaTransactionManager.doBegin()
// - EntityManager 생성
// - tx.begin
// 5. 진짜 process() 실행:
// - findById (SQL: SELECT)
// - markAsShipped (메모리 변경)
// 6. 정상 반환 → doCommit:
// - em.flush() → UPDATE SQL
// - tx.commit()
// - em.close()
// 7. (예외 없음, commit 됨)
// → 박승제 의 코드 = @Transactional + 비즈니스 3 줄
// → Spring 의 동작 = 모든 7 단계 자동
class Shipment { void markAsShipped() {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
ShipmentService shipmentService;
class ShipmentService { void process(Long id) {} }
@interface RestController {}
@interface Autowired {}
@interface PostMapping { String value(); }
@interface PathVariable {}
@interface Service {}
@interface Transactional {}
메서드 호출 시 전체 흐름 (7 단계) 은?
답:
1. 7 단계:
핵심:
commit:
rollback:
5주차 응축:
- 프록시 패턴: CGLIB 자식 클래스
- DI: @Autowired (프록시 주입)
- DIP: PlatformTransactionManager 인터페이스
- OCP: 새 Advisor 추가 (코드 변경 X)
- 템플릿+전략:
- 템플릿: TransactionInterceptor.invoke
- 전략: PlatformTransactionManager 구현체
- 데코레이터: 프록시가 부가 기능
→ 5주차 디자인 패턴 완벽 응축
6주차 응축:
- DataSource: HikariCP (Connection)
- ACID: 트랜잭션 의 원칙
- JdbcTemplate: 자원 자동 관리
- JDBC: Connection 사용
@Transactional 의 활용:
- DataSource 가 Connection 제공
- tx.commit / rollback = ACID
- Connection 자동 관리
→ 6주차 인프라 활용
7주차 응축:
- JPA: EntityManager, Dirty Checking
- Spring Data JPA: Repository
- Querydsl: 동적 쿼리
- PlatformTransactionManager: 추상화
- @Transactional: 자동화
7주차 Part B 의 정점:
- Phase 5 (수동의 한계)
- Phase 6 (PlatformTM 추상화)
- Phase 7 (@Transactional 자동화)
- = @Transactional 동작 원리
→ 7주차 모든 학습 응축
전체 응축:
@Transactional 의 1 줄 안에:
[5주차]
- 프록시 패턴 ✓
- DI / DIP / OCP ✓
- 템플릿+전략 ✓
- 데코레이터 ✓
- AOP ✓
[6주차]
- DataSource ✓
- ACID ✓
- HikariCP ✓
- JDBC ✓
[7주차]
- JPA / EntityManager ✓
- PlatformTransactionManager ✓
- 추상화 ✓
- 자동화 ✓
→ 자바 백엔드의 결정체
→ 박승제 의 ILIC 코드 1020 메서드
→ 모두 이 응축 활용
학습 가치:
1. 면접 정점:
- "@Transactional 어떻게 동작?"
- 위 흐름 설명
- 시니어 시험 통과
2. 실무 디버깅:
- 함정 회피 (Unit 7.3)
- 트러블슈팅
- 성능 분석
3. 깊은 이해:
- 5+6+7주차 정수
- 자바 진영 응축
4. 코드 리뷰:
- 의도 파악
- 효율적 패턴
박승제 의 학습 의의
ILIC 의 1020 메서드:
- 매일 사용
- @Transactional 1 줄
- 7 단계 흐름
학습 후:
- 동작 원리 완전 이해
- 디버깅 능력
- 함정 회피
- 면접 정점
박승제 의 자산:
- 5+6+7주차 응축 마스터
- 자바 백엔드 시니어
- 면접 / 운영 / 코드 리뷰
ILIC 의 매일
ILIC = 1020 @Transactional 메서드:
- 모두 위 7 단계 흐름
- Spring 의 마법 자동
- 박승제 의 비즈니스 코드만
매일 무수한 호출:
- shipmentService.process()
- customerService.update()
- bookingService.create()
- ...
각자 위 7 단계
동시에 (멀티 쓰레드)
ThreadLocal 격리
운영 안정:
- 함정 회피 (다음 Unit)
- 모니터링
- 트러블슈팅
→ 박승제 의 운영 자산
5+6+7주차 응축의 의미는?
답:
1. 5주차:
6주차:
7주차:
응축:
Phase 7.3 — @Transactional 5가지 함정 ★:
학습 후의 깊이:
- 7.2 동작 원리 이해 →
- 7.3 함정 회피 →
- 운영 안전
면접 단골:
- "@Transactional 의 함정은?"
- 5가지 답변
5가지 함정 (Unit 7.3):
1. private 메서드:
- CGLIB / JDK 모두 X
- 프록시 적용 X
- @Transactional 무시
2. self-invocation:
- 같은 클래스 내 호출
- this.method() = 진짜 객체
- 프록시 우회
3. checked exception:
- 기본 rollback X
- rollbackFor 명시 필요
4. 트랜잭션 전파:
- REQUIRED / REQUIRES_NEW / NESTED 등
- 7가지
5. readOnly:
- 진짜 읽기 전용?
- 최적화 X 케이스
// 함정 1: private 메서드
@Service
public class ShipmentService {
public void process(Long id) {
// 프록시 안 통과 (같은 클래스 내!)
doPrivate(id); // ← 진짜 객체의 메서드 호출
}
@Transactional // ← 무시됨!
private void doPrivate(Long id) {
// 프록시 적용 X
// 트랜잭션 X
}
}
// 함정:
// - @Transactional 보고 안전하다고 생각
// - 실제로는 트랜잭션 X
// - 운영 사고
@interface Service {}
@interface Transactional {}
// 함정 2: self-invocation
@Service
public class ShipmentService {
public void outerMethod() {
this.innerMethod(); // ← 진짜 객체!
// 프록시 우회
}
@Transactional
public void innerMethod() {
// 외부에서 호출 시 OK
// 같은 클래스 내 호출 시 X
}
}
// 외부에서:
service.outerMethod();
// → outerMethod 는 프록시 통과
// → 안에서 this.innerMethod() 호출
// → this = 진짜 객체 (프록시 X)
// → innerMethod 의 @Transactional 무시!
@interface Service {}
@interface Transactional {}
ShipmentService service;
class ShipmentService {
void outerMethod() {}
}
종합 졸업 시험 24문항 (Unit 7.3 마지막):
SQL JOIN (Phase 1): 3 문항
ORM (Phase 2): 6 문항
JPA 매핑 (Phase 3-4): 4 문항
수동 트랜잭션 (Phase 5): 2 문항
PlatformTM (Phase 6): 3 문항
@Transactional (Phase 7): 6 문항
→ 7주차 종합 졸업!
🗂️ Part A (완주)
✅ Phase 1-4 (16)
🔄 Part B — 트랜잭션 추상화의 진화
✅ Phase 5 (2)
✅ Phase 6 (3)
✨ Phase 7 — @Transactional (2/3)
✅ Unit 7.1 프록시 패턴 ★
✅ Unit 7.2 @Transactional 동작 원리 ★★★ ← 여기
⏭ Unit 7.3 5가지 함정 ★ + 종합 졸업 시험 24문항
총: 23/24 Unit (96%)
| Q | 핵심 답변 |
|---|---|
| @Transactional 동작? | 7 단계 흐름 |
| Bean Post Processor? | 프록시 생성 |
| CGLIB 자식 클래스? | 동적 생성 |
| TransactionInterceptor? | MethodInterceptor |
| invoke 메서드? | 6 단계 |
| invocation.proceed? | 진짜 메서드 |
| PlatformTM 호출? | 단계 3-4 |
| EntityManager? | 단계 4 |
| commit 시? | flush + tx.commit |
| 5+6+7 응축? | @Transactional 1줄 |
답:
답:
답:
답:
답:
1. 시작 단계 (Spring Boot 시작 시)
2. 호출 단계 (메서드 호출 시 7 단계)
3. 5+6+7주차 응축의 정점
이번 Unit에서 동작 원리를 봤다면, 다음은 5가지 함정 (마지막 Unit).
✨ Phase 7 — @Transactional (모두 ★ 깊이)
✅ Unit 7.1 프록시 패턴 ★
✅ Unit 7.2 @Transactional 동작 원리 ★★★ ← 여기
⏭ Unit 7.3 5가지 함정 ★ — Phase 7 완주 + 7주차 완주
🗂️ Part A (완주)
✅ Phase 1-4 (16)
🔄 Part B
✅ Phase 5 (2)
✅ Phase 6 (3)
✨ Phase 7 (2/3)
총: 23/24 Unit (96%)
★★★ 깊이 파기 — @Transactional 동작 원리, 7주차의 정점