Computer Science

[TIL][CS] 트랜잭션과 ACID, 트랜잭션의 고립 수준

쉬지마 이굥진 2024. 4. 2. 20:57

오늘 스터디 주제는 '트랜잭션'!

트랜잭션에 대해 기본적인 개념만 알고 있었는데, 오늘을 기회로 트랜잭션의 기본부터 고립 수준까지 공부를 해봤다. 더불어 내가 이해한 것을 현업에 가까운 예시를 들면서 포스팅 해보고자 한다.

 

먼저 기본적으로 모두들 잘 알고있는 트랜잭션의 개념과 특성에 대해 간단히 알아보자.

 

📌 트랜잭션이란?

트랜잭션이란, 데이터베이스에서 수행되는 여러 작업을 하나의 논리적 단위로 수행하는 것을 말한다.

속된말로 곧 죽어도 같이 묶여서 수행되어야 하는 연산을 묶은 것이라고 생각하면 되겠다 ㅎㅎ

 

트랜잭션의 연산

하나로 묶은 연산을 수행하며 오류 없이 모든 작업이 성공적으로 마쳐야만 DB에 반영하게 되는데, 이것을 트랜잭션 연산에서는 '커밋' (COMMIT)이라고 한다. 

하지만 중간에 하나에서라도 오류가 발생한다면, 다시 처음으로 돌아가야 한다. 이것을 트랜잭션 연산에서는 '롤백' (ROLLBACK)이라고 한다.

이렇게 트랜잭션의 연산은 크게 커밋과 롤백으로 나뉘어져 있는데, 이건 다음에 설명할 ACID에서 원자성과 관련된 내용이라고 할 수 있다.

 

📌 ACID

트랜잭션을 배울 때, 무조건 딸려 오는 개념이 ACID일 것이다.

ACID란, 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질이다. 각각 네 가지 특성에서 첫 번째 글자를 따와서 ACID라고 하는데, 하나하나 간단히 설명 후 넘어가겠다. 

 

  • 원자성 (Atomicity)
    - 트랜잭션은 원자적 이어야 한다. 
    - All or Nothing 
  • 일관성 (Consistency)
    - 데이터베이스의 무결성 제약 조건 에 맞춰야 한다. 
  • 고립성 (Isolation)
    - 트랜잭션은 독립적 으로 실행되어야 하며, 다른 트랜잭션에 영향을 주어서도 안되고, 영향을 받아서도 안된다.
  • 지속성 (Durability)
    - 트랜잭션이 성공적으로 완료되면 그 결과는 영구적 으로 저장되어야 한다. 

 

여기서 고립성 부분을 보자. 

"트랜잭션은 독립적으로 실행되어야 하며, 다른 트랜잭션에 영향을 주어서도 안되고, 영향을 받아서도 안된다." 이 말인 즉슨 ! 같은 데이터를 수정하게 되는 트랜잭션이 존재 시, 순차적으로 처리해야 고립성을 만족하게 된다는 뜻이다.

 

이것은 현실적으로 성능 상 단점이 될 수 있다.

 

만약 현업에서 10만개의 데이터(트랜잭션)를 수행한다고 하면, 10만 개의 데이터를 순차적으로 처리하게 되므로 시간이 굉장히 오래 소요될 것이다. 

따라서 현실과의 타협을 위해 트랜잭션에서는 네 가지 고립 수준을 소개하고 있다.

고립 수준은 어떤 것들이 있을지, 어떤 상황에서 쓰면 좋을지, 각각의 고립 수준에서 나타날 수 있는 문제점과 그 해결 방법은 무엇이 있을지 본격적으로 살펴보자.

 

 

📌 트랜잭션의 고립 수준 (Isolation Level) 

트랜잭션의 고립(격리) 수준이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 여부를 결정하는 것이다.

 

참고로 아래의 고립 수준들은 모두 자동 커밋(AUTO COMMIT)이 false인 상태에서만 발생한다.

네 가지 고립 수준

 

