우당탕탕

[Spring] 스프링 @Transactional(트랜잭션)에 대해 이해하기 본문

Tech/Spring

[Spring] 스프링 @Transactional(트랜잭션)에 대해 이해하기

모찌모찝 2024. 9. 23. 06:48
@Transactional ( 트랜잭션 )에 대한 이해

트랜잭션의 개념

트랜잭션은 데이터베이스에서 수행되는 작업의 묶음으로, 일련의 작업이 모두 성공적으로 완료되거나 모두 실패해야 하는 특성을 가집니다. 이를 ACID 속성이라고 하며, 아래와 같은 특징을 가집니다

  1. 원자성(Atomicity): 트랜잭션 내의 모든 작업이 성공해야만 데이터베이스에 반영됩니다. 하나라도 실패하면 전체가 롤백되어야 합니다. ( ALL OR NOTHING )
  2. 일관성(Consistency): 트랜잭션이 완료되면 데이터베이스는 일관된 상태를 유지해야 합니다.
  3. 격리성(Isolation): 동시에 실행되는 트랜잭션 간에 서로 영향을 미치지 않아야 합니다.
  4. 지속성(Durability): 트랜잭션이 성공적으로 완료된 후에는 그 결과가 영구적으로 저장되어야 합니다.

ACID

스프링의 @Transactional 어노테이션

스프링에서 @Transactional트랜잭션을 쉽게 관리할 수 있도록 도와주는 특별한 도구(어노테이션)입니다. 이 어노테이션을 사용하면 어떤 작업이 트랜잭션으로 묶여야 하는지를 스프링에게 알려줄 수 있게 됩니다.

예를 들어, 사용자를 데이터베이스에 저장하는 메서드에 @Transactional을 붙이면, 그 메서드가 실행되는 동안 모든 작업이 트랜잭션으로 처리됩니다. 만약 중간에 문제가 생기면, 스프링이 자동으로 롤백(이전상태로 돌림) 해 줍니다.

AOP와 @Transactional의 관계

AOPAspect-Oriented Programming의 약자입니다. 쉽게 말해, 프로그램의 특정 부분(예: 트랜잭션 관리)을 다른 코드와 분리해서 관리하는 방법입니다.

스프링에서 @Transactional은 기본적으로 AOP를 사용하여 동작합니다. AOP를 통해 트랜잭션을 관리하면, 코드의 여러 부분에서 트랜잭션 관리 로직을 중복해서 작성할 필요가 없습니다. 대신, 트랜잭션이 필요한 메서드에 @Transactional을 붙이기만 하면 스프링이 자동으로 트랜잭션을 시작하고 종료해 줍니다.

AOP는 "관심사 분리"를 통해 코드의 가독성과 유지 보수성을 높여줍니다. 즉, 트랜잭션 관리처럼 반복적으로 필요한 작업을 한 곳에서 처리하고, 비즈니스 로직은 더 간결하게 유지할 수 있도록 도와주는 것이죠.

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional // 트랜잭션 어노테이션 선언
    public void createUser(String name, String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        
        userRepository.save(user);

        // 예외 발생 시 트랜잭션 롤백을 테스트할 수 있습니다.
        if (email.equals("error@example.com")) {
            throw new RuntimeException("Rollback transaction");
        }
    }
}

트랜잭션을 처리하는 createUser라는 메서드에 @Transactional을 적용시킨 코드입니다. 정상적인 이메일이 createUser 메서드에 들어가는 경우 정상적으로 데이터베이스에 저장이 되지만 예외가 발생시에는 @Transactional에 의해 롤백처리됩니다.

트랜잭션 전파방식 (Propagation)

전파 방식은 트랜잭션이 어떻게 전파되는지를 결정하는 설정입니다. 여러 메서드가 호출될 때, 한 메서드에서 트랜잭션이 시작되면 그 트랜잭션을 다른 메서드가 어떻게 사용할지를 정의합니다.

  1. REQUIRED: 현재 트랜잭션이 있다면 그 트랜잭션을 사용하고, 없다면 새로 시작합니다.
    • 간단 예시: 친구가 이미 아이스크림을 사기 위해 돈을 모아두고 있으면, 그 돈을 사용해서 아이스크림을 사는 것.
    •  
    • @Transactional(propagation = Propagation.REQUIRED)
  2. REQUIRES_NEW: 항상 새로운 트랜잭션을 시작합니다. 현재 트랜잭션이 있다면 일시 중지하고 새로 시작합니다.
    • 간단 예시: 친구가 새로운 게임을 시작하길 원하면, 기존 게임을 잠시 멈추고 새로운 게임을 시작하는 것.
    • @Transactional(propagation = Propagation.REQUIRES_NEW)
  3. NESTED: 중첩된 트랜잭션을 지원합니다. 부모 트랜잭션이 끝날 때 자식 트랜잭션도 같이 끝납니다.
    • 간단 예시: 부모가 요리를 하면서 자녀가 간식을 준비하면, 부모가 요리를 마치면 자녀의 간식도 같이 마무리되는 것.
    • @Transactional(propagation = Propagation.NESTED)

트랜잭션 격리 수준 (Isolation)

격리 수준은 여러 트랜잭션이 동시에 실행될 때 데이터의 일관성을 어떻게 유지할지를 결정합니다. 주요 격리 수준은 다음과 같습니다:

  1. DEFAULT: 데이터베이스의 기본 격리 수준을 사용합니다.
    • @Transactional(isolation = Isolation.DEFAULT)
  2. READ_UNCOMMITTED: 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있습니다. 마치 친구가 아직 사지 않은 아이스크림을 미리 보는 것과 비슷합니다.
    • @Transactional(isolation = Isolation.READ_UNCOMMITTED)
  3. READ_COMMITTED: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있습니다. 친구가 아이스크림을 사고 나서야 그 아이스크림을 확인할 수 있는 것이죠.
    • @Transactional(isolation = Isolation.READ_COMMITTED)
  4. REPEATABLE_READ: 같은 트랜잭션 내에서 동일한 쿼리를 여러 번 수행할 경우 항상 같은 결과를 보장합니다.
    • @Transactional(isolation = Isolation.REPEATABLE_READ)
  5. SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션이 완전히 순차적으로 실행됩니다.
    • @Transactional(isolation = Isolation.SERIALIZABLE)

 

각 서비스의 특성과 업무 요구에 따라 적절한 트랜잭션 격리 수준과 전파 방식을 선택하는 것이 매우 중요합니다.
예를 들어, 데이터의 일관성이 최우선인 경우에는 높은 격리 수준을 적용해야 할 수 있으며, 여러 작업이 동시에 실행되는 환경에서는 적절한 전파 방식을 고려해야 합니다. 이러한 선택은 애플리케이션의 성능과 안정성에 큰 영향을 미칠 수 있으니 알맞게 선택하여 사용하시면 될 것 같습니다!

 

Comments