공부방/데이터베이스

말만 들어도 어려워 보이는 트랜잭션에 대해 자세히 살펴보자 1편

EVO. 2023. 5. 22. 17:44

개요

 프로젝트를 진행하면서 가끔씩 service부분에서 @Transaction이 readOnly=true로 되어있어 현재 수행하는 작업 단위(save,update등)가 불가능하다는 오류가 발생했다는 에러코드가 뜹니다.

물론 제가 아는 바 로는 트랜잭션이 하나의 단위로 원자성 단위로 진행되기 위해 선언한다고 대충은 알고 있습니다.

 하지만 트랜잭션은 기술면접에서도 중요하게 다루며 자주 생각해야하고 마주쳐야 하는 문제이기 때문에 이번 기회를 통해 자세히 살펴보려고 합니다.

 이번 시간을 통해 굉장히 긴 내용으로 다루게 될것이며 학교수업과 데이터베이스 개론의 책을 보며 정리하는 글이므로 자세하게 공부하고 싶은 분은 이번 글을 통해 배워가셨으면 좋겠습니다.


트랜잭션이란

데이터베이스는 다수의 사용자가 동시에 사용하더라도 항상 모순이 없는 정확한 데이터를 유지해야합니다.

앞으로 위의 문장은 핵심 문장이라 생각하시면 되며 예시로 계좌이체를 떠올립시다. 

A계좌에 10000원이 있고 B계좌에 0원이 있다고 가정을 합시다. 그리고나서 A계좌에서 5000원을 인출을 하여 B계좌에 그 5000원을 이체를 합니다. 그러면 결과적으로 A계좌에는 5000원 B계좌에는 5000원이 있어야 합니다.

 

출처: 데이터베이스 개론(김연희 지음)

 

 

데이터베이스는 위의 작업을 항상 모순이 없는 정확한 데이터를 유지해야합니다.

앞으로 이 예시를 떠올리면서 글을 읽어 주시길 바랍니다.

 

트랜잭션은 작업 하나를 수행하는 데 필요한 데이터베이스의 연산들을 모아놓은 것으로, 데이터베이스에서 논리적인 작업의 단위가 됩니다.

쉽게 말해 트랜잭션을 작업 수행에 필요한 SQL문들의 모임으로 이해하면 됩니다.

 

위 사진에 계좌이체 트랜잭션을 다시 보시면 계좌이체 작업을 완벽하게 수행하기 위해 2개의 데이터베이스 연산을 처리하고 있습니다. 인출하는 연산 하나, 입금하는 연산 하나 이렇게 두 개의 UPDATE문을 필요로 합니다. 계좌이체 트랜잭션을 완벽히 수행하기 위해서는 위 두개의 UPDATE문이 정상적으로 실행되어야 합니다. 만약 첫번째 UPDATE문이 실행된후 시스템에 장애가 발생하여 두번째 UPDATE문이 실행되지 않으면 성호가 5000원을 이체해서 A계좌가 5000원 감소한 상태지만 은경은 5000원을 아직 받지못해 5000원이 사라지게 되고 그들은 입증하기위해 은행을 찾아가 기록을 살펴보어야하는 불편한 상황이 생깁니다.


 

좀더 개발자인 저가 자주 접하게 될 상품주문 트랜잭션을 생각해봅시다. 

 

상품주문 전의 데이터베이스 상태는 주문수량:0개 재고량:100개  

상품주문 트랜잭션은 