위에서부터 READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE 총 이 네 가지가 있다.

위로 올라갈 수록 속도는 빨라지지만 데이터의 일관성을 보장하지 못하고, 아래로 내려올수록 트랜잭션을 순차적으로 처리하므로 속도는 느려지지만, 데이터의 일관성을 보장할 수 있다.


🔹[1]  READ-UNCOMMITTED 

READ-UNCOMMITTED는 말 그대로 아직 커밋되지 않은 데이터를 읽을 수 있도록 한 것이다. 예시를 통해 자세히 알아보자. 

  1. 먼저 DB에는 10만원이 든 계좌가 있다.
  2. 이 상태에서 트랜잭션 A가 읽기 요청을 수행한다. 
  3. DB에서는 계좌에 있는 금액 그대로 10만원을 반환하게 된다.
  4. 이후 트랜잭션 A에서 5만원을 추가한다. 
  5. 이 때 DB는 10만원 👉 15만원으로 금액이 변경된다.
  6. 그 다음 트랜잭션 B가 읽기 요청을 수행한다.
  7. 이때 READ-UNCOMMITTED는 아직 커밋되지 않은 데이터를 읽을 수 있으므로, 15만원을 반환한다. (트랜잭션 A는 아직 커밋이 되지 않은 상태)

만약 트랜잭션 A에서 오류가 발생해서 롤백이 된다면?

데이터베이스에서는 (1번) 10만원으로 기록되어 있는데, 트랜잭션 B에서 반환하는 값은 (7번) 15만원이 되는 현상이 발생한다.

이렇게 되면 둘의 값이 달라지기 때문에 일관성 문제가 발생하게 된다. 

 

Dirty Read

이 문제를 더티 리드 문제라고 한다. (정처기 준비할 때 봤었는데.. 아 ! 이거구나 예시로 이해하니까 쉽게 이해됐다) 
더티 리드는, 특정 트랜잭션에서 데이터를 변경했지만 아직 커밋되지 않았을 때, 다른 트랜잭션이 해당 값을 조회할 수 있는 문제이다. 

 

그럼 더티 리드 문제는 어떻게 해결할 수 있을까?  

 

커밋된 데이터만 읽을 수 있도록 하면 된다. ㅋㅋ 

 

 

🔹[2]  READ-COMMITTED

위에서 설명한 더티 리드의 해결방법 대로, READ-COMMITTED는 커밋된 데이터만 읽을 수 있도록 한 설정이다. 이것도 예시를 통해 이해해 보자.

 

1번 ~ 6번까지의 과정은 전 READ-UNCOMMITTED 방식과 동일하다.

하지만 READ-COMITTED의 경우 커밋된 데이터만 읽을 수 있으므로, 트랜잭션 A가 커밋되지 않은 상태에서 (6번) 트랜잭션 B가 읽기 요청을 보내면 10만원을 반환하게 된다. (7번)

=> Dirty read 문제 해결 

 

 

하지만 여기서 발생할 수 있는 문제점이 있다. (또?)

 

8.  트랜잭션 A가 커밋을 한다.

9.  그 후 트랜잭션 B가 읽기 요청을 하면, 커밋된 데이터를 읽으므로 15만원을 반환할 것이다. 

10. 이때 트랜잭션 B는 같은 데이터를 요청했지만 다른 데이터를 반환(7번에선 10만원, 10번에선 15만원) 받으므로 문제가 발생한다. 

 

Non-Repeatable-Read

이 문제를 Non-Repeatable-Read 라고 한다.
논 리피터블 리드 문제란, 트랜잭션 내에서 같은 데이터를 여러 번 조회할 때 읽은 데이터가 서로 다른 값으로 나오는 문제를 말한다.

 

이 문제는 다음 고립 수준을 통해 해결할 수 있다. 

 

 

🔹[3]  REPEATABLE-READ

세 번째 고립 수준인 REPEATABLE-READ는, 

