14 min read

<DB> Transaction과 Lock 이해하기

개발을 하시면서 Transaction이라는 용어를 정말 많이 들어보셨을 것입니다.저 또한 용어의 정확한 의미는 모른채 사용했었습니다.예를 들어 “뭐 대충 데이터에 변경의 결과를 보장해준다는 거 아냐?” 정도의 느낌으로만 이해했습니다.혹시나 저와 같이 저런 느낌으로만 이해하신분이 있다면 해당 글을 읽고 Transaction에 대해 확실히 알게 되었음합니다.

Transaction

흔히 DB는 OS만큼 복잡한 소프트웨어라고 얘기합니다.이러한 DB의 주요한 기능 중 하나가 트랜잭션입니다.여기서 중요한점은 트랜잭션은 DB가 지원하는 기능이라는 것입니다.본격적으로 트랜잭션이 보장해주는 속성들에 대해 설명해보겠습니다.

쉽게 용돈 기입장을 예시로 설명해보겠습니다.예를 들어 용돈을 만원 받은날 저녁에 용돈 기입장에 만원을 기입하려합니다.자,그러면 그날 용돈 기입장에 일정 금액을 무조건 적어야 합니다.반대로 어느날은 용돈이 들어오지도 쓰지도 않은 날이 있습니다.이 날은 아무것도 기입하지 않습니다.결론적으로 돈을 받거나 쓰면 기입하고 돈을 받거나 쓰지 않으면 기입하지 않는다는 것입니다.또한 본인의 용돈 기입장이니 동생이나 부모님이 와서 고치거나 지우는 행동은 하면 안됩니다.또 다른 경우로 내가 문구점에서 책을 사는 동안 친구에게 내 돈은 주며 아이스크림을 사오라고 했습니다.이러한 경우에 용돈 기입장에는 문구점에서 쓴 돈과 아이스크림을 사면서 친구가 쓴돈은 따로따로 기입해줘야 할 것입니다.마지막으로 하루동안 돈을 쓴 적이 있다면 저녁에 돌아와서 무조건 용돈 기입장에 해당 내역을 기입해줘야합니다.

방금 적은 4가지 예시를 보시면 상식적인 선에서 용돈기입장을 제대로 적기 위해서는 지켜져야할 조건들임을 알 수 있습니다.

위 조건들은 트랜잭션이라는 기능이 수행될 때 보장하는 4가지 속성(ACID)과 동일합니다.구체적으로는 아래와 같습니다.

  1. 원자성 : 트랜잭션 내에서 실행한 작업들은 마치 하나의 작업인 것처럼 모두 성공하거나 모두 실패해야합니다.
  2. 일관성 : 모든 트랜잭션은 일관성있는 데이터베이스 상태를 유지해야합니다.예를 들면 DB에서는 데이터의 무결성 제약 조건 만족해야합니다.
  3. 격리성 : 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리해야합니다.예를 들어 동시에 같은 데이터를 수정하면 안되도록 해야합니다.
  4. 지속성 : 트랜잭션이 성공적으로 끝내면 그 결과가 항상 기록되어야합니다.

그러면 정리를 한번 해보겠습니다.

트랜잭션은 DB라는 소프트웨어가 지원하는 중요한 기능이며,이는 4가지 조건을 지키면서 데이터에 대한 변경을 수행해줍니다.즉,Transaction이라는 뜻에 알맞게 ‘거래’가 발생했을 때 거래로부터 인해 발생하는 데이터들의 변경을 위 4가지 속성을 무조건 지키며 수행합니다.다만’거래’라는 단어는 포괄적이지 못하기에 데이터의 변경이 발생할 경우 위의 4가지 조건을 만족하며 데이터의 변경을 끝 마쳐주는 것이라고 이해해도 좋습니다.

결국 저희가 DB를 쓰는 이유 또한 트랜잭션과 관계가 깊습니다.일련의 데이터들이 위와 같은 4가지 조건을 만족시키지 않고 저장된다면 그 데이터는 올바르지 못할 확률이 높기 때문이죠.올바르지 않는 데이터를 제공하는 프로덕트가 사용자의 선택을 받는것은 힘들것입니다.

