본문 바로가기
CS

[CS] 데이터베이스 트랜잭션 격리 수준과 낙관적 락, 비관적 락의 비교

by 육빔 2025. 3. 28.
728x90

트랜젝션에 대해서 아직 정확히 모르는 것 같아 정리하고자 한다. 

 

우선 트랜젝션은 무엇일까??

 

데이터베이스 트랜잭션(Database Transaction)은 데이터베이스에서 수행되는 작업의 논리적인 단위

라고 한다. 이렇게 정의로만 들으면 이해가 잘 안된다. 논리적인 단위?

 

차라리 사전적 의미인 거래라고 생각하면 편할 것 같다.

내가 다른사람한테 송금할때 내 계좌에서 돈이 빠져나가고 다른 사람 계좌에 입금이 되는 작업이 하나로 이루어져야되기에 이런 것을 트랜잭션으로 이해하면 좋은 것 같다.

 

그래서 이러한 트랜잭션은 4가지 특성이 있는데 아주 유명한 ACID 특성이다.

 

 

Atomicity (원자성)

  • 트랜잭션의 작업들이 모두 수행되거나 전혀 수행되지 않아야 함
  • 중간에 오류가 나면 모든 작업이 취소(Rollback)

Consistency (일관성)

  • 트랜잭션 전후의 데이터 상태가 일관된 상태를 유지해야 함
  • 예: 돈 이체 후 총 잔액은 변하지 않아야 함

Isolation (격리성)

  • 트랜잭션이 실행되는 동안 다른 트랜잭션이 간섭하지 못하게 보호
  • 동시에 여러 트랜잭션이 실행될 때도 각 트랜잭션은 독립적으로 실행됨

Durability (지속성)

  • 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 반영

 

읽고 보면 당연한거같지만 잘 숙지해야되는 내용이다.

 

이러한 트랜잭션은 다양한 격리 수준이 있는데 이런 격리 수준마다 생기는 이상 현상이 있다.

Dirty Read 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 것
Non-repeatable Read 같은 쿼리를 두 번 했을 때 값이 바뀌는 것
Phantom Read 같은 조건의 SELECT가 더 많은(또는 적은) row를 반환하는 것

 

 

되게 어려워보이지만 사실 순서대로 이해하면 당연한 소리긴 하다.

 

우선 격리 수준을 보면서 이해해보자

 

 

READ UNCOMMITTED
B가 A의 커밋 전 출금 후 잔액을 읽을 수 있음 → Dirty Read 발생

가장 격리 수준이 낮은 단계이다. 이름부터 나와있듯이 커밋하기 전에도 다른 상대방이 변경된 내용을 읽을 수 있는 것이다.

 

READ COMMITTED
B는 A가 커밋한 이후의 값만 읽을 수 있음 → Dirty Read 방지

위의 문제점을 보완한 격리수준이다. 무조건 커밋한 다음에만 읽을 수 있다는 것을 의미한다. 근데 문제점은 같은 쿼리를 새로고침 할 경우 다른 값이 나올 수 있다는 것이 문제이다. Non-repeatable 문제

 

REPEATABLE READ
B가 처음 읽은 잔액을 계속 유지 (A가 중간에 바꿔도 영향 없음)

위의 Non-repeatable을 해결하는 격리수준이다. 그러면 초기 쿼리랑 커밋 이후 쿼리랑 같은 값이 나오나?? 그렇다. 이때는 데이터베이스에서 트랜잭션 ID 값으로 결과값을 보여주며 만약 실제 데이터베이스의 값이 바뀌더라도 트랜잭션ID값이 더 높은 경우에는 undo log라는 읽기전용 공간에서 바뀌기 전 데이터를 사용자에게 select for update로 반환해준다. 이 격리수준에서는 삽입까지는 막지않는다. 그래서 중간에 값이 추가되면 Phantom Read현상이 발생한다. undo log에서는 select for update를 사용하지 못해서 실제 데이터를 읽기 때문이다.

 

SERIALIZABLE
A와 B는 순차적으로 실행됨 (완전 차단) → 가장 안전하지만 느림

완전하게 읽기/쓰기를 막아 아무런 이상현상이 발생하지 않는 격리수준이다. 하지만 성능이 너무 떨어져 왠만해선 잘 사용하지 않는 격리수준이다.

 

 

보통 우리가 스프링에서 보통 사용하는 비관적락, 낙관적락을 사용하지만 위와 같은 격리수준을 파악하고 적용하지는 않는다. 하지만 위와 같은 격리수준을 잘 알고 적용한다면 좀 더 성능적 개선이나 생기는 이상현상을 잘 제거할 수 있을 것이다.

 

우선 낙관적락부터 말해보자.

@Entity
public class Product {
    @Id
    private Long id;

    @Version
    private int version;
}

 

 

락을 걸지 않고, 트랜잭션 커밋 시점에 충돌 감지 후 예외 처리

일반적으로 @Version 필드 사용

 

사실 낙관적락은 어플리케이션에서 버전 필드를 보고 커밋이 되어있는지 안되어있는지 확인하는 락이어서 데이터베이스 트랜잭션과 연관이 없다 ㅎㅎ;

 

그 다음은 비관적락이다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findProductForUpdate(@Param("id") Long id);

 

 

 

보통 Repository레벨에서 위와 같이 JPA로 해당 쿼리에 @Lock 어노테이션을 걸어주던가

 

SELECT * FROM product WHERE id = 1 FOR UPDATE;

 

jdbc나 네이티브쿼리를 사용하여 FOR UPDATE문을 걸어주는 방식으로 진행된다. 

 

그래서 비관적락은 REPEATABLE READ 이냐?

 

결론은 그렇기도 하고, 꼭 그런 건 아니다

 

반드시 REPEATABLE READ가 필요한 건 아니고, READ COMMITTED에서도 동작 가능하다고 한다.

하지만 데이터의 정합성이 중요한 작업일 경우 REPEATABLE READ나 SERIALIZABLE을 사용하는 것이 맞다.

또한 사람들이 많이 사용하는 Mysql InnoDB 기준 기본 격리수준이 REPEATABLE READ이고 Gap Lock / Next-Key Lock까지 적용되어 문제점인 팬텀 리드까지 방지할 수 있다.

 

사실 데이터베이스, 각 비즈니스의 데이터 정합성을 고려하며 트랜젝션 격리 수준을 정해야되기에 딱 정해진 건 없다. 근데 보통은 Mysql이므로 REPEATABLE READ로 이해해도 좋을 것 같다.

728x90

'CS' 카테고리의 다른 글

[CS] DB - 트랜잭션과 격리성  (0) 2024.09.05
[CS] 웹 페이지를 접속할 때 생기는 일  (1) 2024.08.28
[CS] Redis 사용이유, 자료구조 살펴보기  (0) 2024.08.24
[CS] 웹 서버와 WAS  (0) 2024.08.22