스프링에서 TransactionManager에 대한 상세한 이해하기

서론

트랜잭션은 데이터베이스 작업에서 일관성과 무결성을 보장하기 위해 필수적인 개념입니다. 스프링 프레임워크에서는 이러한 트랜잭션 관리를 쉽게 할 수 있도록 TransactionManager라는 추상화된 개념을 제공합니다. 이 글에서는 TransactionManager가 무엇인지, 어떻게 동작하는지, 그리고 어떻게 설정하고 사용하는지에 대해 기초부터 상세하게 알아보겠습니다.


본론


1. 트랜잭션(Transaction)이란?

트랜잭션은 데이터베이스에서 **원자적(Atomic)**으로 수행되어야 하는 일련의 작업을 의미합니다. 즉, 여러 작업이 하나의 단위로 처리되어야 하며, 모두 성공하거나 모두 실패해야 합니다. 이는 데이터의 무결성을 유지하는 데 중요합니다.

1.1 트랜잭션의 4가지 특성(ACID)

Atomicity(원자성): 트랜잭션 내의 모든 작업이 완벽하게 수행되거나, 전혀 수행되지 않아야 합니다.

Consistency(일관성): 트랜잭션 전후에 데이터의 일관성이 유지되어야 합니다.

Isolation(격리성): 동시에 실행되는 트랜잭션들이 서로의 작업에 영향을 주지 않아야 합니다.

Durability(지속성): 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 반영되어야 합니다.


2. TransactionManager란?

TransactionManager는 스프링에서 트랜잭션을 관리하기 위한 인터페이스입니다. 다양한 데이터 접근 기술(JDBC, JPA, Hibernate 등)에 대해 일관된 트랜잭션 관리 기능을 제공합니다. 이를 통해 개발자는 구체적인 트랜잭션 관리 로직을 신경 쓰지 않고도 트랜잭션을 제어할 수 있습니다.


스프링은 PlatformTransactionManager 인터페이스를 통해 다양한 구현체를 제공하며, 각 구현체는 특정 데이터 접근 기술에 맞게 동작합니다.


3. 스프링의 TransactionManager 종류

3.1 DataSourceTransactionManager

JDBC를 사용하여 데이터베이스에 접근하는 경우로 MyBatis나 순수 MyBatis나 순수 JDBC를 사용하는 어플리케이션에서 사용하는 Manager입니다. DataSrouce를 반드시 설정해야 하며, JDBC 커넥션을 직접 관리하는 것을 주의해야 합니다.

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

3.2 JpaTransactionManager

JPA(Java Persistence AP)를 사용하여 데이터베이스에 접근하는 경우 사용되며 Spring Data Jpa나 Hibernate를 JPA 구현체로 사용하는 어플리케이션에서 주로 사용됩니다. EntityManagerFactory를 통해 엔티티 매니저를 생성하며, JPA 표준에 따라 동작합니다.

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTranscationManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(entityManagerFactory);
    return transactionManager;
}

3.3 HinbernateTransactionManager

Hibernate를 JPA 없이 직접 사용하여 데이터베이스에 접근하는 경우에 주로 사용합니다. Hinernate 고유의 기능을 사용할 때 적합하며 SessionFactory를 필요로 합니다.

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

3.4 JtaTransactionManager

분산 트랜잭션이나 글로벌 트랜잭션이 필요한 경우 사용하며, 여러개의 데이터 소스나 리소스 매니저를 사용하는 어플리케이션에서 사용합니다. JTA (Java Transaction API)를 구현한 어플리에키션 서버나 분산 트랜잭션 관리 시스템이 필요하여 사용시 충분한 학습이 필요합니다.

@Bean
public PlatformTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager) {
    return new JtaTransactionManager(userTransaction, transactionManager);
}

4. 스프링 부트에서 TransactionManager 설정하기

스프링 부트에서는 자동 설정(auto-configuration)을 통해 대부분의 경우에 적절한 TransactionManager를 자동으로 설정해줍니다. 하지만 여러 개의 데이터 소스를 사용하는 경우나 커스텀 설정이 필요한 경우에는 명시적으로 설정해야 합니다.

4.1 단일 데이터 소스의 경우

