DB에서 하나의 작업단위를 의미합니다.
→ SELECT, UPDATE, ... 들은 하나의 작업 단위가 아닌가요?
→ 한번의 Transaction에서 여러번의 쿼리를 보낼 수도 있습니다.
Transaction 작업 단위는 ACID
의 특성을 가집니다.
Atomicity : 원자성
하나의 Transaction에서 4개의 UPDATE를 요청했는데, 하나라도 실패했다면 4개를 모두 반영하지 않는다.
Consistency : 일관성
DB를 하나의 유효한 상태로부터 그 다음 유효한 상태로 변경하는 것을 보장. (데이터와 데이터 사이의 관계가 오류가 있는 상태로 존재하지 않음)
Durability : 내구성
한번 Transaction이 commit되면 어떤 경우에도 그 데이터 또는 그 상태는 유지된다. (전원이 꺼지더라도)
Isolation : 격리
하나의 Transaction은 다른 Transaction과 독립되어있다. commit되지 않은 다른 Transaction에 영향을 받지 않는다.
Isolation을 완벽하게 하지 않으면 다음과 같은 문제가 발생할 수 있다.
Dirty Read : 자신의 Transaction에서 처리한 작업이 완료되지 않았음에도 다른 Transaction에서 볼 수 있게 되는 현상 (Isolation이 없는 수준)
NON REPEATABLE READ : 동일한 SELECT 쿼리를 실행했음에도 다른 Transaction의 변경이 반영돼서 항상 같은 결과를 보장하지 못하는 현상 (같은 Transaction 안에서는 본인이 데이터를 변경하지 않은 이상 DB는 항상 같은 결과를 보장해야 한다)
PHANTOM READ : 다른 Transaction에서 수행한 변경 작업에 의해 레코드가 보였다가 안보였다 하는 현상
위와 같은 문제를 해결하기 위해서 4가지 격리 수준을 제공한다.
READ UNCOMMITTED (커밋되지 않은 읽기)
Transaction 안에서 commit하지 않은 데이터를 다른 Transaction이 볼 수 있다.
격리수준이 가장 낮은 단계
READ COMMITED (커밋된 읽기)
Transaction에서 commit된 데이터만 다른 Transaction이 볼 수 있다.
PEPEATABLE READ (반복 가능한 읽기)
Transaction 내에서 한 번 조회한 데이터를 반복해서 조회해도 결과가 항상 동일하다.
MySQL JDBC의 Default 값
SERIALIZABLE (직렬화 가능)
완벽한 읽기 일관성 모드 제공
가장 엄격한 격리 수준
성능에 영향을 주기 때문에 많이 쓰지 않음
특별한 경우를 제외하면 SERIALIZABLE을 제외하고는 성능차이가 크지 않다.
격리 수준 | DIRTY READ | NON-PEPEATABLE READ | PHANTOM READ |
---|---|---|---|
READ UNCOMMITTED | O | O | O |
READ COMMITTED | O | O | |
REPEATABLE READ | O(MySQL은 발생하지 않음) | ||
SERIALIZABLE |
product
테이블 사용product
테이블
Transaction 1에서는 id = 1 인 레코드를 id = 101로 변경한 뒤 id = 101인 레코드를 조회한 뒤 commit 없이 종료했다.
Transaction 2에서는 id = 1인 레코드를 조회했다.
만약 Transaction 1이 Transaction 2와 격리되어 있지 않다면 id = 1의 레코드는 101로 변경되어 조회되지 않을것이다.
ResultSet의 데이터를 POJO class로 매핑하기 글에서 매핑해둔 클래스와 메소드를 통해 출력하겠습니다.
//ResultSetMapper를 import
import java.sql.*;
public class Main {
public static void main(String[] args) throws SQLException {
// Transaction 1
Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jdbc", "root", "1234");
con.setAutoCommit(false); // Transaction 1은 커밋을 자동으로 하지 않음
Statement stmt = con.createStatement();
stmt.executeUpdate("update product set id = 101 where id = 1");
// id = 1 인 레코드의 id를 101로 변경
ResultSet rs = stmt.executeQuery("select * from product where id = 101");
while(rs.next()) {
printRs(rs);
}
con.close(); // 커밋하지 않고 반납
// Transaction 2
Connection con2 = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jdbc", "root", "1234");
Statement stmt2 = con2.createStatement();
ResultSet rs2 = stmt2.executeQuery(" select * from product where id = 1 ");
// id = 1이 조회된다면 Transaction 1과 Transaction 2는 격리되어 있음을 증명
while(rs2.next()) {
printRs(rs2);
}
con2.close();
}
private static void printRs(ResultSet rs) throws SQLException {
System.out.println(ResultSetMapper.create(rs));
}
}
두 Transaction이 격리되어 있음을 확인할 수 있다.
------ Transaction 1 ---------
101 shoes1 2022-08-01 This is shoes1 40000
------ Transaction 2 ---------
1 shoes1 2022-08-01 This is shoes1 40000
commit을 하지 않았기 때문에 테이블도 변경되지 않았다.
수동으로 커밋을 하려면 con.commit()
을 추가하면 된다.
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');
review
테이블
review
테이블의 1 ~ 5 번 레코드는 product
테이블의 1 ~ 5 번 레코드의 id
를 FK로 참조하고 있기 때문에 product
테이블의 1 ~ 5 번 레코드는 지울 수 없다. 따라서 Transaction 1은 4번째 쿼리( product
테이블에 id
= 1 인 레코드를 삭제 )에서 오류가 발생한다. 만약 Transaction 1이 원자성을 가지고 있다면 id
가 1, 2, 3 인 레코드의 price
값에 10000원을 더해준 쿼리도 commit되지 않았을 것이다.
import java.sql.*;
public class Main {
public static void main(String[] args) throws SQLException {
// Transaction 1
Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jdbc", "root", "1234");
con.setAutoCommit(false);
Statement stmt = con.createStatement();
System.out.println("------ Transaction 1 ---------");
System.out.println();
// 1, 2, 3번 쿼리 : id가 1, 2, 3 인 레코드에 price 값에 10000원을 더해준다.
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");
try {
// 4번 쿼리 : product 테이블에서 id = 1 인 레코드를 지운다
stmt.executeUpdate("delete from product where id = 1");
} catch (SQLException sqlException) {
// 쿼리에서 에러가 발생하면 에러코드와 메세지를 출력
System.out.println(sqlException.getErrorCode() + ", " + sqlException.getMessage());
}
System.out.println();
// 5번 쿼리 : id가 1, 2, 3 인 레코드를 확인
ResultSet rs = stmt.executeQuery("select * from product where id between 1 and 3");
while(rs.next()) {
printRs(rs);
}
con.commit(); // Transaction 커밋
con.close();
System.out.println();
System.out.println("------ Transaction 2 ---------");
System.out.println();
// Transaction 2
Connection con2 = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/jdbc", "root", "1234");
Statement stmt2 = con2.createStatement();
// 다시 product 테이블을 확인
ResultSet rs2 = stmt2.executeQuery("select * from product where id between 1 and 3");
while (rs2.next()) {
printRs(rs2);
}
con2.close();
}
private static void printRs(ResultSet rs) throws SQLException {
System.out.println(ResultSetMapper.create(rs));
}
}
Transaction 1 의 1 ~ 3 번째 쿼리는 작동했지만 4번째 쿼리는 작동하지 못하고 에러메세지를 출력했다.
Transaction 2에서 id
가 1, 2, 3 인 레코드를 조회한 결과 Transaction 1 의 모든 쿼리가 적용되지 않았음을 확인할 수 있다.
------ Transaction 1 ---------
1451, Cannot delete or update a parent row: a foreign key constraint fails (`jdbc`.`review`, CONSTRAINT `review_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`))
1 shoes1 2022-08-01 This is shoes1 30000
2 shoes2 2022-08-01 This is shoes2 40000
3 shoes3 2022-08-01 This is shoes3 50000
------ Transaction 2 ---------
1 shoes1 2022-08-01 This is shoes1 30000
2 shoes2 2022-08-01 This is shoes2 40000
3 shoes3 2022-08-01 This is shoes3 50000