7주차 Unit 5.2 — 수동 트랜잭션 3가지 함정

Psj·2026년 6월 1일

F-lab

목록 보기
230/240

Unit 5.2 — 수동 트랜잭션 3가지 함정

F-LAB JAVA · 7주차 · Phase 5 · 수동 트랜잭션의 한계
🏆 Phase 5 완주 — 수동 트랜잭션의 결정적 함정


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 3가지 함정 개요 는?
  • 함정 1 — rollback 누락 의 위험은?
  • 함정 1 — close 누락 (Connection 누수) 은?
  • 함정 2 — 트랜잭션 경계 모호 는?
  • 함정 2 — commit 후 외부 작업 의 함정은?
  • 함정 3 — 트랜잭션 전파 어려움 은?
  • 함정 3 — 중첩 트랜잭션 의 복잡성은?
  • 실무 영향과 해결 방향 은?
  • Phase 6 (PlatformTransactionManager) 예고 는?

🎯 핵심 한 문장

수동 트랜잭션의 3가지 결정적 함정 — (1) rollback/close 누락 같은 실수 누락 (운영 사고로 직결), (2) 트랜잭션 경계가 모호해 commit 후 외부 API 호출 같은 잘못된 위치의 코드, (3) 메서드 A 가 메서드 B 를 호출할 때 트랜잭션 전파 (새 트랜잭션? 참여? 중첩?) 가 수동으로는 거의 불가능 — 이 세 함정이 @Transactional 의 propagation 옵션과 AOP 자동화의 직접적 동기다.
수동 트랜잭션의 결정적 함정 3가지는 운영 사고와 직결되는 실무 문제다.
함정 1 — 실수 누락: rollback() 또는 close() 누락 시 — Connection 이 풀로 반환 안 되어 Connection 누수 (HikariCP 풀 고갈), 트랜잭션 leak (이상한 데이터 영속), 운영 사고 (서비스 다운).
함정 2 — 트랜잭션 경계 모호: 어디부터 어디까지가 트랜잭션? — commit 후 외부 API 호출 (이메일/Slack/결제) 시, commit 은 됐는데 외부 호출 실패면 일관성 깨짐 (트랜잭션 후 처리 = After Commit 패턴 필요).
함정 3 — 트랜잭션 전파 어려움: 메서드 A 가 메서드 B 호출 시 — B 가 새 트랜잭션? A 의 트랜잭션 참여? 중첩? — 수동으로는 거의 불가능 (전파 정책 자체 표현 X).
이 세 함정이 — @Transactional 의 7가지 propagation 옵션 (REQUIRED / REQUIRES_NEW / NESTED / ...) + AOP 자동화 의 직접적 동기다 (Phase 6-7 에서 해결).

비유 — 셰프의 3가지 실수

3가지 함정 = 셰프의 3가지 실수:

[1] 식기 안 정리 (rollback/close 누락):
  - 사고 시 식기 안 치움
  - 다음 손님 못 받음 (풀 고갈)
  - 부엌 마비

[2] 식기 정리 후 손님 만남 (commit 후 외부):
  - 식기 정리 (commit) 끝
  - "이메일 보내자" (외부 API)
  - 이메일 실패!
  - "어, 식기는 정리됐는데?"
  - 일관성 깨짐

[3] 다른 셰프 도와줘 (트랜잭션 전파):
  - 메서드 A 가 B 부름
  - B 도 트랜잭션?
  - 둘이 합쳐? 따로?
  - 수동으로 표현 어려움

@Transactional 의 답:
  - 함정 1: 어노테이션이 알아서 (실수 X)
  - 함정 2: @TransactionalEventListener (After Commit)
  - 함정 3: propagation 옵션 (REQUIRED/REQUIRES_NEW/NESTED)

ILIC:
  - 1020 메서드 × 함정 위험 = 운영 사고 risk
  - @Transactional 도입 = 안정성

면접 단골:
  - propagation
  - 5가지 함정 (Phase 7)
  - 프록시 동작

→ 3가지 함정 = 실수 + 경계 + 전파, 운영 사고 risk, @Transactional 답.


🧭 9개 섹션 로드맵

1. 3가지 함정 개요
2. 함정 1 — rollback 누락
3. 함정 1 — close 누락 (Connection 누수)
4. 함정 2 — 트랜잭션 경계 모호
5. 함정 2 — commit 후 외부 작업
6. 함정 3 — 트랜잭션 전파 어려움
7. 함정 3 — 중첩 트랜잭션
8. 실무 영향 + 해결 방향
9. Phase 5 완주 + Phase 6 예고

1️⃣ 3가지 함정 개요

1.1 3가지 함정

3가지 함정:

  함정 1 — 실수 누락:
    - rollback 누락
    - close 누락

  함정 2 — 트랜잭션 경계 모호:
    - 어디까지가 트랜잭션?
    - commit 후 작업

  함정 3 — 트랜잭션 전파 어려움:
    - 메서드 A → B 호출
    - 새/참여/중첩

1.2 함정의 결과

함정의 결과:

  함정 1 → 운영 사고:
    - Connection 누수
    - 풀 고갈
    - 서비스 다운

  함정 2 → 일관성 깨짐:
    - 부분 성공
    - 데이터 불일치
    - 비즈니스 사고

  함정 3 → 코드 복잡 / 버그:
    - 의도 불명확
    - 재사용 어려움
    - 디버깅 ↓

1.3 면접 빈도

