[RDBMS / JDBC를 이용해 데이터 처리하기] Transaction

Hyunjun Kim·2025년 4월 29일
0

Data_Engineering

목록 보기
55/153

5 Transaction

5.1 Transaction이란

Transaction은 데이터베이스에서 하나의 작업 단위를 말한다.
Transaction 작업 단위는 ACID 라는 특징을 반드시 가져야 한다.

Atomicity 모두 성공하거나, 모두 실패

  • 예: 하나의 작업의 단위에서 4개의 update가 이루어져야 하는데, 마지막 update가 실패했다면, 4개를 모두 반영하지 않는다.

Consistency 데이터베이스를 하나의 유효한 상태로부터 그 다음 유효한 상태로 변경하는 것을 보장. 데이터(또는 데이터 사이의 관계)가 오류가 있는 상태로 존재하지 않는다.

  • 예: FK가 RESTRICT로 설정되어있는데, 자식테이블의 데이터가 존재하는 상태로 부모테이블의 row가 지워지는 경우는 있을 수 없다. 그런 쿼리는 실패한다.

Durability 한 번 transaction이 commit되면, 어떤 경우에도 그 데이터 또는 상태는 유지된다. 심지어는 컴퓨터의 전원이 꺼져도 유지된다.

Isolation 하나의 transaction은 다른 transaction과 독립되어있다. commit되지 않은 다른 transaction의 변경사항에 영향을 받지 않는다.

5.2 Isolation Level (격리 수준)

하나의 transaction이 데이터를 변경할 때, 다른 transaction에서 어떤 영향을 받는지에 대해서 다음과 같은 문제가 있다.

5.2.1 Isolation을 완벽하게 하지 않으면 발생하는 문제

Dirty Read: 자신의 transaction에서 처리한 작업이 완료되지 않았음에도 불구하고 다른 transaction에서 볼 수 있게 되는 현상

NON REPEATABLE READ: 동일한 SELECT 쿼리를 실행했음에도 다른 transaction의 변경이 반영되어서 항상 같은 결과를 보장하지 못하는 현상

  • REPEATABLE READ: 동일한 SELECT 쿼리 수행에 대해서 항상 같은 결과를 보장한다.(동일한 쿼리를 한 transaction에서 수행하면 항상 같은 결과가 나와야 한다.)

PHANTOM READ: 다른 tranaction에서 수행한 변경 작업에 의해 레코드가 보였다가 안보였다 하는 현상

5.2.2 위 문제들을 해결하기 위한 4가지 격리 수준 설정

  • READ UNCOMMITTED(커밋되지 않은 읽기)

    • transaction 안에서 커밋하지 않은 데이터를 다른 tranaction이 볼 수 있다.
  • READ COMMITTED(커밋된 읽기)

    • transaction 에서 커밋된 데이터만 다른 transaction이 볼 수 있다.
  • REPEATABLE READ(반복 가능한 읽기)

    • transaction 내에서 한 번 조회한 데이터를 반복해서 조회해도 결과가 항상 동일
    • MySQL JDBC의 기본값.
  • SERIALIZABLE(직렬화 가능)

    • 가장 엄격한 격리 수준. 완벽한 읽기 일관성 모드 제공 (사실 많이 쓰지는 않는다)

위에서 아래로 순서대로 READ UNCOMMITTED의 격리 수준이 가장 낮고 SERIALIZABLE의 격리 수준이 가장 강력하다.
특별한 경우를 제외하면, SERIALIZABLE을 제외하고는 성능차이가 크지 않다.

5.2.3 격리 수준 별 발생할 수 있는 문제 정리

격리수준DIRTY READNON-REPEATABLE READPHANTOM READ
READ UNCOMMITTEDooo
READ COMMITTEDoo
REPEATABLE READo(InnoDB는 발생x)
SERIALIZABLE

별 다른 일이 없다면 repeatable read를 쓰는 게 가장 좋고 안전하다.
MySQL에서는 InnoDB를 쓰는데 InnoDB는 PHANTOM READ가 발생 x > 가장 강력한 수준의 격리수준과 같다~

5.3 Transaction 실습 세팅

5.3.1 실습 데이터

실습 세팅: 다음 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');

5.3.2 Auto commit false

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);

5.4 Transaction Isolation 확인하기

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로 조회할 수 있는 것을 알 수 있다.

5.5 commit 으로 변경사항 묶어서 반영하기

auto commit = false 인 connection 객체를 이용해서 만든 모든 query는 commit 이전까지는 데이터베이스의 원본 테이블에 최종 반영되지 않는다. 같은 connection 객체를 이용하는 경우에만 commit 이
전에 변경된 내용을 조회할 수 있다.

5.5.1 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());
    }
}

5.5.2 commit 실패로 변경사항 모두 되돌려지기

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();
    }
}

5.6 Rollback 으로 변경사항 취소하기

5.6.1 명시적인 rollback으로 변경사항 취소하기

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);
        }
    }
}

5.6.2 SavePoint로 rollback시 복구시점 지정하기

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);
        }
    }
}

tip

우리가 단순히 프로그램을 한 번 실행해보는 것과, 실제 운영 환경에서 여러 애플리케이션이 동시에 동작하는 상황, 특히 여러 부서가 하나의 데이터베이스를 공유하는 협업 환경에서는 트랜잭션 처리가 쉽지 않다.
따라서 초기 설계 단계부터 다음과 같은 요소들을 명확히 설정하는 것이 중요하다:

  • 각 애플리케이션의 접근 방식 (예: 어떤 시스템은 read only, 어떤 시스템은 write only)
  • 테이블 별 접근 권한 설정 (누가 어떤 테이블에 접근할 수 있는지)
  • 적절한 Isolation Level 지정

이러한 사항들을 사전에 잘 설계해두면, 트랜잭션 처리로 인한 충돌이나 디버깅 부담을 크게 줄일 수 있다.

profile
Data Analytics Engineer 가 되

0개의 댓글