Database/Spring DB 접근 1

3. 트랜잭션 이해

hongod 2022. 7. 3. 16:43

1. 거래(영어 : Transaction) 한 건의 완성

 A에게서 B에게로 5000원을 송금하는 과정은 아래의 두 가지 동작으로 나눌 수 있다.

 

    1) A의 잔고에서 5000원을 차감

    2) B의 잔고에서 5000원을 증가

 

 만약에 1)의 과정만 수행되고 2)의 과정이 수행되기 전에 오류로 인해 DB 커넥션이 끊어져버리면 어떻게될까? 거래가 제대로 수행되지 않고 A의 잔고에서 5000원이 증발해버린 상황이 펼처지게된다. 회계로 따지면 차/대가 불일치하는 상황(?)이 펼쳐지게 되는 것이다. 이렇게, 작게 봤을 때에는 부분으로 나뉘어지지만 전체적으로 하나의 과정으로 처리돼야하는 DB 통신 과정은 DB에서 제공하는 트랜잭션이라는 기능을 이용하면 각 파트들이 모두 다 함께 수행되거나, 그렇지 않으면 모두 수행 되지 않을 것을 보장할 수 있다. 위의 예를 이용해 말하자면 1)의 과정이 완료되고 2)의 과정 중에 문제가 발생하면 이미 수행된 1)의 과정 역시 이전 상태로 복구하여, 결과적으로 차감된 A의 잔고가 이 전 상태로 복구됨을 보장할 수 있다는 것이다.

 이를 위해서 모든 작업이 성공했으니 완료된 결과들을 반영해달라고 요청하는 commit이라는 액션과 작업 중 오류가 발생하여 이전 과정으로 복구를 요청하는 rollback 액션이 추가적으로 필요하다.

2. 트랜잭션 격리 수준

 트랜잭션이 보장해야하는 몇 가지 특성 중 격리성(isolation)이라는 것이 있는데, 그 뜻은 동시에 여러 개의 트랜잭션 요청이 오면 각각이 서로에게 영향을 미치지 않도록 격리돼야한다는 것이다. 예들 들면 동시에 한 데이터가 수정될 수 없도록 해야한다는 것이다. 가장 확실한 방법은 모든 트랜잭션이 동시에 수행되는 것을 불가능하게 만들고 요청된 순서대로 처리하는 것인데, 이는 DB 성능이슈와 연결되는 부분이기 때문에 보통 완벽하게 동시 수행을 막지 않고 4단계의 격리 수준을 사용자가 선택할 수 있도록 한다. 4 단계의 격리 수준은 아래와 같다.

 

 1) READ UNCOMMITED : 커밋 안된 것도 읽을 수 있게

 2) READ COMMITED : 커밋된 것들만 읽을 수 있게

 3) REPEATABLE READ : 한 트랜잭션 내에서의 읽기 결과는 항상 같도록

 4) SERIALIZABLE : 직렬화 가능

 

3)의 repeatable read에 대해서 조금 더 설명하자면, 현재 읽기 트랜잭션에 여러 개의 select 문이 있다고 가정하자. 이 때 각 첫 select문이 실행되고 그 다음 select문이 실행되기 전 그 중간 시점에 다른 transaction에서 수정된 내용이 커밋됐 을 수 있는데, 이후에 select문이 실행되더라도 repeatable read 상태에서는 이 커밋된 내용을 무시하고 수정되기 전 상태의 정보를 읽는다. 즉 한 transaction 내에서는 반복(repeatable)해서 같은 select문을 수행하면 항상 같은 결과를 내놓을 것을 보장한다는 것이다. 이 격리 수준에 대한 내용은 아래 블로그를 참조하면 더 자세히 공부할 수 있다.

 

참조 : https://nesoy.github.io/articles/2019-05/Database-Transaction-isolation

 

트랜잭션의 격리 수준(isolation Level)이란?

 

nesoy.github.io

 

 현업에서 일반적으로는 2)READ COMMITED 를 많이 사용한다. 따라서 이 부분 아래의 내용은 READ COMMIT 기준으로 설명한다.

3.  DB 커넥션과 세션

 웹 어플리케이션이나 WorkBench같은 DB 툴이 DB에 연결을 요청하게되면 DB 서버에서는 커넥션을 맺고나서 내부에서는 세션이라는 것을 만든다. 그리고 해당 커넥션을 통한 요청은 이 세션을 통해서 처리한다. 클라이언트가 SQL을 요청하면 현재 커넥션과 연결된 세션이 이 SQL을 실행해준다. 이러한 세션은 트랜잭션을 시작하고 커밋이나 롤백을 통해 트랜잭션을 종료할 수 있으며 트랜잭션이 종료되면 또 다른 트랜잭션을 실행할 수 있다. 사용자가 커넥션을 닫거나 DBA가 강제료 종료하면 이 세션은 없어진다.

 