DB 세션과 트랜잭션

이번에는 위에서 배운 트랜잭션이라는 기능이 DB 내에서 어떻게 동작하는지 간단하게 알아보겠습니다.우선 트랜잭션은 DB내의 세션이라는 것을 통해 시작되고 종료됩니다.세션부터 한번 알아봅시다.

앞선 커넥션 풀 포스팅을 통해 DB의 커넥션이 어떤 역할을 수행하는지 이해할 수 있습니다.이러한 커넥션들마다 가질 수 있는 것이 세션입니다.DB와 클라이언트간의 커넥션이 맺어지게 되면 해당 커넥션은 DB내에 세션을 생성합니다.이 때 생성된 세션이 트랜잭션의 시작과 종료를 담당하는 역할을 수행하게 됩니다.세션의 생명주기를 정리해보겠습니다.

  1. 클라이언트와 DB가 커넥션 맺음
  2. 이후,DB에서 해당 커넥션과 연결된 세션을 생성
  3. 해당 커넥션을 통해 들어오는 모든 요청은 방금 생성된 세션을 통해 실행
  4. 요청이 끝나면 또 다른 요청을 받을 수 있게 세션은 유지됨
  5. 세션을 끝내려면 커넥션을 끊거나 강제 종료해야함

또한 세션이 만약 트랜잭션을 시작했으면 롤백 혹은 커밋을 통해 해당 트랜잭션을 종료시킨뒤,다음 트랜잭션을 수행하도록 기다리고 있습니다.또한 커넥션마다 세션을 만들어 서로 연결하고 있다고 했는데,만약 커넥션 풀에 10개의 커넥션이 있다면 해당 커넥션들마다 세션을 가지고 있습니다.즉,10개의 커넥션 = 10개의 세션입니다.

김영한 - 스프링 DB 1
김영한 - 스프링 DB 1

Transaction 실습하기

참고로 H2 데이터베이스를 활용해 실습을 진행합니다.

이번에는 간단한 예제를 통해 직접 DB에 SQL을 작성하며 트랜잭션이 어떻게 작동하는지 알아보겠습니다.예제의 시나리오는 다음과 같습니다.

  1. 회원 A,B는 모두 10000원의 금액을 보유하고 있습니다.
  2. A -> B에게 2000원의 송금 발생

직접 쿼리를 날리며 알아봅시다.우선 간단한게 member 테이블을 하나 작성합니다.

drop table member if exists;
  create table member (
      member_id varchar(10),
      money integer not null default 0,
      primary key (member_id)
);

아래와 같이 아무 데이터도 없는 테이블이 생성된것을 확인해주시면 됩니다.

이번에는 insert 쿼리를 직접 날려볼텐데 이에 앞서 자동 커밋과 수동 커밋에 대한 개념을 말씀드리겠습니다.일단 커밋이라고 함은 데이터의 변경을 확정을 짓는 것이라고 생각하셔도 됩니다.그리고 자동 커밋의 경우 데이터를 변경하는 쿼리가 발생할 경우 해당 쿼리로부터 발생한 변경을 DB 테이블에 바로 커밋하여 반영시키는것입니다.반면 수동 커밋의 경우 데이터에 대한 쿼리가 발생하여도 DB 테이블에는 바로 반영되지 않고 직접 커밋 명령어를 입력해줘야 반영되는 것입니다.아래 SQL을 통해 insert 쿼리를 자동 커밋과 함께 날려봅시다.

 	set autocommit true; //자동 커밋 모드 설정
	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_id = 'memberB';

그리고 나서 해당 H2 DB 세션에서는 아래와 같이 반영이 된것같은 테이블을 확인 할 수 있습니다.

하지만 url에 localhost:8082로 새롭게 창을 띄우고 해당 세션을 통해 DB를 조회해보면 아래와 같이 DB에 반영이 아직 되지 않을것을 확인 할 수 있습니다.(이전과 동일하게 10000원입니다.이전 결과 이미지와 url 상의 세션 id가 다른것 또한 확인 가능합니다)

