F-LAB JAVA · 7주차 · Phase 7 · @Transactional
★ 깊이 파기 — 7주차 마지막 Unit, Phase 7 완주 + Part B 완주 + 7주차 완주
+ 종합 졸업 시험 24문항
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
@Transactional 의 5가지 함정 — (1) private 메서드 (CGLIB/JDK 동적 모두 가로채기 X) (2) self-invocation (같은 클래스 내 this 호출은 프록시 우회) (3) checked exception (기본 rollback 안 함, rollbackFor 명시 필요) (4) 트랜잭션 전파 7가지 (REQUIRED/REQUIRES_NEW/NESTED 등) (5) readOnly 의 실제 최적화 효과 — 모두 Unit 7.2 의 프록시 동작 원리에서 자연스럽게 파생되는 함정이며 ILIC 같은 실무 시스템의 운영 사고를 방지하는 핵심 지식이다.
@Transactional 의 5가지 함정 은 Unit 7.2 의 동작 원리에서 자연스럽게 파생되며 — 운영 사고와 직결되는 실무 핵심 지식이다.
함정 1 — private 메서드 — CGLIB 의 자식 클래스는 private 메서드를 오버라이드 X, JDK 동적 프록시는 인터페이스의 public 만 가로채기 → @Transactional 무시 → 트랜잭션 X → 운영 사고.
함정 2 — self-invocation — 같은 클래스 안에서this.method()호출 시 this 는 진짜 객체 (프록시 X) → 프록시 우회 → @Transactional 무시 → 외부 호출은 OK 지만 내부 호출은 X → 가장 흔한 함정.
함정 3 — checked exception — 기본 rollback 정책은 RuntimeException / Error 만 rollback, Checked Exception 은 commit → 비즈니스 예외가 Checked 면 데이터 불일치 →rollbackFor = Exception.class명시 필요.
함정 4 — 트랜잭션 전파 7가지 — REQUIRED (기본, 참여) / REQUIRES_NEW (항상 새로) / NESTED (SAVEPOINT) / SUPPORTS / MANDATORY / NOT_SUPPORTED / NEVER — 의미 정확히 알아야 메서드 호출 체인의 트랜잭션 동작 예측 가능.
함정 5 — readOnly —@Transactional(readOnly = true)가 진짜 읽기 전용? — JPA 의 경우 Dirty Checking 비활성화 (성능 최적화) + Hibernate 의 flush mode = MANUAL + DB 수준 강제는 X (코드로 UPDATE 가능하지만 권장 X) — 의도된 사용.
ILIC 의 1020 메서드 운영에서 — 이 5 함정 회피 = 운영 안정성, 면접에서 자주 묻는 시니어 시험.
5가지 함정 = 자율주행의 5가지 함정:
[1] 비밀 통로 (private 메서드):
- 자율주행 모듈 (프록시) 못 들어옴
- 운전자 직접 운전 (트랜잭션 X)
- 사고 위험
[2] 차고 안 운전 (self-invocation):
- 차고 (같은 클래스) 안에선
- 자율주행 비활성
- "왜 트랜잭션 안 되지?"
[3] 약한 사고 (checked exception):
- 자율주행: "RuntimeException 만 비상 정지"
- Checked = "괜찮은 사고" 로 인식
- rollback X
[4] 7 가지 운전 모드 (propagation):
- REQUIRED (기본 모드)
- REQUIRES_NEW (긴급 모드)
- NESTED (서브 운전)
- ...
- 정확히 알아야 정확한 동작
[5] 읽기 전용 (readOnly):
- "관광 모드" (구경만)
- 실제로는 UPDATE 가능 (운전자 의지)
- 최적화 (Dirty Checking X)
5 함정 모두 = Unit 7.2 의 동작 원리에서 파생:
- 프록시 = 1, 2 함정
- 예외 처리 = 3 함정
- 트랜잭션 정책 = 4, 5 함정
ILIC 의 운영:
- 5 함정 회피 = 안전
- 1020 메서드 검토 시
- 코드 리뷰 핵심
면접:
- 5 함정 = 시니어 시험
- 자주 묻는 주제
→ 5 함정, 프록시 원리에서 파생, 운영 안전, 면접 시니어.
1. 5가지 함정 개요
2. 함정 1 — private 메서드
3. 함정 2 — self-invocation
4. 함정 3 — checked exception
5. 함정 4 — 트랜잭션 전파 (7가지)
6. 함정 5 — readOnly
7. 종합 졸업 시험 24문항
8. 7주차 완주 정리
9. 8주차 예고
| 함정 | 원인 | 결과 | 해결 |
|---|---|---|---|
| 1. private 메서드 | 프록시 가로채기 X | @Transactional 무시 | public 변경 / 별도 클래스 |
| 2. self-invocation | this = 진짜 객체 | 프록시 우회 | 별도 클래스 / 자기 주입 |
| 3. checked exception | 기본 rollback X | 데이터 불일치 | rollbackFor 명시 |
| 4. propagation 오해 | 7 종류 정책 | 의도 다른 동작 | 정확한 이해 |
| 5. readOnly | 최적화 vs 강제 X | 잘못된 이해 | 의도 이해 |
모두 Unit 7.2 에서 파생:
함정 1, 2 (프록시 관련):
- CGLIB 자식 클래스 한계
- this = 진짜 객체
- Unit 7.2 의 프록시 동작 원리에서
함정 3 (예외):
- TransactionInterceptor 의 rollbackOn
- Unit 7.2 의 호출 흐름 단계 7
함정 4 (전파):
- createTransactionIfNecessary
- Unit 7.2 의 propagation 처리
함정 5 (readOnly):
- PlatformTransactionManager 의 최적화
- Unit 7.2 의 TransactionDefinition
→ Unit 7.2 이해 = 함정 자연 회피
운영 위험 비중:
함정 1 (private):
- 발견 쉬움 (IDE 경고)
- 운영 사고 적음
함정 2 (self-invocation):
- 가장 흔한 사고
- 발견 어려움
- 가장 위험
함정 3 (checked):
- Checked 거의 안 씀 (현대 자바)
- 가끔 위험
함정 4 (propagation):
- 잘못된 동작
- 자주 발생
- 디버깅 어려움
함정 5 (readOnly):
- 최적화 의도
- 사고 적음 (잘못 사용은 가능)
박승제의 가치:
ILIC 의 1020 메서드:
- 모두 @Transactional
- 모두 함정 가능
Unit 7.3 학습 후:
- 5 함정 회피
- 코드 리뷰 효율
- 운영 안정
- 면접 우위
시니어 개발자:
- 동작 원리 (7.2)
- 함정 회피 (7.3)
- 둘 다 마스터
5가지 함정 개요는?
답:
1. 5가지:
파생:
위험:
회피:
함정 1 — private 메서드:
@Transactional + private 메서드:
- CGLIB / JDK 동적 모두 가로채기 X
- @Transactional 무시
- 트랜잭션 X
원인:
- CGLIB: 자식 클래스 (private 못 봄)
- JDK 동적: 인터페이스 (private X)
// ❌ 함정
@Service
public class ShipmentService {
@Transactional
private void processInternal(Long id) { // ← private!
// 트랜잭션 적용 X
// 프록시 가로채기 X
}
}
// 외부 호출:
@Autowired ShipmentService service;
service.processInternal(1L); // ← 어차피 private 라 호출 불가
// 내부 호출:
public void publicMethod() {
processInternal(id); // 호출 가능, 하지만 트랜잭션 X
}
@interface Service {}
@interface Transactional {}
@interface Autowired {}
ShipmentService service;
class ShipmentService {
void processInternal(Long id) {}
public void publicMethod() {}
}
Long id;
왜 동작 X?:
CGLIB 의 자식 클래스:
class ShipmentService$$EnhancerByCGLIB extends ShipmentService {
@Override
public void publicMethod() {
// 가로채기 OK
}
// private 메서드는 오버라이드 X!
// (private 메서드는 상속 X)
// → 가로채기 불가
}
JDK 동적 프록시:
- 인터페이스의 public 메서드만
- private 메서드는 인터페이스에 X
→ 둘 다 private 못 봄
IDE 경고:
IntelliJ + Spring:
- @Transactional + private = 경고
- "Transactional method called by..."
- 코드 작성 시 발견
하지만:
- 경고 무시 가능
- 실수 위험
// 해결 1: public 변경
@Service
public class ShipmentService {
@Transactional
public void processInternal(Long id) { // private → public
// 트랜잭션 OK
}
}
// 해결 2: 별도 클래스로 분리
@Service
public class ShipmentService {
@Autowired ShipmentInternalService internal;
public void publicMethod(Long id) {
internal.process(id); // 다른 빈 (프록시 통과)
}
}
@Service
public class ShipmentInternalService {
@Transactional
public void process(Long id) { // public + 다른 클래스
// 트랜잭션 OK
}
}
// 해결 3: AspectJ 모드
// @EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
// 바이트코드 위빙 (private 가로채기 가능)
// 복잡 / 거의 안 씀
@interface Service {}
@interface Transactional {}
@interface Autowired {}
class ShipmentInternalService { void process(Long id) {} }
ShipmentInternalService internal;
class ShipmentService {}
Long id;
ILIC 의 함정 1 회피
ILIC 의 정책:
- 모든 @Transactional 메서드 = public
- IDE 경고 활용
- 코드 리뷰 시 확인
필요 시:
- 별도 Service 분리
- 책임 분리 (SoC)
→ 함정 1 거의 발생 X
함정 1 — private 메서드 왜 X?
답:
1. CGLIB:
JDK 동적:
결과:
해결:
함정 2 — self-invocation (자기 자신 호출):
같은 클래스 내 메서드 호출:
public void outer() {
this.inner(); // ← 진짜 객체!
// 프록시 우회
}
@Transactional
public void inner() {
// 외부에서 호출 시 OK
// 같은 클래스 내 호출 시 X
}
→ 가장 흔한 함정
→ 운영 사고 빈도 높음
왜 X?:
Spring 의 동작:
- 빈은 프록시
- 외부에서 service.method() = 프록시 호출 ✓
같은 클래스 내:
public void outer() {
// this = 진짜 객체 (프록시 X)
// CGLIB 의 자식 클래스에서 super 호출 시
// 또는 직접 메서드 호출 시
this.inner(); // → 진짜 객체.inner()
// 프록시 X
}
→ this = 진짜 객체 (영원히)
→ 프록시 우회
시각화:
외부 호출:
Client → 프록시.outer() ✓
↓
트랜잭션 시작 (프록시)
↓
진짜.outer() (proceed)
↓
this.inner() ← 진짜.inner() (프록시 X)
↓
@Transactional 무시!
트랜잭션 commit (outer 의 것만)
결과:
- outer 는 트랜잭션 ✓
- inner 의 @Transactional 무시
- inner 가 propagation = REQUIRES_NEW 라도 X
// ❌ 함정 사례 (가장 흔한 시나리오)
@Service
public class OrderService {
public void createOrderAndLog(Long id) {
// 비즈니스 로직
Order order = createOrder(id);
// 로그는 별도 트랜잭션 (의도)
this.logAction(order); // ← self-invocation!
// 의도: REQUIRES_NEW (새 트랜잭션)
// 실제: 프록시 우회 → 같은 트랜잭션 / 또는 트랜잭션 X
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(Order order) {
// 별도 트랜잭션 의도
// 하지만 self-invocation 으로 우회
// → 의도 동작 X
}
}
// 외부 호출:
orderService.createOrderAndLog(1L);
// 1. 프록시.createOrderAndLog (트랜잭션 시작)
// 2. 진짜.createOrderAndLog 실행
// 3. this.logAction(order) ← 진짜.logAction
// 4. logAction 의 @Transactional 무시 (프록시 우회)
// 5. 같은 트랜잭션 안에서 실행
class Order {}
@interface Service {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
Order createOrder(Long id) { return null; }
OrderService orderService;
class OrderService { void createOrderAndLog(Long id) {} }
// 해결 1: 별도 클래스 (권장)
@Service
public class OrderService {
@Autowired LogService logService; // 별도 빈
public void createOrderAndLog(Long id) {
Order order = createOrder(id);
logService.logAction(order); // ← 다른 빈 (프록시)
// 정상 동작 (REQUIRES_NEW)
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(Order order) {
// 별도 트랜잭션 OK
}
}
// 장점:
// - 깔끔
// - SoC (책임 분리)
// - 프록시 정상 동작
class Order {}
@interface Service {}
@interface Autowired {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class LogService { void logAction(Order o) {} }
LogService logService;
class OrderService {}
Order createOrder(Long id) { return null; }
// 해결 2: 자기 자신 주입 (덜 깔끔)
@Service
public class OrderService {
@Autowired @Lazy OrderService self; // 자기 자신 (프록시)
public void createOrderAndLog(Long id) {
Order order = createOrder(id);
self.logAction(order); // ← 프록시 호출!
// 정상 동작
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(Order order) {
// 별도 트랜잭션
}
}
// 장점:
// - 같은 클래스 유지
// 단점:
// - @Lazy 필요 (순환 의존성 방지)
// - 약간 어색
class Order {}
@interface Service {}
@interface Autowired {}
@interface Lazy {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
OrderService self;
class OrderService { void createOrderAndLog(Long id) {} }
Order createOrder(Long id) { return null; }
// 해결 3: ApplicationContext 로 자기 자신 (가능하지만 권장 X)
@Service
public class OrderService {
@Autowired ApplicationContext ctx;
public void createOrderAndLog(Long id) {
Order order = createOrder(id);
OrderService self = ctx.getBean(OrderService.class); // 프록시
self.logAction(order);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(Order order) {
// 별도 트랜잭션
}
}
// 단점:
// - ApplicationContext 의존
// - 테스트 어려움
class Order {}
class ApplicationContext { <T> T getBean(Class<T> c) { return null; } }
@interface Service {}
@interface Autowired {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class OrderService { void createOrderAndLog(Long id) {} }
Order createOrder(Long id) { return null; }
ApplicationContext ctx;
// ILIC 의 함정 2 회피
// ❌ 잘못된 패턴 (가정)
@Service
public class ShipmentService {
public void processWithLog(Long id) {
process(id);
this.logShipment(id); // ← self-invocation!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logShipment(Long id) { ... }
}
// ✅ 권장 패턴
@Service
public class ShipmentService {
@Autowired ShipmentLogService logService;
@Transactional
public void processWithLog(Long id) {
process(id);
logService.logShipment(id); // 별도 빈 (프록시)
}
}
@Service
public class ShipmentLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logShipment(Long id) {
// 별도 트랜잭션 OK
}
}
// ILIC 의 1020 메서드:
// - 함정 2 빈번 발생 가능
// - 별도 Service 분리 표준
// - 코드 리뷰 핵심
@interface Service {}
@interface Autowired {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class ShipmentLogService { void logShipment(Long id) {} }
ShipmentLogService logService;
class ShipmentService { void process(Long id) {} }
함정 2 — self-invocation 왜 X?
답:
1. this:
결과:
빈번:
해결:
함정 3 — checked exception:
Spring 의 기본 정책:
- RuntimeException → rollback
- Error → rollback
- Checked Exception → commit (!!)
의도와 다름:
- "예외 = rollback" 일반 인식
- 실제: Checked 는 commit
→ 데이터 불일치 위험
왜 이렇게?:
EJB 시대의 관례:
- Checked = 비즈니스 예외 (정상)
- RuntimeException = 시스템 예외 (비정상)
Spring 이 계승:
- Checked = commit (비즈니스 처리)
- RuntimeException = rollback (시스템 오류)
하지만:
- 현대 자바 = Checked 거의 안 씀
- 의도와 다름
// ❌ 함정
@Service
public class OrderService {
@Transactional
public void createOrder(Long customerId) throws BusinessException {
// 비즈니스 로직
Customer c = customerRepo.findById(customerId).orElseThrow();
if (!c.canOrder()) {
throw new BusinessException("주문 불가");
// Checked → commit (rollback X!)
// 위 비즈니스 로직은 DB 에 반영!
}
// 정상 처리
}
}
// 결과:
// - BusinessException 발생
// - 하지만 commit
// - 일부 데이터 영속 (이상한 상태)
class Customer { boolean canOrder() { return false; } }
class BusinessException extends Exception { BusinessException(String s) {} }
CustomerRepository customerRepo;
interface CustomerRepository { java.util.Optional<Customer> findById(Long id); }
@interface Service {}
@interface Transactional {}
// 해결: rollbackFor 명시
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class) // ← 모든 예외 rollback
public void createOrder(Long customerId) throws BusinessException {
Customer c = customerRepo.findById(customerId).orElseThrow();
if (!c.canOrder()) {
throw new BusinessException("주문 불가");
// rollbackFor = Exception → rollback ✓
}
}
}
// 옵션:
@Transactional(rollbackFor = {BusinessException.class, SqlException.class})
// 또는
@Transactional(noRollbackFor = {SomeOtherException.class})
// (특정 예외는 commit 유지)
class Customer { boolean canOrder() { return false; } }
class BusinessException extends Exception {}
class SqlException extends Exception {}
class SomeOtherException extends Exception {}
@interface Service {}
@interface Transactional {
Class<? extends Throwable>[] rollbackFor() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
}
CustomerRepository customerRepo;
interface CustomerRepository { java.util.Optional<Customer> findById(Long id); }
// 권장: RuntimeException 사용
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
@Service
public class OrderService {
@Transactional // rollbackFor 불필요 (RuntimeException → rollback)
public void createOrder(Long customerId) {
Customer c = customerRepo.findById(customerId).orElseThrow();
if (!c.canOrder()) {
throw new BusinessException("주문 불가"); // RuntimeException → rollback
}
}
}
// 장점:
// - throws 선언 불필요 (call site 깔끔)
// - 기본 rollback (의도와 일치)
// - 현대 자바 표준
class Customer { boolean canOrder() { return false; } }
@interface Service {}
@interface Transactional {}
CustomerRepository customerRepo;
interface CustomerRepository { java.util.Optional<Customer> findById(Long id); }
// ILIC 의 표준 패턴
// ILIC 의 비즈니스 예외 = RuntimeException (권장)
public class ShipmentException extends RuntimeException {
public ShipmentException(String message) {
super(message);
}
}
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
@Transactional // 기본 (RuntimeException → rollback)
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
if (!s.canShip()) {
throw new ShipmentException("선적 불가");
// RuntimeException → rollback ✓
}
s.markAsShipped();
}
}
// ILIC 의 정책:
// - 비즈니스 예외 = RuntimeException
// - throws 선언 불필요
// - 기본 rollback
// - rollbackFor 거의 명시 X
// 만약 외부 라이브러리의 Checked Exception 처리:
// - try/catch 로 변환
// - 또는 rollbackFor 명시
class Shipment {
boolean canShip() { return false; }
void markAsShipped() {}
}
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional {}
함정 3 — checked exception 왜 rollback X?
답:
1. 기본:
Checked:
EJB 관례:
해결:
| Propagation | 설명 |
|---|---|
| REQUIRED (기본) | 기존 트랜잭션 참여, 없으면 새로 |
| REQUIRES_NEW | 항상 새 트랜잭션 (기존 보류) |
| NESTED | 기존 안에 SAVEPOINT (부분 rollback) |
| SUPPORTS | 기존 있으면 참여, 없으면 X |
| MANDATORY | 기존 필수 (없으면 예외) |
| NOT_SUPPORTED | 기존 보류, 트랜잭션 없이 |
| NEVER | 기존 없어야 (있으면 예외) |
@Service
public class OrderService {
@Autowired LogService logService;
@Transactional // REQUIRED (기본)
public void createOrder() {
// 트랜잭션 T1 시작
logService.log(); // REQUIRED 면 T1 에 참여
// 같은 트랜잭션
}
}
@Service
public class LogService {
@Transactional // REQUIRED (기본)
public void log() {
// T1 에 참여 (새 트랜잭션 X)
}
}
// 결과:
// - OrderService 와 LogService 가 같은 트랜잭션
// - 하나 실패 → 모두 rollback
@interface Service {}
@interface Autowired {}
@interface Transactional {}
class LogService { void log() {} }
LogService logService;
@Service
public class OrderService {
@Transactional // T1
public void createOrder() {
Order o = saveOrder(); // T1
try {
logService.log(o); // T2 (REQUIRES_NEW)
// T1 보류, T2 시작
// T2 commit
// T1 재개
} catch (Exception e) {
// T2 rollback
// T1 은 계속
log.warn("로그 실패");
}
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(Order o) {
// 별도 트랜잭션 (T1 과 독립)
}
}
// 효과:
// - 로그가 실패해도 메인 트랜잭션 계속
// - 로그 성공 → T2 commit (즉시 DB 반영)
class Order {}
Order saveOrder() { return null; }
@interface Service {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class LogService { void log(Order o) {} }
LogService logService;
org.slf4j.Logger log;
@Service
public class OrderService {
@Transactional // T1
public void createOrder() {
Order o = saveOrder(); // T1
try {
stockService.deduct(); // T1 안의 SAVEPOINT
// SAVEPOINT 생성
// 변경 사항이 SAVEPOINT 이후 기록
} catch (Exception e) {
// SAVEPOINT 까지만 rollback
// T1 은 계속
log.warn("재고 차감 실패");
}
// T1 commit (메인 데이터)
}
}
@Service
public class StockService {
@Transactional(propagation = Propagation.NESTED)
public void deduct() {
// T1 안의 부분 트랜잭션 (SAVEPOINT)
}
}
// 차이 (REQUIRES_NEW vs NESTED):
// - REQUIRES_NEW: 별도 트랜잭션, 즉시 commit 가능
// - NESTED: T1 안, T1 의 commit/rollback 따름
class Order {}
Order saveOrder() { return null; }
@interface Service {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, NESTED }
class StockService { void deduct() {} }
StockService stockService;
org.slf4j.Logger log;
SUPPORTS:
- 기존 있으면 참여
- 없으면 트랜잭션 없이 실행
- 거의 안 씀
MANDATORY:
- 기존 트랜잭션 필수
- 없으면 예외
- 호출자가 트랜잭션 보장 시
NOT_SUPPORTED:
- 기존 보류 (suspend)
- 트랜잭션 없이 실행
- 외부 API 호출 시 (commit 불필요)
NEVER:
- 기존 없어야 함
- 있으면 예외
- 트랜잭션 없는 작업 강제
// 함정 시나리오: REQUIRES_NEW + self-invocation
@Service
public class OrderService {
@Transactional
public void createOrder() {
this.logAction(); // ← self-invocation!
// REQUIRES_NEW 의도
// 실제: 프록시 우회, 같은 트랜잭션
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction() {
// 의도 동작 X (함정 2 + 함정 4)
}
}
// 결과:
// - REQUIRES_NEW 동작 X
// - 같은 트랜잭션 안에서 실행
// - 의도와 다름
@interface Service {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class OrderService { void createOrder() {} }
// ILIC 의 propagation 활용
// 메인 비즈니스 (REQUIRED)
@Service
public class ShipmentService {
@Autowired AuditLogService auditService;
@Transactional // REQUIRED (기본)
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
// 감사 로그 (REQUIRES_NEW: 메인 실패해도 로그는 남음)
try {
auditService.logAction(id, "SHIPPED");
} catch (Exception e) {
log.warn("Audit failed", e);
// 메인 트랜잭션은 계속
}
}
}
@Service
public class AuditLogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(Long id, String action) {
// 별도 트랜잭션
// 메인이 rollback 해도 로그는 commit
}
}
// ILIC 의 패턴:
// - 메인 = REQUIRED
// - 로그 / 감사 = REQUIRES_NEW
// - 별도 클래스 분리 (함정 2 회피)
class Shipment { void markAsShipped() {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Autowired {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, REQUIRES_NEW }
class AuditLogService { void logAction(Long id, String s) {} }
AuditLogService auditService;
org.slf4j.Logger log;
함정 4 — 트랜잭션 전파 (7가지) 는?
답:
1. 7가지:
REQUIRED:
REQUIRES_NEW:
NESTED:
함정 5 — readOnly:
@Transactional(readOnly = true):
- 진짜 읽기 전용?
- DB 가 강제? X
- 최적화 효과?
실제:
- JPA: Dirty Checking 비활성화
- Hibernate: FlushMode = MANUAL
- 성능 최적화
- 코드로 UPDATE 가능 (강제 X)
실제 효과:
JPA / Hibernate:
1. Dirty Checking 스킵
- 영속성 컨텍스트 비교 X
- 메모리 / CPU 절약
2. FlushMode = MANUAL
- flush 자동 X
- 명시적 flush 만
3. PlatformTransactionManager 의 hint
- 일부 DB 에서 최적화
하지만:
- DB 강제 X
- 코드로 UPDATE 가능
- 의도 위반
// ✅ 올바른 사용
@Service
public class ShipmentService {
@Autowired ShipmentRepository repo;
@Transactional(readOnly = true)
public List<Shipment> findAll() {
return repo.findAll();
// 단순 조회
// Dirty Checking 비활성 (최적화)
}
@Transactional(readOnly = true)
public Shipment findById(Long id) {
return repo.findById(id).orElseThrow();
}
}
// 효과:
// - 메모리 절약
// - CPU 절약
// - 일부 DB 최적화
class Shipment {}
ShipmentRepository repo;
interface ShipmentRepository {
java.util.List<Shipment> findAll();
java.util.Optional<Shipment> findById(Long id);
}
@interface Service {}
@interface Autowired {}
@interface Transactional { boolean readOnly() default false; }
// ❌ 잘못된 사용
@Service
public class ShipmentService {
@Transactional(readOnly = true) // 읽기 전용
public void updateStatus(Long id, String status) {
Shipment s = repo.findById(id).orElseThrow();
s.setStatus(status); // 변경!
// Dirty Checking 비활성 → UPDATE 안 됨!
// 또는 명시적 save → UPDATE 됨 (의도 위반)
}
}
// 함정:
// - readOnly = true 인데 변경 코드
// - 동작 미묘 (Dirty Checking X)
// - 의도와 다름
class Shipment { void setStatus(String s) {} }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
@interface Service {}
@interface Transactional { boolean readOnly() default false; }
// ILIC 의 표준 패턴
// 조회 메서드:
@Transactional(readOnly = true)
public List<Shipment> findAll() {
return repo.findAll();
}
@Transactional(readOnly = true)
public Page<Shipment> search(Pageable p) {
return repo.search(p);
}
// 쓰기 메서드:
@Transactional // readOnly 명시 X (기본 = false)
public void process(Long id) {
Shipment s = repo.findById(id).orElseThrow();
s.markAsShipped();
// Dirty Checking 활성 → UPDATE 자동
}
// 규칙:
// - 조회만 = readOnly = true
// - 변경 = readOnly = false (기본)
// - 혼합 시 = false
class Shipment { void markAsShipped() {} }
class Page<T> {}
class Pageable {}
ShipmentRepository repo;
interface ShipmentRepository {
java.util.List<Shipment> findAll();
Page<Shipment> search(Pageable p);
java.util.Optional<Shipment> findById(Long id);
}
@interface Transactional { boolean readOnly() default false; }
함정 5 — readOnly 의 진짜 의미는?
답:
1. 효과:
강제 X:
최적화:
권장:
문제 1: INNER JOIN 과 LEFT JOIN 의 차이는?
답: INNER JOIN 은 양쪽 테이블에 일치하는 행만 (교집합), LEFT JOIN 은 왼쪽 테이블의 모든 행 + 오른쪽의 일치하는 행 (없으면 NULL).
문제 2: FULL OUTER JOIN 은 MySQL 에서?
답: MySQL 미지원. UNION 으로 LEFT JOIN + RIGHT JOIN 합쳐 시뮬레이션.
문제 3: JOIN 선택의 기준은?
답: (1) 어떤 데이터 필요? — 교집합 / 한쪽 전체 / 양쪽 전체, (2) NULL 허용?, (3) 성능 영향.
문제 4: 객체-관계 미스매치 5가지는?
답: (1) 입도 (객체 < 행), (2) 상속 (객체 vs 테이블 정규화), (3) 식별성 (==/equals vs PK), (4) 연관 (참조 vs FK), (5) 객체 그래프 탐색.
문제 5: ORM 의 정의?
답: Object-Relational Mapping. 객체와 관계형 DB 의 자동 매핑 기술.
문제 6: SQL Mapper (JdbcTemplate/MyBatis) 의 한계?
답: 매핑 수동, 객체 그래프 어려움, 변경 시 다중 SQL 수정, 미스매치 해결 X.
문제 7: JPA 의 위치는?
답: 애플리케이션 ↔ JDBC 사이 중간 계층. 4 단계: App → JPA → JDBC → DB Driver.
문제 8: Spring Data JPA 의 핵심?
답: Repository 인터페이스 자동 구현 + 메서드 이름 쿼리 + Pageable.
문제 9: Querydsl 의 가치?
답: 타입 안전 동적 쿼리. 컴파일 시점 검증. IDE 자동완성.
문제 10: @Entity 의 3가지 조건?
답: (1) @Entity 어노테이션, (2) 기본 생성자 필수 (JPA 가 리플렉션으로 객체 생성), (3) final 클래스 / 메서드 X (CGLIB 프록시 생성 위해).
문제 11: IDENTITY 의 단점?
답: persist 즉시 INSERT → JPA 의 쓰기 지연 (배치 INSERT) 불가능. SEQUENCE 는 allocationSize 로 미리 받아 배치 가능.
문제 12: @Enumerated 의 권장 옵션?
답: EnumType.STRING. ORDINAL 은 enum 순서 변경 시 데이터 오염 위험.
문제 13: Spring Boot 의 자동 명명?
답: SpringPhysicalNamingStrategy. camelCase → snake_case (blNo → bl_no).
문제 14: 수동 트랜잭션의 3가지 함정?
답: (1) rollback/close 누락 (실수), (2) 트랜잭션 경계 모호 (commit 후 외부 작업), (3) 트랜잭션 전파 어려움 (메서드 호출 시).
문제 15: After Commit 패턴?
답: @TransactionalEventListener(phase = AFTER_COMMIT). commit 성공 후만 외부 호출 (이메일/Slack 등). 일관성 보장.
문제 16: PlatformTransactionManager 의 3 메서드?
답: getTransaction(definition) → TransactionStatus, commit(status), rollback(status).
문제 17: 3가지 구현체?
답: DataSourceTransactionManager (JDBC/JdbcTemplate), HibernateTransactionManager (Hibernate 직접, 드묾), JpaTransactionManager (JPA/Spring Data JPA, 표준).
문제 18: JPA + JdbcTemplate 혼용 시 TM 선택?
답: JpaTransactionManager. EntityManagerFactory + DataSource 모두 인식. 같은 Connection / 단일 트랜잭션 보장.
문제 19: @Transactional 의 동작 원리?
답: 7 단계 — (1) Client → (2) CGLIB 프록시 → (3) TransactionInterceptor.invoke → (4) PlatformTM.getTransaction (EntityManager 생성 + tx.begin) → (5) invocation.proceed() = 진짜 메서드 → (6) 정상 → commit (em.flush + tx.commit + em.close) → (7) 예외 → rollback.
문제 20: Spring Boot 2.x+ 의 프록시?
답: CGLIB 기본 (proxyTargetClass = true). 인터페이스 유무 무관, 일관성. JDK 동적은 spring.aop.proxy-target-class=false 로 변경.
문제 21: @Transactional 의 5가지 함정?
답: (1) private 메서드 (프록시 가로채기 X), (2) self-invocation (this = 진짜 객체, 프록시 우회), (3) checked exception (기본 rollback X, rollbackFor 명시), (4) propagation 7가지 정확한 이해, (5) readOnly (DB 강제 X, JPA 최적화 효과).
문제 22: self-invocation 해결 방법?
답: (1) 별도 Service 클래스 분리 (권장), (2) @Lazy 로 자기 자신 주입, (3) ApplicationContext 로 자기 빈 호출, (4) AspectJ 모드.
문제 23: REQUIRED vs REQUIRES_NEW?
답: REQUIRED (기본): 기존 트랜잭션 있으면 참여. REQUIRES_NEW: 항상 새 트랜잭션 (기존 보류). 별도 commit 가능.
문제 24: @Transactional(readOnly = true) 효과?
답: JPA Dirty Checking 비활성 (메모리/CPU 절약) + FlushMode = MANUAL + 일부 DB 최적화 hint. DB 강제 X (코드로 UPDATE 가능).
졸업 시험 24 문항:
SQL JOIN: 3 문항 ✓
ORM: 6 문항 ✓
JPA 매핑: 4 문항 ✓
수동 트랜잭션: 2 문항 ✓
PlatformTM: 3 문항 ✓
@Transactional: 6 문항 ✓
통과 기준:
- 모두 답할 수 있으면 7주차 마스터
- 면접 합격 가능 수준
→ 7주차 졸업!
🎯 7주차 — ORM / JPA / 트랜잭션 추상화
🗂️ Part A — 데이터 모델링과 ORM (16 Unit)
📚 Phase 1 — SQL JOIN (5)
- Unit 1.1 JOIN 의 필요성
- Unit 1.2 INNER JOIN
- Unit 1.3 LEFT/RIGHT JOIN
- Unit 1.4 FULL OUTER JOIN
- Unit 1.5 JOIN 선택 가이드 ★
🔄 Phase 2 — ORM 패러다임 (2)
- Unit 2.1 객체-관계 미스매치
- Unit 2.2 ORM 정의
🌱 Phase 3 — JPA 입문 (4)
- Unit 3.1 SQL Mapper 한계
- Unit 3.2 JPA 등장 ★
- Unit 3.3 JPA 동작 위치
- Unit 3.4 Spring Data JPA + Querydsl ★
🏷️ Phase 4 — JPA 엔티티 매핑 (5)
- Unit 4.1 @Entity
- Unit 4.2 @Id
- Unit 4.3 @GeneratedValue ★
- Unit 4.4 @Column
- Unit 4.5 자동 매핑
🔄 Part B — 트랜잭션 추상화의 진화 (8 Unit)
🔧 Phase 5 — 수동 트랜잭션의 한계 (2)
- Unit 5.1 트랜잭션 결합
- Unit 5.2 3가지 함정
🎯 Phase 6 — PlatformTransactionManager (3)
- Unit 6.1 인터페이스 추상화
- Unit 6.2 3가지 구현체
- Unit 6.3 사용 전후 비교
✨ Phase 7 — @Transactional (3, 모두 ★)
- Unit 7.1 프록시 패턴 ★
- Unit 7.2 동작 원리 ★★★
- Unit 7.3 5가지 함정 ★ ← 여기
총 24 Unit + 종합 졸업 시험 24문항
모든 ★ 깊이 Unit 7개
7주차 핵심 메시지:
"SQL 부터 JPA 까지 데이터 모델링과 ORM 의 완전한 그림 +
수동 트랜잭션의 한계 부터 @Transactional 의 자동화까지
트랜잭션 추상화의 진화 = 자바 백엔드의 정수.
5주차 디자인 패턴 (DI/DIP/OCP/프록시/AOP) +
6주차 DB 접근 (DataSource/ACID/JdbcTemplate) +
7주차 (JPA + PlatformTM + @Transactional)
= 자바 백엔드의 결정체.
@Transactional 1 줄 안에 모든 학습이 응축되며,
ILIC 의 1020 메서드가 이 결정체로 안정 운영된다."
1~7주차 흐름:
1주차 OOP →
2주차 JVM →
3주차 람다 / 함수형 →
4주차 동시성 →
5주차 디자인 패턴 →
6주차 DB 접근 →
7주차 ORM + 트랜잭션 추상화 (지금) ✓
각 주차의 가치:
1주차: 자바 기본
2주차: JVM 이해 (성능)
3주차: 함수형 (현대 자바)
4주차: 동시성 (서버 핵심)
5주차: 디자인 패턴 (확장)
6주차: DB 접근 (영속성)
7주차: ORM + 트랜잭션 (자동화)
→ 자바 백엔드의 7 기둥
박승제의 성장
학습 시작 (1주차) →
학습 완주 (7주차):
ILIC 의 1020 메서드:
- 모두 @Transactional
- 모두 자동
- 박승제 = 비즈니스만
학습 후의 가치:
1. 면접 준비:
- 시니어 시험 합격
- 5+6+7주차 응축 답변
2. 운영 안정:
- 5 함정 회피
- 트러블슈팅
- 코드 리뷰
3. 깊은 이해:
- 7 단계 흐름
- 자바 백엔드 정수
- 자신감
4. ILIC 의 가치:
- 박승제 + 1 풀스택
- 102 테이블 안정 운영
- 신뢰
→ 자바 백엔드 시니어 마스터
자바 백엔드의 정수:
@Transactional 1 줄 안에:
5주차 (디자인 패턴):
✓ 프록시 패턴 (CGLIB)
✓ DI / DIP / OCP
✓ 템플릿+전략 (TransactionTemplate)
✓ 데코레이터 (부가 기능)
✓ AOP
6주차 (DB 접근):
✓ DataSource (HikariCP)
✓ ACID (트랜잭션)
✓ JDBC (Connection)
✓ JdbcTemplate
7주차 (ORM + 트랜잭션):
✓ JPA (EntityManager)
✓ Spring Data JPA
✓ Querydsl
✓ PlatformTransactionManager
✓ @Transactional + AOP/프록시
→ 7 주차 학습의 모든 응축
→ 1 줄 어노테이션의 깊이
면접 종합:
자주 묻는 7주차 Top 10:
1. @Transactional 동작 원리? (Unit 7.2)
2. 프록시 패턴? (Unit 7.1)
3. 5가지 함정? (Unit 7.3)
4. self-invocation? (Unit 7.3)
5. propagation 7가지? (Unit 7.3)
6. JPA vs JdbcTemplate? (Phase 3)
7. IDENTITY vs SEQUENCE? (Unit 4.3)
8. PlatformTransactionManager? (Unit 6.1)
9. Spring Data JPA + Querydsl? (Unit 3.4)
10. ORM 미스매치? (Unit 2.1)
→ 7주차 완주 = 모두 답변 가능
7주차 완주 정리는?
답:
1. 24 Unit:
★ 깊이:
응축:
자산:
7주차 완주 →
8주차 (예정):
가능한 8주차 주제:
- 분산 시스템 (마이크로서비스)
- 비동기 / 이벤트 기반
- 메시징 (Kafka / RabbitMQ)
- 캐싱 (Redis)
- 성능 / 최적화
- 보안 (Spring Security)
- 테스트 (JUnit + Mockito)
- 또는 다른 주제
→ 박승제 의 우선순위에 따라
ILIC 와 연계:
ILIC 의 다음 단계:
- 분산 시스템 (멀티 서버)
- 성능 최적화 (대량 데이터)
- 캐싱 (Redis)
- 모니터링 / 옵저버빌리티
7주차 다음 학습:
- ILIC 의 실제 도전 과제
- 시니어 개발자의 영역
박승제의 다음 자산
이미 (7주차 완주):
- 자바 OOP
- JVM
- 함수형
- 동시성
- 디자인 패턴
- DB 접근
- ORM + 트랜잭션 ✓
다음 (8주차+):
- 분산 / 마이크로서비스
- 성능 / 최적화
- 모니터링
- 보안 / 테스트
→ 풀스택 시니어의 완성
7주차의 의의:
7주차 학습 후:
- 자바 백엔드의 핵심 이해
- 면접 통과 가능
- 시니어 개발자
하지만:
- 다음 단계 필요 (분산, 성능)
- 끝없는 학습
- 깊이 + 폭
→ 7주차 = 시니어로 가는 핵심 관문
마지막 메시지:
7주차 완주 축하 🎉
ILIC 의 1020 메서드가
@Transactional 1 줄로 안정 운영되는 이유는
7주차 학습의 모든 응축이기 때문.
5주차 디자인 패턴 +
6주차 DB 접근 +
7주차 ORM 과 트랜잭션 추상화 =
자바 백엔드의 정수.
박승제 의 자산이며,
ILIC 의 안정 운영이며,
면접의 정점이며,
자바 시니어의 시작이다.
다음 주차 학습으로 →
계속 성장 → 시니어 풀스택 마스터.
7주차 완주의 의의는?
답:
1. 자바 백엔드 정수:
면접:
운영:
다음:
1. 5가지 함정
2. 종합 졸업 시험 24문항
3. 7주차의 결정체
✨ Phase 7 — @Transactional (모두 ★ 깊이)
✅ Unit 7.1 프록시 패턴 ★
✅ Unit 7.2 동작 원리 ★★★
✅ Unit 7.3 5가지 함정 ★ ← 여기, Phase 7 완주
→ @Transactional 완전 정복
→ 5가지 함정 마스터
→ 면접 시니어 시험 통과
🔄 Part B — 트랜잭션 추상화의 진화
✅ Phase 5 — 수동 트랜잭션의 한계 (2)
✅ Phase 6 — PlatformTransactionManager (3)
✅ Phase 7 — @Transactional (3, 모두 ★) ← 완주
→ 트랜잭션 추상화 완전 정복
→ 5+6+7주차 응축 마스터
🎯 7주차 — ORM / JPA / 트랜잭션 추상화
🗂️ Part A — 데이터 모델링과 ORM (16/16) ✓
🔄 Part B — 트랜잭션 추상화의 진화 (8/8) ✓
총: 24/24 Unit (100%) 🎉
★ 깊이 Unit: 7 개 ✓
종합 졸업 시험 24문항 ✓
→ 자바 백엔드의 정수 마스터
→ 시니어 시험 통과 가능
→ ILIC 의 1020 메서드 안정 운영
박승제의 다음 학습 주제 — 분산 시스템 / 성능 최적화 / 캐싱 / 메시징 / 모니터링 / 보안 / 테스트 등 시니어 풀스택의 다음 단계.
1주차: OOP
2주차: JVM
3주차: 람다 / 함수형
4주차: 동시성
5주차: 디자인 패턴
6주차: DB 접근
7주차: ORM + 트랜잭션 ← 여기, 완주 ✓
🎯 5+6+7주차 응축 마스터 — 자바 백엔드의 정수