Java 트랜잭션 처리 방법 — commit과 rollback 제대로 이해하기
데이터가 절반만 저장된다면 어떻게 될까
쇼핑몰에서 상품을 주문했는데 주문 내역은 생성됐지만 결제는 실패했다면 어떻게 될까. 반대로 결제는 됐는데 주문 내역이 없다면? 은행 이체에서 내 계좌에서 돈이 빠져나갔는데 상대방 계좌에는 입금이 안 됐다면?
이런 상황이 실제로 발생한다면 서비스는 신뢰를 잃는다. 그리고 이런 문제는 데이터베이스 작업 도중 오류가 발생했을 때 실제로 일어날 수 있다.
이 문제를 해결하는 것이 바로 트랜잭션(Transaction) 이다. 트랜잭션은 여러 개의 데이터베이스 작업을 하나의 묶음으로 처리해서, 전부 성공하거나 전부 실패하도록 보장하는 메커니즘이다. 그리고 당연하게도 다중 처리 작업 중 실패한 일부에 대해서만 실패 처리도 가능하다.
이전 글에서는 Java에서 INSERT, UPDATE, DELETE 실행하는 방법에 대해서 살펴 보았다. 그렇다면 이제 데이터의 안정성에 대해서도 알아볼 차례가 되었다.

