[F-Lab 멘토링 학습]

스프링 @Transactional어노테이션의 동작원리와 전파 속성들

everydeveloper 2023. 9. 26. 20:40

스프링 @Transactional어노테이션의 동작원리와 전파 속성들

스프링 @Transactional어노테이션의 동작원리와 전파 속성들

스프링 @Transactional어노테이션

스프링의 @Transactional 어노테이션은 트랜잭션 관리를 선언적으로 처리할 수 있게 해줍니다. 이 어노테이션을 메소드나 클래스에 붙이면 그 범위에서 실행되는 작업은 트랜잭션으로 처리됩니다.

주요 특징

  1. 원자성(Atomicity): 어노테이션을 사용한 메소드 내의 작업은 모두 성공하거나 실패합니다.
  2. 일관성(Consistency): 트랜잭션이 성공적으로 커밋되면, 데이터는 일관된 상태를 유지합니다.
  3. 격리성(Isolation): 동시에 여러 트랜잭션이 발생하더라도 각 트랜잭션은 독립적입니다.
  4. 지속성(Durability): 트랜잭션이 성공적으로 커밋되면, 해당 변경사항은 데이터베이스에 영구적으로 저장됩니다.

사용 예시

@Service
public class MyService {

    @Autowired
    private MyRepository repository;

    @Transactional
    public void doSomething() {
        repository.save(new Entity());
        // 여기서 예외가 발생하면 위의 save도 롤백됩니다.
    }
}

주의사항

  1. Propagation: 트랜잭션의 전파 방식을 설정할 수 있습니다. 예를 들어, REQUIRED, REQUIRES_NEW, NESTED 등이 있습니다.
  2. Isolation: 데이터베이스의 격리 수준을 설정할 수 있습니다. 예를 들어, READ_COMMITTED, READ_UNCOMMITTED, SERIALIZABLE 등이 있습니다.
  3. Timeout: 트랜잭션의 타임아웃 시간을 설정할 수 있습니다. 타임아웃이 되면 트랜잭션은 자동으로 롤백됩니다.
  4. Read-only: 트랜잭션이 읽기 전용이라면 이를 설정하여 성능을 높일 수 있습니다.

추가 설정

  • rollbackFor: 롤백을 수행할 예외 클래스를 지정할 수 있습니다.
  • noRollbackFor: 롤백을 하지 않을 예외 클래스를 지정할 수 있습니다.

예를 들어,

@Transactional(rollbackFor = Exception.class, noRollbackFor = SomeSpecificException.class)
public void someMethod() {
  // ...
}

@Transactional은 매우 강력하지만 제대로 사용하지 않으면 예상치 못한 문제를 야기할 수 있습니다. 따라서 그 작동 방식과 옵션을 정확히 이해하는 것이 중요합니다.

@Transactional 어노테이션 - 좀 더

Propagation 속성

  1. REQUIRED: 부모 트랜잭션이 존재하면 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
  2. REQUIRES_NEW: 항상 새 트랜잭션을 시작하며, 부모 트랜잭션이 있으면 일시 중단합니다.
  3. NESTED: 부모 트랜잭션이 존재하면 중첩 트랜잭션을 시작합니다.

Isolation 속성

  1. READ_COMMITTED: 커밋된 데이터만 읽습니다. Dirty Read를 방지합니다.
  2. READ_UNCOMMITTED: 커밋되지 않은 데이터도 읽습니다.
  3. REPEATABLE_READ: 트랜잭션 동안 같은 레코드의 여러 번의 읽기를 허용합니다.
  4. SERIALIZABLE: 트랜잭션이 완전히 격리될 수 있도록 합니다. 가장 높은 격리 수준이지만 성능에 부담이 될 수 있습니다.

Timeout

  • timeout: 트랜잭션의 최대 실행 시간을 지정합니다. 이 시간이 초과하면 자동으로 롤백됩니다.

예제

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 120)
public void exampleMethod() {
    // 메소드 로직
}