면접 빈도:

  함정 1:
    - 기본 (Connection 누수 등)
    - 자주 질문

  함정 2:
    - 중급 (After Commit)
    - 가끔

  함정 3:
    - 고급 (propagation)
    - 자주 (시니어급)

  Phase 7 에서 7.3 ★ 깊이 = 5가지 함정 (확장)

1.4 ILIC 의 맥락

ILIC 의 위험 시나리오 (수동 가정)

ILIC = 102 테이블 × 1020 메서드:

  함정 1 (실수):
    - 1020 메서드 중 일부 rollback/close 누락
    - 운영에서 발견 (사고)
    - HikariCP 풀 고갈 (1-2 시간 후)
    - 서비스 다운

  함정 2 (경계):
    - 결제 → DB 업데이트 → 이메일 발송
    - 이메일 실패 → DB 만 업데이트
    - 사용자에게 알림 X
    - 불일치

  함정 3 (전파):
    - 배송 처리 → 운임 계산 → 이메일
    - 각자 트랜잭션?
    - 합쳐?
    - 코드 복잡

→ 모든 함정 = ILIC 의 운영 위험
→ @Transactional 의 필요성

1.5 자기 점검 답변

3가지 함정 개요는?

:
1. 함정 1:

  • 실수 누락
  1. 함정 2:

    • 경계 모호
  2. 함정 3:

    • 전파 어려움
  3. 결과:

    • 운영 사고 / 불일치 / 복잡

2️⃣ 함정 1 — rollback 누락

2.1 rollback 누락의 결과

rollback 누락 결과:

  예외 발생했는데 rollback X:
    - 트랜잭션 메모리에 (uncommitted)
    - DB 락 유지
    - Connection 점유 (풀 X)
    - 운영 사고

→ 가장 흔한 실수

2.2 흔한 실수 코드

// 흔한 실수 (rollback 누락)
public void process() {
    try {
        tx.begin();
        // 비즈니스 로직
        throw new RuntimeException("에러");
        tx.commit();   // 도달 X
    } catch (Exception e) {
        // tx.rollback();   ← 빠뜨림!
        log.error("Error", e);
        throw e;
    } finally {
        em.close();
    }
}

// 결과:
// - 트랜잭션 미완료 상태
// - Connection 풀에서 빌린 상태
// - DB 락 유지
EntityTransaction tx;
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
EntityManager em;
class EntityManager { void close() {} }
org.slf4j.Logger log;

2.3 발생 빈도

발생 빈도:

  - 신입 개발자 / 급한 코드 / 코드 복사
  - 모든 메서드에 같은 패턴 → 한두 개 누락
  - 1020 메서드 → 1-5개 누락 가능

  운영에서 발견:
    - 부하 시 풀 고갈
    - 1-2 시간 후 다운

2.4 일관성 보정 코드

// 안전한 패턴 (active 체크)
try {
    tx.begin();
    // 비즈니스
    tx.commit();
} catch (Exception e) {
    if (tx.isActive()) {   // active 체크
        tx.rollback();
    }
    throw new RuntimeException(e);
}

// 또는 finally 에서 (이중 안전)
} finally {
    if (tx.isActive()) {
        tx.rollback();   // 만약 commit 안 됐으면
    }
    em.close();
}
EntityTransaction tx;
class EntityTransaction {
    void begin() {}
    void commit() {}
    void rollback() {}
    boolean isActive() { return false; }
}
EntityManager em;
class EntityManager { void close() {} }

2.5 일부만 rollback (또 다른 실수)

// 또 다른 실수 (특정 예외만 rollback)
try {
    tx.begin();
    // 비즈니스
    tx.commit();
} catch (BusinessException e) {
    tx.rollback();   // 비즈니스 예외만
    throw e;
}
// catch (RuntimeException e) 없음!
// → 런타임 예외 발생 시 rollback X
// → 트랜잭션 leak
EntityTransaction tx;
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
class BusinessException extends Exception {}

2.6 ILIC 의 맥락

// ILIC 에서 실수 위험 (수동, 가정)
@Service
public class ShipmentService {
    public void processBatch(List<Long> ids) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            for (Long id : ids) {
                Shipment s = em.find(Shipment.class, id);
                s.process();
                em.merge(s);
            }
            tx.commit();
        } catch (BusinessException e) {
            tx.rollback();
            log.error("Business error", e);
            throw e;
        }
        // ❌ RuntimeException 처리 X
        // ❌ finally 없음 (em.close X)
        // → 운영에서 풀 누수
    }
}
class Shipment { void process() {} }
class BusinessException extends Exception {}
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager {
    EntityTransaction getTransaction() { return null; }
    <T> T find(Class<T> c, Object id) { return null; }
    <T> T merge(T t) { return null; }
    void close() {}
}
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
org.slf4j.Logger log;

2.7 자기 점검 답변

함정 1 — rollback 누락의 위험은?

:
1. 결과:

  • 트랜잭션 leak
  1. 빈도:

    • 흔한 실수
  2. 시나리오:

    • catch 누락 / 부분 처리
  3. 운영:

    • 풀 고갈

3️⃣ 함정 1 — close 누락 (Connection 누수)

3.1 Connection 누수

Connection 누수:

  em.close() 또는 conn.close() 누락:
    - Connection 풀로 반환 X
    - 영원히 점유

  결과:
    - HikariCP 풀 고갈
    - 후속 요청 timeout
    - 서비스 다운

3.2 누수 시나리오

// 누수 시나리오 1: finally 누락
public void process() {
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    try {
        tx.begin();
        // 비즈니스
        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    }
    // finally 없음!
    // em.close() 누락
    // → Connection 누수
}

