Transaction은 데이터베이스에서 하나의 작업 단위를 말한다.
Transaction 작업 단위는 ACID 라는 특징을 반드시 가져야 한다.
Atomicity 모두 성공하거나, 모두 실패
Consistency 데이터베이스를 하나의 유효한 상태로부터 그 다음 유효한 상태로 변경하는 것을 보장. 데이터(또는 데이터 사이의 관계)가 오류가 있는 상태로 존재하지 않는다.
Durability 한 번 transaction이 commit되면, 어떤 경우에도 그 데이터 또는 상태는 유지된다. 심지어는 컴퓨터의 전원이 꺼져도 유지된다.
Isolation 하나의 transaction은 다른 transaction과 독립되어있다. commit되지 않은 다른 transaction의 변경사항에 영향을 받지 않는다.
하나의 transaction이 데이터를 변경할 때, 다른 transaction에서 어떤 영향을 받는지에 대해서 다음과 같은 문제가 있다.
Dirty Read: 자신의 transaction에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 transaction에서 볼 수 있게 되는 현상
NON REPEATABLE READ: 동일한 SELECT 쿼리를 실행했음에도 다른 transaction의 변경이 반영되어서 항상 같은 결과를 보장하지 못하는 현상
PHANTOM READ: 다른 tranaction에서 수행한 변경 작업에 의해 레코드가 보였다가 안보였다 하는 현상
READ UNCOMMITTED(커밋되지 않은 읽기)
READ COMMITTED(커밋된 읽기)
REPEATABLE READ(반복 가능한 읽기)
SERIALIZABLE(직렬화 가능)
위에서 아래로 순서대로 READ UNCOMMITTED의 격리 수준이 가장 낮고 SERIALIZABLE의 격리 수준이 가장 강력하다.
특별한 경우를 제외하면, SERIALIZABLE을 제외하고는 성능차이가 크지 않다.
격리수준 | DIRTY READ | NON-REPEATABLE READ | PHANTOM READ |
---|---|---|---|
READ UNCOMMITTED | o | o | o |
READ COMMITTED | o | o | |
REPEATABLE READ | o(InnoDB는 발생x) | ||
SERIALIZABLE |
별 다른 일이 없다면 repeatable read를 쓰는 게 가장 좋고 안전하다.
MySQL에서는 InnoDB를 쓰는데 InnoDB는 PHANTOM READ가 발생 x > 가장 강력한 수준의 격리수준과 같다~
실습 세팅: 다음 SQL문을 통해서 product 테이블에 연결된 review 테이블을 생성하고 데이터를 넣는다.
CREATE TABLE `review` (
`id` int NOT NULL,
`content` varchar(2048) DEFAULT NULL,
`user_id` int DEFAULT NULL,
`product_id` int unsigned NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
);
insert into `review` (id, content, user_id, product_id) values (1, 'review1', '1', '1');
insert into `review` (id, content, user_id, product_id) values (2, 'review2', '2', '2');
insert into `review` (id, content, user_id, product_id) values (3, 'review3', '3', '3');
insert into `review` (id, content, user_id, product_id) values (4, 'review4', '4', '4');
insert into `review` (id, content, user_id, product_id) values (5, 'review5', '5', '5');
auto commit 이 true 이면 executeXXX 함수를 실행하는 즉시 변경사항이 commit 된다.
auto commit 을 false로 바꾼 connection 객체에서 statement 등을 생성해야 한다.
Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/de-jdbc", "root", null);
con.setAutoCommit(false);
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.de.jdbc.mapper.ResultSetMapper;
public class Main {
public static void main(String[] args) throws SQLException {
Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
con.setAutoCommit(false);
Statement stmt = con.createStatement();
stmt.executeUpdate("UPDATE product SET `id` = 101 where `id` = 1;");
ResultSet rs = stmt.executeQuery(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = 101");
while (rs.next()) {
// id = 101
ResultSetMapper.printRs(rs);
}
// no commit
con.close();
Connection con2 =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
Statement stmt2 = con2.createStatement();
ResultSet rs2 = stmt2.executeQuery(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = 1");
while (rs2.next()) {
// id = 1
ResultSetMapper.printRs(rs2);
}
}
}
connection 에서 commit을 수행하지 않았다. 새로운 연결을 통해서 데이터를 확인해보면, 변경사항이 반영되지 않아서 변경이전의 id로 조회할 수 있는 것을 알 수 있다.
auto commit = false 인 connection 객체를 이용해서 만든 모든 query는 commit 이전까지는 데이터베이스의 원본 테이블에 최종 반영되지 않는다. 같은 connection 객체를 이용하는 경우에만 commit 이
전에 변경된 내용을 조회할 수 있다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class Main {
public static void main(String[] args) throws SQLException {
Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
con.setAutoCommit(false);
Statement stmt = con.createStatement();
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 1");
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 2");
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 3");
stmt.executeUpdate("DELETE from review where id = 1");
stmt.executeUpdate("DELETE from product where id = 1");
con.commit();
con.close();
Connection con2 =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
Statement stmt2 = con2.createStatement();
ResultSet rs2 = stmt2.executeQuery(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = 1");
System.out.println("exist result?: " + rs2.next());
}
}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.de.jdbc.mapper.ResultSetMapper;
public class Main {
public static void main(String[] args) throws SQLException {
Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
try {
con.setAutoCommit(false);
// default is REPEATABLE READ
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = 1");
while (rs.next()) {
System.out.println("==== Before start update ======");
ResultSetMapper.printRs(rs);
}
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 1");
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 2");
stmt.executeUpdate("UPDATE product SET `price` = `price`+10000 where `id` = 3");
stmt.executeUpdate("DELETE from product where id = 1");
con.commit();
con.close();
} catch (SQLException sqlException) {
System.out.println(sqlException.getErrorCode() + ", " + sqlException.getMessage());
}
System.out.println("==== After commit failed ==== ");
Connection con2 =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
Statement stmt2 = con2.createStatement();
ResultSet rs2 = stmt2.executeQuery(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = 1");
while (rs2.next()) {
ResultSetMapper.printRs(rs2);
}
con2.close();
}
}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.de.jdbc.mapper.ResultSetMapper;
public class Main {
public static void main(String[] args) throws SQLException {
Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
try {
con.setAutoCommit(false);
// default is REPEATABLE READ
PreparedStatement updateStmt = con
.prepareStatement("UPDATE product SET `price` = `price`+10000 where `id` = ?");
PreparedStatement selectStmt = con.prepareStatement(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = ?");
System.out.println("==== Before Start Update ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
updateStmt.setInt(1, 1);
updateStmt.executeUpdate();
updateStmt.setInt(1, 2);
updateStmt.executeUpdate();
updateStmt.setInt(1, 3);
updateStmt.executeUpdate();
System.out.println("==== After Update in Tranaction ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
con.rollback();
System.out.println("==== After Rollback in Tranaction ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
con.close();
} catch (SQLException sqlException) {
System.out.println(sqlException.getErrorCode() + ", " + sqlException.getMessage());
}
}
private static void selectAndPrintRow(PreparedStatement stmt, int id) throws SQLException {
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
ResultSetMapper.printRs(rs);
}
}
}
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import org.de.jdbc.mapper.ResultSetMapper;
public class Main {
public static void main(String[] args) throws SQLException {
Connection con =
DriverManager.getConnection("jdbc:mysql://localhost:3306/de-jdbc", "root", null);
try {
con.setAutoCommit(false);
// default is REPEATABLE READ
PreparedStatement updateStmt = con
.prepareStatement("UPDATE product SET `price` = `price`+10000 where `id` = ?");
PreparedStatement selectStmt = con.prepareStatement(
"select `id`, `name`, `updated_at`, `contents`, `price` from product where id = ?");
System.out.println("==== Before Start Update ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
updateStmt.setInt(1, 1);
updateStmt.executeUpdate();
Savepoint sp1 = con.setSavepoint();
updateStmt.setInt(1, 2);
updateStmt.executeUpdate();
Savepoint sp2 = con.setSavepoint();
updateStmt.setInt(1, 3);
updateStmt.executeUpdate();
System.out.println();
System.out.println("==== After Update in Transaction ====");
selectAndPrintRow(selectStmt, 1);
System.out.println("save point 1 here");
selectAndPrintRow(selectStmt, 2);
System.out.println("save point 2 here");
selectAndPrintRow(selectStmt, 3);
con.rollback(sp2);
System.out.println();
System.out.println("==== After Rollback to savepoint2 in Transaction ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
con.rollback(sp1); // error
System.out.println();
System.out.println("==== After Rollback to savepoint1 in Transaction ====");
selectAndPrintRow(selectStmt, 1);
selectAndPrintRow(selectStmt, 2);
selectAndPrintRow(selectStmt, 3);
con.close();
} catch (SQLException sqlException) {
System.out.println(sqlException.getErrorCode() + ", " + sqlException.getMessage());
}
}
private static void selectAndPrintRow(PreparedStatement stmt, int id) throws SQLException {
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
ResultSetMapper.printRs(rs);
}
}
}
우리가 단순히 프로그램을 한 번 실행해보는 것과, 실제 운영 환경에서 여러 애플리케이션이 동시에 동작하는 상황, 특히 여러 부서가 하나의 데이터베이스를 공유하는 협업 환경에서는 트랜잭션 처리가 쉽지 않다.
따라서 초기 설계 단계부터 다음과 같은 요소들을 명확히 설정하는 것이 중요하다:
- 각 애플리케이션의 접근 방식 (예: 어떤 시스템은 read only, 어떤 시스템은 write only)
- 테이블 별 접근 권한 설정 (누가 어떤 테이블에 접근할 수 있는지)
- 적절한 Isolation Level 지정
이러한 사항들을 사전에 잘 설계해두면, 트랜잭션 처리로 인한 충돌이나 디버깅 부담을 크게 줄일 수 있다.