추가적인 정보

  1. readOnly: 이 트랜잭션이 읽기 전용인 경우 성능 최적화가 가능합니다.
  2. rollbackFor: 특정 예외에 대해서만 롤백을 실행하려면 이 속성을 사용하세요.
  3. noRollbackFor: 특정 예외에 대해서는 롤백을 하지 않으려면 이 속성을 사용하세요.

트랜잭션의 경계

트랜잭션의 경계는 어디에서 설정해야 할까요? 일반적으로 서비스 계층에서 트랜잭션을 관리합니다. 비즈니스 로직이 복잡하게 얽혀 있을 수 있기 때문에 DAO 계층보다는 서비스 계층에서 트랜잭션을 처리하는 것이 좋습니다.

AOP와의 관계

@Transactional은 AOP(Aspect-Oriented Programming)를 기반으로 작동합니다. 이 어노테이션은 프록시 기반의 AOP를 사용하여 트랜잭션을 관리합니다. 따라서 @Transactional이 붙은 메소드가 다른 메소드 내에서 직접 호출되면 트랜잭션 처리가 제대로 되지 않을 수 있습니다.

이러한 세부 사항을 정확히 이해하고 적용하는 것이 중요합니다. @Transactional은 강력한 기능을 제공하지만 잘못 사용하면 데이터 무결성 문제 등 여러 문제를 일으킬 수 있습니다.

@Transactional어노테이션의 동작원리

@Transactional 어노테이션의 동작 원리를 이해하기 위해서는 스프링의 AOP(Aspect-Oriented Programming)와 프록시에 대한 이해가 필요합니다. 다음은 주요 동작 원리입니다:

AOP와 프록시

  1. 프록시 생성: @Transactional이 붙은 클래스 또는 메서드를 스프링이 인식하면, 해당 클래스 또는 메서드에 대한 프록시 객체를 생성합니다.
  2. 메서드 호출: 프록시를 통해 원래의 메서드가 호출될 때, 트랜잭션 관련 로직이 먼저 실행됩니다.

트랜잭션 관리

  1. 트랜잭션 시작: 메서드 호출 시, 새로운 트랜잭션을 시작하거나 이미 존재하는 트랜잭션에 참여합니다. 이는 Propagation 설정에 따라 달라집니다.
  2. 트랜잭션 실행: 실제 비즈니스 로직이 실행됩니다. 이 과정에서 데이터베이스 연산이 발생하면, 이 연산은 트랜잭션에 의해 관리됩니다.
  3. 예외 처리: 메서드 실행 중 예외가 발생하면, rollbackFor 또는 noRollbackFor 설정에 따라 트랜잭션을 롤백하거나 커밋합니다.
  4. 트랜잭션 종료: 메서드 실행이 끝나면 트랜잭션을 커밋하거나 롤백합니다. 이는 메서드의 성공 여부와 설정된 속성에 따라 달라집니다.

내부 동작

  1. PlatformTransactionManager: 이 인터페이스를 구현한 객체가 실제 트랜잭션을 관리합니다. 대표적으로 DataSourceTransactionManager가 있습니다.
  2. TransactionAspectSupport: @Transactional 어노테이션을 처리하는 내부 클래스입니다. 이 클래스는 트랜잭션의 시작, 롤백, 커밋 등을 담당합니다.
  3. TransactionInterceptor: 실제로 AOP 프록시가 호출하는 인터셉터입니다. 이 인터셉터를 통해 TransactionAspectSupport가 호출되고, 트랜잭션 로직이 수행됩니다.

예시

// AOP 프록시를 통해 이 메서드가 호출됩니다.
@Transactional
public void someTransactionalMethod() {
    // 비즈니스 로직
}

  1. someTransactionalMethod가 호출되면, 프록시는 먼저 TransactionInterceptor를 실행합니다.
  2. TransactionInterceptor는 TransactionAspectSupport를 사용하여 트랜잭션을 시작하거나 기존 트랜잭션에 참여합니다.
  3. 비즈니스 로직이 실행되고, 예외가 발생하면 설정에 따라 롤백하거나 커밋합니다.
  4. 메서드가 정상적으로 종료되면, TransactionAspectSupport를 통해 트랜잭션을 커밋합니다.

