Spring/Spring

[Spring][Data Access] 1-2. Transaction Manager: Understanding the Spring Framework Transaction Abstraction

noahkim_ 2024. 8. 11. 06:01

1. Transaction Stretegy

  • Spring이 트랜잭션을 어떻게 처리할지 정의
  • TransactionManager 인터페이스에서 담당

 

2. TransactionDefinition

  • 트랜잭션의 동작 방식 정의

 

코드) TransactionDefinition 인터페이스

더보기
public interface TransactionDefinition {
    int PROPAGATION_REQUIRED       =  0; 
    int PROPAGATION_SUPPORTS       =  1;
    int PROPAGATION_MANDATORY      =  2;
    int PROPAGATION_REQUIRES_NEW   =  3;
    int PROPAGATION_NOT_SUPPORTED  =  4;
    int PROPAGATION_NEVER          =  5;
    int PROPAGATION_NESTED         =  6;

    int ISOLATION_DEFAULT          = -1;  // 데이터베이스 기본 격리 수준 사용
    int ISOLATION_READ_UNCOMMITTED =  1;  // same as java.sql.Connection.TRANSACTION_READ_UNCOMMITTED;
    int ISOLATION_READ_COMMITTED   =  2;  // same as java.sql.Connection.TRANSACTION_READ_COMMITTED;
    int ISOLATION_REPEATABLE_READ  =  4;  // same as java.sql.Connection.TRANSACTION_REPEATABLE_READ;
    int ISOLATION_SERIALIZABLE     =  8;  // same as java.sql.Connection.TRANSACTION_SERIALIZABLE;

    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() { return PROPAGATION_REQUIRED; }			
    default int getIsolationLevel() { return ISOLATION_DEFAULT; }			
    default int getTimeout() { return TIMEOUT_DEFAULT; }			
    default boolean isReadOnly() { return false; }

    @Nullable
    default String getName() { return null; }

    static TransactionDefinition withDefaults() { return StaticTransactionDefinition.INSTANCE; }
}

 

Propagation

  • 트랜잭션이 다른 트랜잭션 컨텍스트 내에서 어떻게 동작해야 하는지 정의
Propagation 외부 트랜잭션 존재 시 외부 트랜잭션 없을 시 예외 발생 여부 기타 설명
REQUIRED Join New
가장 일반적인 설정
SUPPORTS Join None
트랜잭션 있어도 되고 없어도 됨
MANDATORY Join
반드시 외부 트랜잭션 있어야 함
REQUIRES_NEW New
(외부 트랜잭션 일시정지)
New
기존 트랜잭션 일시 정지
(내부 트랜잭션 먼저 수행됨)
NOT_SUPPORTED None
(외부 트랜잭션 일시정지)
None 트랜잭션 무시
NEVER None
트랜잭션 있으면 안 됨
NESTED Join
(Savepoint)
New
부모 트랜잭션이 있어야 효과적
DataSourceTransactionManager만 지원

 

예제) REQUIRED

더보기

1. 외부 트랜잭션 존재 O (JOIN)

@Transactional
public void join_required(String username) {
    memberRepository.save(username);
    if (username.contains("fail")) {
        logService.save("비정상 로그: " + username);
        throw new RuntimeException("[exception] MemberService#join (가입 실패)");
    }

    logService.save("정상 로그: " + username);
}
memberService.join_required_transactional("user");
memberService.join_required_transactional("fail_user");

 

  • logService에서 예외 발생 X: member, log 테이블 모두 기록 O
  • logService에서 예외 발생 O: member, log 테이블 모두 기록 X. 전체 롤백됨

 

2. 외부 트랜잭션 존재 X (NEW)

public void join_required_nontransactional(String username) {
    memberRepository.save(username);
    if (username.contains("fail")) {
        logService.save("비정상 로그: " + username);
        throw new RuntimeException("[exception] MemberService#join (가입 실패)");
    }

    logService.save("정상 로그: " + username);
}
@Transactional
public void save(String msg) {
    logRepository.save(msg);
}
memberService.required_nontransactional("user");
memberService.required_nontransactional("fail_user");
  • NEW 이므로 log 기록은 logService.save() 내부에서 예외 발생 여부에 따라 결정됨

 

예제) SUPPORTS

더보기

1. 외부 트랜잭션 존재 X (NONE)

public void supports_nontransactional(String username) {
    memberRepository.save(username);
    logService.save_supports("정상 로그: " + username);
    throw new RuntimeException("[exception] MemberService#join (가입 실패)");
}
@Transactional(propagation = Propagation.SUPPORTS)
public void save_supports(String msg) {
    logRepository.save(msg);
    throw new RuntimeException("[exception] LogService#save (로그기록 실패)");
}
memberService.supports_nontransactional("user");
  • 예외 발생 관련 없이 무조건 log 기록 O
  • 외부 트랜잭션이 없으므로 NONE으로 동작하고, jdbc 기본 동작인 auto-commit이 동작됨
    • db에 즉시 자동으로 반영됨
    • 트랜잭션이 없으므로 롤백이 불가함

 