스프링 부트는 기본적으로 DataSourceTransactionManager나 JpaTransactionManager를 자동으로 설정합니다. 별도의 설정 없이도 @Transactional 어노테이션을 사용하여 트랜잭션을 관리할 수 있습니다.

4.2 다중 데이터 소스의 경우

여러 개의 데이터 소스를 사용하는 경우 각 데이터 소스에 대해 별도의 TransactionManager를 설정해야 합니다. @Primary 어노테이션을 사용하여 기본 TransactionManager를 지정할 수 있습니다. 각 트랜젝션 매니저는 해당하는 데이터 소스를 사용하도록 설정합니다.

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource firstDataSource() {
        // 첫 번째 데이터 소스 설정
    }

    @Bean
    public DataSource secondDataSource() {
        // 두 번째 데이터 소스 설정
    }

    @Bean
    @Primary
    public PlatformTransactionManager firstTransactionManager() {
        return new DataSourceTransactionManager(firstDataSource());
    }

    @Bean
    public PlatformTransactionManager secondTransactionManager() {
        return new DataSourceTransactionManager(secondDataSource());
    }
}

4.3 @Transactional에서 트랜잭션 매니저 지정하기

여러 개의 TransactionManager가 있는 경우 @Transactional 어노테이션에서 transactionManager 속성을 사용하여 어떤 매니저를 사용할지 지정할 수 있습니다. value로 지정하는 transactionManager 속성에는 TransactionManager를 bean으로 등록한 이름을 문자열로 기입합니다.

@Service
public class MyService {

    @Transactional(value="firstTransactionManager")
    public void methodUsingFirstDataSource() {
        // 첫 번째 데이터 소스 사용
    }

    @Transactional(value="secondTransactionManager")
    public void methodUsingSecondDataSource() {
        // 두 번째 데이터 소스 사용
    }
}

5. @Transactional과 TransactionManager의 관계 이해하기

@Transactional 어노테이션은 트랜잭션 범위를 선언적으로 지정하기 위한 방법입니다. 스프링은 TransactionManager를 사용하여 해당 범위 내의 트랜잭션을 관리합니다.

5.1 @Transactional 어노테이션의 주요 속성

  • propagation: 트랜잭션 전파 방식을 지정합니다.
  • isolation: 트랜잭션 격리 수준을 지정합니다.
  • timeout: 트랜잭션의 최대 지속 시간을 지정합니다(초 단위).
  • readOnly: 트랜잭션이 읽기 전용인지 여부를 지정합니다.
  • rollbackFor: 롤백을 유발할 예외를 지정합니다.
  • noRollbackFor: 롤백하지 않을 예외를 지정합니다.

5.2 트랜젝션 전파 (propagation)

  • REQUIRED: 기본 값으로, 현재 트랜잭션이 존재하면 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
  • REQUIRES_NEW: 항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션은 보류됩니다.
  • SUPPORTS: 현재 트랜잭션이 존재하면 참여하고, 없으면 트랜잭션 없이 실행합니다.
  • NOT_SUPPORTED: 트랜잭션 없이 실행하며, 기존 트랜잭션은 보류됩니다.
  • MANDATORY: 현재 트랜잭션이 존재해야 하며, 없으면 예외를 발생시킵니다.
  • NEVER: 트랜잭션이 존재하면 예외를 발생시킵니다.
  • NESTED: 중첩 트랜잭션을 시작합니다(특정 TransactionManager에서만 지원).

5.3 트랜젝션 격리 수준

  • DEFAULT: 데이터베이스의 기본 격리 수준을 사용합니다.
  • READ_UNCOMMITTED: 커밋되지 않은 데이터를 읽을 수 있습니다(Dirty Read 허용).
  • READ_COMMITTED: 커밋된 데이터만 읽을 수 있습니다.
  • REPEATABLE_READ: 동일한 트랜잭션 내에서 동일한 데이터를 읽으면 항상 같은 결과를 반환합니다.
  • SERIALIZABLE: 가장 엄격한 격리 수준으로, 동시성은 떨어지지만 데이터 일관성은 높습니다.
/**
transactionManager: 사용할 트랜잭션 매니저를 지정합니다.
propagation: 트랜잭션 전파 방식을 REQUIRED로 설정합니다.
isolation: 격리 수준을 READ_COMMITTED로 설정합니다.
timeout: 트랜잭션 최대 지속 시간을 30초로 설정합니다.
readOnly: 읽기 전용이 아님을 표시합니다.
rollbackFor: Exception이 발생하면 롤백합니다.
*/

