목차
Transaction 가이드
데이터베이스 기반 애플리케이션 개발에서 트랜잭션(Transaction)은 데이터의 일관성과 신뢰성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 Java 환경에서 트랜잭션을 완벽하게 이해하고 활용할 수 있도록 트랜잭션의 기본 개념부터 고급 기능, 주의사항까지 상세하게 다룹니다.
1. 트랜잭션이란 무엇인가?
트랜잭션은 데이터베이스의 상태를 변화시키기 위해 수행되는 작업의 논리적인 단위입니다. 이는 하나 이상의 데이터베이스 연산(SQL 쿼리 등)으로 구성될 수 있으며, 트랜잭션 내의 모든 연산은 전부 성공하거나 전부 실패해야 합니다. 이를 통해 데이터베이스의 무결성을 보장하고, 예기치 않은 오류나 시스템 장애 발생 시에도 데이터의 일관성을 유지할 수 있습니다.
예시: 은행 계좌 이체
- 작업 1: A 계좌에서 100만원을 차감
- 작업 2: B 계좌에 100만원을 입금
위 두 작업은 하나의 트랜잭션으로 묶여야 합니다. 만약 작업 1은 성공했지만 작업 2가 실패했다면, A 계좌에서는 돈이 빠져나갔지만 B 계좌에는 입금되지 않는 문제가 발생합니다. 트랜잭션을 사용하면 작업 2가 실패했을 때 작업 1도 자동으로 롤백(Rollback)되어 A 계좌의 잔액이 원래대로 복구됩니다.
2. 트랜잭션 주요 개념 및 트랜잭션 격리 레벨
2.1. 트랜잭션 주요 개념 (ACID)
트랜잭션은 ACID라 불리는 네 가지 중요한 속성을 가집니다.
- Atomicity (원자성): 트랜잭션 내의 모든 연산은 분리될 수 없는 하나의 단위로 취급됩니다. 즉, 트랜잭션이 완료되려면 모든 연산이 성공해야 하며, 하나라도 실패하면 전체 트랜잭션이 롤백됩니다.
- Consistency (일관성): 트랜잭션은 데이터베이스의 일관성 있는 상태를 유지해야 합니다. 트랜잭션이 시작되기 전과 완료된 후에도 데이터베이스는 미리 정의된 규칙과 제약 조건을 만족해야 합니다.
- Isolation (격리성): 동시에 실행되는 트랜잭션은 서로에게 영향을 주지 않아야 합니다. 각 트랜잭션은 마치 자신만이 데이터베이스에 접근하는 것처럼 격리된 환경에서 실행되어야 합니다.
- Durability (지속성): 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 데이터베이스에 저장되어야 합니다. 시스템 장애가 발생하더라도 데이터는 손실되지 않아야 합니다.
2.2. 트랜잭션 격리 레벨 (Isolation Level)
트랜잭션 격리 레벨은 동시에 실행되는 트랜잭션 간의 격리 정도를 정의합니다. 격리 레벨이 높을수록 데이터의 일관성은 높아지지만, 동시성은 낮아져 성능 저하를 유발할 수 있습니다. 반대로 격리 레벨이 낮을수록 동시성은 높아지지만, 데이터 불일치 문제가 발생할 가능성이 커집니다.
- READ UNCOMMITTED (커밋되지 않은 읽기): 가장 낮은 격리 레벨로, 다른 트랜잭션에서 아직 커밋되지 않은 데이터도 읽을 수 있습니다. 이 레벨에서는 Dirty Read, Non-Repeatable Read, Phantom Read 문제가 발생할 수 있습니다.
- READ COMMITTED (커밋된 읽기): 다른 트랜잭션에서 커밋된 데이터만 읽을 수 있습니다. Dirty Read 문제는 해결되지만, Non-Repeatable Read, Phantom Read 문제는 여전히 발생할 수 있습니다. 대부분의 데이터베이스 시스템에서 기본적으로 사용하는 격리 레벨입니다.
- REPEATABLE READ (반복 가능한 읽기): 트랜잭션이 시작된 후에는 동일한 데이터를 여러 번 읽어도 항상 같은 값을 반환합니다. Non-Repeatable Read 문제는 해결되지만, Phantom Read 문제는 여전히 발생할 수 있습니다. MySQL의 InnoDB 스토리지 엔진에서 기본적으로 사용하는 격리 레벨입니다.
- SERIALIZABLE (직렬화 가능): 가장 높은 격리 레벨로, 트랜잭션을 순차적으로 실행하는 것과 동일한 효과를 냅니다. 모든 동시성 문제가 해결되지만, 동시성이 크게 저하되어 성능에 심각한 영향을 미칠 수 있습니다.
각 격리 레벨별 발생 가능한 문제:
격리 레벨 | Dirty Read | Non-Repeatable Read | Phantom Read |
READ UNCOMMITTED | O | O | O |
READ COMMITTED | X | O | O |
REPEATABLE READ | X | X | O |
SERIALIZABLE | X | X | X |
- Dirty Read: 아직 커밋되지 않은 데이터를 다른 트랜잭션에서 읽는 현상
- Non-Repeatable Read: 트랜잭션 내에서 동일한 데이터를 여러 번 읽었는데, 다른 트랜잭션에서 해당 데이터를 수정하여 읽을 때마다 값이 달라지는 현상
- Phantom Read: 트랜잭션 내에서 특정 조건에 맞는 데이터를 검색했는데, 다른 트랜잭션에서 해당 조건을 만족하는 새로운 데이터를 삽입하여 다시 검색했을 때 결과가 달라지는 현상
2.3. 트랜잭션 격리 레벨 예시
트랜잭션 격리 레벨은 동시성 제어와 데이터 일관성 유지 사이의 균형을 맞추는 중요한 요소입니다. 각 격리 레벨별로 발생할 수 있는 문제를 실제 상황에 빗대어 이해하기 쉽도록 예시를 들어 설명하겠습니다.
등장인물:
- 앨리스 (Alice): 은행 직원, 계좌 관리 시스템 사용
- 밥 (Bob): 은행 고객, 온라인 뱅킹 시스템 사용
계좌 정보:
- 계좌 A: 잔액 100만원
- READ UNCOMMITTED (커밋되지 않은 읽기)
상황:
- 밥은 온라인 뱅킹 시스템을 통해 계좌 A에서 50만원을 송금하려고 합니다. 송금 트랜잭션이 시작되었지만, 아직 커밋되지 않은 상태입니다.
- 앨리스는 계좌 관리 시스템을 통해 계좌 A의 잔액을 확인합니다. READ UNCOMMITTED 격리 레벨에서는 아직 커밋되지 않은 밥의 송금 트랜잭션 결과를 앨리스가 읽을 수 있습니다.
- 앨리스는 계좌 A의 잔액이 50만원으로 표시되는 것을 확인합니다. (실제로는 100만원이지만, 밥의 트랜잭션이 반영된 임시 값)
- 하지만 밥의 송금 트랜잭션이 어떤 이유로 롤백됩니다.
- 앨리스는 다시 계좌 A의 잔액을 확인하면 100만원으로 돌아와 있습니다.
문제점 (Dirty Read):
앨리스는 밥의 트랜잭션이 커밋되기 전에 임시 값을 읽었기 때문에 잘못된 정보를 기반으로 업무 처리를 할 수 있습니다. 예를 들어, 앨리스가 50만원으로 표시된 잔액을 기준으로 추가 대출을 승인했다면, 롤백 이후 대출 승인에 문제가 발생할 수 있습니다.
- READ COMMITTED (커밋된 읽기)
상황:
- 밥은 온라인 뱅킹 시스템을 통해 계좌 A에서 50만원을 송금합니다. 송금 트랜잭션이 완료되어 커밋되었습니다.
- 앨리스는 계좌 관리 시스템을 통해 계좌 A의 잔액을 확인합니다. READ COMMITTED 격리 레벨에서는 밥의 송금 트랜잭션이 커밋된 후에만 앨리스가 변경된 잔액을 읽을 수 있습니다.
- 앨리스는 계좌 A의 잔액이 50만원으로 표시되는 것을 확인합니다.
- 잠시 후, 앨리스는 다시 계좌 A의 잔액을 확인합니다. 이번에는 다른 트랜잭션 (예: 이자 지급)이 발생하여 계좌 A의 잔액이 52만원으로 변경되었습니다.
- 앨리스는 계좌 A의 잔액이 50만원에서 52만원으로 변경된 것을 확인합니다.
문제점 (Non-Repeatable Read):
앨리스가 동일한 트랜잭션 내에서 계좌 A의 잔액을 여러 번 읽었는데, 다른 트랜잭션에 의해 값이 변경되어 읽을 때마다 값이 달라지는 문제가 발생합니다. 앨리스가 50만원을 기준으로 보고서를 작성했는데, 나중에 52만원으로 변경되어 보고서 내용의 신뢰성이 떨어질 수 있습니다.
- REPEATABLE READ (반복 가능한 읽기)
상황:
- 앨리스는 계좌 관리 시스템을 통해 계좌 A의 잔액을 확인하는 트랜잭션을 시작합니다. 계좌 A의 잔액은 현재 100만원입니다.
- 밥은 온라인 뱅킹 시스템을 통해 계좌 A에서 30만원을 송금하고, 트랜잭션을 커밋합니다.
- 앨리스는 다시 계좌 A의 잔액을 확인합니다. REPEATABLE READ 격리 레벨에서는 앨리스의 트랜잭션이 시작된 이후에 발생한 밥의 송금 트랜잭션이 앨리스의 트랜잭션에 영향을 미치지 않습니다.
- 앨리스는 여전히 계좌 A의 잔액이 100만원으로 표시되는 것을 확인합니다.
- 밥은 새로운 계좌를 개설하고, 계좌 B라고 명명합니다.
- 앨리스는 계좌 목록을 조회하는 쿼리를 실행합니다. REPEATABLE READ 격리 레벨에서는 앨리스의 트랜잭션이 시작된 이후에 생성된 계좌 B가 앨리스의 트랜잭션 결과에 반영되지 않습니다.
문제점 (Phantom Read):
앨리스의 트랜잭션 내에서 계좌 목록을 조회하는 쿼리를 실행했을 때, 다른 트랜잭션에서 새로운 계좌 (Phantom)가 추가되면, 앨리스가 다시 쿼리를 실행했을 때 결과가 달라지는 문제가 발생할 수 있습니다.
- SERIALIZABLE (직렬화 가능)
상황:
- 앨리스는 계좌 관리 시스템을 통해 계좌 A의 잔액을 확인하는 트랜잭션을 시작합니다.
- 밥은 온라인 뱅킹 시스템을 통해 계좌 A에서 50만원을 송금하려고 시도합니다. SERIALIZABLE 격리 레벨에서는 앨리스의 트랜잭션이 완료될 때까지 밥의 송금 트랜잭션은 대기 상태로 유지됩니다.
- 앨리스의 트랜잭션이 완료되면 밥의 송금 트랜잭션이 실행됩니다.
장점:
모든 동시성 문제가 해결됩니다. 앨리스와 밥은 마치 혼자서만 데이터베이스를 사용하는 것처럼 안전하게 트랜잭션을 처리할 수 있습니다.
단점:
동시성이 크게 저하되어 성능에 심각한 영향을 미칠 수 있습니다. 밥의 송금 트랜잭션은 앨리스의 트랜잭션이 완료될 때까지 기다려야 하므로 응답 시간이 길어질 수 있습니다.
각 격리 레벨은 데이터 일관성과 동시성 사이의 트레이드오프 관계를 가집니다. 애플리케이션의 요구 사항과 데이터베이스 시스템의 특성을 고려하여 적절한 격리 레벨을 선택해야 합니다. 일반적으로 READ COMMITTED 또는 REPEATABLE READ가 많이 사용되며, 특정 상황에 따라 SERIALIZABLE을 사용해야 할 수도 있습니다.
2.4 트랜잭션 전파 (Transaction Propagation)
트랜잭션 전파는 여러 트랜잭션이 호출될 때 트랜잭션 경계를 어떻게 관리할지 결정하는 설정입니다. Spring Framework에서 주로 사용되며, 다음과 같은 전파 옵션이 있습니다.
- REQUIRED: 이미 진행중인 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
- REQUIRES_NEW: 항상 새로운 트랜잭션을 시작합니다. 이미 진행중인 트랜잭션이 있다면 일시 중단됩니다.
- SUPPORTS: 이미 진행중인 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 진행합니다.
- NOT_SUPPORTED: 트랜잭션 없이 진행합니다. 이미 진행중인 트랜잭션이 있다면 일시 중단됩니다.
- MANDATORY: 반드시 진행중인 트랜잭션이 있어야 합니다. 없으면 예외가 발생합니다.
- NEVER: 트랜잭션 없이 진행합니다. 이미 진행중인 트랜잭션이 있다면 예외가 발생합니다.
- NESTED: 이미 진행중인 트랜잭션이 있으면 중첩된 트랜잭션을 시작합니다. 외부 트랜잭션에 영향을 주지 않고 롤백할 수 있습니다.
2.5 트랜잭션 전파 예제 (Spring Framework)
트랜잭션 전파는 Spring Framework에서 여러 메서드가 호출될 때 각 메서드의 트랜잭션 경계를 어떻게 관리할지 결정하는 중요한 설정입니다. 각 전파 유형별로 예제 시나리오를 통해 설명하겠습니다.
시나리오:
온라인 쇼핑몰에서 주문 처리 시스템을 구축한다고 가정합니다. 주문 생성, 결제 처리, 재고 감소 등의 작업이 필요합니다.
컴포넌트:
- OrderService: 주문 생성 및 처리 로직
- PaymentService: 결제 처리 로직
- InventoryService: 재고 관리 로직
테이블 구조:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
order_number VARCHAR(255),
total_amount DECIMAL(10, 2)
);
CREATE TABLE payments (
id INT PRIMARY KEY AUTO_INCREMENT,
order_id INT,
payment_date DATETIME,
amount DECIMAL(10, 2)
);
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
stock INT
);
2.5.1. REQUIRED (기본값)
설명: 이미 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로운 트랜잭션을 시작합니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
Order order = new Order();
order.setOrderNumber(orderNumber);
order.setTotalAmount(totalAmount);
orderRepository.save(order);
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
@Transactional
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
Payment payment = new Payment();
payment.setOrderId(orderId);
payment.setPaymentDate(new Date());
payment.setAmount(amount);
paymentRepository.save(payment);
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()도 @Transactional 어노테이션이 적용되어 있지만, 이미 진행 중인 트랜잭션이 있으므로 해당 트랜잭션에 참여합니다 (REQUIRED).
- 만약 PaymentService.processPayment()에서 예외가 발생하면, OrderService.createOrder()에서 시작된 트랜잭션 전체가 롤백됩니다. 주문 생성과 결제 정보 저장이 모두 취소됩니다.
2.5. 2. REQUIRES_NEW
설명: 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있다면 일시 중단됩니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 REQUIRES_NEW 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션)을 일시 중단하고 새로운 트랜잭션을 시작합니다.
- 만약 PaymentService.processPayment()에서 예외가 발생하면, PaymentService의 트랜잭션만 롤백됩니다. OrderService.createOrder()의 트랜잭션은 영향을 받지 않습니다. 즉, 주문 생성은 완료되고 결제 정보 저장은 취소될 수 있습니다.
3. SUPPORTS
설명: 이미 진행 중인 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 진행합니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.SUPPORTS)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 SUPPORTS 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션)에 참여합니다.
- 만약 OrderService.createOrder()가 트랜잭션 없이 호출되면 (예: @Transactional 어노테이션이 없는 메서드에서 호출), PaymentService.processPayment()도 트랜잭션 없이 실행됩니다.
4. NOT_SUPPORTED
설명: 트랜잭션 없이 진행합니다. 이미 진행 중인 트랜잭션이 있다면 일시 중단됩니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 NOT_SUPPORTED 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션)을 일시 중단하고 트랜잭션 없이 실행됩니다.
- PaymentService.processPayment()에서 예외가 발생하더라도 OrderService.createOrder()의 트랜잭션은 영향을 받지 않습니다.
5. MANDATORY
설명: 반드시 진행 중인 트랜잭션이 있어야 합니다. 없으면 예외가 발생합니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.MANDATORY)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 MANDATORY 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션)에 참여합니다.
- 만약 OrderService.createOrder()가 트랜잭션 없이 호출되면, PaymentService.processPayment()는 IllegalTransactionStateException 예외를 발생시킵니다.
6. NEVER
설명: 트랜잭션 없이 진행합니다. 이미 진행 중인 트랜잭션이 있다면 예외가 발생합니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.NEVER)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 NEVER 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션)이 존재하므로 IllegalTransactionStateException 예외를 발생시킵니다.
7. NESTED
설명: 이미 진행 중인 트랜잭션이 있으면 중첩된 트랜잭션을 시작합니다. 외부 트랜잭션에 영향을 주지 않고 롤백할 수 있습니다.
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(String orderNumber, double totalAmount) {
// 1. 주문 생성
// ...
// 2. 결제 처리 (PaymentService.processPayment() 호출)
paymentService.processPayment(order.getId(), totalAmount);
// 3. ... 추가 작업 ...
}
}
@Service
public class PaymentService {
@Transactional(propagation = Propagation.NESTED)
public void processPayment(int orderId, double amount) {
// 1. 결제 정보 저장
// ...
// 2. ... 추가 작업 ...
}
}
동작:
- OrderService.createOrder()가 호출되면 새로운 트랜잭션이 시작됩니다 (REQUIRED).
- OrderService.createOrder() 내부에서 PaymentService.processPayment()가 호출됩니다. PaymentService.processPayment()는 NESTED 전파 속성을 가지고 있으므로, 기존 트랜잭션 (OrderService의 트랜잭션) 내에서 새로운 세이브포인트를 생성하고 중첩된 트랜잭션을 시작합니다.
- 만약 PaymentService.processPayment()에서 예외가 발생하면, PaymentService의 트랜잭션만 롤백됩니다. OrderService.createOrder()의 트랜잭션은 영향을 받지 않습니다. 하지만 OrderService 트랜잭션이 롤백되면 PaymentService 트랜잭션도 함께 롤백됩니다.
트랜잭션 전파는 복잡한 트랜잭션 시나리오에서 데이터 일관성을 유지하고 오류를 효과적으로 처리하는 데 중요한 역할을 합니다. 각 전파 유형의 동작 방식을 이해하고, 애플리케이션의 요구 사항에 맞는 적절한 전파 유형을 선택해야 합니다.
주의:
- NESTED 전파 유형은 모든 데이터베이스에서 지원되는 것은 아닙니다.
- 잘못된 트랜잭션 전파 설정은 예상치 못한 데이터 불일치 문제를 야기할 수 있으므로 주의해야 합니다
3. 주요 용어 설명
- Commit (커밋): 트랜잭션 내의 모든 연산이 성공적으로 완료되었음을 확정하고, 변경 사항을 데이터베이스에 영구적으로 반영하는 작업입니다.
- Rollback (롤백): 트랜잭션 내의 연산 중 하나라도 실패했거나, 트랜잭션을 취소해야 할 경우, 데이터베이스를 트랜잭션 시작 이전의 상태로 되돌리는 작업입니다.
- Savepoint (세이브포인트): 트랜잭션 내에서 롤백할 특정 지점을 지정하는 기능입니다. 트랜잭션 전체를 롤백하는 대신, 특정 세이브포인트까지만 롤백할 수 있습니다.
- Transaction Manager (트랜잭션 매니저): 트랜잭션의 시작, 커밋, 롤백 등 트랜잭션 생명주기를 관리하는 컴포넌트입니다.
- Local Transaction (로컬 트랜잭션): 단일 데이터베이스 연결 내에서 수행되는 트랜잭션입니다.
- Global Transaction (글로벌 트랜잭션): 여러 데이터베이스 또는 시스템에 걸쳐 수행되는 트랜잭션입니다. XA 프로토콜을 사용하여 관리됩니다.
- XA Transaction: 분산 트랜잭션을 관리하기 위한 표준 프로토콜입니다. 여러 자원 관리자(데이터베이스, 메시지 큐 등)에 걸쳐 하나의 트랜잭션을 처리할 수 있도록 합니다.
4. 트랜잭션의 특징
- 데이터 무결성 보장: 트랜잭션을 통해 데이터베이스의 일관성을 유지하고, 예기치 않은 오류 발생 시 데이터 손실을 방지합니다.
- 동시성 제어: 여러 사용자가 동시에 데이터베이스에 접근하더라도 데이터 충돌을 방지하고, 각 사용자가 격리된 환경에서 작업할 수 있도록 지원합니다.
- 오류 복구: 트랜잭션 실패 시 롤백을 통해 데이터베이스를 이전 상태로 복구하여 시스템의 안정성을 높입니다.
- 비즈니스 로직의 응집도 향상: 여러 데이터베이스 연산을 하나의 논리적인 작업 단위로 묶어 코드의 가독성과 유지보수성을 향상시킵니다.
5. 트랜잭션 예제 및 상세 설명
5.1. Java JDBC를 이용한 트랜잭션 예제
import java.sql.*;
public class TransactionExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/testdb";
String user = "root";
String password = "password";
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
try {
// 1. JDBC 드라이버 로드
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 데이터베이스 연결
conn = DriverManager.getConnection(url, user, password);
// 3. 트랜잭션 시작
conn.setAutoCommit(false); // 자동 커밋 모드 해제
// 4. SQL 쿼리 준비 및 실행
String sql1 = "UPDATE accounts SET balance = balance - 100 WHERE id = 1";
pstmt1 = conn.prepareStatement(sql1);
pstmt1.executeUpdate();
String sql2 = "UPDATE accounts SET balance = balance + 100 WHERE id = 2";
pstmt2 = conn.prepareStatement(sql2);
pstmt2.executeUpdate();
// 5. 트랜잭션 커밋
conn.commit();
System.out.println("트랜잭션 성공!");
} catch (Exception e) {
// 6. 트랜잭션 롤백
if (conn != null) {
try {
conn.rollback();
System.out.println("트랜잭션 롤백!");
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
} finally {
// 7. 리소스 해제
try {
if (pstmt1 != null) pstmt1.close();
if (pstmt2 != null) pstmt2.close();
if (conn != null) {
conn.setAutoCommit(true); // 자동 커밋 모드 복원
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
예제 설명:
- JDBC 드라이버 로드: 데이터베이스와 연결하기 위해 JDBC 드라이버를 로드합니다.
- 데이터베이스 연결: 데이터베이스 URL, 사용자 이름, 비밀번호를 사용하여 데이터베이스에 연결합니다.
- 트랜잭션 시작: conn.setAutoCommit(false)를 호출하여 자동 커밋 모드를 해제합니다. 이는 트랜잭션을 명시적으로 시작함을 의미합니다.
- SQL 쿼리 준비 및 실행: PreparedStatement를 사용하여 SQL 쿼리를 준비하고 실행합니다. 이 예제에서는 accounts 테이블에서 ID가 1인 계좌의 잔액을 100 감소시키고, ID가 2인 계좌의 잔액을 100 증가시키는 두 개의 UPDATE 쿼리를 실행합니다.
- 트랜잭션 커밋: 모든 쿼리가 성공적으로 실행되면 conn.commit()을 호출하여 트랜잭션을 커밋합니다. 이는 변경 사항을 데이터베이스에 영구적으로 반영함을 의미합니다.
- 트랜잭션 롤백: 예외가 발생하면 conn.rollback()을 호출하여 트랜잭션을 롤백합니다. 이는 데이터베이스를 트랜잭션 시작 이전의 상태로 되돌림을 의미합니다.
- 리소스 해제: finally 블록에서 PreparedStatement와 Connection 객체를 닫아 리소스를 해제합니다. 또한, conn.setAutoCommit(true)를 호출하여 자동 커밋 모드를 복원합니다.
5.2. Spring Framework를 이용한 트랜잭션 예제
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, double amount) {
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new IllegalArgumentException("출금 계좌가 존재하지 않습니다."));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new IllegalArgumentException("입금 계좌가 존재하지 않습니다."));
fromAccount.withdraw(amount);
toAccount.deposit(amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
예제 설명:
- @Transactional 어노테이션: transferMoney 메서드에 @Transactional 어노테이션을 적용하여 해당 메서드가 트랜잭션 내에서 실행되도록 지정합니다. Spring Framework는 이 어노테이션을 감지하고 트랜잭션 시작, 커밋, 롤백을 자동으로 처리합니다.
- AccountRepository: 데이터베이스에 접근하기 위한 Spring Data JPA Repository입니다.
- Account 객체: 계좌 정보를 담는 엔티티 클래스입니다.
- transferMoney 메서드:
- accountRepository.findById를 사용하여 출금 계좌와 입금 계좌를 조회합니다.
- fromAccount.withdraw와 toAccount.deposit 메서드를 호출하여 계좌 잔액을 업데이트합니다.
- accountRepository.save를 사용하여 변경된 계좌 정보를 데이터베이스에 저장합니다.
Spring Framework의 트랜잭션 관리 장점:
- 선언적 트랜잭션 관리: @Transactional 어노테이션을 사용하여 트랜잭션을 선언적으로 관리할 수 있습니다. 이는 트랜잭션 관련 코드를 비즈니스 로직에서 분리하여 코드의 가독성과 유지보수성을 향상시킵니다.
- AOP (Aspect-Oriented Programming) 기반: Spring AOP를 사용하여 트랜잭션 관련 코드를 핵심 비즈니스 로직에 삽입합니다. 이를 통해 코드 중복을 줄이고, 트랜잭션 관리를 중앙 집중화할 수 있습니다.
- 다양한 트랜잭션 매니저 지원: JDBC, JPA, JTA 등 다양한 트랜잭션 매니저를 지원하여 다양한 환경에서 트랜잭션을 사용할 수 있습니다.
- 트랜잭션 전파 옵션 지원: REQUIRED, REQUIRES_NEW, SUPPORTS 등 다양한 트랜잭션 전파 옵션을 지원하여 복잡한 트랜잭션 시나리오를 처리할 수 있습니다.
6. 트랜잭션 사용 시 주의사항
- 트랜잭션 격리 레벨 설정: 애플리케이션의 요구 사항에 맞는 적절한 트랜잭션 격리 레벨을 선택해야 합니다. 격리 레벨이 높을수록 데이터 일관성은 높아지지만, 동시성이 낮아져 성능 저하를 유발할 수 있습니다.
- Long-Lived Transaction 피하기: 트랜잭션이 너무 오래 지속되면 데이터베이스 리소스를 과도하게 점유하고, 다른 트랜잭션의 실행을 방해하여 성능 문제를 일으킬 수 있습니다. 트랜잭션은 가능한 한 짧게 유지해야 합니다.
- 예외 처리: 트랜잭션 내에서 예외가 발생하면 반드시 롤백해야 합니다. 예외를 제대로 처리하지 않으면 데이터 불일치 문제가 발생할 수 있습니다.
- 데드락 (Deadlock) 주의: 여러 트랜잭션이 서로의 리소스를 기다리는 상황이 발생하면 데드락이 발생할 수 있습니다. 데드락을 방지하기 위해 트랜잭션 실행 순서를 일관성 있게 유지하고, 타임아웃 설정을 적절하게 조정해야 합니다.
- 분산 트랜잭션 관리: 여러 데이터베이스 또는 시스템에 걸쳐 트랜잭션을 수행해야 하는 경우, XA 트랜잭션과 같은 분산 트랜잭션 관리 기술을 사용해야 합니다.
- 리소스 누수 방지: 트랜잭션이 완료된 후에는 반드시 데이터베이스 연결, Statement, ResultSet 등의 리소스를 해제해야 합니다. 리소스 누수는 시스템 성능 저하 및 장애를 유발할 수 있습니다.
- 자동 커밋 모드 확인: 데이터베이스 연결 시 자동 커밋 모드가 활성화되어 있는지 확인해야 합니다. 자동 커밋 모드가 활성화되어 있으면 각 SQL 쿼리가 자동으로 커밋되어 트랜잭션이 제대로 동작하지 않을 수 있습니다.
- 트랜잭션 경계 설정: 트랜잭션을 시작하고 종료하는 시점을 명확하게 정의해야 합니다. 트랜잭션 경계가 모호하면 예상치 못한 데이터 불일치 문제가 발생할 수 있습니다.
- 로깅 및 모니터링: 트랜잭션 관련 로그를 기록하고, 트랜잭션 처리 시간, 롤백 횟수 등을 모니터링하여 시스템의 상태를 파악하고 성능 문제를 해결해야 합니다.
이 글에서는 트랜잭션에 대한 전반적인 내용을 다루었습니다. 트랜잭션의 기본 개념, ACID 속성, 격리 레벨, 전파 옵션, 예제 코드, 주의사항 등을 숙지하고, 실제 개발 환경에서 트랜잭션을 적절하게 활용한다면 데이터베이스 기반 애플리케이션의 안정성과 신뢰성을 크게 향상시킬 수 있습니다.
'이직&취업 > Java 기초 상식' 카테고리의 다른 글
Java Collection Framework란? (8) | 2025.03.23 |
---|---|
Checked Exception 와 Unchecked Exception (16) | 2025.03.23 |
SOLID 원칙 (4) | 2025.03.22 |
JAVA thread safe 란? (12) | 2025.03.21 |
브라우저에 www.naver.com을 입력하면??? (2) | 2025.03.19 |