// 누수 시나리오 2: 예외 발생 후
public void process() {
    EntityManager em = emf.createEntityManager();
    
    Object data = riskyOperation();   // 예외 발생!
    // em.close() 도달 X
    
    em.close();
}
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager {
    EntityTransaction getTransaction() { return null; }
    void close() {}
}
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
Object riskyOperation() { return null; }

3.3 HikariCP 의 동작

HikariCP 의 동작:

  풀 크기 = 20:
    - Connection 1-20 풀에서 빌림 / 반환
    - 사용 후 close() = 반환

  누수 시:
    - 빌리고 반환 X
    - 풀 점차 고갈 (20 → 0)
    - 새 요청 = waiting
    - 30초 timeout → 에러

  운영 모니터링:
    - HikariCP 의 metric 추적
    - 비정상 시 알람

3.4 LeakDetectionThreshold

# 누수 탐지 (HikariCP 설정)
spring:
  datasource:
    hikari:
      leak-detection-threshold: 30000   # 30초

# 동작:
# - Connection 30초 이상 사용
# - 경고 로그
# - 호출 스택 출력
# - 누수 원인 추적

3.5 누적 효과

누적 효과:

  메서드별 누수 가능성:
    - 1020 메서드 × 0.1% 누수 = 1 메서드
    - 부하 시 자주 호출 → 누수 ↑

  시간 따라:
    - 1시간: 풀 50% 사용
    - 2시간: 풀 80% 사용
    - 3시간: 풀 고갈 → 서비스 다운

  → 운영 사고

3.6 ILIC 의 맥락

# ILIC 의 HikariCP 설정
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      leak-detection-threshold: 30000   # 누수 탐지

# 운영 모니터링:
# - HikariCP metric (active / idle / pending)
# - 누수 시 즉시 경고
# - 비정상 패턴 추적
// ILIC 의 자원 관리 (수동 시)
// 보일러플레이트 + 실수 위험 ↑

// @Transactional (실제 사용)
// → Spring 이 자동
// → 누수 X

3.7 자기 점검 답변

함정 1 — close 누락 (Connection 누수) 은?

:
1. 누수:

  • 반환 X
  1. 결과:

    • 풀 고갈
  2. HikariCP:

    • leak-detection-threshold
  3. 운영 사고:

    • 시간 따라 누적

4️⃣ 함정 2 — 트랜잭션 경계 모호

4.1 경계의 중요성

경계의 중요성:

  트랜잭션 = ACID 보장 범위:
    - begin 부터 commit 까지
    - 이 안의 작업은 모두 / 모두 X

  경계 모호 시:
    - "어디까지 트랜잭션?"
    - 헷갈림
    - 잘못된 코드 위치

4.2 경계 안 vs 밖

// 명확한 예
try {
    tx.begin();
    // === 트랜잭션 안 ===
    em.persist(entity);
    em.merge(other);
    // === 트랜잭션 안 끝 ===
    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

// 트랜잭션 밖:
// - tx.begin 전
// - tx.commit 후
// - tx.rollback 후
EntityTransaction tx;
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
EntityManager em;
class EntityManager {
    void persist(Object o) {}
    <T> T merge(T t) { return null; }
    void close() {}
}
Object entity;
Object other;

4.3 모호한 케이스

// 모호 1: commit 후 작업
try {
    tx.begin();
    em.persist(order);
    tx.commit();
    
    // commit 후
    sendEmailToCustomer();    // ← 여기 트랜잭션?
    // 만약 실패 → 어떻게?
} catch (Exception e) {
    tx.rollback();
    // 이메일 실패 시 rollback?
    // 이미 commit 됐는데?
}

// 모호 2: begin 전 작업
EntityManager em = emf.createEntityManager();
List<Order> orders = repository.findOrders();   // 다른 트랜잭션?
EntityTransaction tx = em.getTransaction();
tx.begin();
// ...
class EntityManager { void persist(Object o) {} void close() {} EntityTransaction getTransaction() { return null; } }
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
EntityTransaction tx;
EntityManager em;
Object order;
void sendEmailToCustomer() {}
Repository repository;
class Repository { java.util.List<Order> findOrders() { return null; } }
class Order {}

4.4 시각화의 어려움

시각화의 어려움:

  수동 코드에서:
    - try/catch 범위
    - tx.begin / tx.commit
    - 시각적 표시 X

  vs 어노테이션:
    - @Transactional 명시적
    - 메서드 = 트랜잭션
    - 명확

→ 자동화의 가치

4.5 다른 쓰레드 / 비동기

// 함정: 비동기에 트랜잭션 전달?
try {
    tx.begin();
    Order order = em.persist(newOrder);
    
    // 비동기 작업
    CompletableFuture.runAsync(() -> {
        em.merge(order);   // ← 다른 쓰레드!
        // 다른 EntityManager / 다른 트랜잭션?
    });
    
    tx.commit();
} catch (Exception e) {
    tx.rollback();
}
// 비동기 작업과 메인 트랜잭션 분리
// 매우 복잡
EntityTransaction tx;
EntityManager em;
class Order {}
Object newOrder;
class CompletableFuture {
    static void runAsync(Runnable r) {}
}
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
class EntityManager {
    <T> T persist(T t) { return null; }
    <T> T merge(T t) { return null; }
}

4.6 ILIC 의 맥락

// ILIC 의 트랜잭션 경계 시나리오

@Service
public class ShipmentService {
    public void processShipment(Long id) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        try {
            tx.begin();
            
            // ① DB 작업 (트랜잭션 안)
            Shipment s = em.find(Shipment.class, id);
            s.markAsShipped();
            em.merge(s);
            
            tx.commit();
            
            // ② commit 후 외부 호출 (트랜잭션 밖)
            slackNotifier.notify("Shipped " + id);     // 외부 1
            emailService.send(s.getCustomerEmail());   // 외부 2
            trackingService.register(s.getBlNo());     // 외부 3
            
            // 만약 외부 3 실패 → DB 는 이미 commit
            // 일관성 깨짐!
            
        } catch (Exception e) {
            if (tx.isActive()) tx.rollback();
            throw new RuntimeException(e);
        } finally {
            em.close();
        }
    }
}
// → 경계 모호 + 외부 작업 위험
class Shipment {
    void markAsShipped() {}
    String getCustomerEmail() { return null; }
    String getBlNo() { return null; }
}
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager {
    EntityTransaction getTransaction() { return null; }
    <T> T find(Class<T> c, Object id) { return null; }
    <T> T merge(T t) { return null; }
    void close() {}
}
class EntityTransaction {
    void begin() {}
    void commit() {}
    void rollback() {}
    boolean isActive() { return false; }
}
SlackNotifier slackNotifier;
class SlackNotifier { void notify(String s) {} }
EmailService emailService;
class EmailService { void send(String s) {} }
TrackingService trackingService;
class TrackingService { void register(String s) {} }