@Transactional(
    transactionManager = "firstTransactionManager",
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 30,
    readOnly = false,
    rollbackFor = Exception.class
)
public void transactionalMethod() {
    // 트랜잭션이 필요한 로직
}

6. 프로그래밍 방식의 트랜잭션 관리하기

스프링은 선언적 트랜잭션 관리(@Transactional) 외에도 프로그래밍 방식으로 트랜잭션을 제어할 수 있는 방법을 제공합니다.

6.1 TransactionTemplate 사용하기

TransactionTemplate을 사용하면 프로그래밍 방식으로 트랜잭션을 관리할 수 있습니다. transactionTemplate.execute() 메소드는 트랜잭션을 시작하고, 콜백 내부의 로직을 실행합니다. 예외 발생시 자동으로 롤백합니다.

@Service
public class MyService {

    private final TransactionTemplate transactionTemplate;

    public MyService(PlatformTransactionManager transactionManager) {
        this.transactionTemplate = new TransactionTemplate(transactionManager);
    }

    public void executeInTransaction() {
        transactionTemplate.execute(status -> {
            // 트랜잭션 내에서 실행할 로직
            return null;
        });
    }
}

6.2 TransactionManager 직접 사용하기

PlatformTransactionManager를 직접 사용하여 트랜잭션을 제어할 수 있습니다. getTransaction() 메소드로 직접 트랜잭션을 시작하며 commit() 메소드로 트랜잭션을 커밋 혹은, rollback() 메소드로 롤백 합니다.

@Service
public class MyService {

    private final PlatformTransactionManager transactionManager;

    public MyService(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void executeInTransaction() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            // 트랜잭션 내에서 실행할 로직

            transactionManager.commit(status);
        } catch (Exception ex) {
            transactionManager.rollback(status);
            throw ex;
        }
    }
}

7. 심화 주제

7.1 ChainedTransactionManager

여러 개의 TransactionManager를 동시에 사용할 때 ChainedTransactionManager를 사용하여 트랜잭션을 체인으로 묶을 수 있습니다. ChainedTransactionManager는 순차적으로 트랜잭션을 관리하며, 하나의 트랜잭션이라도 실패하면 전체를 롤백합니다.

@Bean
public PlatformTransactionManager chainedTransactionManager() {
    return new ChainedTransactionManager(firstTransactionManager(), secondTransactionManager());
}

7.2 Custom TransactionManager

특정한 요구사항이 있는 경우 커스텀 트랜잭션 매니저를 구현할 수 있습니다. 트랜잭션 관리의 복잡성을 고려하여 충분한 이해가 있을 때만 구현하는 것을 권장드립니다.

public class CustomTransactionManager implements PlatformTransactionManager {
    @Override
    public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        // 커스텀 로직
    }

    @Override
    public void commit(TransactionStatus status) throws TransactionException {
        // 커밋 로직
    }

    @Override
    public void rollback(TransactionStatus status) throws TransactionException {
        // 롤백 로직
    }
}

8. 실전 팁

트랜잭션 관리는 애플리케이션의 안정성과 데이터의 무결성을 유지하는 데 핵심적인 역할을 합니다. 실무에서 트랜잭션을 효과적으로 관리하기 위해서는 몇 가지 중요한 사항들을 고려해야 합니다. 아래에서는 트랜잭션 관리 시 유의해야 할 실전 팁을 구체적인 상황과 예시를 통해 자세히 설명하겠습니다.

8.1 적절한 TransactionManager 선택하기

사용하는 데이터 접근 기술에 맞는 TransactionManager를 선택해야 합니다. 예를 들어, JPA를 사용하면서 DataSourceTransactionManager를 사용하면 예상치 못한 문제가 발생할 수 있습니다.

@Configuration
public class AppConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

위의 설정은 DataSourceTransactionManager를 사용하여 트랜잭션을 관리합니다. 그런데 애플리케이션에서 JPA를 사용하고 있다면, 이 설정은 적절하지 않습니다. JPA는 내부적으로 엔티티 매니저를 사용하며, DataSourceTransactionManager는 JDBC 커넥션을 직접 제어하기 때문에 트랜잭션이 제대로 동작하지 않을 수 있습니다. 때문에 JPA를 사용하는 경우 JpaTransactionManager를 사용해줍니다.