1. 새로운 주문 내역 추가 (INSERT INTO 주문(주문번호,주문고객,주문제품,주문수량,주문날짜) VALUES (생략)

2. 주문제품의 재고량 수정 (UPDATE 제품 SET 재고량=재고량-10 WHERE 제품번호='P01') 

상품주문 후의 데이터베이스 상태는 주문수량:10개 재고량:90개

 

상품주문 트랜잭션은 INSERT문과 UPDATE문으로 구성되어 있습니다. 새로운 주문 내역을 주문 테이블에 삽입하는 INSERT문과 제품의 재고량을 주문한 수량만큼 감소시키는 UPDATE문이 필요합니다. 이 두 명령문이 모두 완전하게 처리되어야 상품 주문 트랜잭션이 성공적으로 수행됩니다.

 

만약 둘중 하나라도 처리과정에서 오류가 발생하면 모든 명령문의 실행을 취소하고 트랜잭션 작업 전의 데이터베이스 상태로 되돌아가게 해야합니다.

 

트랜잭션의 모든 명령문이 완벽하게 처리 되는것을 ALL

하나도 처리되지않게 하는것을 NOTHING 이라 하며 트랜잭션은 원자성을 지키기 위해 이 둘 중하나를 만족시켜야 합니다.

트랜잭션은 이 특성 뿐만이 아니라 다른 특성도 가지고 있습니다. 하나씩 살펴보겠습니다.


트랜잭션의 특성

1. 원자성

트랜잭션을 구성하는 연산들이 모두 정상적으로 실행되거나 하나도 실행되지 않아야 한다는 all-or-nothing 방식을 취해야합니다.

만약 트랜잭션을 수행하다가 장애가 발생하여 작업을 완료하지 못했다면, 지금까지 실행한 연산처리를 모두 취소하고 데이터베이스를 트랜잭션 작업 전의 상태로 되돌려 트랜잭션의 원자성을 보장해야 합니다.

계좌이체에서 5000원을 A계좌에서 빼는 연산은 수행되고 B계좌에서 5000원을 더하는 연산이 실패되면 결과적으로 (A:5000원 B:0원) 이라는 심각한 결과를 가져오는 것을 막기 위해 데이터베이스의 원래상태로 복구하는 회복기능을 사용합니다.

 

2. 일관성

트랜잭션이 성공적으로 수행된 후에도 데이터베이스가 일관된 상태를 유지해야 합니다. 무슨 말이냐면

계좌이체를 하기전 A:10000원 B:0원 이 둘의 계좌잔액 합계는 10000원입니다.

트랜잭션을 수행하고 나서 A:5000원 B: 5000원 이 둘의 계좌잔액 합계는 10000원입니다. 이처럼 이 둘의 합이 일관성을 유지해야 트랜잭션이 성공적으로 이루어졌다고 볼 수 있습니다.

 

3. 격리성

현재 수행중인 트랜잭션이 완료될 때까지 트랜잭션이 생성한 중간 연산 결과에 다른 트랜잭션들이 접근할 수 없음을 의미합니다. 다시말하면 성호가 계좌이체 트랜잭션을 수행할때 A계좌에서 10000원을 읽고 5000원을 빼는 연산을 수행하는 동안 다른 싱싱한싱호라는 사람이 계좌이체를 하기 위해 또 A계좌에서 10000원을 읽고 5000원을 빼는 연산을 하는것을 막는 것입니다. 운영체제를 배웠다면 동시성제어 뮤텍스를 떠올리면 매우 쉽습니다. 자원 하나(변수 하나)를 차지하면 그 차지한 스레드가 자원을 쓰지 못하도록 자물쇠로 잠가(뮤택스 기능) 다른 스레드가 기다리는 행동이죠.

그렇게 해야 둘이 10000원을 읽고 5000원을 빼고 5000원을 저장하면 A계좌에는 0원이 되어야하는데 5000원이 남아있을겁니다. 은행은 상당한 손해를 입게되겠죠 (어떻게 현실적으로 A계좌에 두명이 접근할수 있냐고 생각할수도 있을텐데 카카오 모임통장 생각하시면 될거같습니다.그거말고도 다른 예시도 생각해보세요) 

이것말고도 계좌이체 트랜잭션과 계좌입금 트랜잭션이 동시에 실행되면 둘이 같은 자원에 접근하고 동시성 문제가 생길 수 있기에 (운영체제에서 세마포어) 트랜잭션의 격리성을 보장해야 합니다. 즉 순서대로 실행하도록 만들어 정확하고 일관된 결과를 얻게 될것입니다.

 

4. 지속성

트랜잭션이 성공적으로 완료된 후 데이터베이스에 반영한 수행 결과는 어떠한 경우에도 손실되지 않고 영구적이어야 함을 의미합니다. 즉 시스템에 장애가 발생하더라도 트랜잭션 작업 결과는 없어지지 않고 데이터베이스에 그대로 남아있어야 한다는 의미 입니다. 

 

데이터베이스 관리시스템은 트랜잭션의 4가지 특성을 보장하기 위한 지원 기능을 제공합니다. 다음 절에서 각 기능이 어떻게 지원되는지 배우겠습니다.

 

출처: 데이터베이스 개론 (김연희 지음)


트랜잭션의 상태

트랜잭션의 연산

commit: 트랜잭션이 성공적으로 수행되었음을 선언(작업완료)

rollback:트랜잭션을 수행하는데 실패했음을 선언(작업취소)

출처: https://rebro.kr/162

 

트랜잭션 상태를 보시면 부분완료가 이해가 안되실수도 있습니다. "연산이 마지막 까지 완성되고 commit까지 요청이 들어왔는데 왜 데이터베이스에 아직 반영이 되지 않고 실패로 가버릴수 있는가?"

 

하드웨어의 구조를 생각해보면 쉽습니다.

 

데이터베이스종류마다 다르니 그중 RDBMS의 관계형 데이터베이스인 오라클이나 MySQL의 구조로 설명하겠습니다. 

예를들어 계좌입금이라는 트랜잭션을 수행하기위해 디스크의 섹터에 저장된 B계좌의 잔액을 읽어야합니다. 오라클은 운영체제에게 디스크의 어떤 섹터에 저장된 값을 요청합니다.운영체제는 알수없는 시간에 그 값을 읽어 버퍼에 갖다놓습니다.

그리고 오라클은 운영체제가 읽어들인 이 B계좌의 잔액의 값을 버퍼에서 RAM으로 옮깁니다. 그리고나서 백엔드에서 계좌입금 함수를 수행합니다. 그리고 변화된 값을 오라클에게 디스크에 반영해달라고 요청합니다. 오라클은 버퍼에 이 변화된 값을 버퍼에 갖다놓고 운영체제에게 디스크에 이 값을 저장해달라고 요청합니다. 운영체제는 이제 알수없는 시간에 버퍼에 있는 값을 읽어들여 디스크에 어느 섹터에 저장할 것 입니다.

 

이 일련의 과정이 끝나야 드디어 완벽히 트랜잭션이 수행되었다고 할수 있습니다.

 

다시 위의 사진에 대한 부분완료에 대한 의문점은 이런 답을 도출해 낼 수 있습니다.

부분완료같은 경우 트랜잭션에 있는 명령을 모두 수행하고 이제 commit 요청을 오라클에게 내리기 직전을 말합니다. 이때는 당연히 디스크에 적혀있지 않습니다.

단, 완료한 commit상태는 운영체제가 디스크에게 변경된 내용을 반영하라는 I/O요청을 내립니다. 이때는 오라클과 운영체제의 임무는 끝났지만 디스크에 정상적으로 반영이 됐는지는 모릅니다. 즉 언제 반영해줄지는 디스크의 마음입니다.

 

그러면 디스크에 기록하기도 전에 장애가 발생했을때 어떻게 될까요? 이 점은 다음 시간에 자세히 알아보도록 하겠습니다.