@Transactional 어노테이션은 이러한 복잡한 트랜잭션 관리 작업을 개발자로부터 추상화하여, 선언적으로 편리하게 트랜잭션을 관리할 수 있게 도와줍니다.

트랜잭션의 원자성

트랜잭션의 원자성(Atomicity)은 "모든 것 또는 아무 것도"의 원칙을 의미합니다. 즉, 하나의 트랜잭션 내에서 수행되는 일련의 연산들이 중간 단계에서 실패하면, 트랜잭션 전체가 실패하고 모든 변경 사항은 롤백되어야 합니다. 반대로 모든 연산이 성공적으로 수행되면, 트랜잭션은 커밋되고 모든 변경 사항이 데이터베이스에 반영됩니다.

원자성이 필요한 이유

  1. 데이터 무결성 보장: 원자성을 통해 여러 연산이 하나의 단위로 처리되기 때문에, 데이터베이스는 항상 일관된 상태를 유지합니다.
  2. 오류 복구: 원자성을 통해 실패한 트랜잭션을 쉽게 롤백할 수 있습니다. 이를 통해 시스템은 예상치 못한 오류나 실패 상황에서도 데이터 무결성을 유지할 수 있습니다.

원자성의 구현

  1. Two-Phase Commit (2PC): 두 단계 커밋 프로토콜은 트랜잭션 참여자들이 먼저 커밋 준비를 하고, 준비가 완료되면 실제 커밋을 수행하는 방식입니다.
  2. Savepoint: 트랜잭션 내에서 중간 상태를 저장하여, 필요한 경우 해당 상태로 롤백할 수 있게 하는 기능입니다.
  3. Locking Mechanisms: 락을 사용하여 동시에 여러 트랜잭션이 같은 데이터에 접근하는 것을 제어합니다.
  4. Logging and Recovery: 변경 사항과 트랜잭션 상태를 로그에 기록하여, 시스템 장애가 발생한 경우 원래 상태로 복구할 수 있습니다.

스프링에서의 원자성

스프링에서는 @Transactional 어노테이션을 사용하여 원자성을 보장합니다. 이 어노테이션이 붙은 메서드가 호출되면, 스프링은 이 메서드 내에서 발생하는 모든 데이터베이스 연산을 하나의 트랜잭션으로 처리합니다. 만약 이 과정에서 예외가 발생하면 자동으로 롤백을 수행합니다.

예:

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transfer(Long fromId, Long toId, int amount) {
        Account fromAccount = accountRepository.findById(fromId);
        Account toAccount = accountRepository.findById(toId);

        fromAccount.setBalance(fromAccount.getBalance() - amount);
        toAccount.setBalance(toAccount.getBalance() + amount);

        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
    }
}

위 코드에서 transfer 메서드 내에서 발생하는 모든 연산은 하나의 트랜잭션으로 묶여 있으므로, 중간에 오류가 발생하면 변경사항은 롤백됩니다.

원자성은 데이터 무결성을 보장하는 중요한 특성 중 하나입니다. 따라서 트랜잭션 로직을 설계할 때 이 원칙을 반드시 고려해야 합니다.

일관성은 데이터가 테이블에서 미리 정한 제약조건과 not null, unique등 여러 조건 등 미리 정한 조건안에서 틀에서 벗어나지 않으면서 데이터는 변 할 수 있는 그정도는 말하는 건 아니네 미리 정한 틀 안에서 그 틀을 꺠지 않는 데이터의 속성

'일관성(Consistency)'은 데이터가 변하더라도 그 변화가 미리 정의된 규칙이나 무결성 제약 조건(예: NOT NULL, UNIQUE 등)을 준수해야 한다는 원칙입니다. 이러한 제약 조건이나 규칙은 데이터베이스 설계 시점이나 애플리케이션 로직에서 정의됩니다.