예제) MANDATORY

더보기

1. 외부 트랜잭션 존재 X (EXCEPTION)

public void mandatory_exception(String username) {
    memberRepository.save(username);
    logService.save_mandatory("정상 로그: " + username);
}
@Transactional(propagation = Propagation.MANDATORY)
public void save_mandatory(String msg) {
    logRepository.save(msg);
}
memberService.mandatory_exception("user");
  • 외부 트랜잭션 없을 경우, 항상 예외 발생

 

예제) REQUIRES_NEW

더보기

1. 외부 트랜잭션 존재 O (NEW)(외부 트랜잭션 일시정지)

@Transactional
public void pause_required_new_transactional(String username) {
    memberRepository.save(username);
    try {
        logService.save_requires_new("정상 로그: " + username);
    } catch (RuntimeException e) {
        System.out.println(e.getMessage());
    }
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save_requires_new(String s) {
    logRepository.save(s);
    throw new RuntimeException("[exception] LogService#save (로그기록 실패)");
}
memberService.pause_required_new_transactional("user");
  • 내부 -> 외부 순으로 처리됨
  • 내부에서 예외 발생하면, 내부에서만 롤백됨
    • 외부에 영향 X
    • 단, 던져진 예외를 처리하지 않으면 외부에 영향 O

 

예제) NESTED

더보기

1. 외부 트랜잭션 존재 O (Join)(Savepoint)

@Transactional
public void join_nested_transactional(String username) {
    memberRepository.save(username);
    try {
        logService.save_nested("정상 로그: " + username);
    } catch (RuntimeException e) {
        System.out.println(e.getMessage());
    }
}
@Transactional(propagation = Propagation.NESTED)
public void save_nested(String s) {
    logRepository.save(s);
    throw new RuntimeException("[exception] LogService#save (로그기록 실패)");
}
memberService.join_nested_transactional("user");
  • 동작은 REQUIRES_NEW와 같음
  • 내부 동작이 다름
    • 하나의 트랜잭션 내에서 SAVEPOINT로 롤백 구간을 정해 외부 트랜잭션을 유지함

 

Isolation

  • 트랜잭션이 다른 트랜잭션으로 얼마나 독립적으로 동작하는지 정의
  • 레벨이 높아질수록 데이터 무결성은 높아지지만, 성능상 비용이 많이 듬

 

Isolation Level Dirty Read Non-repeatable Read Phantom Read 설명 성능 부담
READ_UNCOMMITTED 커밋되지 않은 데이터도 읽을 수 있음 매우 낮음
READ_COMMITTED 커밋된 데이터만 읽음
(가장 많이 사용되는 기본 설정: Oracle 등)
낮음
REPEATABLE_READ 읽기 잠금
- 동일 쿼리 결과가 트랜잭션 동안 동일함
(MySQL 기본값)
중간
SERIALIZABLE 읽기 잠금, 쓰기 잠금
- 트랜잭션을 순차적으로 처리
매우 높음

 

용어 설명
Dirty Read
다른 트랜잭션이 커밋하지 않은 데이터를 읽는 것
Non-repeatable Read
같은 데이터를 읽었는데 값이 바뀌는 현상
Phantom Read
같은 조건으로 검색했는데 행(row) 수가 바뀌는 현상

 

예제) READ_COMMITTED (Non-repeatable Read)

더보기
@Transactional
public void updateUserName() {
    sleep(1000);
    memberRepository.updateUsernameById("Dirty", 1);
    System.out.println("갱신 완료");
}

@Transactional(isolation = Isolation.READ_COMMITTED)
public void readCommitted() {
    System.out.println("첫번째 조회: "+memberRepository.findUsernameById(1));
    sleep(3000);
    System.out.println("두번째 조회: "+memberRepository.findUsernameById(1));
}
Thread t1 = new Thread(() -> memberService.updateUserName());
Thread t2 = new Thread(() -> memberService.readCommitted());

t1.start();
t2.start();

t1.join();
t2.join();
첫번째 조회: user
갱신 완료
두번째 조회: Dirty
  • 같은 데이터에 대해 다른 트랜잭션에서 수정한 값을 읽게됨

 

Timeout

  • 트랜잭션이 얼마나 오래 실행될 수 있는지 설정

 

Read-only

  • 트랜잭션이 데이터 읽기만 할지 여부 결정

 

3. TrasactionStatus