출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런) / 김영한

 일반적으로 여러 개의 커넥션이 DB 서버와 맺어져있고 따라서 여러 세션이 생성돼있으며, 각 세션에서 수정한 내용들은 commit 되기 전까지는 다른 세션에서 볼 수 없다. commit을 실행하기 전까지는 한 세션에서 어떤 액션을 취해도 서로 다른 세션에서는 서로를 볼 수 없는 다른 세상인 것이다.

4.  자동/수동 커밋

 트랜잭션을 시작하려면 자동커밋, 수동커밋 개념을 알아야한다. 자동 커밋은 말 그대로 쿼리가 실행된 직후 커밋도 함께 자동으로 실행되는 것이고 수동 커밋 모드에서는 명시적으로 commit을 호출해야 한다. 따라서 우리가 원하는 트랜잭션을 구현하기 위해서는 기본적으로 수동 커밋모드로 set 돼야 한다. 그래야 혹시 일부 쿼리에서 에러가 발생했을 때 오류가 발생한 쿼리의 이전 쿼리들이 만들어낸 결과가 반영되기 전에 rollback할 수 있게 되고 이를 통해 DB 트랜잭션의 원자성을 유지할 수 있다.

set autocommit true; //자동 커밋 모드 설정
insert into member(member_id, money) values ('data1',10000); //자동 커밋
insert into member(member_id, money) values ('data2',10000); //자동 커밋

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

 대부분의 많은 경우 자동 커밋으로 설정된 경우가 많기 때문에 수동 커밋으로 설정하는 것을 트랜잭션의 시작으로 본다. 수동/자동 커밋 설정은 한 번 설정하면 해당 세션에서는 계속 유효하며 중간에 변경할 수 있다.

5. DB 락(lock)

 하나의 데이터에 여러 트랜잭션 요청이 거의 동시에 도착했다고 가정해보자. 이를 순서 없이 처리하면 예를 들어 요청 A가 변경 중인 데이터를 요청 B가 수정하고, 전혀 기대하지 않았던 값을 가지고 요청 A는 그 다음 쿼리를 이어서 수행하는, 속된 말로 개판 오분전이 된다. 이렇게 되면 혹여 요청 A의 트랜잭션이 롤백되면 트랜잭션 B는 수행 도중 갑자기 값이 초깃값으로 변경되기도 한다. 즉, 이런 경우는 막아야한다.

 

 이를 막기 위해서는 당연하게도, 하나의 세션이 트랜잭션 수행 중에는 다른 세션이 그 데이터를 수정할 수 없게 해야한다. 이를 위해서 운영체제에서 사용하는 세마포어나 뮤텍스와 비슷한 개념을 사용하는데 이를 DB lock 이라고 한다. 작업하고자 하는 데이터에 대한 lock을 얻은 세션이 트랜잭션을 수행할 권한을 가지고, 다른 세션들은 lock을 얻은 세션이 트랜잭션을 종료할 때까지 대기한다.

 

출처 : 스프링 DB 1편 - 데이터 접근 핵심 원리(인프런) / 김영한

위 그림에서 이미 트랜잭션을 수행 중인 세션 1이 lock을 가지고 있기 때문에 세션2는 대기에 들어간다. 세션1이 commit을 수행하여 트랜잭션을 종료하면 lock을 반납하고, 비로소 세션 2는 트랜잭션을 수행할 권한을 가지게된다.

 

일반적으로 lock은 변경 시에만 적용되고 조회 시에는 lock을 얻을 필요가 없다. 그래서 세션 A가 lock을 가지고 있어도 세션 B가 해당 데이터를 조회하는 것은 가능한다. 물론 변경은 lock이 없기 때문에 불가능한다. 하지만 만약에 조회한 값을 애플리케이션 로직에서 가공해야하고, 이 데이터가 매우 중요한 것이라 계산 완료시까지 다른 곳에서 변경하는 것을 금지하고싶다면 select for update을 사용하여 lock을 얻을 수 있다.

6. 트랜잭션을 시작하는 계층

 애플리케이션 코드에서 트랜잭션이 동작하는 부분은 어디가 돼야할까? 즉 어디서 트랜잭션을 시작하고 commit을 수행해야하는 걸까? 결론부터 얘기하면 비즈니스 계층이다. 비즈니스 계층에서 수행한 모든 데이터 수정 로직들은 모두 다 정상 처리 되거나, 혹시 일부 오류가 생긴다면 모두 수행되지 않은 상태로 돌아가야하기 때문이다. 또한 하나의 트랜잭션을 시작시키고 종료 시키는 것은 세션이 담당하기 때문에 비즈니스 계층이 커넥션을 얻고,이 커넥션을 쭉 유지하고, 최종 commit 후 이를 닫는 과정까지 마무리 해야한다.