4.7 자기 점검 답변

함정 2 — 트랜잭션 경계 모호는?

:
1. 경계:

  • begin ~ commit
  1. 모호:

    • 어디까지?
  2. 시각화 X:

    • 수동 코드
  3. 비동기:

    • 더 복잡

5️⃣ 함정 2 — commit 후 외부 작업

5.1 commit 후 외부 호출

commit 후 외부 호출:

  시나리오:
    1. DB 업데이트 (트랜잭션)
    2. commit
    3. 이메일 발송 (외부 API)
    
  외부 API 실패 시:
    - DB 는 이미 commit
    - rollback X (이미 commit)
    - 사용자는 알림 못 받음
    - 일관성 깨짐

5.2 코드 예시

// 문제 코드
try {
    tx.begin();
    
    // 1. DB 업데이트
    Order order = new Order();
    em.persist(order);
    
    tx.commit();   // ← 커밋!
    
    // 2. 이메일 발송 (외부)
    emailService.send(order.getEmail());
    // 만약 이메일 서비스 다운 →
    // 예외 발생
    // DB 는 이미 commit
    // 사용자에게 알림 X
} catch (Exception e) {
    if (tx.isActive()) tx.rollback();
    // 이미 commit → rollback 의미 X
    throw e;
}
EntityTransaction tx;
class EntityTransaction { void begin() {} void commit() {} void rollback() {} boolean isActive() { return false; } }
EntityManager em;
class EntityManager { void persist(Object o) {} }
class Order { String getEmail() { return null; } }
EmailService emailService;
class EmailService { void send(String s) {} }

5.3 반대 패턴 (이메일 먼저)

// 반대 패턴 (이메일 먼저)
try {
    tx.begin();
    
    // 1. 이메일 먼저
    emailService.send(email);   // ← 외부 API 안에서!
    
    // 2. DB 업데이트
    em.persist(order);
    
    tx.commit();
} catch (Exception e) {
    tx.rollback();
    // 이메일은 이미 보냄 (외부 효과)
    // rollback 못 함 (메일은 외부)
    // 일관성 깨짐
}

// 이메일 보내고 → 트랜잭션 실패 → DB 안 만들어짐
// 사용자: "메일 받았는데 주문 없네?"
EntityTransaction tx;
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }
EntityManager em;
class EntityManager { void persist(Object o) {} }
EmailService emailService;
class EmailService { void send(String s) {} }
String email;
Object order;

5.4 해결 방향 — After Commit 패턴

해결 방향:

  After Commit 패턴:
    - 트랜잭션 commit 후에만 외부 호출
    - rollback 시 외부 호출 X
    - 일관성 보장

  수동으로 구현:
    - boolean flag
    - 메모리에 작업 큐
    - 복잡 / 실수 위험

  Spring 의 답:
    - @TransactionalEventListener(AFTER_COMMIT)
    - 자동 처리

5.5 Spring 의 답 (미리보기)

// Spring 의 답 (@TransactionalEventListener)
@Service
public class OrderService {
    @Autowired ApplicationEventPublisher publisher;
    
    @Transactional
    public void createOrder() {
        // DB 작업
        Order order = saveOrder();
        
        // 이벤트 발행 (commit 후 처리)
        publisher.publishEvent(new OrderCreatedEvent(order));
    }
}

// 이벤트 리스너 (commit 후만 실행)
@Component
public class OrderEventListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        emailService.send(event.getOrder().getEmail());
        // commit 성공 시에만 실행
        // rollback 시 실행 X
    }
}
// → After Commit 패턴 자동
class Order { String getEmail() { return null; } }
class OrderCreatedEvent {
    OrderCreatedEvent(Order o) {}
    Order getOrder() { return null; }
}
class ApplicationEventPublisher { void publishEvent(Object e) {} }
ApplicationEventPublisher publisher;
Order saveOrder() { return null; }
class TransactionPhase { static int AFTER_COMMIT = 0; }
@interface TransactionalEventListener { int phase(); }
@interface Transactional {}
EmailService emailService;
class EmailService { void send(String s) {} }

5.6 ILIC 의 맥락

