PostgresSQL (2)

teal·2023년 8월 14일
0

DBMS

목록 보기
2/4

백엔드 개발을 하다보면 트랜잭션이야기를 안할 수 없게된다. 코딩을 해도 그 코드가 반드시 내가 원하는 값, 환경에서 돌 수 있다는 보장이 없고 만약 쿼리를 순서대로 보내서 내가 원하는 결과를 얻으려고 하는데 중간에 다른 코드, 시스템에서 내가 원하는 쿼리의 결과값을 바뀌게하는, 즉 수정, 삭제, 추가생성이 얼마든지 가능하다.

ACID

위와 같은 이유로 백엔드 개발을 할시에는 트랜잭션의 사용이 거의 반드시 필요하다고 말할 수 있다. 트랜잭션은 ACID원칙을 준수하게 된다. ACID는 어떤 용어일까?

원자성

  1. 원자성(Atomicity) : 간단히 말하면 트랜잭션 블록 내의 모든 연산이 다 성공해서 DB에 반영(commit)되거나 중간에 실패할시 아무것도 안한 것 처럼 복구(rollback)되어야 한다는 점이다.

2가지 데이터가 존재한다고 하자 a = 10, b = 10
a의 값 10을 b에 넘겨주는 작업을 진행한다.

트랜잭션 시작
a : - 10
(에러 발생)
b : + 10
트랜잭션 종료

네크워크, 서버 다운 등의 문제가 발생해서 트랜잭션 중간에 종료되는 문제가 발생했다.

만약 원자성을 만족하지 못한다면?
a의 값이 그대로 데이터베이스에 적용되면 a만 -10되고 b는 10 그대로인 문제가 발생한다. 에러는 어느상황에, 언제 발생할지 모르므로 만약 원자성을 만족하지 않았다면 어느 작업을 할 때나 저런 문제를 걱정해야할 것이다.

일관성

  1. 일관성(Consistency) : 트랜잭션 수행 전과 후에 데이터베이스가 일관된 상태를 유지해야 함을 의미한다.

이게 대체 무슨 말일까? 저 말만보면 대체 어떤 동작인지 이해가 잘 가지 않는다.

일관된 상태는 데이터베이스가 무결성 제약 조건을 만족하는 상태라는 것이다. 보통 예시를 드는것은 아래와 같다

a, b 필드는 0 이상이여야한다.(check constraint)
a = 5, b= 10 이다

트랜잭션 시작
a : - 10
b : + 10
트랜잭션 종료

위와같은 트랜잭션이 동작하여 commit되면 a와 b의 합이 15인 일관성은 만족하지만 a가 -5가 되므로 자료형에 맞지않아 무결성 제약 조건을 위반한다. 그래서 트랜잭션 동작에서 abort되는 것이다. 그런데 여기서 개인적으로 좀 애매하다고 생각되는 부분이있다. DB에서 관리해주는 PK, FK, Unique 등의 필드는 일관성을 DB에서 주로 관리하는 것이 맞다.

그런데 저 위에서 예시로 드는 부분들은 사실 어플리케이션 단의 책임도 많이 있지 않은가 싶다. 위의 예시만 보면 어플리케이션에선 무결성 상관없이 트랜잭션을 동작시켜도 DBMS가 알아서 해준다는 이야기처럼 보여서 좀 애매하다고 생각된다. 기본적으로 어플리케이션 상에서 저 무결성 제약 조건을 지키고 원자성, 격리성 등 나머지의 특징을 통해 무결성 제약 조건을 만족하는 것이 아닌가 싶다. 물론 최종적으로 DB를 통해 무결성 제약 조건을 만족하는 상태가 되는 것은 맞다.

격리성

  1. 격리성(Isolation) : 동시에 여러 트랜잭션이 실행될 때 하나의 트랜잭션이 다른 트랜잭션의 중간 단계의 연산 결과를 볼 수 없게 하는 성질을 말한다. 이를 통해 각 트랜잭션이 독립적으로 실행되는 것처럼 보이게 한다.

다른 트랜잭션의 중간 단계의 연산 결과를 볼 수 없게 한다는 것이 어떤 말일까? 만약 자유롭게 볼 수 있게 만든다고 하자. 트랜잭션이 동시에 여러개가 실행될때는 어느 트랜잭션이 먼저 끝날지 어떤 쿼리문이 먼저 돌지 사실상 모른다.

a = 10, b = 10
A 트랜잭션
트랜잭션 시작
a : - 10
(이 시점에 B 트랜잭션 동작)
b : + 10
트랜잭션 종료

B 트랜잭션
트랜잭션 시작
c : a 카피
트랜잭션 종료

위와 같은 트랙잭션이 돈다고 할때 최종 결과값은 뭐가 나와야 할까?
a와 b는 0, 20일 것이다. 그런데 c는 어떤값이 나오게 될까? 자유롭게 볼 수 있으므로 a를 0으로 읽고 c는 0이 될 것이다. 그런데 만약 A 트랜잭션이 실패해서 롤백된다면? 그래서 a는 10, b는 10으로 돌아갈 것이고 c는 어떻게 되는것일까? B 트랜잭션이 실패하지 않고 c는 0으로 commit되었으니 그대로 둘수 밖에 없다.

