[Database] Transaction Isolation

[Database] Transaction Isolation

Transaction Isolation은 다수의 Transaction이 동시에 실행 될 때, 각각의 Transaction이 다른 Transaction에게 영향을 주지 않도록 보장하는 개념입니다. 즉, 각각의 Transaction이 자신이 보는 데이터에 대해 다른 Transaction의 변경을 볼 수 없도록 보장합니다. 이 글에서는 Transaction Isolation에 대해서 설명합니다.

Transaction Isolaction에 대해서 블로그를 쓰고싶어 Transaction Isolation level에 대해서 설명해줄 수 있어? 데이터베이스에서 Transaction Isolation level은 여러 개의 트랜잭션들이 서로 간섭 없이 동시에 실행될 수 있도록 보장하는 것입니다. Transaction Isolation level을 설정함으로써, 데이터베이스의 일관성과 동시성을 유지하면서 데이터를 안전하게 처리할 수 있습니다. 보통 데이터베이스에서는 4개의 Transaction Isolation level을 제공합니다. 이것은 다음과 같습니다. Read Uncommitted: 이 레벨에서는 다른 트랜잭션에서 변경한 데이터를 읽을 수 있습니다. 이는 가장 낮은 Isolation level이며, 데이터 일관성에 문제가 있을 수 있습니다. Read Committed: 이 레벨에서는 다른 트랜잭션이 커밋된 데이터만 읽을 수 있습니다. 이 레벨에서는 Dirty read(커밋되지 않은 데이터를 읽는 것)는 발생하지 않지만, Non-repeatable read(동일한 쿼리를 실행해도 결과가 달라질 수 있는 경우)와 Phantom read(동일한 쿼리를 실행해도 결과가 달라질 수 있는 경우)가 발생할 수 있습니다. Repeatable Read: 이 레벨에서는 트랜잭션이 시작될 때 읽은 데이터를 유지합니다. 이 레벨에서는 Non-repeatable read는 발생하지 않지만, Phantom read는 발생할 수 있습니다. Serializable: 이 레벨에서는 모든 트랜잭션이 직렬화된 순서로 실행됩니다. 이 레벨에서는 Dirty read, Non-repeatable read, Phantom read가 발생하지 않지만, 동시성이 매우 낮아집니다. Transaction Isolation level을 선택할 때는 데이터 일관성과 동시성 사이에서 타협점을 찾아야합니다. Isolation level이 높을수록 데이터 일관성은 유지되지만 동시성이 낮아집니다. 따라서 트랜잭션의 성격과 상황에 따라 적절한 Isolation level을 선택해야 합니다. 이지영 Transaction Isolation Level이 `SERIALIZABLE` 일 때는 실행이 안되고, `REPEATABLE READ`인 경우에는 실행되는 테스트 코드를 Java로 작성해줄 수 있어? 네, Java에서 JDBC API를 사용하여 트랜잭션 Isolation level을 설정하고 테스트할 수 있습니다. 아래는 트랜잭션 Isolation level이 Serializable일 때 실행되지 않고, Repeatable Read일 때 실행되는 간단한 테스트 코드입니다. java Copy code import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class IsolationLevelTest { private static final String URL = "jdbc:mysql://localhost/testdb"; private static final String USER = "username"; private static final String PASSWORD = "password"; public static void main(String[] args) throws ClassNotFoundException, SQLException { // JDBC 드라이버 로드 Class.forName("com.mysql.jdbc.Driver"); // Connection 생성 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); // Isolation level 설정 conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // Repeatable Read // 트랜잭션 시작 conn.setAutoCommit(false); try { // SELECT 쿼리 실행 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); // 결과 출력 while(rs.next()) { System.out.println(rs.getString("name") + " - " + rs.getString("email")); } // 시간 지연을 위한 sleep Thread.sleep(10000); // 커밋 conn.commit(); } catch (Exception e) { // 롤백 conn.rollback(); } // Connection 종료 conn.close(); } } 위 코드에서는 Connection 객체의 setTransactionIsolation() 메서드를 사용하여 Isolation level을 설정하고, setAutoCommit(false)를 호출하여 트랜잭션을 수동으로 컨트롤합니다. 이후 SELECT 쿼리를 실행하고 결과를 출력한 후, 10초 동안 기다린 후 커밋을 시도합니다. 만약 에러가 발생하면 롤백합니다. 위 코드를 Serializable로 변경하려면, setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE)로 설정하면 됩니다. 이 경우 위 코드에서는 SELECT 쿼리 실행 시 다른 트랜잭션에서 해당 레코드를 업데이트하면 LockTimeoutException이 발생합니다. 이지영 이번에는 `REPEATABLE READ`인 경우에는 실행되지 않고, `READ COMMITTED` 인 경우에 실행되는 테스트 코드를 작성해줘 네, 아래는 Isolation level이 Repeatable Read인 경우에는 실행되지 않고, Read Committed인 경우에는 실행되는 간단한 테스트 코드입니다. java Copy code import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class IsolationLevelTest { private static final String URL = "jdbc:mysql://localhost/testdb"; private static final String USER = "username"; private static final String PASSWORD = "password"; public static void main(String[] args) throws ClassNotFoundException, SQLException { // JDBC 드라이버 로드 Class.forName("com.mysql.jdbc.Driver"); // Connection 생성 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); // Isolation level 설정 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // Read Committed // 트랜잭션 시작 conn.setAutoCommit(false); try { // SELECT 쿼리 실행 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); // 결과 출력 while(rs.next()) { System.out.println(rs.getString("name") + " - " + rs.getString("email")); } // 시간 지연을 위한 sleep Thread.sleep(10000); // 커밋 conn.commit(); } catch (Exception e) { // 롤백 conn.rollback(); } // Connection 종료 conn.close(); } } 위 코드에서는 Connection 객체의 setTransactionIsolation() 메서드를 사용하여 Isolation level을 설정하고, setAutoCommit(false)를 호출하여 트랜잭션을 수동으로 컨트롤합니다. 이후 SELECT 쿼리를 실행하고 결과를 출력한 후, 10초 동안 기다린 후 커밋을 시도합니다. 만약 에러가 발생하면 롤백합니다. 위 코드를 Repeatable Read로 변경하려면, setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ)로 설정하면 됩니다. 이 경우 위 코드에서는 SELECT 쿼리 실행 시 다른 트랜잭션에서 해당 레코드를 업데이트하더라도 읽어온 결과가 변경되지 않습니다. 이지영 마지막으로 `Read Committed`에서는 실행되지 않고, `Read Uncommitted`에서는 실행되는 테스트 코드를 작성해줘 네, 아래는 Isolation level이 Read Committed인 경우에는 실행되지 않고, Read Uncommitted인 경우에는 실행되는 간단한 테스트 코드입니다. java Copy code import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class IsolationLevelTest { private static final String URL = "jdbc:mysql://localhost/testdb"; private static final String USER = "username"; private static final String PASSWORD = "password"; public static void main(String[] args) throws ClassNotFoundException, SQLException { // JDBC 드라이버 로드 Class.forName("com.mysql.jdbc.Driver"); // Connection 생성 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); // Isolation level 설정 conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // Read Uncommitted // 트랜잭션 시작 conn.setAutoCommit(false); try { // SELECT 쿼리 실행 Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); // 결과 출력 while(rs.next()) { System.out.println(rs.getString("name") + " - " + rs.getString("email")); } // 시간 지연을 위한 sleep Thread.sleep(10000); // 커밋 conn.commit(); } catch (Exception e) { // 롤백 conn.rollback(); } // Connection 종료 conn.close(); } } 위 코드에서는 Connection 객체의 setTransactionIsolation() 메서드를 사용하여 Isolation level을 설정하고, setAutoCommit(false)를 호출하여 트랜잭션을 수동으로 컨트롤합니다. 이후 SELECT 쿼리를 실행하고 결과를 출력한 후, 10초 동안 기다린 후 커밋을 시도합니다. 만약 에러가 발생하면 롤백합니다. 위 코드를 Read Committed로 변경하려면, setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED)로 설정하면 됩니다. 이 경우 위 코드에서는 SELECT 쿼리 실행 시 다른 트랜잭션에서 해당 레코드를 업데이트하면 해당 레코드는 읽을 수 없습니다. 이지영 Transaction Isolation Level을 낮추면 발생 할 수 있는 문제는 뭘까? 한 단계씩 설명해줄 수 있어? Transaction Isolation Level을 낮추면 데이터의 일관성을 유지하는 대신 동시성을 높일 수 있습니다. 하지만 이러한 경우, 다음과 같은 문제가 발생할 수 있습니다. Dirty Read: 다른 트랜잭션에서 아직 커밋되지 않은 데이터를 읽는 것입니다. 이 경우, 다른 트랜잭션에서 롤백할 경우 해당 데이터가 유실될 수 있습니다. Non-repeatable Read: 같은 트랜잭션에서 동일한 쿼리를 실행하더라도 결과가 달라지는 것입니다. 이 경우, 다른 트랜잭션에서 해당 레코드를 업데이트하면 결과가 변경될 수 있습니다. Phantom Read: 같은 트랜잭션에서 동일한 쿼리를 실행하더라도 결과에 새로운 레코드가 추가될 수 있는 것입니다. 이 경우, 다른 트랜잭션에서 해당 테이블에 레코드를 추가하거나 삭제하면 결과가 변경될 수 있습니다. 위 문제들은 각각 Read Uncommitted, Read Committed, Repeatable Read Isolation level에서 발생할 수 있습니다. 이러한 문제를 방지하기 위해서는 Isolation level을 높이거나, 다른 방법을 사용해야 합니다. 예를 들어, Dirty Read를 방지하기 위해서는 Read Committed 이상의 Isolation level을 사용하거나, 레코드 락을 사용할 수 있습니다. Non-repeatable Read와 Phantom Read를 방지하기 위해서는 Repeatable Read 이상의 Isolation level을 사용하거나, MVCC(Multi-Version Concurrency Control)와 같은 방법을 사용할 수 있습니다. 타이틀 이미지: [Unsplash](https://unsplash.com/ko/%EC%82%AC%EC%A7%84/AcW1ZwD-qC0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)의 [Jandira Sonnendeck](https://unsplash.com/fr/@jandira_sonnendeck?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)