// ILIC 의 패턴 (실제 @Transactional 사용)

@Service
public class ShipmentService {
    @Autowired ApplicationEventPublisher publisher;
    
    @Transactional
    public void processShipment(Long id) {
        // DB 작업 (트랜잭션 안)
        Shipment s = shipmentRepository.findById(id).orElseThrow();
        s.markAsShipped();
        
        // 이벤트 발행 (commit 후 처리)
        publisher.publishEvent(new ShipmentShippedEvent(s));
    }
}

@Component
public class ShipmentEventListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onShipped(ShipmentShippedEvent event) {
        // commit 성공 후만 실행
        slackNotifier.notify("Shipped");
        emailService.send(event.getShipment().getCustomerEmail());
        trackingService.register(event.getShipment().getBlNo());
    }
}

// 효과:
// - DB commit 실패 → 외부 호출 X
// - DB commit 성공 → 외부 호출
// - 일관성 보장
class Shipment {
    void markAsShipped() {}
    String getCustomerEmail() { return null; }
    String getBlNo() { return null; }
}
ShipmentRepository shipmentRepository;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
class ShipmentShippedEvent {
    ShipmentShippedEvent(Shipment s) {}
    Shipment getShipment() { return null; }
}
ApplicationEventPublisher publisher;
class ApplicationEventPublisher { void publishEvent(Object e) {} }
SlackNotifier slackNotifier;
class SlackNotifier { void notify(String s) {} }
EmailService emailService;
class EmailService { void send(String s) {} }
TrackingService trackingService;
class TrackingService { void register(String s) {} }
@interface Transactional {}
@interface TransactionalEventListener { int phase(); }
class TransactionPhase { static int AFTER_COMMIT = 0; }

5.7 자기 점검 답변

함정 2 — commit 후 외부 작업의 함정은?

:
1. 외부 호출:

  • 트랜잭션 밖
  1. 실패 시:

    • rollback 의미 X
  2. 해결:

    • After Commit 패턴
  3. Spring:

    • @TransactionalEventListener

6️⃣ 함정 3 — 트랜잭션 전파 어려움

6.1 전파 (Propagation) 의 의미

전파 (Propagation):

  메서드 A 가 메서드 B 호출 시:
    - B 가 트랜잭션 가져야?
    - A 의 트랜잭션 참여?
    - 새 트랜잭션?
    - 중첩?

  = 트랜잭션 전파 정책

6.2 시나리오 (간단)

// 시나리오:
// A 는 트랜잭션, A 가 B 호출

public void serviceA() {
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    try {
        tx.begin();
        // A 의 작업
        serviceB();   // ← B 호출
        // A 의 작업
        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
}

public void serviceB() {
    // B 도 트랜잭션 필요?
    // - 새로? (B 만의)
    // - A 의 트랜잭션 참여? (같은 거)
}
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager { EntityTransaction getTransaction() { return null; } void close() {} }
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }

6.3 옵션 1 — 참여 (REQUIRED)

옵션 1 — 참여 (REQUIRED, 가장 일반):

  B 는 A 의 트랜잭션 참여:
    - A 의 commit 시 B 도 commit
    - A 의 rollback 시 B 도 rollback
    - 하나의 트랜잭션

  수동 구현:
    - B 가 A 의 EntityManager 받음
    - 또는 ThreadLocal 사용
    - 복잡!

6.4 옵션 2 — 새 트랜잭션 (REQUIRES_NEW)

옵션 2 — 새 트랜잭션 (REQUIRES_NEW):

  B 는 별도 트랜잭션:
    - A 는 잠시 정지
    - B 가 commit / rollback 후
    - A 계속

  예시:
    - 로그 기록 (반드시 저장)
    - A 가 rollback 해도 로그는 남음

  수동 구현:
    - 별도 EntityManager
    - 트랜잭션 시작
    - 복잡!

6.5 옵션 3 — 중첩 (NESTED)

옵션 3 — 중첩 (NESTED):

  B 는 A 의 부분 트랜잭션:
    - SAVEPOINT 사용
    - B 실패 시 SAVEPOINT 까지만 rollback
    - A 는 계속 진행 가능

  수동 구현:
    - SAVEPOINT 명시
    - JDBC 직접
    - 매우 복잡

6.6 수동의 어려움

// 수동 REQUIRED 구현 시도
public void serviceA() {
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    try {
        tx.begin();
        // A 작업
        
        // B 호출 (같은 트랜잭션 참여)
        serviceB(em);   // ← em 전달
        
        // A 작업
        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        em.close();
    }
}

public void serviceB(EntityManager em) {
    // em 받음 (A 의 트랜잭션)
    // B 작업
    // commit / rollback X (A 가 결정)
}

// 문제:
// - 메서드 시그너처 변경 (em 전달)
// - 모든 호출 체인 변경
// - 매우 침투적
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager { EntityTransaction getTransaction() { return null; } void close() {} }
class EntityTransaction { void begin() {} void commit() {} void rollback() {} }

6.7 ILIC 의 맥락

// ILIC 의 전파 시나리오 (수동, 가정)

