스프링 DB - 트랜잭션 이해

Heeeoh·2024년 2월 23일
0

스프링 DB

목록 보기
3/9
post-thumbnail

🌿 시작하기 앞서


스프링 부트 3.2.2 버전을 기준으로 작성됨
H2 데이터베이스 Version 2.2.224 (2023-09-17)
개념 위주로 정리 실질적인 실습은 강의 자료 참조

🌱 트랜잭션


데이터를 파일에 저장하지 않고 데이터베이스에 저장하는 이유는
트랜잭션 개념을 지원하기 때문이다.

이름 그대로 번역하면 거래 라는 뜻이며
쉽게 말해서 하나의 거래를 안전하게 처리하도록 보장하는 것 이다.

계좌이체가 예시가 된다.

💠 트랜잭션 ACID


트랜잭션은 ACID를 보장해야한다.

  • 원자성(Atomicity) : 트랜잭션 내에서 실핸한 자겅ㅂ들은 마치 하나의 작업인 것처럼 모두 성공 or 실패 해야 한다.

  • 일관성(Consistency) : 모든 트랜잭션은 일관성 있는 데이터베이스 상태를 유지해야한다.

  • 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다. (동시에 데이터 수정 방지)

    • 격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준(Isolation level)을 선택할 수 있다.
  • 지속성(Durability) : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야하며 중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다.

트랜잭션 간 격리성을 완벽히 보장시 트랜잭션을 거의 순서대로 실행해야 하고 동시 처리 성능이 매우 나빠진다.
이런 문제로 인해 ANSI 표준은 격리 수준을 4단계로 나누어 정리했다.

✔️ 트랜잭션 격리 수준 - Isolation level

  • READ UNCOMMITTED(커밋되지 않은 읽기)
  • READ COMMITTED(커밋된 읽기)
  • REPEATABLE READ(반복 가능한 읽기)
  • SERIALIZABLE(직렬화 가능)

강의에 따라 READ COMMITTED 를 기준으로 사용



✔️ 데이터베이스 연결 구조와 DB 세션


사용자가 웹 애플리케이션 서버나 DB 접근 툴 같은 클라이언트를 사용해서 데이터베이스 서버에 접근 시
클라이언트는 DB 서버에 연결을 요청하여 커넥션을 맺는다.
이때 DB 서버는 내부에 '세션'을 만든다.
해당 커넥션을 통한 모든 요청은 세션을 통해서 실행하게 된다.

세션은 SQL문을 실행하거나 트랜잭션 시작, 커밋, 롤백을 통해 트랜잭션 종료를 한다.

사용자가 커넥션을 닫거나 DBA가 세션을 강제 종료하면 세션은 종료된다.
커넥션 풀이 n개의 커넥션을 생성하면, 세션도 n개 만들어 진다.

✔️ 트랜잭션 개념 이해


현재 세션과 DB의 상태라 가정한다.

트랜잭션 사용법

  1. 트랜잭션을 시작한다. (autoCommit false)
  2. SQL 문을 날린다.
  3. 결과가 세션에 담긴다.
  4. 결과를 반영하려면 commit, 그렇지 않다면 rollback을 호출한다.
  5. 트랜잭션을 종료한다. (autoCommit true)

여기서 주의할 점은
세션 1 에서 트랜잭션을 시작하여 새로운 데이터를 추가하거나 변경해도 세션 1 이 커밋하지 않는 이상 세션 2가 DB를 조회하면 기존 DB를 조회한다.
즉, 세션 1반영하지 않는 한 기존 DB는 그대로 유지된다.


자동 커밋

쿼리 실행 후 자동으로 커밋을 호출하는 것으로
기본값이 True 로 설정된 경우가 많다.
트랜잭션을 사용하려면 수동 커밋 설정이 필요하다.

set autocommit false; //수동 커밋 모드 설정
insert into member(member_id, money) values ('data3',10000);
insert into member(member_id, money) values ('data4',10000);
commit; //수동 커밋

자바 서비스 계층에서 설정

import java.sql.Connection;

Connection con = dataSource.getConnection();
con.setAutoCommit(false); // 커넥션이 제공하는 커밋 설정

수동 커밋 모드로 설정하는 것 = 트랜잭션 시작



✔️ 쿼리 실행 오류시


처음 기본 값 설정

set autocommit true;
delete from member;
insert into member(member_id, money) values ('memberA',10000);
insert into member(member_id, money) values ('memberB',10000);

트랜잭션 시작

set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA'; //성공
update member set money=10000 + 2000 where member_iddd = 'memberB'; //쿼리 예외 발생

오류 메시지

Column "MEMBER_IDDD" not found; SQL statement:
update member set money=10000 + 2000 where member_iddd = 'memberB' [42122-200]
42S22/42122

memberB의 id 가 틀려서 memberA만 돈이 나간 상태이지만 실질적으로는 세션 1 안에서만 처리되었기에 이 경우에는 rollback을 호출하면 된다.

만약 자동 커밋이였다면 심각한 일이 발생한 것이다.



✔️ DB 락


세션 1이 트랜잭션을 시작하고 세션 2가 중간에 트랜잭션을 시작하여 데이터를 수정한다면? -> 원자성이 깨지고, 세션 1이 롤백하는 순간 골치아픈 일이 발생하는 것이다.

이러한 문제를 방지하기 위해 세션은 트랜잭션 시작 후 데이터를 수정하는 동안에는 커밋 or 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 락(Lock)을 제공한다.

세션 1이 커밋이나 롤백하면 락은 반환한다.
대기중이던 세션 2가 락을 획득한다.

락 획득 시간 설정

SET LOCK_TIMEOUT <milliseconds>
// SET LOCK_TIMEOUT 60000; // 60초 

해당 시간 안에 락을 얻지 못하면 예외가 발생


조회

일반적인 조회는 락 사용 X

  • 조회할 때도 락 획득하는 방법
    • select for update 구문을 사용
set autocommit false;
select * from member where member_id='memberA' for update;

조회 + 선택한로우의 락도 획득
물론 락이 없다면 대기해야한다.

조회 락이 필요한 경우

트랜잭션 종료 시점까지 해당 데이터를 다른 곳에서 변경하지 못하도록 강제로 막아야 하는 경우

  • 돈 관련된 매우 중요한 계산


✔️ 트랜잭션 적용


트랜잭션은 비지니스 로직이 있는 서비스 계층에서 시작해야 한다.
-> 비지니스 로직이 잘못되면 해당 비지니스 로직으로 인해 문제가 되는 부분을 함께 롤백해야 하기 때문

애플리케이션에서 DB 트랜잭션을 사용하려면
트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 한다.


❗ 남은 문제


서비스 계층이 지저분하고 생각보다 복잡한 코드를 요구
커넥션 유지인 채로 코드변경이 쉽지 않음
예외 처리 문제


📓 정리


거래요청이 온다는 가정하에
서비스 계층
1. 커넥션 획득
2. 트랜잭션 시작 (수동 커밋 설정)
3. 비지니스 로직 실행 (sql문 실행 등)
4. 성공시 커밋
4-1. 실패시 롤백
5. 트랜잭션 종료


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); // 예외는={}같은 걸 안넣는다.

        }
    }
}




🔖 학습내용 출처

스프링 DB 1편 - 데이터 접근 핵심 원리

profile
열심히 살자

0개의 댓글