특정 데이터를 반복 조회 시 같은 값을 반환하도록 하는 고립 수준이다. 

 

 

1 ~ 8번째까지의 과정은 전 단계와 같다.이후 트랜잭션 B가 DB로 읽기 요청을 했을 때,  6번에서 조회한 데이터를 9번에서 반복 조회하는 것 이므로 같은 값인 10만원을 반환한다.=> Non-Repeatable-Read 문제 해결

 

하지만 REPEATABLE-READ도 문제점이 없는 것은 아니다. 발생할 수 있는 문제점이 있다 ㅎㅎ (또)이 문제는 다른 예시를 들어서 설명하겠다. 

  1. DB에 10만원이 든 계좌가 한 개 있는 상태에서 은행 계좌의 갯수를 세는 쿼리를 날린다.
  2. 계좌가 한 개 있으니 1개를 반환한다. 
  3. 이 때 트랜잭션 A가 새로운 5만원이 든 계좌를 추가한다. 
  4. 그럼 DB에는 10만원 계좌, 5만원 계좌 총 2개가 생긴다.
  5. 이 때 트랜잭션 B가 이전에 요청했던 계좌 개수를 세는 쿼리를 날린다.
  6. 그럼 2개가 반환된다.
    같은 요청을 했는데 첫 번째는 1이 나오고, 두 번째는 2가 나온다.

 

Phantom-Read

위와 같은 문제를 팬텀 리드 문제라고 한다. 
팬텀 리드란 Non-Repeatable-Read의 한 종류로, 새로운 데이터가 생기거나, 기존의 데이터가 사라지는 문제를 말한다. 

 

🔹[4] SERIALIZABLE 

 

마지막 고립 수준이다. SERIALIZABLE은, 트랜잭션이 고립성을 완전히 만족하게 되어 순차적으로 트랜잭션이 수행되게 된다는 것이다. 

 

위 예시를 보면, 트랜잭션 A는 READ와 WRITE를 수행한 후 커밋을 한다. (1 ~ 6번)

그 이후가 되서야 트랜잭션 B가 DB에 읽기 요청을 하고 (7번), 15만원을 반환한 상태(8번) 에서 커밋을 할 수 있다.

 

즉, 트랜잭션 A가 끝나고 나서야 순차적으로 B가 수행된다는 것이다.

 


📌 정리

  • 위로 올라갈수록 속도는 빠르지만 데이터의 일관성을 보장하지 못하고, 밑으로 갈수록 속도는 느리지만 데이터의 일관성을 보장할 수 있다.
  • READ-UNCOMMITTED
    • 아직 커밋되지 않은 데이터를 읽을 수 있다.
    • Dirty Read, Non-Repeatable-Read, Phantom Read 문제 발생 가능
  • READ-COMMITTED
    • 커밋된 데이터만 읽을 수 있다 (Dirty read 문제 해결)
    • 아직 Non-Repeatable-Read, Phantom Read 문제를 갖고 있다.
  • REPEATABLE-READ
    • 특정 데이터를 반복 조회 시 같은 값을 반환한다
    • Phantom Read 문제 발생 가능
  • SERIALIZABLE
    • 트랜잭션이 순차적으로 실행되서 속도가 느림 
    • but 앞선 모든 문제점을 해결할 수 있음
    • 가장 안전하지만 가장 성능이 떨어지므로, 극단적으로 안전한 작업이 필요한 경우가 아니면 사용을 지양하자

 

정리한 걸 토대로 생각을 해 보면, 각 고립 단계마다 장/단점이 존재해서 꼭 이렇게 적용해라는 정답은 없는 것 같다고 느꼈다. 같이 개발하는 팀원들과 심도 깊은 토론과 생각을 해본 후에 현재 개발중인 프로젝트에는 어떤 고립 수준을 적용하면 좋을지 결정하면 좋을 것 같다. 

 


참고 자료 

https://www.youtube.com/watch?v=taUeIi6a6hk&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=64

https://mangkyu.tistory.com/299