본문으로 건너뛰기
김신건의 로그

[Spring] Transaction: @Transactional, propagation, isolation

· 수정 · 📖 약 2분 · 585자/단어 #spring #transaction #jpa #jdbc
spring transaction, @Transactional, propagation, isolation level, rollback, PlatformTransactionManager

정의

Spring 트랜잭션은 AOP 기반 선언형 트랜잭션 관리. @Transactional 한 줄로 메서드 전체를 트랜잭션 범위로. JDBC, JPA, MongoDB 등 backend에 동일 API.

기본

@Service
public class TransferService {
    private final AccountRepository accountRepository;

    @Transactional
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId).orElseThrow();
        Account to = accountRepository.findById(toId).orElseThrow();
        from.withdraw(amount);
        to.deposit(amount);
        // dirty checking이 자동 UPDATE
    }
}

메서드 시작 시 트랜잭션 begin, 정상 반환 시 commit, 예외 시 rollback (기본은 unchecked만).

readOnly

@Service
@Transactional(readOnly = true)
public class UserService {

    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }

    @Transactional    // override
    public User update(Long id, String name) {
        ...
    }
}

readOnly=true로 Hibernate가 dirty checking 생략 → 성능 향상. read 메서드에 권장.

Propagation

중첩 트랜잭션 처리.

Propagation동작
REQUIRED (기본)트랜잭션 있으면 참여, 없으면 새로
REQUIRES_NEW항상 새 트랜잭션 (기존은 suspend)
NESTED부모 안의 savepoint
SUPPORTS트랜잭션 있으면 참여, 없으면 비트랜잭션
NOT_SUPPORTED트랜잭션 suspend, 비트랜잭션으로 실행
MANDATORY트랜잭션 필수, 없으면 예외
NEVER트랜잭션 없어야 함, 있으면 예외
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAudit(...) {
    // 부모 트랜잭션과 독립적으로 commit/rollback
}

자주 쓰는 case: 감사 로그는 REQUIRES_NEW로 메인 작업이 실패해도 기록 남김.

Isolation Level

@Transactional(isolation = Isolation.READ_COMMITTED)
public void doSomething() { ... }
Isolation동작
READ_UNCOMMITTEDdirty read 가능 (가장 약)
READ_COMMITTEDdirty read 방지 (대부분 DB 기본)
REPEATABLE_READnon-repeatable read 방지 (MySQL 기본)
SERIALIZABLEphantom read 방지 (가장 강)
DEFAULTDB의 기본

PostgreSQL 기본은 READ_COMMITTED. SERIALIZABLE은 데드락 가능 + 성능 손해.

rollbackFor

@Transactional(rollbackFor = Exception.class)    // checked exception도 rollback
public void process() throws IOException { ... }

@Transactional(noRollbackFor = BusinessException.class)
public void doIt() { ... }

기본은 RuntimeException + Error만 rollback. Checked 예외는 commit. 의도에 맞게 override.

timeout

@Transactional(timeout = 5)    // 5초
public void longOperation() { ... }

지정 시간 초과 시 rollback.

트랜잭션 매니저

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
    return new JpaTransactionManager(emf);
}

// JDBC만
@Bean
public PlatformTransactionManager transactionManager(DataSource ds) {
    return new DataSourceTransactionManager(ds);
}

Spring Boot 자동. 여러 DB는 명시.

@Transactional("primary")
@Transactional("secondary")

수동 트랜잭션

@Service
public class MyService {
    private final TransactionTemplate txTemplate;

    public MyService(PlatformTransactionManager txManager) {
        this.txTemplate = new TransactionTemplate(txManager);
    }

    public void doIt() {
        txTemplate.executeWithoutResult(status -> {
            // 트랜잭션 안
            try {
                ...
            } catch (Exception e) {
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}

선언형이 보통 충분하나 동적 결정 시 유용.

트랜잭션 이벤트

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAfterCommit(UserCreatedEvent event) {
    sendWelcomeEmail(event.user());
}

phase:

  • BEFORE_COMMIT
  • AFTER_COMMIT (가장 흔함)
  • AFTER_ROLLBACK
  • AFTER_COMPLETION

이메일 발송은 commit 후 → 트랜잭션 rollback 시 이메일 미발송 보장.

@Service
public class UserService {
    @Transactional
    public User create(...) {
        User user = userRepository.save(...);
        eventPublisher.publishEvent(new UserCreatedEvent(user));
        return user;
    }
}

자주 보는 패턴

Service-level 트랜잭션

@Service
@Transactional(readOnly = true)
public class UserService {
    // 모든 메서드 read-only 기본

    @Transactional
    public User create(...) { ... }    // write

    @Transactional
    public void delete(Long id) { ... }    // write
}

읽기가 대부분이고 쓰기만 명시.

Bulk update

@Transactional
public int bulkUpdate(List<Long> ids) {
    return em.createQuery("UPDATE User u SET u.active = false WHERE u.id IN :ids")
        .setParameter("ids", ids)
        .executeUpdate();
}

함정

1. 자기 호출 (self-invocation)

@Service
public class UserService {
    public void a() {
        b();    // @Transactional 무시!
    }

    @Transactional
    public void b() { ... }
}

proxy 거치지 않아 트랜잭션 시작 안 됨. 해결: 다른 Bean으로 분리.

2. checked exception은 기본 rollback X

@Transactional
public void process() throws IOException {
    ...
    throw new IOException();    // commit! (rollback X)
}

명시: @Transactional(rollbackFor = Exception.class).

3. private 메서드에 @Transactional

@Transactional    // 무시됨!
private void doIt() { ... }

public만. 또는 AspectJ load-time weaving.

4. 트랜잭션 너무 길게

@Transactional
public void big() {
    // DB 작업
    externalApiCall();    // 트랜잭션 안에서 외부 호출 → DB 락 길게
    // DB 작업
}

외부 호출은 트랜잭션 밖으로 분리.

5. lazy loading 실패

@Transactional
public User findById(Long id) { ... }

// controller에서
User user = userService.findById(1L);
user.getPosts();    // LazyInitializationException (트랜잭션 끝남)

JOIN FETCH 또는 OSIV (Open Session In View, 비권장).

6. @Transactional과 try-catch

@Transactional
public void doIt() {
    try {
        riskyOperation();
    } catch (Exception e) {
        log.error("", e);    // 예외 삼킴 → commit!
    }
}

예외 삼키면 rollback 안 됨. 명시:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

또는 그냥 예외 던지기.

모니터링

logging:
  level:
    org.springframework.transaction: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.type: TRACE

P6Spy, datasource-proxy로 SQL 추적.

Reactive Transaction (WebFlux)

@Transactional
public Mono<User> create(...) { ... }

R2DBC, Reactive Mongo 등. 동작 원리 동일하나 reactive context.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기