이 문제는 Dirty Read이다. 이 격리 수준(Isolation Level)을 READ UNCOMMITED라고 한다.

마찬가지로 Dirty Write도 존재한다

a = 10, b = 10
A 트랜잭션
트랜잭션 시작
a : - 10
(이 시점에 B 트랜잭션 동작)
b : + 10
트랜잭션 종료

B 트랜잭션
트랜잭션 시작
a : 20
트랜잭션 종료

똑같이 a가 0 인 상황에서 B트랜잭션에서 0으로 읽고 + 10을 시키고 저장한다. B의 입장에선 20으로 커밋했다. 그런데 A 트랜잭션이 종료될때는 0으로 커밋해버린다. 그래서 B트랜잭션의 동작이 덮어 씌워지게 된다. 이 문제는 먼저 특정 데이터를 수정하려고 하는 트랜잭션이 끝나지 않았는데 다른 트랜잭션이 먼저 수정해서 커밋하여 그 행동이 아예 무시되는 문제다. 특정 트랜잭션에서 특정 행을 수정할때 잠금이 발생하지 않아 일어나는 문제이다.

그런데 이 문제는 보통 대부분의 DBMS에서 중요하게 여겨 어떤 격리수준이던 막으려고 한다.

위와 같이 어느 수준까지 트랙잭션을 격리시킬 건지에 따라 아래와 같이 나뉘게 된다.

격리 수준(Isolation Level)

  1. READ UNCOMMITED
  2. READ COMMITED
  3. REPEATABLE READ
  4. SERIALIZABLE

아래로 내려갈 수록 격리 수준은 높아지고 그에 따라 성능은 떨어진다.

출처

그런데 위의 표를 보면 READ UNCOMMITED에서 Dirty Read가 발생안한다고한다. 이는 곧 postgres에서 READ UNCOMMITED와 READ COMMITED의 동작방식이 똑같다는 것이다.

그러면 READ COMMITED은 어떤 방식으로 동작하기에 Dirty Read가 발생 안할까? 간단하다 commit된 내역만 읽게 만들면 된다.
아래의 내용을 보자

a = 10, b = 10
A 트랜잭션
트랜잭션 시작
a : - 10
(이 시점에 B 트랜잭션 동작)
b : + 10
트랜잭션 종료

B 트랜잭션
트랜잭션 시작
c : a 카피
(A 트랜잭션 종료)
d : a 카피
트랜잭션 종료

위의 경우에서 만약에 A트랜잭션이 동작하다가 abort가 되고 B 트랜잭션은 종료되었다고 하자 그래도 B 트랜잭션은 a의 초기값인 10을 읽어서 c, d에 저장하므로 정상 동작을한다. 그런데 이 경우 트랙잭션이 성공하면 다른 문제가 발생한다.

a가 0인 시점에서 B트랜잭션이 동작해도 A트랜잭션이 commit된 상황이 아니기 때문에 c에는 10이 들어가게된다. 그런데 잘 생각해보자 d에는 어떤 값이 들어갈까? c에서는 a를 읽었더니 10이 들어갔으므로 d에서도 a를 읽었을때 10일까? 아니다. d에서 a를 읽었을때는 A트랜잭션이 종료되었으므로 a의 값은 0으로 commit되었다. 그래서 d는 0으로 저장된다. 동일한 트랜잭션에서 동일한 쿼리를 보냈는데 결과가 달라지게된다. 이를 Non-Repeatable Read라고 한다.

위의 문제를 해결한 격리 수준을 REPEATABLE READ이라고 한다. 이를 해결하기 위한 방법으로는

  1. 스냅샷 고립(Snapshot Isolation)

    • 트랜잭션이 시작될때의 시점을 스냅샷으로 만들어 해당 트랜잭션에서의 요청은 전부 이 스냅샷을 보게 만들어서 해결
  2. 공유 잠금(Shared Locks)

    • 트랜잭션 내에서 읽은 데이터에 대해 락을 건다. 이 락을건 데이터는 다른 트랜잭션에서 읽을수는 있으나 변경하는 것을 막아 문제를 해결

등 여러 방법이 있다고한다.

postgres는 Snapshot Isolation을 통해서 REPEATABLE READ를 만족한다고 하는데 일단 생각 해보자. 만약에 트랜잭션이 발생할때마다 현재 데이터를 스냅샷을 떠서 거기를 읽는다?? 여러 트랜잭션이 동시에 발생하면 그 스냅샷을 여러번 뜨게 되는건가? 생각만해보면 이보다 끔찍할 수 없을 것 같다.

postgres에서는 이 문제를 해결하기위해 Snapshot Isolation을 구현하기 위해 MVCC(MultiVersion Concurrency Control)를 사용한다고 한다.
참조