// 배송 처리 + 운임 계산 + 알림
public void processShipment(Long id) {
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    try {
        tx.begin();
        
        // 1. 배송 업데이트
        Shipment s = em.find(Shipment.class, id);
        s.markAsShipped();
        
        // 2. 운임 계산 (서비스 호출)
        freightService.calculate(em, s);   // em 전달!
        
        // 3. 로그 기록 (별도 트랜잭션 원함)
        // → 수동으로는?
        // → 별도 EntityManager 만들고 별도 begin/commit/close
        EntityManager logEm = emf.createEntityManager();
        EntityTransaction logTx = logEm.getTransaction();
        try {
            logTx.begin();
            logEm.persist(new ShipmentLog(id, "SHIPPED"));
            logTx.commit();
        } catch (Exception e) {
            if (logTx.isActive()) logTx.rollback();
            // 로그 실패 → 메인 트랜잭션은?
        } finally {
            logEm.close();
        }
        
        tx.commit();
    } catch (Exception e) {
        if (tx.isActive()) tx.rollback();
    } finally {
        em.close();
    }
}
// → 매우 복잡
// → 실수 위험

// @Transactional + propagation
// → 간단
// → 다음 Phase
class Shipment { void markAsShipped() {} }
class ShipmentLog { ShipmentLog(Long id, String s) {} }
FreightService freightService;
class FreightService { void calculate(EntityManager em, Shipment s) {} }
EntityManagerFactory emf;
class EntityManagerFactory { EntityManager createEntityManager() { return null; } }
class EntityManager {
    EntityTransaction getTransaction() { return null; }
    <T> T find(Class<T> c, Object id) { return null; }
    void persist(Object o) {}
    void close() {}
}
class EntityTransaction {
    void begin() {}
    void commit() {}
    void rollback() {}
    boolean isActive() { return false; }
}

6.8 자기 점검 답변

함정 3 — 트랜잭션 전파 어려움은?

:
1. 전파:

  • 메서드 호출 시 정책
  1. 3 옵션:

    • 참여 / 새로 / 중첩
  2. 수동:

    • 거의 불가능
  3. @Transactional:

    • propagation 옵션

7️⃣ 함정 3 — 중첩 트랜잭션

7.1 중첩 (NESTED) 의 의미

중첩 트랜잭션 (NESTED):

  부분 rollback 가능:
    - 외부 트랜잭션 안에
    - 내부 트랜잭션 (SAVEPOINT)
    - 내부 실패 시 SAVEPOINT 까지만 rollback
    - 외부는 계속

  유용한 케이스:
    - "한 단계 실패해도 전체 X"

7.2 시나리오

시나리오:

  주문 처리 (외부):
    - 1. 재고 차감 (내부 - 가능하면)
    - 2. 결제 (필수)
    - 3. 배송 (필수)

  목표:
    - 재고 차감 실패해도 결제 / 배송 진행
    - 결제 / 배송 실패는 전체 rollback

  중첩 트랜잭션:
    - 재고 차감 = 내부 (SAVEPOINT)
    - 결제 / 배송 = 외부

7.3 JDBC SAVEPOINT

// JDBC 의 SAVEPOINT
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);

try {
    // 외부 트랜잭션
    
    // 내부 (SAVEPOINT)
    Savepoint savepoint = conn.setSavepoint();
    try {
        // 재고 차감
        updateStock(conn, ...);
    } catch (Exception e) {
        // 부분 rollback (SAVEPOINT 까지)
        conn.rollback(savepoint);
        log.warn("재고 차감 실패", e);
    }
    
    // 외부 계속
    processPayment(conn);
    processShipping(conn);
    
    conn.commit();
} catch (Exception e) {
    conn.rollback();   // 전체 rollback
} finally {
    conn.close();
}
DataSource dataSource;
interface DataSource { Connection getConnection() throws SQLException; }
void updateStock(Connection conn, Object... a) {}
void processPayment(Connection conn) {}
void processShipping(Connection conn) {}
org.slf4j.Logger log;

7.4 수동의 복잡성

수동의 복잡성:

  - SAVEPOINT 명시
  - 부분 rollback 처리
  - 예외 분기
  - 모든 경우 처리

  실수 시:
    - 부분 rollback 누락
    - 전체 commit / rollback 잘못
    - 디버깅 어려움

7.5 @Transactional 의 NESTED

// @Transactional NESTED (간단)
@Service
public class OrderService {
    @Transactional   // 외부
    public void createOrder() {
        // 1. 외부 트랜잭션
        
        try {
            stockService.deduct();   // NESTED
        } catch (StockException e) {
            // 부분 rollback (자동)
            log.warn("재고 차감 실패");
        }
        
        // 2. 외부 계속
        paymentService.process();
        shippingService.create();
        // 자동 commit
    }
}

@Service
public class StockService {
    @Transactional(propagation = Propagation.NESTED)
    public void deduct() {
        // 부분 트랜잭션
    }
}
// → 간단, 어노테이션만
class OrderService {}
class StockService {}
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, NESTED, REQUIRES_NEW }
class StockException extends RuntimeException {}
StockService stockService;
PaymentService paymentService;
ShippingService shippingService;
class PaymentService { void process() {} }
class ShippingService { void create() {} }
org.slf4j.Logger log;

7.6 ILIC 의 맥락

// ILIC 의 중첩 시나리오 (가상)

@Service
public class ShipmentService {
    @Transactional   // 외부
    public void processShipment(Long id) {
        // 1. 배송 업데이트
        Shipment s = shipmentRepo.findById(id).orElseThrow();
        s.markAsShipped();
        
        // 2. 운임 계산 (NESTED - 실패해도 OK)
        try {
            freightService.calculate(s);
        } catch (FreightCalculationException e) {
            // 부분 rollback (운임만)
            log.warn("운임 자동 계산 실패, 수동 처리 필요");
            // 메인 트랜잭션은 계속
        }
        
        // 3. 알림 로그
        notificationService.logShippedEvent(id);
        // 자동 commit
    }
}