@Configuration
public class AppConfig {

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

이렇게 하므로써 JPA의 영속성 컨텍스트와 트랜잭션이 올바르게 연동될 수 있습니다. 각 데이터 접근 기술은 트랜잭션을 처리하는 방식이 다릅니다. 따라서 사용하는 기술에 맞는 TransactionManager를 선택해야 트랜잭션이 예상대로 동작하고 데이터의 무결성을 보장할 수 있습니다.

 

스프링 부트를 사용하면 대부분의 경우 자동으로 적절한 TransactionManager를 설정해주지만, 여러 데이터 소스를 사용하거나 복잡한 설정이 필요한 경우에는 직접 명시적으로 설정해야 합니다.

8.2 트랜잭션 범위 최소화하기

트랜잭션은 가능한 한 짧게 유지하는 것이 좋습니다. 트랜잭션 범위가 길어지면 데이터베이스 락(lock)으로 인해 동시성 문제가 발생할 수 있습니다.

@Transactional
public void processOrder() {
    // 사용자 입력 대기
    String userInput = getUserInput();

    // 비즈니스 로직 처리
    performBusinessLogic(userInput);

    // 외부 서비스 호출
    externalService.call();

    // 데이터베이스 업데이트
    updateDatabase();
}

위의 메소드에서는 트랜잭션이 시작된 이후 사용자 입력을 기다리고, 외부 서비스도 호출합니다. 이는 트랜잭션 범위를 불필요하게 넓혀 데이터베이스 커넥션과 락이 오래 유지되어 성능에 악영향을 미칩니다. 때문에 트랜잭션이 필요한 부분만 트랜잭션으로 감싸도록 메소드를 분리합니다.

public void processOrder() {
    // 사용자 입력 대기
    String userInput = getUserInput();

    // 비즈니스 로직 처리
    performBusinessLogic(userInput);

    // 외부 서비스 호출
    externalService.call();

    // 데이터베이스 업데이트
    updateDatabaseTransactional();
}

@Transactional
public void updateDatabaseTransactional() {
    // 데이터베이스 업데이트 로직
    updateDatabase();
}

이렇게 하면 트랜잭션 범위가 데이터베이스 업데이트 부분으로 제한되어 자원 사용을 최소화할 수 있습니다.

 

트랜잭션이 활성화된 동안에는 데이터베이스 커넥션과 자원이 점유되며, 데이터베이스 락이 걸릴 수 있습니다. 이는 다른 트랜잭션이 해당 자원에 접근하는 것을 방해하여 동시성 문제가 발생할 수 있습니다. 때문에 트랜잭션이 필요한 최소한의 범위를 식별하고, 그 부분만 트랜잭션으로 처리합니다. 이를 위해 비즈니스 로직을 잘 분리하고 설계하는 것이 중요합니다.

8.3 예외 처리 주의하기

기본적으로 RuntimeException과 Error가 발생할 때 트랜잭션이 롤백됩니다. 체크 예외(Exception)에 대해서도 롤백이 필요하면 rollbackFor 속성을 사용해야 합니다.

@Transactional
public void transferFunds(Account from, Account to, BigDecimal amount) {
    try {
        from.withdraw(amount);
        to.deposit(amount);
    } catch (InsufficientFundsException e) {
        // 잔액 부족 예외 처리
        log.error("잔액이 부족합니다.");
    }
}

위의 코드에서 InsufficientFundsException은 체크 예외이며, 트랜잭션은 기본적으로 체크 예외가 발생해도 롤백되지 않습니다. 또한 예외를 catch하여 처리했기 때문에 예외가 밖으로 전파되지 않아 트랜잭션이 커밋됩니다.

@Transactional
public void transferFunds(Account from, Account to, BigDecimal amount) throws InsufficientFundsException {
    from.withdraw(amount);
    to.deposit(amount);
}

때문에 이처럼 예외를 다시 던져서 InsufficientFundsException을 호출자에게 전달하여 트랜잭션이 롤백되도록 하는 방법이 있고,

@Transactional(rollbackFor = InsufficientFundsException.class)
public void transferFunds(Account from, Account to, BigDecimal amount) {
    try {
        from.withdraw(amount);
        to.deposit(amount);
    } catch (InsufficientFundsException e) {
        // 추가적인 예외 처리
        log.error("잔액이 부족합니다.");
        throw e; // 예외를 다시 던져 롤백되도록 함
    }
}

rollbackFor 속성을 사용하여 특정 체크 예외에 대해서도 롤백되도록 설정하는 방법 등이 있습니다.

 

트랜잭션은 예외 발생 시 자동으로 롤백되지만, 체크 예외에 대해서는 기본적으로 롤백되지 않습니다. 또한 예외를 catch하여 내부에서 처리하면 트랜잭션은 커밋됩니다. 이는 데이터의 무결성을 해칠 수 있습니다. 개인적인 생각이지만 예외를 적절히 처리하고, 필요한 경우 rollbackFor를 사용하여 롤백 대상을 명시적으로 지정합니다. 예외를 catch한 후 로그만 남기고 다시 예외를 던지는 것이 좋다고 생각합니다.

8.4 테스트환경에서의 트랜잭션 관리 주의하기

테스트 코드에서 @Transactional을 사용하면 기본적으로 테스트가 끝나면 롤백됩니다. 데이터베이스 상태를 확인하려면 @Rollback(false) 어노테이션을 사용하여 롤백을 방지할 수 있습니다.

8.5 데이터베이스 설정 확인하기

데이터베이스의 autoCommit 설정이 false로 되어 있어야 합니다. JDBC 드라이버와 데이터베이스 버전이 호환되는지 확인해야 합니다.

8.6 트랜잭션 전파(propagation) 속성 이해하기

복잡한 서비스 계층 구조에서 메소드 간에 트랜잭션 전파 방식이 잘못 설정되면 트랜잭션이 예상치 못하게 동작할 수 있습니다.

@Service
public class OuterService {

