트랜잭션(Transaction)이란?
트랜잭션은 데이터베이스의 작업의 단위로, 일련의 작업들이 모두 성공하거나 실패해야 함을 보장한다.
트랜잭션을 설명할 때 가장 많이 드는 예시로 은행에서의 금액 송금을 예로 들 수 있다.
사진과 같이 사용자 1이 사용자 2에게 1000원을 송금할 때 벌어지는 일
- 총 두가지의 작업이 데이터베이스에 요청되게 된다.
- 사용자 1의 계좌에서 1000원을 차감한다.
- 사용자 2의 계좌에 1000원을 추가한다.
만약 이 두개의 작업이 하나의 단위로 묶여있지 않고 처리된다면 1번 혹은 2번의 작업 중 하나만 실패했을 때 심각한 문제가 발생하게 된다.
- 1번 작업 실패 : 사용자 1의 계좌에서 1000원이 차감되지 않았지만 사용자 2의 계좌에 1000원이 입금된다.
- 2번 작업 실패 : 사용자 1의 계좌에서 1000원이 차감되었지만 사용자 2의 계좌에 1000원이 입금되지 않는다.
서로 같은 작업으로 묶여있지 않은 경우 서로 다른 커넥션을 사용해 데이터베이스에 요청을 보내게 된다. 이때 데이터베이스는 별도의 세션으로 할당되어 개별 작업으로 처리된다.
따라서, 함께 처리되야 하는 작업은 트랜잭션으로 묶어 모두 처리되거나 모두 처리되지 말아야 한다.
하나의 커넥션을 이용해 데이터베이스에 요청을 보내면, 데이터베이스에서 하나의 세션으로 할당되어 하나의 작업으로 처리가 가능하다.
트랜잭션 관리 방식
프로그래밍 방식의 트랜잭션 관리
다음과 같이 직접 프로그래밍을 이용해 트랜잭션을 적용할 수 있다.
public void transferMoney(Long fromAccountId, Long toAccountId, Double amount) {
// 트랜잭션 정의
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setName("transferMoneyTransaction");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 송금 출발 계좌에서 돈을 인출
Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
fromAccount.withdraw(amount);
accountRepository.save(fromAccount);
// 송금 대상 계좌에 돈을 입금
Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
toAccount.deposit(amount);
accountRepository.save(toAccount);
// 트랜잭션 커밋
transactionManager.commit(status);
} catch (Exception e) {
// 트랜잭션 롤백
transactionManager.rollback(status);
throw e;
}
}
문제점
- 트랜잭션 로직을 생성할 때 마다 중복 코드가 많아지게 된다.
- 실제 서비스 로직 외 불필요한 코드가 많다.
- 실제 서비스 코드는 인출하고 입금하는 부분 뿐이다.
해결사는 AOP (Aspect-Oriented Programming)
AOP는 횡단 관심사(Cross-cutting Concerns)를 분리하여 코드의 모듈성을 높이는 프로그래밍 패러다임이다.
AOP를 사용하면 비즈니스 로직과 무관한 보안, 로깅, 트랜잭션 관리 등의 기능을 별도의 '애스펙트'로 분리할 수 있다.
선언적 방식 (@Transactional 사용)
- @Transcational
Spring의 @Transactional은 AOP를 기반으로 동작한다. 해당 어노테이션이 적용된 메서드는 AOP의 프록시(Proxy) 메커니즘을 통해 트랜잭션 관리를 수행하게 된다. @Transactional이 붙은 메서드를 호출할 때, 해당 메서드를 포함하는 클래스를 프록시 객체로 감싼다. 이 프록시 객체는 메서드 호출 전후에 트랜잭션을 시작하고 종료하는 등의 어드바이스를 적용한다.
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, Double amount) {
// 송금 출발 계좌에서 돈을 인출
Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow();
fromAccount.withdraw(amount);
accountRepository.save(fromAccount);
// 송금 대상 계좌에 돈을 입금
Account toAccount = accountRepository.findById(toAccountId).orElseThrow();
toAccount.deposit(amount);
accountRepository.save(toAccount);
}
동작 원리
- 프록시 객체를 생성
- 메서드 호출 가로채기: 프록시는 @Transactional이 붙은 메서드 호출을 가져온다.
- 트랜잭션 시작: 프록시는 메서드 실행 전에 트랜잭션을 시작
- 메서드 실행: 실제 비즈니스 로직이 실행
- 트랜잭션 완료: 메서드 실행이 성공적으로 끝나면 트랜잭션을 커밋하고, 예외가 발생하면 롤백
Transcational 속성
propagation | 트랜잭션 전파 방식 설정 |
isolation | 트랜잭션 격리 수준 설정 |
timeout | 트랜잭션 제한 시간 설정 (기본값 -1 : 제한 없음) |
readOnly | 읽기 전용 트랜잭션 여부 설정 (기본값 false) |
rollbackFor | 특정 예외 발생 시 롤백 설정 |
propagation (전파 속성)
REQUIRED (기본 값) | 현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 생성 |
REQUIRES_NEW | 항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션이 존재하면 일시적으로 중단 |
MANDATORY | 현재 트랜잭션이 존재해야 하며, 없으면 예외를 발생 |
SUPPORTS | 현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행 |
NOT_SUPPORTED | 트랜잭션 없이 실행하며, 기존 트랜잭션이 존재하면 일시적으로 중단 |
NEVER | 트랜잭션 없이 실행되며, 트랜잭션이 존재하면 예외를 발생 |
NESTED | 현재 트랜잭션 안에서 중첩된 트랜잭션을 생성. 부모 트랜잭션이 롤백되면 중첩된 트랜잭션도 롤백 |
Isolation (격리 수준)
DEFAULT (기본 값) | 기본 데이터베이스 격리 수준을 준수한다. |
READ_UNCOMMITTED | 가장 낮은 격리 수준으로, 다른 트랜잭션의 커밋되지 않은 변경사항을 읽을 수 있다. |
READ_COMMITTED | 트랜잭션이 커밋된 데이터만 읽을 수 있다. |
REPEATABLE_READ | 트랜잭션이 시작된 시점의 데이터를 고정하여 읽는다. |
SERIALIZABLE | 가장 높은 격리 수준으로, 트랜잭션 간에 완전히 격리되어 동작한다. |
timeout (제한 시간)
- timeaout을 통해 트랜잭션 완료까지의 최대 시간을 초 단위로 지정할 수 있다.
- 이 시간안에 트랜잭션이 완료되지 않으면 트랜잭션이 롤백된다.
- 기본 값은 시간 제한 없음을 뜻하는 -1이다.
readOnly
- readOnly를 통해 트랜잭션이 오직 읽기 작업에만 사용될 것임을 명시할 수 있다.
- 기본 값은 false 이며 true로 사용시 다음과 같은 이점을 얻을 수 있다.
- 메모리 사용량 절감
- 변경 감지가 비활성화되므로 엔티티의 스냅샷을 저장할 필요가 없어 메모리 사용량이 줄어든다. 특히, 대량의 데이터를 읽어야 하는 작업에서 메모리 사용량 절감 효과가 크다.
- 데이터베이스 락 오버헤드 감소
- 읽기 전용 트랜잭션에서는 데이터베이스 락을 최소화하거나 피할 수 있어, 동시에 많은 읽기 작업이 수행되는 환경에서 성능이 향상된다.
rollbackFor
- rollbackFor 을 통해 지정된 예외가 발생할 때 트랜잭션이 롤백하도록 할 수 있다.
- 롤백할 예외의 클래스를 지정하여 사용한다.
@Transactional(rollbackFor = MyException.class)
'벡엔드 > SpringBoot' 카테고리의 다른 글
[Redis] 레디스를 활용한 Spring-boot 캐싱 적용 해결하기 (트러블 슈팅) (0) | 2024.12.02 |
---|---|
JPA, ORM이란? 기본 개념 잡기 (0) | 2023.07.26 |
[spring] 스프링, 스프링 부트란? what is Spring and Spring boot? (0) | 2023.07.21 |
[Spring] 스프링 Lombok 어노테이션 정리 (0) | 2023.07.21 |