@Service
public class FreightService {
    @Transactional(propagation = Propagation.NESTED)
    public void calculate(Shipment s) {
        // 부분 트랜잭션
        // 실패 시 SAVEPOINT 까지만 rollback
    }
}

// → 어노테이션만으로
// → 수동은 매우 복잡
class Shipment { void markAsShipped() {} }
ShipmentRepository shipmentRepo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
FreightService freightService;
class FreightCalculationException extends RuntimeException {}
NotificationService notificationService;
class NotificationService { void logShippedEvent(Long id) {} }
@interface Transactional { Propagation propagation() default Propagation.REQUIRED; }
enum Propagation { REQUIRED, NESTED }
org.slf4j.Logger log;

7.7 자기 점검 답변

함정 3 — 중첩 트랜잭션의 복잡성은?

:
1. 중첩:

  • 부분 rollback
  1. JDBC:

    • SAVEPOINT
  2. 수동:

    • 매우 복잡
  3. @Transactional NESTED:

    • 간단

8️⃣ 실무 영향 + 해결 방향

8.1 실무 영향 종합

실무 영향:

  함정 1 (실수):
    - 운영 사고 (Connection 누수)
    - 서비스 다운
    - 1-2 시간 내 발견

  함정 2 (경계):
    - 데이터 불일치
    - 사용자 경험 ↓
    - 비즈니스 사고

  함정 3 (전파):
    - 코드 복잡
    - 재사용 X
    - 디버깅 ↓

→ 모두 운영 / 비즈니스 위험

8.2 해결 방향

해결 방향:

  1. 자동화 (보일러플레이트 X):
     - @Transactional 어노테이션
     - AOP / 프록시
     → Phase 7

  2. 추상화 (인터페이스):
     - PlatformTransactionManager
     - DataSource 정신
     → Phase 6

  3. 전파 정책 명시:
     - propagation 옵션
     - 7가지
     → Phase 7

  4. After Commit:
     - @TransactionalEventListener
     → Phase 7 (5가지 함정)

8.3 5주차 + 6주차 + 7주차

5주차 + 6주차 + 7주차:

5주차 (패턴):
  - DI / OCP / 템플릿+전략
  - 프록시

6주차 (DB 접근):
  - DataSource
  - ACID
  - JdbcTemplate (자원 자동)

7주차 Part B (트랜잭션 추상화):
  - PlatformTransactionManager (DataSource 정신)
  - @Transactional (프록시 + AOP)
  - propagation
  - = 모든 학습의 응축

8.4 실무 선택

실무 선택:

  거의 모든 자바 백엔드:
    - @Transactional 사용
    - 수동 트랜잭션 안 씀
    - Spring 의 표준

  예외:
    - 매우 특수 (수동 제어 필요)
    - 미세 튜닝

→ @Transactional 압도적

8.5 학습 가치

학습 가치:

  수동 트랜잭션 이해 →
  @Transactional 의 자동화 가치 절감 →
  깊은 이해 →
  함정 회피

  Phase 7 의 7.3 (5가지 함정):
    - 모두 수동의 함정에서 파생
    - 어노테이션 시 발생 가능
    - 면접 단골

8.6 ILIC 의 맥락

ILIC 의 실무

ILIC = @Transactional 표준:
  - 102 테이블
  - 1020 메서드
  - 모두 어노테이션

  수동의 함정 학습:
    - 함정 의식
    - @Transactional 사용 시도 회피
    - 깊은 이해

  Phase 6-7 학습으로:
    - propagation 정책 활용
    - After Commit 패턴
    - 함정 회피

  → 안정 운영

8.7 자기 점검 답변

실무 영향과 해결 방향은?

:
1. 영향:

  • 사고 / 불일치 / 복잡
  1. 해결:

    • 자동화 / 추상화
  2. 5+6+7주차:

    • 응축
  3. 실무:

    • @Transactional 표준

9️⃣ Phase 5 완주 + Phase 6 예고

9.1 Phase 5 학습 종합

🔧 Phase 5 — 수동 트랜잭션의 한계

Unit 5.1 — 결합 문제:
  - 비즈니스 + 인프라 혼재
  - SoC 위반
  - 보일러플레이트

Unit 5.2 — 3가지 함정 ← 여기:
  - 함정 1: 실수 누락
  - 함정 2: 경계 모호
  - 함정 3: 전파 어려움

→ 수동 트랜잭션의 결정적 한계
→ 자동화의 동기

9.2 핵심 메시지

Phase 5 핵심 메시지:

  "수동 트랜잭션은 비즈니스 로직과 인프라의 결합 + 
   실수 누락 + 경계 모호 + 전파 어려움 의 4 결정적 문제.
   이 4 문제 모두를 풀어주는 게 Spring 의 트랜잭션 추상화."

9.3 Phase 6 예고

🎯 Phase 6 — PlatformTransactionManager (3 Unit)

  Unit 6.1 — 인터페이스 추상화:
    - PlatformTransactionManager
    - 5주차 DI / DIP 정신
    - 6주차 DataSource 와 같은 사상

  Unit 6.2 — 3가지 구현체:
    - DataSourceTransactionManager (JDBC)
    - HibernateTransactionManager (Hibernate)
    - JpaTransactionManager (JPA)

  Unit 6.3 — 사용 전후 비교:
    - 수동 vs PlatformTransactionManager
    - 코드 개선

9.4 Phase 6 의 가치