    @Autowired
    private InnerService innerService;

    @Transactional
    public void outerMethod() {
        // 일부 로직
        innerService.innerMethod();
        // 추가 로직
    }
}

@Service
public class InnerService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        // 트랜잭션이 필요한 로직
    }
}

위의 코드에서 innerMethod()는 Propagation.REQUIRES_NEW로 설정되어 새로운 트랜잭션을 시작합니다. 만약 innerMethod()에서 예외가 발생하여 롤백되더라도 outerMethod()의 트랜잭션은 영향을 받지 않고 커밋될 수 있습니다.

 

때문에 트랜잭션 전파 속성을 적절히 설정하여 의도한 대로 동작하도록 합니다. 만약 innerMethod()에서 예외가 발생하면 전체 트랜잭션을 롤백하고 싶다면 Propagation.REQUIRED를 사용합니다.

@Service
public class InnerService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void innerMethod() {
        // 트랜잭션이 필요한 로직
    }
}

트랜잭션 전파 속성은 메소드 간 트랜잭션의 전파 방식을 결정하며, 잘못 설정하면 데이터의 무결성이 훼손될 수 있습니다. 때문에 기본적으로 Propagation.REQUIRED를 사용하고, 특별한 이유가 있는 경우에만 다른 전파 속성을 사용합니다. 전파 속성을 설정할 때는 트랜잭션의 경계를 명확히 이해하고 설정해야 합니다.

8.7 트랜잭션 격리 수준 (isolation level) 고려하기

동시에 여러 트랜잭션이 수행될 때 데이터의 일관성을 유지하기 위해 트랜잭션 격리 수준을 조정해야 할 수 있습니다. 이 부분은 특정 상황을 예로 들어 보겠습니다. 아래 코드는 재고 수량을 조회하고 업데이트하는 트랜잭션이 동시에 발생하여 일관성 문제가 생기는 경우입니다. 두 개의 트랜잭션이 동시에 실행되면 currentStock이 동일하게 조회되어 재고가 음수가 될 수 있습니다.

@Transactional
public void updateStock(Long productId, int quantity) {
    int currentStock = stockRepository.findStockByProductId(productId);
    if (currentStock >= quantity) {
        stockRepository.updateStock(productId, currentStock - quantity);
    } else {
        throw new InsufficientStockException();
    }
}

때문에 격리 수준을 Isolation.SERIALIZABLE로 설정하여 트랜잭션 간 격리를 강화합니다.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateStock(Long productId, int quantity) {
    // 동일한 로직
}