즉, '일관성'은 데이터가 변할 수 있되, 그 변화가 일어날 때 특정 조건을 만족시켜야 함을 의미합니다. 예를 들어, UNIQUE 제약이 있는 컬럼에 동일한 값을 가진 두 개의 레코드가 존재하면 안 되고, NOT NULL 제약이 있는 컬럼에는 NULL 값이 들어갈 수 없습니다.

데이터의 변화는 허용되지만, 그 변화는 항상 일정한 '틀' 또는 '범위' 내에서 이루어져야 하며, 이를 통해 데이터의 일관성과 무결성이 유지됩니다.

격리성

'격리성(Isolation)'은 데이터베이스 트랜잭션에서 중요한 개념 중 하나입니다. 격리성은 여러 트랜잭션이 동시에 실행될 때 하나의 트랜잭션이 다른 트랜잭션에게 영향을 주지 않도록 보장하는 속성입니다. 이를 통해 각 트랜잭션은 독립적인 작업을 수행할 수 있습니다.

예를 들어, 두 개의 트랜잭션이 동시에 같은 데이터를 변경하려고 할 경우, 격리성이 보장되지 않으면 데이터의 일관성이 깨질 수 있습니다. 따라서 격리성을 보장하기 위한 다양한 레벨의 '격리 수준(Isolation Level)'이 있습니다. 대표적인 격리 수준은 다음과 같습니다:

  1. READ UNCOMMITTED: 가장 낮은 격리 수준으로, 다른 트랜잭션에서 변경 중인 데이터를 읽을 수 있습니다. 이렇게 되면 일관성이 깨질 위험이 높습니다.
  2. READ COMMITTED: 이 격리 수준에서는 커밋된 데이터만 읽을 수 있습니다. 여전히 'Non-repeatable reads'와 'Phantom reads' 같은 문제가 발생할 수 있습니다.
  3. REPEATABLE READ: 한 트랜잭션 내에서 같은 쿼리를 여러 번 실행하더라도 처음 읽은 데이터와 동일한 데이터를 읽을 수 있습니다. 하지만 새로운 데이터의 삽입(Phantom reads)은 막을 수 없습니다.
  4. SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 완전히 독립적으로 실행되도록 보장합니다. 이는 성능에 부담을 줄 수 있지만, 데이터의 일관성은 가장 높게 유지됩니다.

이러한 격리 수준을 통해 데이터베이스는 동시성과 데이터 일관성 사이의 균형을 찾으려고 합니다. 격리성이 높을수록 데이터의 일관성은 높아지지만, 동시성은 떨어질 수 있습니다. 따라서 애플리케이션의 요구 사항과 특성에 맞는 적절한 격리 수준을 선택하는 것이 중요합니다.

격리성이 보장되지 않으면 데이터의 일관성이 깨질 수 있습니다이 맞는 말인 원리

'격리성(Isolation)'이 보장되지 않으면 여러 트랜잭션이 동시에 실행될 때 한 트랜잭션의 중간 결과가 다른 트랜잭션에게 영향을 줄 수 있습니다. 이러한 상황에서 데이터의 '일관성(Consistency)'이 깨질 위험이 있습니다. 다음은 그 원리에 대한 몇 가지 예입니다.

  1. Dirty Read: 한 트랜잭션 A가 데이터를 변경했지만 아직 커밋하지 않았을 때, 다른 트랜잭션 B가 그 변경된 데이터를 읽는 경우입니다. 만약 A 트랜잭션이 롤백되면 B 트랜잭션은 잘못된 데이터를 읽게 됩니다.
  2. Non-Repeatable Read: 한 트랜잭션 A가 데이터를 읽은 후, 다른 트랜잭션 B가 그 데이터를 변경하거나 삭제하고 커밋한 경우, A 트랜잭션 내에서 동일한 데이터를 다시 읽을 때 일관성이 깨집니다.
  3. Phantom Read: 한 트랜잭션 A가 특정 조건으로 데이터를 읽은 후, 다른 트랜잭션 B가 그 조건에 맞는 새로운 데이터를 삽입하고 커밋하는 경우, A 트랜잭션에서는 처음에 없던 데이터가 나타나게 됩니다.