Phase 6 의 가치:

  - 트랜잭션의 인터페이스 추상화
  - 5주차 DI / DIP / OCP 정신
  - 6주차 DataSource 와 같은 사상
  - @Transactional 의 기반

→ 이해 깊이 ↑

9.5 Phase 6 → Phase 7

Phase 6 → Phase 7:

Phase 6:
  - 인터페이스 추상화 (TM)
  - 수동에서 한 단계 자동

Phase 7 (★★★ 핵심):
  - @Transactional
  - 프록시 + AOP
  - 5가지 함정
  - 면접 단골

→ 점진적 추상화
→ Phase 7 가 정점

9.6 7주차 진행

🗂️ Part A — 데이터 모델링과 ORM (완주)
  ✅ Phase 1-4 (16)

🔄 Part B — 트랜잭션 추상화의 진화
  ✅ Phase 5 (2) ← 여기, 완주!
  ⏭ Phase 6 (3)
  ⏭ Phase 7 (3) — 모두 ★깊이

총: 18/24 Unit (75%, Phase 5 완주!)

9.7 면접 단골 질문 매핑

Q핵심 답변
3가지 함정?실수 / 경계 / 전파
rollback 누락?트랜잭션 leak
close 누락?Connection 누수
풀 고갈?서비스 다운
commit 후 외부?After Commit
@TransactionalEventListener?AFTER_COMMIT
트랜잭션 전파?REQUIRED 등
NESTED?SAVEPOINT
@Transactional 답?모두 해결
Phase 6-7?추상화 + 자동화

9.8 자기 점검 체크리스트

함정 1 (실수)

  • rollback / close

Connection 누수

  • 풀 고갈

함정 2 (경계)

  • commit 후

After Commit

  • 패턴

함정 3 (전파)

  • 메서드 호출

NESTED

  • SAVEPOINT

영향

  • 실무 위험

해결

  • @Transactional

Phase 6 예고

  • PlatformTransactionManager

9.9 추가 심화 질문

Q1: 트랜잭션 leak 의 발견?

답:

  • HikariCP metrics (active / idle)
  • leak-detection-threshold
  • 운영 모니터링 (APM)
  • 로그 분석

Q2: After Commit 의 구현 방법?

답:

  • TransactionSynchronization
  • @TransactionalEventListener
  • 수동: synchronizationManager.registerSynchronization()

Q3: 트랜잭션 격리 레벨?

답:

  • READ UNCOMMITTED (가장 약함)
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE (가장 강함)
  • DB 별 기본 다름 (MySQL = REPEATABLE READ)

Q4: 분산 트랜잭션 (XA)?

답:

  • 여러 DB 동시 트랜잭션
  • 2PC (Two-Phase Commit)
  • 복잡 / 성능 ↓
  • 마이크로서비스 환경: Saga 패턴 권장

Q5: read-only 트랜잭션?

답:

  • @Transactional(readOnly = true)
  • 성능 최적화 (snapshot)
  • 변경 X (Dirty Checking X)
  • 조회 전용

🎯 핵심 요약 — 3줄 정리

1. 3가지 함정

  • 함정 1: rollback/close 누락 → Connection 누수, 풀 고갈, 서비스 다운
  • 함정 2: 트랜잭션 경계 모호 → commit 후 외부 작업 실패, 일관성 깨짐
  • 함정 3: 트랜잭션 전파 어려움 → 메서드 호출 시 참여/새로/중첩 표현 X

2. 실무 영향

  • 운영 사고 (Connection 누수, 서비스 다운)
  • 데이터 불일치 (commit 후 외부 호출 실패)
  • 코드 복잡 (전파 정책 수동 구현 불가능)

3. 해결 방향

  • Phase 6: PlatformTransactionManager (인터페이스 추상화, 5주차 + 6주차 정신)
  • Phase 7: @Transactional (프록시 + AOP + propagation 7가지)
  • @TransactionalEventListener (After Commit 패턴)
  • 자바 백엔드의 표준

🏆 Phase 5 완주 — 수동 트랜잭션의 한계

🔧 Phase 5 — 수동 트랜잭션의 한계
  ✅ Unit 5.1 트랜잭션이 비즈니스 로직과 결합
  ✅ Unit 5.2 수동 트랜잭션 3가지 함정 ← 여기, Phase 5 완주

→ 수동의 결정적 한계 이해
→ 자동화의 동기 정확히 인식
→ Phase 6 (인터페이스 추상화) 의 기반

📚 다음으로...

Phase 6 — PlatformTransactionManager

🎯 Phase 6 — PlatformTransactionManager (3 Unit)
  Unit 6.1 — 인터페이스 추상화
  Unit 6.2 — 3가지 구현체 (DataSource/Hibernate/JPA TM)
  Unit 6.3 — 사용 전후 비교

Phase 6 주제:

  • Spring 의 트랜잭션 추상화
  • PlatformTransactionManager 인터페이스
  • 3가지 구현체 (JDBC / Hibernate / JPA)
  • 5주차 DI + 6주차 DataSource 정신
  • @Transactional 의 기반

7주차 누적 진행

🗂️ Part A — 데이터 모델링과 ORM (완주)
  ✅ Phase 1-4 (16)

🔄 Part B — 트랜잭션 추상화의 진화
  ✅ Phase 5 (2) ← 완주
  ⏭ Phase 6 (3)
  ⏭ Phase 7 (3) — 모두 ★깊이

총: 18/24 Unit (75%, Phase 5 완주!)

🏆 Phase 5 완주 — 수동 트랜잭션의 한계

profile
Software Developer

0개의 댓글