하여튼 위와같은 방법을 사용해서 동일한 스냅샷의 데이터만을 읽게되므로 동일한 트랜잭션에서 동일한 쿼리를 여러번 동작시켰을때 값이 달라지는 문제는 없게된다.

그런데 일반적인 경우에 동일한 쿼리가 만약에 행의 갯수를 세는거라면 어떻게 될까? 스냅샷은 트랜잭션 시작시점에 찍고 데이터에 대한 접근을 허용한다. 그런데 만약에 트랜잭션 실행중에 새로운 행이 추가되었다면 어떻게 될까?

rows : 1, 1, 1, 3, 3
A 트랜잭션
트랜잭션 시작
a : 값이 1인 row의 갯수
(이 시점에 B 트랜잭션 동작)
b : 값이 1인 row의 갯수
트랜잭션 종료

B 트랜잭션
트랜잭션 시작
rows에 1 추가
트랜잭션 종료

만약 위와같은 상황이 있다고 하자. a의 값은 3일것이다. 그런데 b의 값은 B트랜잭션이 commit 되었으므로 1의 갯수가 4개가 되고 b에는 4가 저장된다. 분명히 a, b는 동일한 쿼리를 사용했는데 값이 다른 팬텀 리드(Phantom Read) 현상이 발생한다.

이 문제를 해결하기위해서는 각 트랜잭션간 충돌이 발생하면 안된다. 어떤 한 트랜잭션이 커밋되려는데 그와 연관된 쿼리를 진행중인 트랜잭션이 있다면 충돌이 발생한다. 이 문제를 postgres에서는 SSI(Serializable Snapshot Isolation)로 해결한다.

SSI는 의존성 검사를 진행한다. 의존성이란 A 트랜잭션이 어떤 행을 읽고 만약 B 트랜잭션에서 그 행을 바꾼다면 A, B 트랜잭션간의 의존성이 있는 것이다. 그래서 트랜잭션 commit시 의존성을 검사하여 만약 어떤 트랜잭션이 row의 갯수를 조회하는데 다른 트랜잭션이 row를 추가 할 경우 충돌한 트랜잭션을 rollback 시켜버린다. 이를 통해 팬텀 리드를 해결하고 이 격리 수준을 SERIALIZABLE이라고 한다.
참조

postgres에서는 MVCC를 사용하여 특정 버전 시점의 데이터베이스를 조회할 수 있기 때문에 행이 추가 삭제된 내역도 반영되지 않아 Repeatable Read에서도 Phantom Read가 발생하지 않는다고한다.

트랜잭션이 여러개 동시에 실행되었을때 트랜잭션이 순차적으로 실행되었을때와 달리 일관성을 만족하지 못하는 경우가 있다.

간단한 경우를 생각해보자

pk  name  value
1    a      1
2    a      2
3    a      3
4    b      2
5    b      3
6    b      4
A 트랜잭션
트랜잭션 시작
row 추가 : name = a, value = 모든 b row의 value 총합
트랜잭션 종료

B트랜잭션 시작
row 추가 : name = b, value = 모든 a row의 value 총합
트랜잭션 종료

위와같은 경우에서 A, B 순차적으로 진행되면 (a, 9), (b, 15)의 결과가 나올것이다. B, A 순으로 진행되면 (b, 6), (a, 15)의 결과가 나타난다.

그런데 트랜잭션이 동시에 진행된다면?
A 트랜잭션이 commit되지 않은 상태로 B 트랜잭션이 동작한다면
(a, 9), (b, 6)이 커밋될것이다. 이것은 우리가 기대한 A, B 혹은 B, A 의 동작결과와 일치하지 않으므로 문제가 발생한 것이다. 즉 트랜잭션을 순차적으로 진행했을때 얻을 수 있는 결과값의 집합에 포함되지않는다. 이것을 Serialization Anomaly라고 한다. postgres에서는 이것을 SSI를 통하여 충돌 발생한 트랜잭션을 취소시키는 방식으로 문제를 해결한다.

지속성

지속성(Durability)은 트랜잭션이 커밋된 후에는 그 내용이 영구히 남아야 한다는 것이다. 만약에 트랜잭션 실행후 시스템 문제로 다운된 경우에도 비휘발성 장치에 해당값이 기록되어 다시 읽어들일 수 있어야 하거나, 트랜잭션과 관련된 모든 변경 사항을 디스크의 로그파일에 기록하여 시스템 장애시 다시 데이터베이스를 원상복구 하는데 사용한다.

결과

위의 4가지 ACID 원칙에 의하여 우리는 트랜잭션을 사용하여 우리가 원하는 결과값을 얻을 수 있게된다. 백엔드 개발을 하면서 엄청 신경쓰는 부분들은 사실 아니지만 한번쯤 정리하고 넘어가야 나중에 관련된 비즈니스 문제를 해결하는데 도움이 되지않을까 싶어 적게 되었다. 적어도 뭐라도 알고 있어야 검색이라도 할 수 있지 않겠는가?

profile
고양이를 키우는 백엔드 개발자

0개의 댓글