격리 수준은 트랜잭션 간에 얼마나 데이터를 공유할 수 있는지를 결정합니다. 격리 수준이 낮으면 동시성은 높아지지만, 일관성 문제가 발생할 수 있습니다. 때문에 필요에 따라 격리 수준을 조정하되, 높은 격리 수준은 성능 저하를 유발할 수 있으므로 주의해야 합니다.

8.8 Lazy Initialization 예외 방지하기

@Transactional 메소드 내에서 지연 로딩된 엔티티를 서비스 계층 밖에서 접근하려고 하면 LazyInitializationException이 발생할 수 있습니다.

@Transactional
public Order getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    return order; // Order 엔티티의 연관 엔티티들은 아직 로딩되지 않음
}

// 컨트롤러나 뷰에서 order.getItems()를 호출하면 LazyInitializationException 발생

이에 대한 해결 방법은 여러가지가 있는데 3가지 방법을 소개하겠습니다.

 

첫 번째, 트랜젝션 범위 내에서 필요한 데이터를 모두 로드하는 방법입니다. size() 메소드는 연관 엔티티 로딩 이후 사용이 가능하기 때문에 적절하게 사용해준다면 lazy initialzation 예외를 방지하는데 도움이 됩니다.

@Transactional
public Order getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    order.getItems().size(); // 연관 엔티티 로딩
    return order;
}

두 번째,  페치 조인을 사용하는 방법입니다.

@Transactional
public Order getOrder(Long orderId) {
    return orderRepository.findWithItemsById(orderId);
}

// OrderRepository
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :orderId")
Order findWithItemsById(@Param("orderId") Long orderId);

세 번째, DTO로 변환하여 반환하는 방법입니다.

@Transactional
public OrderDto getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId);
    return new OrderDto(order);
}

트랜잭션 범위를 벗어난 후에 지연 로딩된 연관 엔티티에 접근하면 LazyInitializationException이 발생합니다. 이는 트랜잭션 관리와 엔티티의 생명 주기에 대한 이해 부족에서 비롯됩니다. 서비스 계층에서 필요한 데이터를 모두 로드하거나, DTO를 사용하여 필요한 데이터만 반환하는 것이 좋습니다. 필요하다면 트랜잭션 범위를 조정합니다.

8.9 프록시 모드 확인하기

스프링은 기본적으로 JDK 동적 프록시를 사용하며, 인터페이스 기반으로 프록시를 생성합니다. 클래스 기반 프록시(CGLIB)를 사용하려면 추가 설정이 필요합니다. 때문에 아래 코드는 인터페이스 없이 클래스를 직접 사용하기 때문에 트랜잭션이 적용되지 않을 수 있고, JDK 동적 프로시는 인터페이스가 없으므로 프록시를 생성하지 못하게 되는 현상이 발생합니다.

@Service
public class OrderService {

    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}

// 주입받는 곳
@Autowired
private OrderService orderService;

때문에 인터페이스를 도입하거나 프록시 모드를 CGLIB로 변경하는 등의 해결방법이 있고, 본인의 경우 인터페이스 기반 프로그래밍을 선호하는 방향성을 추천합니다.

// 인터페이스 도입
public interface OrderService {
    void processOrder();
}

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Transactional
    public void processOrder() {
        // 주문 처리 로직
    }
}


// 프록시모드 CGLIB로 변경
@EnableTransactionManagement(proxyTargetClass = true)
public class AppConfig {
    // 기타 설정
}

// 프록시모드 CGLIB로 변경2 - xml 형식의 spring
// properties file
spring.aop.proxy-target-class=true

 


결론

TransactionManager는 스프링에서 트랜잭션 관리를 추상화하여 제공하는 중요한 구성 요소입니다. 이 글에서는 TransactionManager의 역할과 종류, 그리고 설정과 사용 방법에 대해 기초부터 상세히 알아보았습니다. 트랜잭션은 애플리케이션의 데이터 무결성과 일관성을 유지하는 핵심적인 요소이므로, 정확한 이해와 적절한 사용이 필수적입니다.

이 글이 TransactionManager에 대한 이해를 높이고, 실무에서 올바르게 트랜잭션을 관리하는 데 도움이 되기를 바랍니다.