이런 문제들 때문에 격리성을 제대로 보장하지 않으면 다수의 트랜잭션이 동시에 실행될 때 데이터의 일관성이 깨질 수 있습니다. 따라서 데이터베이스 관리 시스템(DBMS)에서는 다양한 격리 수준을 제공하여 이러한 문제를 최소화하려고 합니다.

격리성을 완전히 통제 관리하면 좋겠지만 그게 현실적으로 어려우니까 여러 격리단계로 나눠서 하는 것

완벽한 격리성을 보장하려면 모든 트랜잭션을 순차적으로 실행해야 하기 때문에 성능이 크게 저하될 수 있습니다. 이러한 이유로 데이터베이스 관리 시스템(DBMS)은 다양한 격리 수준(Isolation Level)을 제공합니다. 이렇게 하면 애플리케이션의 요구 사항과 성능, 그리고 데이터의 일관성 사이에서 적절한 균형을 찾을 수 있습니다.

여러 격리 수준을 제공하는 것은 데이터의 일관성과 시스템의 동시성(concurrency) 사이에서 트레이드오프를 관리하기 위한 방법입니다. 예를 들어, "SERIALIZABLE" 격리 수준은 일관성은 최대한 보장하지만 동시성이 떨어질 수 있습니다. 반면 "READ UNCOMMITTED" 격리 수준은 동시성은 높지만 일관성이 떨어질 위험이 있습니다.

따라서 실제 시스템에서는 이러한 트레이드오프를 고려하여 가장 적합한 격리 수준을 선택하게 됩니다. 선택된 격리 수준은 애플리케이션의 비즈니스 로직, 데이터 접근 패턴, 성능 요구 사항 등에 따라 달라질 수 있습니다.

@Transactional어노테이션의 전파 속성들

Spring Framework에서 @Transactional 어노테이션은 트랜잭션의 전파 속성(Propagation)을 설정할 수 있게 해줍니다. 전파 속성은 한 트랜잭션 메서드가 다른 트랜잭션 메서드를 호출할 때 어떻게 행동해야 하는지를 정의합니다. 아래는 주요 전파 속성에 대한 설명입니다.

  1. REQUIRED: 디폴트 값입니다. 호출한 메서드에 이미 트랜잭션이 있으면 그 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작합니다.
  2. SUPPORTS: 이미 존재하는 트랜잭션이 있으면 그 트랜잭션을 사용합니다. 트랜잭션이 없으면 트랜잭션 없이 실행됩니다.
  3. MANDATORY: 이미 존재하는 트랜잭션이 있어야만 메서드가 실행됩니다. 없으면 예외가 발생합니다.
  4. REQUIRES_NEW: 항상 새로운 트랜잭션을 시작합니다. 이미 있는 트랜잭션은 일시 중단됩니다.
  5. NOT_SUPPORTED: 트랜잭션을 사용하지 않고 실행됩니다. 이미 있는 트랜잭션은 일시 중단됩니다.
  6. NEVER: 트랜잭션을 사용하지 않고 실행됩니다. 이미 있는 트랜잭션이 있으면 예외가 발생합니다.
  7. NESTED: 이미 존재하는 트랜잭션이 있으면 중첩 트랜잭션을 시작합니다. 중첩 트랜잭션은 외부 트랜잭션이 롤백되어도 독립적으로 커밋 또는 롤백할 수 있습니다.

이러한 전파 속성을 적절히 사용하면 여러 비즈니스 로직과 작업을 조합할 때 트랜잭션의 범위와 동작을 유연하게 관리할 수 있습니다. 따라서 애플리케이션의 복잡성과 요구 사항에 따라 적절한 전파 속성을 선택해야 합니다.