구분 메서드 / 기능 설명
트랜잭션 상태 확인
isNewTransaction()
현재 트랜잭션이 새로 시작된 것인지, 아니면 기존 트랜잭션에 참여한 것인지 확인
isRollbackOnly()
현재 트랜잭션이 롤백 전용 상태인지 확인
isCompleted()
현재 트랜잭션이 완료되었는지 확인 (커밋 or 롤백 여부)
트랜잭션 동작 flush()
현재까지의 변경사항을 DB에 강제로 반영 (JPA에서는 영속성 컨텍스트 → DB)
트랜잭션 설정 setRollbackOnly()
현재 트랜잭션을 롤백 전용으로 설정 (이후 커밋 불가능, 반드시 롤백됨)

 

예제) Service에서 사용법

더보기
@Service
public class ExampleService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void performTransactionalOperation() {
        // 트랜잭션 정의
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

        // 트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(definition);

        try {
            // 트랜잭션이 새로운 것인지 확인
            if (status.isNewTransaction()) { ... }

            // 데이터베이스 작업 수행
            jdbcTemplate.update("INSERT INTO my_table (column1) VALUES (?)", "Test Value");

            // 플러시를 통해 쓰기 작업을 강제로 데이터베이스에 반영
            status.flush();

            // 비즈니스 로직에 따른 롤백 전용 설정
            if (/* 어떤 조건 */) { status.setRollbackOnly(); }

            // 롤백 전용 상태인지 확인
            if (status.isRollbackOnly()) {
                System.out.println("트랜잭션은 롤백 전용 상태로 설정되었습니다.");
                // 명시적으로 롤백
                transactionManager.rollback(status);
                return;
            }

            // 트랜잭션 커밋
            transactionManager.commit(status);
        } catch (Exception e) {
            // 예외 발생 시 트랜잭션 롤백
            transactionManager.rollback(status);
        }
    }
}

 

4. TransactionManager (PlatformTransactionManager)

  • Spring 트랜잭션 관리의 핵심
장점 설명
추상화
기술이나 트랜잭션 종류에 상관없이 동일한 방식으로 트랜잭션 처리 가능 (@Transactional 등)
비침투적
비즈니스 로직에 트랜잭션 처리 코드가 개입되지 않음 → 코드가 깔끔하고 유지보수 용이 (try-catch-finally 없음)

 

코드) PlatformTransactionManager

더보기
public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

 

Implementataion

TransactionManager 종류
설명 트랜잭션 범위 주요 연동 대상
DataSourceTransactionManager JDBC 기반 데이터 소스 트랜잭션 관리 Local Transaction JDBC
DataSource
HibernateTransactionManager Hibernate와 연동된 트랜잭션 관리
(start, commit, rollback 직접 제어)
Local Transaction Hibernate
SessionFactory
JpaTransactionManager JPA 전용 트랜잭션 매니저
(@Transactional로 메서드 단위 관리)
Local Transaction JPA
EntityManagerFactory
JtaTransactionManager 분산 트랜잭션 관리
여러 리소스 참여 가능
Global Transaction
JTA
여러 트랜잭션 리소스

 

예제) HibernateTransactionManager

더보기
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {

    @Autowired
    private DataSource dataSource; // DataSource는 DB 연결을 관리

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setPackagesToScan("com.example.myapp.entity"); // 엔티티 클래스가 있는 패키지 설정
        sessionFactory.setHibernateProperties(hibernateProperties());
        return sessionFactory;
    }

    @Bean
    public PlatformTransactionManager hibernateTransactionManager(SessionFactory sessionFactory) {
        return new HibernateTransactionManager(sessionFactory);
    }

    private Properties hibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
        properties.put("hibernate.show_sql", "true");
        properties.put("hibernate.format_sql", "true");
        properties.put("hibernate.hbm2ddl.auto", "update");
        return properties;
    }
}

LocalSessionFactoryBean

  • SessionFactory 설정 및 초기화
  • Spring 컨테이너에 의해 트랜잭션을 관리할 수 있음 (PlatformTransactionManager와 통합됨)

 

예제) JpaTransactionManager

더보기
@Configuration
@EnableTransactionManagement
public class DatabaseConfig {

    @Autowired
    private DataSource dataSource; // DataSource는 DB 연결을 관리

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.example.myapp.entity"); // JPA 엔티티 클래스 패키지 스캔

        // Hibernate JPA 공급자 설정
        em.setJpaVendorAdapter(new org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter());

        return em;
    }

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

LocalContainerEntityManagerFactoryBean

  • JPA의 EntityManagerFactory 설정
  • JPA와 Hibernate를 쉽게 연동하도록 도와줌

 

예제) JtaTransactionManager

더보기
@Bean
public JtaTransactionManager transactionManager() {
    return new JtaTransactionManager();
}

 

5. DataSource

  • 데이터베이스 연결 관리

예제) DataSource

더보기
@Bean
public DataSource dataSource() {
    DriverManagerDataSource ds = new DriverManagerDataSource();
    ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
    ds.setUrl("jdbc:mysql://localhost:3306/spring_dataaccess?serverTimezone=UTC");
    ds.setUsername("sa");
    ds.setPassword("12345678");
    return ds;
}

 


출처