ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 DB 데이터접근 원리 - 3. 트랜잭션 이해
    Spring-Boot/스프링 DB 1편 - 데이터 접근 핵심 원리 2022. 6. 19. 14:49

    트랜잭션 - 개념 이해

    - 데이터 베이스에서 트랜잭션은 하나의 거래를 안전하게 처리하도록 보장해주는 것을 뜻한다

    - 모든 작업이 성공해서 데이터베이스에 정상 반영하는 것을 커밋( Commit ) 이라 하고, 작업 중 하나라도 실패해서 거래 이전으로 되돌리는 것을 롤백 ( Rollback ) 이라 한다

     

    트랜잭션 ACID

    - * 원자성 * : 트랜잭션 내에서 실행한 작업들을 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다.

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

        ㆍ예를 들어 데이터베이스에서 정한 무결성 제약 조건을 항상 만족해야한다

    - * 격리성 * : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리한다.

        ㆍ예를 들어 동시에 같은 데이터를 수정하지 못하도록 해야한다

        ㆍ격리성은 동시성과 관련된 성능 이슈로 인해 트랜잭션 격리 수준 (Isolation level)을 선택할 수 있다

    - * 지속성 * : 트랜잭션을 성공적으로 끝내면 그 결과가 항상 기록되어야 한다

        ㆍ중간에 시스템에 문제가 발생해도 데이터베이스 로그 등을 사용해서 성공한 트랜잭션 내용을 복구해야 한다

     

    - 트랜잭션은 원자성, 일관성, 지속성을 보장한다.

    - 문제는 격리성인데 트랜잭션 간에 격리성을 완벽히 보장하려면 트랜잭션을 거의 순서대로 실행해야한다.

    - 이렇게 하면 동시 처리 성능이 매우 나빠진다

    - 이런 문제로 인해 ANSI 표준은 트랜잭션의 격리 수준을 4단계로 나누어 정의했다

     

    * 트랜잭션 격리 수준 - Isolation level *

    - READ UNCOMMITED ( 커밋되지 않은 읽기 )

    - READ COMMITTED ( 커밋된 읽기 )

    - REPEATABLE READ ( 반복 가능한 읽기 )

    - SERIALIZABLE ( 직렬화 가능 )

    * 단계가 높아질수록 성능이 느려짐, 보통 커밋된 읽기를 많이 씀

     

    * 참고 *

    - 강의에서는 일반적으로 많이 사용하는 READ COMMITTED ( 커밋된 읽기 ) 트랜잭션 격리 수준을 기준으로 설명한다

    - 트랜잭션 격리 수준은 데이터베이스에 관한 부분이어서 이 강의 내용을 넘어선다


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

    * 데이터베이스 연결 구조 *

    - 사용자는 웹 어플리케이션 서버(WAS)나 DB 접근 툴 같은  클라이언트를 사용해서 데이터베이스 서버에 접근

        ㆍ클라이언트는 데이터베이스 서버에 연결을 요청하고 커넥션을 맺게된다.

        ㆍ이때 데이터베이스 서버는 내부에 세션이라는 것을 만든다

        ㆍ그리고 앞으로 해당 커넥션을 통해 모든 요청은 이 세션을 통해서 실행

    - 쉽게 이야기해서 개발자가 클라이언트를 통해 SQL을 전달하면 현재 커넥션에 연결된 세션이 SQL을 실행한다

    - 세션은 트랜잭션을 시작하고, 커밋 또는 롤백을 통해 트랜잭션을 종료한다

    - 그리고 이후에 새로운 트랜잭션을 다시 시작할 수 있다

    - 사용자가 커넥션을 닫거나, 또는 DBA(DB 관리자)가 세션을 강제로 종료하면 세션은 종료된다

    - 커넥션 풀이 10개의 커넥션을 생성하면, 세션도 10개 만들어진다.

     


    트랜잭션 - DB예제1 - 개념 이해

    * 트랜잭션 사용법 *

    - 데이터 변경 쿼리를 실행하고 데이터베이스에 그 결과를 반영하려면 커밋 명령어인 'commit'을 호출하고, 결과에 반영하고 싶지 않으면 롤백 명령어인 'rollback'을 호출하면 된다

    - *커밋을 호출하기 전까지는 임시로 데이터를 저장*하는 것이다. 따라서 해당 트랜잭션을 시작한 세션(사용자)에게만 변경 데이터가 보이고 다른 세션(사용자)에게는 변경 데이터가 보이지 않는다

    - 등록, 수정, 삭제 모두 같은 원리로 동작한다. 앞으로 등록, 수정, 삭제를 간단히 *변경*이라는 단어로 표현하겠다.

     

    * 커밋하지 않은 데이터를 다른 곳에서 조회할 수 있으면 어떤 문제가 발생할까? *

    - 예를 들어 커밋하지 않은 데이터가 보인다면,

        ㆍ세션2가 데이터를 조회했을 때 어떤 로직을 수행
        ㆍ그런데 세션1이 롤백을 수행하면 데이터가 사라진다

        ㆍ데이터 정합성에 큰 문제가 발생

     

    - ' commit '으로 새로운 데이터가 실제 데이터베이스에 반영된다

        ㆍ데이터의 상대도 임시 → 완료로 변경

    - ' rollback ' 호출 시 수정하거나 삭제한 데이터도 모두 트랜잭션을 시작하기 직전의 상태로 복구

     

    트랜잭션 - DB예제2 - 자동 커밋, 수동 커밋

    - 자동 커밋으로 설정하면 각각의 쿼리 실행 직후에 자동으로 커밋을 호출한다.

    - 따라서 커밋이나 롤백을 직접 호출하지 않아도 되는 편리함이 있다

    - 하지만 쿼리를 하나하나 실행할 때마다 자동으로 커밋되어버리기 때문에 우리가 원하는 트랜잭션 기능 제대로 사용 X

    - 따라서 commit, rollback을 직접 호출하면서 트랜잭션 기능을 제대로 수행하려면 자동 커밋을 끄고 수동 커밋을 사용해야한다

    // 자동 커밋모드 설정
    set autocommit true;
    // 수동 커밋 설정
    set autocommit false;
    commit; // 수동 커밋

    - 보통 자동 커밋모드가 기본으로 설정된 경우가 많기 때문에, *수동 커밋 모드로 설정하는 것을 트랜잭션을 시작 *한다고 표현할 수 있다

    - 수정 커밋 설정을 하면 이후에 꼭 'commit', 'rollback'을 호출해야한다

    - 참고로 수동 커밋모드나 자동 커밋모드는 한번 설정하면 해당 세션에서는 계속 유지된다. 중간에 변경하는 것은 가능

     

     

    * 정리 *

    *원자성* : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야 한다

    - 트랜잭션의 원자성 덕분에 여러 SQL 명령어를 마치 하나의 작업인 것처럼 처리할 수 있었다.

    - 성공하면 한번에 반영하고, 중간에 실패해도 마치 하나의 작업을 되돌리는 것처럼 간단히 되돌릴 수 있다.

     

    *오토 커밋*

    - 만약 오토 커밋 모드로 동작하는데, 쿼리를 하나 실행할 때 마다 바로바로 커밋

    - 중간에 실패 시 심각한 문제 발생

     

    *트랜잭션 시작*

    - 이런 종류의 작업은 꼭 수동 커밋 모드를 사용해서 수동으로 커밋, 롤백 할 수 있도록 해야 한다.

    - 보통 이렇게 자동 커밋 모드에서 수동 커밋 모드로 전환하는 것을 트랜잭션을 시작한다고 표현

     


    DB 락 - 개념 이해

    - 세션1이 트랜잭션을 시작하고 데이터를 수정하는 동안 아직 커밋을 수행하지 않았는데, 

      세션2에서 동시에 같은 데이터를 수정하게 되면 여러가지 문제가 발생

    - 바로 트랜잭션의 원자성이 깨지는 것이다.

    - 여기에 더해서 세션1이 중간에 롤백을 하게 되면 세션2는 잘못된 데이터를 수정하는 문제가 발생

     

    - 이런 문제를 방지하려면, 세션이 트랜잭션을 시작하고 데이터를 수정하는 동안에는 커밋이나 롤백 전까지 다른 세션에서 해당 데이터를 수정할 수 없게 막아야 한다

     

    락 ( Lock )

    - 데이터베이스 두 세션이 동시에 데이터를 수정하고 싶을 때, 즉 위와 같은 문제가 생길때 락( Lock )이라는 개념을 제공

    - 작동방식

    1. 세션1은 트랜잭션을 시작

    2. 세션1은 변경시도

        ㆍ이때 해당 로우의 락을 먼저 획득해야한다

        ㆍ락이 남아 있으므로 세션1은 락을 획득한다 ( 세션1이 세션2보다 조금 더 빨리 요청 )

    3. 세션1은 락을 획득했으므로 해당 로우에 update sql  수행

     

    4. 세션2는 트랜잭션을 시작

    5. 세션2도 변경 시도

        ㆍ이 때 해당 로우의 락을 먼저 획득해야한다

        ㆍ락이 없으므로 락이 돌아올 때까지 대기

        ㆍ참고로 세션2가 락을 무한정 대기하는 것이 아니다. 락 대기 시간을 넘어가면 락 타임아웃 오류가 발생한다. 락 대기 시간은 설정할 수 있다.

     

    6. 세션1은 커밋을 수행한다.

        ㆍ커밋으로 트랜잭션이 종료되었으므로 락도 반납한다.

        ㆍ 락을 획득하기 위해 대기하던 세션2가 락을 획득한다

    7. 세션2는 update sql을 수행한다.

    8. 세션2는 커밋을 수행하고 트랜잭션이 종료되었으므로 락을 반납한다.

     

    SET LOCK_TIMEOUT <milliseconds>

    : 락 타임아웃 시간을 설정한다.

    - 예) SET LOCK_TIMEOUT 10000 : 10초, 세션2에 설정하면 세션2가 10초동안 대기해도 락을 얻지 못하면 락 타임아웃 오류가 발생한다.

     

    DB 락 - 조회

    * 일반적인 조회는 락을 사용하지 않는다 *

    - 데이터베이스마다 다르지만, 보통 데이터를 조회할 때는 락을 획득하지 않고 바로 데이터를 조회할 수 있다

    - 예를 들어 세션1이 락을 획득하고 데이터를 변경하고 있어도, 세션2에서 데이터를 조회는 할 수 있다

     

    * 조회와 락 *

    - 데이터를 조회할 때도 락을 획득하고 싶을 땐 ' select for update ' 구문을 사용하면 된다.

    - 이렇게 하면 세션1이 조회 시점에 락을 가져가버리기 떄문에 다른 세션에서 해당 데이터를 변경 X

    - 물론 이 경우도 트랜잭션을 커밋하면 락을 반납한다

     

    * 조회 시점에 락이 필요한 경우는 언제일까? *

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

    -  예를 들어 애플리케이션 로직에서 ' memberA '의 금액을 조회한 다음에 이 금액 정보로 애플리케이션에서 어떤 계산을 수행한다. 그런데 계산이 돈과 관련된 매우 중요한 계산이어서 계산을 완료할 때까지 변경하면 안된다. 이럴 때 조회 시점에 락을 획득하면된다.

     

    set autocommit false;
    select * from member where member_id='memberA' for update;

    - ' select for update ' 구문을 사용하면 조회를 하면서 동시에 선택한 로우의 락도 획득한다

        ㆍ물론 락이 없다면 락을 획득할 때 까지 대기해야한다

    - 세션 1은 트랜잭션을 종료할 때까지 로우의 락을 보유한다.

     

    * 정리 *

    - 트랜잭션과 락은 데이터베이스마다 실제 동작하는 방식이 조금씩 다르기 떄문에, 해당 데이터베이스 메뉴얼을 확인해보고, 의도한대로 동작하는지 테스트한 이후에 사용하자

     


    트랜잭션 적용

    - 애플케이션에서 트랜잭션을 어떤 계층에 걸어야 할까? 쉽게 이야기해서 트랜잭션을 어디에서 시작하고, 어디에서 커밋해야 할까?

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

    - 그런데 트랜잭션을 시작(set autocommit false)할려면 커넥션이 필요하다. 결구 서비스 계층에서 커넥션을 만들고, 트랜잭션 커밋 이후에 커넥션을 종료해야 한다.

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

     

    * 커넥션과 세션 *

    - 애플리케이션에서 같은 커넥션을 유지하려면 어떻게 해야할까?

    - 가장 단순한 방법은 커넥션을 파라미터로 전달해서 같은 커넥션이 사용되도록 유지하는 것이다.

     

    - 먼저 리포지토리가 파라미터를 통해 같은 커넥션을 유지할 수 있도록 파라미터를 추가하자!

    findById(Connection con, String memberId)
    update(Connection con, String memberId, int money)

    * 주의 *

    1. 커넥션 유지가 필요한 두 메서드는 파라미터로 넘어온 커넥션을 사용해야 한다. 따라서 con = getConnection( ) 코드가 있으면 안된다.

    2. 커넥션 유지가 필요한 두 메서드는 리포지토리에서 커넥션을 닫으면 안된다. 커넥션을 전달 받은 리포지토리 뿐만 아니라 이후에도 커넥션을 계속 이어서 사용하기 때문이다

    이후 서비스 로지이 끝날 때 트랜재션을 종료하고 닫아야 한다.

     

    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
            Connection con = dataSource.getConnection();	// 트랜잭션을 위한 커넥션
            try {
                con.setAutoCommit(false);   // 트랜잭션 시작
                // 비즈니스 로직
                
                con.commit();   // 성공시 커밋
    
            } catch (Exception e) {
                con.rollback(); // 실패시 롤백
                throw new IllegalStateException(e);
            } finally {
                con.setAutoCommit(true);    // 커넥션 풀 고려, 기본값인 자동커밋모드로 변경
                con.close();
            }
        }

     

    * 남은 문제 *

     - 애플리케이션에서 DB 트랜잭션을 적용하려면 서비스 계층이 매우 지저분해지고, 생각보다 매우 복잡한 코드를 요구한다. 추가로 커넥션을 유지하도록 코드를 변경하는 것도 쉬운 일은 아니다.

    댓글

Designed by Tistory.