이번 글에서는 트랜잭션이 무엇인지, Java 트랜잭션은 어떻게 구현하는지, 그리고 실무에서 자주 발생하는 실수까지 실제 코드 예제와 함께 단계별로 설명한다.
트랜잭션이란 무엇인가 — ACID로 이해하기
트랜잭션을 설명할 때 빠지지 않는 개념이 ACID다. 트랜잭션이 반드시 보장해야 하는 네 가지 특성의 앞 글자를 딴 것이다. 어렵게 느껴질 수 있지만, 일상적인 예시로 풀어보면 이해하기 쉽다.
Atomicity(원자성) — 전부 아니면 전무 트랜잭션 안의 모든 작업은 하나의 단위로 처리된다. 주문 생성과 재고 감소가 하나의 트랜잭션으로 묶여 있다면, 주문만 생성되고 재고는 그대로인 어중간한 상태는 존재하지 않는다.
Consistency(일관성) — 데이터는 항상 올바른 상태를 유지해야 한다 계좌 이체에서 출금 계좌와 입금 계좌의 합산 금액은 이체 전후가 동일해야 한다. 트랜잭션이 완료된 후에도 데이터베이스는 정의된 규칙에 맞는 상태를 유지한다.
Isolation(격리성) — 각 트랜잭션은 서로 독립적이다 동시에 여러 사람이 같은 상품을 주문하더라도, 각 트랜잭션은 서로의 작업에 간섭하지 않아야 한다.
Durability(지속성) — 커밋된 데이터는 사라지지 않는다 트랜잭션이 완료된 후에는 서버가 갑자기 꺼지더라도 그 결과는 영구적으로 보존된다.
이 네 가지가 보장될 때 데이터베이스를 신뢰할 수 있다. 트랜잭션은 ACID를 구현하는 핵심 수단이다.
JDBC에서 트랜잭션을 제어하는 방법
Java에서 데이터베이스와 연결할 때 사용하는 JDBC는 기본적으로 AutoCommit 모드로 동작한다. AutoCommit이 켜진 상태에서는 SQL 하나를 실행할 때마다 자동으로 커밋이 이루어진다. 즉, 트랜잭션을 별도로 설정하지 않으면 각 SQL은 실행 즉시 독립적으로 데이터베이스에 반영된다.
// AutoCommit 기본값 확인
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println(conn.getAutoCommit()); // true 출력 — 자동 커밋 활성화 상태
여러 SQL을 하나의 트랜잭션으로 묶으려면 setAutoCommit(false)를 호출해 AutoCommit을 비활성화해야 한다. 이 시점부터 직접 commit() 또는 rollback()을 호출하기 전까지 모든 SQL은 하나의 트랜잭션으로 처리된다.
JDBC 트랜잭션의 기본 패턴:
Connection conn = null;
try {
conn = DriverManager.getConnection(URL, USER, PASSWORD);
conn.setAutoCommit(false); // ① 트랜잭션 시작
// ② SQL 작업들 실행
conn.commit(); // ③ 모든 작업 성공 시 확정
} catch (SQLException e) {
if (conn != null) {
conn.rollback(); // ④ 하나라도 실패 시 전체 취소
}
} finally {
if (conn != null) {
conn.setAutoCommit(true); // ⑤ AutoCommit 원래대로 복원
conn.close();
}
}① AutoCommit 비활성화로 트랜잭션을 시작하고, ③ 모든 작업이 성공하면 commit()으로 확정한다. 도중에 예외가 발생하면 ④ rollback()으로 모든 변경사항을 취소한다. ⑤ 작업이 끝나면 반드시 AutoCommit을 원래 상태로 복원해야 한다는 점을 잊지 말자. 특히 Connection Pool을 사용하는 환경에서 이를 누락하면 다음 요청에서 예상치 못한 동작이 발생할 수 있다.
실전 예제 — 주문 생성과 재고 감소를 하나로 묶기
개념을 이해했다면 실제 코드로 확인해보자. 쇼핑몰에서 상품을 주문할 때는 두 가지 작업이 동시에 처리되어야 한다. 주문 테이블에 새 주문을 추가(INSERT)하고, 상품 테이블의 재고를 줄이는(UPDATE) 것이다. 이 두 작업은 반드시 함께 성공하거나 함께 실패해야 한다.
예제 테이블 구조:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL
);
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL DEFAULT 0
);
주문 처리 트랜잭션 코드:
public void placeOrder(int userId, int productId, int quantity) {
Connection conn = null;
try {
conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
conn.setAutoCommit(false); // 트랜잭션 시작
// 1단계: 주문 생성
String insertSql = "INSERT INTO orders (user_id, product_id, quantity) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
pstmt.setInt(1, userId);
pstmt.setInt(2, productId);
pstmt.setInt(3, quantity);
pstmt.executeUpdate();
}
// 2단계: 재고 감소 (재고가 충분할 때만 UPDATE)
String updateSql = "UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?";
try (PreparedStatement pstmt = conn.prepareStatement(updateSql)) {
pstmt.setInt(1, quantity);
pstmt.setInt(2, productId);
pstmt.setInt(3, quantity);
int updated = pstmt.executeUpdate();
if (updated == 0) {
// 재고 부족 — 예외를 던져 rollback 유도
throw new IllegalStateException("재고가 부족합니다.");
}
}
conn.commit(); // 두 작업 모두 성공 시 커밋
System.out.println("주문이 완료되었습니다.");
} catch (Exception e) {
if (conn != null) {
try {
conn.rollback(); // 실패 시 주문 INSERT까지 함께 취소
System.out.println("주문 실패 — 롤백 완료: " + e.getMessage());
} catch (SQLException ex) {
ex.printStackTrace();
}
}
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
여기서 핵심은 재고 UPDATE가 실패했을 때 rollback()이 호출되면 앞서 실행된 주문 INSERT까지 함께 취소된다는 점이다. 주문만 생성되고 재고는 그대로인 어중간한 상태가 발생하지 않는다.
Savepoint — 트랜잭션 일부만 롤백하기
일반적인 rollback()은 트랜잭션 전체를 되돌린다. 그러나 상황에 따라 일부 작업은 유지하고 특정 시점부터만 취소해야 하는 경우가 있다. 이때 사용하는 것이 Savepoint다.
Savepoint는 트랜잭션 내에 중간 저장 지점을 설정하는 기능이다. rollback(savepoint)를 호출하면 해당 Savepoint 이후의 작업만 취소되고, 그 이전 작업은 유지된다.
Savepoint 예제 — 여러 사용자에게 포인트 일괄 지급:
특정 사용자 지급이 실패해도 나머지 사용자에게는 계속 지급을 이어가야 하는 배치 작업이다.
public void batchGrantPoints(List<Integer> userIds, int points) throws SQLException {
Connection conn = DriverManager.getConnection(DBConfig.URL, DBConfig.USER, DBConfig.PASSWORD);
conn.setAutoCommit(false);
String sql = "UPDATE users SET point = point + ? WHERE id = ?";
int successCount = 0;
for (int userId : userIds) {
Savepoint savepoint = conn.setSavepoint(); // 각 사용자 작업 전 저장 지점 설정
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, points);
pstmt.setInt(2, userId);
int result = pstmt.executeUpdate();
if (result == 0) {
conn.rollback(savepoint); // 이 사용자 작업만 취소
System.out.println("존재하지 않는 사용자 — 건너뜀: " + userId);
} else {
conn.releaseSavepoint(savepoint); // 성공 시 Savepoint 해제
successCount++;
}
} catch (SQLException e) {
conn.rollback(savepoint); // 오류 발생 시 이 사용자 작업만 취소
System.out.println("포인트 지급 실패 — userId: " + userId);
}
}
conn.commit(); // 성공한 작업들만 최종 확정
System.out.println("완료 — 성공: " + successCount + "/" + userIds.size());
conn.setAutoCommit(true);
conn.close();
}
Savepoint를 사용하면 “전부 성공 또는 전부 실패”라는 원칙을 유연하게 확장할 수 있다. 단, Savepoint는 트랜잭션 내에서만 유효하며, 전체 commit() 또는 전체 rollback()이 호출되면 모든 Savepoint는 사라진다.
트랜잭션 처리 시 자주 발생하는 실수 3가지
처음 트랜잭션을 구현할 때 반복적으로 저지르는 실수들이 있다. 미리 알아두면 디버깅 시간을 크게 줄일 수 있다.
실수 1: catch 블록에서 rollback을 빠뜨리는 경우
예외가 발생했는데 rollback()을 호출하지 않으면 트랜잭션이 미완료 상태로 남는다. Connection이 닫힐 때 자동으로 rollback되기도 하지만, 구현체마다 다를 수 있으므로 명시적으로 호출하는 것이 원칙이다.
// 잘못된 코드
} catch (SQLException e) {
e.printStackTrace(); // rollback 누락
}
// 올바른 코드
} catch (SQLException e) {
if (conn != null) {
conn.rollback(); // 반드시 명시적으로 호출
}
}
실수 2: finally 블록에서 setAutoCommit(true) 복원을 누락하는 경우
Connection Pool 환경에서는 Connection이 닫히지 않고 Pool로 반환된다. setAutoCommit(false) 상태로 반환되면 다음 요청에서 해당 Connection을 재사용할 때 예상치 못한 동작이 발생한다. 반드시 setAutoCommit(true)로 복원한 뒤 close()를 호출해야 한다.
실수 3: 작업마다 서로 다른 Connection을 사용하는 경우
트랜잭션은 하나의 Connection 내에서만 유효하다. 메서드마다 새로운 Connection을 열면 각 SQL이 별개의 트랜잭션으로 처리되어 묶이지 않는다.
// 잘못된 코드 — 각 메서드가 별도 Connection을 사용
insertOrder(userId, productId); // Connection A<br>
updateStock(productId, quantity); // Connection B — 트랜잭션 연결 안 됨
// 올바른 코드 — 동일한 Connection을 파라미터로 전달
Connection conn = DriverManager.getConnection(...);
conn.setAutoCommit(false);
insertOrder(conn, userId, productId); // 동일 Connection
updateStock(conn, productId, quantity); // 동일 Connection
conn.commit();트랜잭션을 이해하면 데이터를 신뢰할 수 있다
트랜잭션은 단순히 commit()과 rollback()을 호출하는 문법이 아니다. 데이터의 정합성을 보장하는 설계 원칙이며, 실무에서 발생하는 데이터 오염 문제의 많은 부분이 트랜잭션을 제대로 다루지 않아서 발생한다.
이번 글에서 다룬 내용을 정리하면 다음과 같다.
| 개념 | 핵심 내용 |
|---|---|
| AutoCommit | 기본값 true, 트랜잭션 제어 시 false로 변경 필요 |
| commit | 모든 작업이 성공했을 때 변경사항을 최종 확정 |
| rollback | 하나라도 실패했을 때 모든 작업을 취소 |
| Savepoint | 트랜잭션 일부만 롤백할 때 사용하는 중간 저장 지점 |
JDBC에서 직접 트랜잭션을 구현하면 코드가 길고 반복적이다. 실무에서는 Spring의 @Transactional 어노테이션 하나로 이 복잡한 코드를 모두 자동으로 처리한다. JDBC 트랜잭션의 원리를 이해한 상태에서 @Transactional을 접하면, 어노테이션 뒤에서 어떤 일이 일어나는지 정확하게 파악할 수 있다.
다음 단계로는 MyBatis에서의 트랜잭션 처리와 Spring @Transactional 방식을 학습하는 것을 권장한다.
Add your first comment to this post