김영한 님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
DB에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다
트랜잭션 기능을 사용하면 n개의 작업이 합쳐져서 하나의 작업처럼 동작을 해야하는 경우, n개의 작업이 모두 성공해야 DB에 반영하고, 하나라도 실패하면 작업 전의 상태로 돌아간다
모든 작업이 성공해서 DB에 정상 반영하는 것을 커밋( Commit
)이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백( Rollback
)이라 한다
트랜잭션은 ACID라 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장해야 한다
Atomicity : 트랜잭션 내에서 실행한 여러 개의 작업들은 하나의 작업인 것처럼 전체적으로 수행되거나 전혀 수행되지 않아야한다
Consistency
트랜잭션은 DB를 일관된 상태에서 일관된 상태로 변환해야한다
즉, 모든 트랜잭션은 일관성 있는 DB 상태를 유지해야한다
Isolation
트랜잭션은 서로 독립적으로 실행되며, 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 해야한다
ex> 동시에 같은 데이터를 수정하지 못하도록 해야한다
동시성과 관련된 성능 이슈로 인해 트랜잭션 Isolation level을 선택할 수 있다
Durability
성공적으로 끝난 트랜잭의 결과는 DB에 영구적으로 기록되며, 뒤의 트랜잭션의 실패로 인해 손실되면 안된다
중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다
트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야 하는데 이렇게 하면 동시 처리 성능이 매우 나빠진다
이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의
READ UNCOMMITED( 커밋되지 않은 읽기 ) : 다른 곳에서 데이터를 변경 중이고 커밋하지 않았는데 그 데이터를 사용하는 것
READ COMMITTED( 커밋된 읽기 )
REPEATABLE READ( 반복 가능한 읽기 )
SERIALIZABLE( 직렬화 가능 )
사용자는 웹 어플리케이션 서버( WAS )나 DB 접근 툴 같은 클라이언트를 사용해서 데이터베이스 서버에 접근 가능
클라이언트는 DB 서버에 연결을 요청하고 커넥션을 맺게 되는데 이 때, DB 서버는 내부에서 세션을 만든다
앞으로 해당 커넥션을 이용한 모든 요청은 생성된 세션을 통해 실행된다
즉, 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다
세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다
트랜잭션 종료 이후에 새로운 트랜잭션을 다시 시작할 수 있다
사용자가 커넥션을 닫거나, DBA( DB 관리자 )가 세션을 강제로 종료하면 세션은 종료된다
커넥션 풀이 커넥션을 생성하는 커넥션만큼 세션도 동일한 개수만큼 생성된다
( 수동커밋의 경우 ) 쿼리를 실행하고 DB에 결과를 반영하려면 commit
을 호출하고, 결과를 반영하고 싶지 않으면 rollback
을 호출
( 수동커밋의 경우 ) 커밋을 호출하기 전까지는 임시로 데이터를 저장하는 것이기 때문에 해당 트랜잭션을 시작한 세션( 사용자 )에게만 변경 데이터가 보이고 다른 세션( 사용자 )에게는 변경 데이터가 보이지 않는다
Isolation level을 READ UNCOMMITED로 설정하면 임시로 저장되는 데이터가 다른 세션( 사용자 )에게 보여지게 된다
자동 커밋 : 각 쿼리 실행 직후에 자동으로 커밋을 호출하기 때문에 커밋이나 롤백을 호출하지 않아도 DB에 반영된다
수동 커밋 : 쿼리 이후에 커밋이나 롤백을 직접 호출해주어야 한다
자동 커밋의 문제점
ex> 계좌이체를 생각했을 때 A에서 2000원을 빼는 쿼리가 성공하고 B에서 2000을 더하는 쿼리가 실패했다면
자동 커밋인 경우, B는 변화가 없고 A에서만 2000원이 빠지는 문제가 발생한다
자동 커밋이나 수동 커밋 모드는 한 번 설정하면 해당 세션에서는 계속 유지되며, 중간에 변경하는 것이 가능하다
set autocommit = true ( false )
수동 커밋 모드로 설정하는 것을 트랜잭션을 시작한다고 표현한다
세션 1 ( 1번, 2번, 3번 )
트랜잭션을 시작하고 데이터를 수정하려면 먼저 수정하려는 데이터가 있는 row의 lock을 획득해야한다
lock을 획득하면 해당 row에 update 쿼리를 수행할 수 있다
세션 2 ( 4번, 5번 )
데이터를 수정하려고 하는데 row에 lock이 없는 경우, lock이 돌아올 때까지 대기한다
lock 대기 시간을 넘어가면 lock 타임아웃 오류가 발생 ( 대기 시간은 설정 가능 )
ex> SET LOCK_TIMEOUT 60000;
세션 1 ( 세션 1의 6번 )
세션 2 ( 세션 2의 6번, 7번, 8번 )
보통 데이터를 조회할 때는 lock을 획득하지 않고 바로 데이터를 조회할 수 있다
세션 1에서 데이터를 수정하고 있어도 세션 2에서 조회가 가능한데 이 때 조회되는 데이터는 세션1이 수정하기 전의 데이터
select ~~~ for update
를 사용하면 조회할 때도 lock을 획득하게 된다
select * from member where member_id='memberA' for update;
조회 시점에 lock을 획득해야 하는 경우
트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 할 때 사용
ex> 어플리케이션 로직에서 금액을 조회한 다음 이 금액 정보로 어플리케이션에서 계산을 수행하는 경우, 계산을 완료할 때 까지 조회한 금액을 다른곳에서 변경하면 안되기 때문에 조회 시점에 lock을 획득하면 된다
어플리케이션에서 트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작해야한다
정확하게는 비지니스 로직을 수행하는 메서드 안에서 시작
비지니스 로직이 잘못되면 해당 비지니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야하기 때문
트랜잭션을 시작하려면 커넥션이 필요하기 때문에 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료한다
어플리케이션에서 DB 트랜잭션을 사용하려면 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 같은 세션을 사용할 수 있다
➡️커넥션을 파라미터로 전달해서 같은 커넥션을 사용하도록한다
➡️즉, Service 계층에서 생성한 커넥션을, 데이터를 변경하는 Repository의 메서드를 수행할때 파라미터로 넘겨준다
public class MemberRepositoryV2 {
// 파라미터에 Connection 추가
public Member findById(Connection con, String memberId) throws SQLException {
// Connection con = null; ➜ 제거
...
try {
// con = getConnection(); ➜ 제거
...
} catch (SQLException e) {
...
} finally {
// close(con, pstmt, rs); ➜ 제거
JdbcUtils.closeResultSet(rs); // 추가
JdbcUtils.closeStatement(pstmt); // 추가
}
}
}
동일한 커넥션을 사용해야하기 때문에 파라미터에 Connection 추가
메서드에서 Connection 선언하는 부분과 getConnection()
제거
Connection은 닫으면 안되기 때문에 메서드를 이용해서 한 번에 닫지 않고 다른 리소스들을 닫는 코드 추가
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작, 수동 커밋 모드로 설정
bizLogic(con, fromId, toId, money); // 비지니스 로직 수행
con.commit(); // 정상적으로 수행된 경우 커밋 호출
} catch (Exception e) {
con.rollback(); // 실패 시 롤백
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private static void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true); // 커넥션 풀 고려
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
}
메서드를 수행할 때 getConnection()
을 통해 Connection을 획득
트랜잭션 시작 전, setAutoCommit(false)
로 수동 커밋 모드 설정
정상적으로 처리된 경우, commit()
호출, 예외가 발생하면 rollback()
호출
con.close()
를 하면 커넥션 풀에 돌아가는데 처음에 수동 커밋 모드로 설정을 했으니 설정이 유지된 상태로 커넥션 풀에 돌아감
보통 자동 커밋 모드로 기본값이 설정되어 있기 때문에 close()
를 호출하기 전에 자동 커밋 모드로 변경해준다
서비스 계층에서 비지니스 로직 + 트랜잭션 처리를 하기 때문에 코드가 지저분하다
트랜잭션 처리 = 비지니스 로직 수행 전, 후에 커밋 모드 설정과 commit / rollback 처리
또한 Repository에 동일한 기능을 수행하지만 Connection을 생성해서 사용하는 메서드와 동일한 Connection을 사용하는 메서드와 두 개가 존재하게 된다