이후 변경을 실행한 DB 세션에서 아래의 커밋 명령어를 넣어주면 완전히 DB 테이블에 데이터가 변경이 완료됨을 확인할 수 있습니다.

commit;

여기서 가장 중요한 점을 Trancsaction을 사용하면 시작과 동시에 set auto commit false명령어로 수동커밋으로 설정을 한다는것입니다.이러한 명령어를 쓰는이유는 트랜잭션 중간에 예외상황이 발생할 경우 해당 트랜잭션에서 발생한 데이터 변경을 rollback을 통해 무효화 시킬 수 있기 때문입니다.

예를 들어 계좌이체를 하던 도중 네트워크 에러가 생겨 본인의 계좌 잔고에서만 돈이 빠져나가고 상대방에게는 입금이 되지 않았다고 하면 이건 말이 안되는 상황이 벌어지게 되는것이죠.이러한 상황을 예방하기 위해 set autocommit false 명령어를 트랜잭션의 시작과 함께 사용하는것 입니다.

Lock 이해하기

DB에서 Lock이라는 용어는 동사라기보다 명사로 사용됩니다.주로 “락을 획득한다,락을 반납한다”라는 문장과 함께 자주 사용됩니다.

본격적으로 Lock에 대해 이해해봅시다.아래와 같은 상황이 있습니다.

  1. Autocommit = false 설정
  2. memberA의 money를 12000원으로 세션 A에서 변경
  3. 이후 memberA의 money를 20000원으로 세션 B에서 변경 시도

상식적인 선에서 생각을 한번 해봅시다.저희가 국민은행 계좌에서 10000원을 출금을 ATM기를 통해 합니다.그리고 동시에 토스로 20000원을 또 다시 출금하려고 합니다.그러면 당연히 ATM을 통해 계좌에서 돈이 빠져나가는 트랜잭션이 진행되는 동안은 토스에서 출금하려는 트랜잭션이 실행되면 안됩니다.이러한 기능을 수행해주는 도구가 락(Lock)입니다.(실제로는 찰나의 순간이라 동시에 가능한것처럼 보일것 같습니다)

즉,트랜잭션이 시작되어 autocommit이 false가 되고 이후 commit 또는 rollback을 통해 해당 트랜잭션이 종료될때까지 해당 column의 Lock은 현재 트랜잭션을 실행중인 세션에게 주어집니다.결론적으로 아래와 같은 순서로 정리됩니다.

  1. 트랜잭션 시작(autocommit = false)
  2. 해당 Column의 Lock 존재 유무 확인후, 있으면 획득
  3. 변경 query 진행
  4. Commit or rollback
  5. 획득했던 Lock 반납

만약 여기서 또 다른 세션이 해당 컬럼에 대한 변경을 진행하려한다면 2번에서 걸려 대기를 하다가 아래와 같이 에러를 발생시킵니다.(Lock이 없을 경우 세션은 일정시간 기다립니다)

조회 Lock 알아보기

이제 Lock이 DB에 어떤 역할을 수행하는지 알게되었습니다.이번에는 Column을 조회할때,select문을 사용할때도 Lock을 한번 적용시켜보겠습니다.

일반적인 select 쿼리에서는 Lock 없이 해당 데이터에 대한 조회가 가능합니다.예를 들어 세션 A에서 해당 Column에 대해 트랜잭션을 진행중이지만 세션 B에서는 해당 컬럼에 대한 select 쿼리를 정상적으로 날릴 수 있다는 뜻입니다.

만약 조회시에도 락을 걸고 싶으면 아래와 같은 명령어를 붙여줍니다.

	select * from member where member_id="memberA" for update;

정리하자면 일반적인 select 쿼리 뒤에 for upate 구문을 덧붙여 주시면 됩니다.

만약 위와 같은 명령어를 사용하게 된다면 commit 또는 rollback을 통해 트랜잭션을 끝내 락을 반납하는 절차를 수행해야합니다.

지금까지 Transaction과 Lock에 관한 이론적인 내용을 알아보았습니다.다음번에는 해당 트랜잭션을 Java의 JDBC를 통해 직접 구현하고 이를 통해 Spring 프레임워크의 소중함을 한번 알아봅시다.