트랜잭션(Transaction)이란?
데이터베이스는 단순히 데이터를 저장하는 장소가 아니라, 다양한 작업이 동시에 이루어지는 복잡한 환경이기 때문에 트랜잭션 관리와 동시성 제어가 필수적입니다. 트랜잭션(Transaction)은 데이터베이스에서 하나의 논리적 작업 단위로 여러 작업이 하나로 묶여 실행되는 과정입니다. 그러나 여러 트랜잭션이 동시에 실행될 경우 경쟁 상태나 데이터 충돌 등의 문제가 발생할 수 있기 때문에 이를 해결하기 위한 메커니즘이 필요합니다.
이번 글에서는 이러한 문제를 해결하는 주요 메커니즘인 격리 수준과 DB Lock에 대해 다루어보겠습니다.
트랜잭션의 특성
먼저 트랜잭션의 개념을 좀 더 자세히 살펴보겠습니다.
앞서 언급했듯이 트랜잭션은 하나의 논리적 작업 단위로, 여러 작업을 하나로 묶어 처리하는 단위입니다. 트랜잭션은 모든 작업이 성공적으로 완료되거나, 그렇지 않으면 전부 취소(롤백)되는 것을 보장해야 합니다. 이를 통해 데이터의 일관성과 무결성을 유지할 수 있습니다.
예를 들어, 은행 송금을 생각해볼 수 있습니다. A가 B에게 송금을 할 때, A의 잔액은 줄어들고 B의 잔액은 증가합니다. 만약 이 과정에서 문제가 발생하면, 두 작업 모두 취소되어야 합니다. 만약 B의 잔액만 취소되고 A의 잔액은 취소되지 않는다면, 데이터에 일관성 문제가 생기게 됩니다.
이러한 트랜잭션의 특성을 잘 나타내는 것이 바로 ACID 원칙입니다.
원자성(Atomicity)은 트랜잭션의 모든 작업이 전부 수행되거나 전혀 수행되지 않음을 보장합니다. 즉, 트랜잭션이 실패하면 모든 변경 사항이 취소(롤백)되어야 합니다.
일관성(Consistency)은 트랜잭션 실행 전과 실행 후 데이터베이스가 일관된 상태를 유지해야 한다는 원칙입니다. 트랜잭션이 완료되면 데이터는 항상 정의된 규칙과 제약 조건을 충족해야 합니다.
격리성(Isolation)은 여러 트랜잭션이 동시에 실행될 때, 각각의 트랜잭션이 독립적으로 실행되어야 하며, 트랜잭션 간의 간섭이 없어야 한다는 원칙입니다.
지속성(Durability)은 트랜잭션이 성공적으로 완료되면 그 결과가 영구적으로 저장되어야 한다는 것입니다. 즉, 시스템 장애가 발생하더라도 트랜잭션의 결과는 유지되어야 합니다.
트랜잭션 시작과 종료
트랜잭션을 시작하는 방법에는 두 가지가 있습니다.
이 중 START TRANSACTION과 BEGIN은 동일한 기능을 수행하지만, 차이점은 START TRANSACTION을 사용하면 트랜잭션의 격리 수준을 지정할 수 있다는 점입니다.
트랜잭션을 종료하는 방법에도 두 가지가 있습니다.
COMMIT은 트랜잭션에서 수행한 모든 변경 사항을 데이터베이스에 영구적으로 반영하는 것이고, ROLLBACK은 트랜잭션에서 수행한 모든 변경 사항을 취소하고, 이전 상태로 되돌리는 것입니다.
위와 같이 트랜잭션을 시작한 후, 트랜잭션의 상태를 확인할 수 있습니다.
트랜잭션 상태는 데이터베이스마다 다를 수 있지만, 이 글에서는 가장 대중적으로 사용되는 MySQL과 PostgreSQL을 중심으로 설명하겠습니다.
트랜잭션 상태 (MySQL)
MySQL에서는 트랜잭션 상태를 4가지로 구분할 수 있으며, 이를 확인하려면 아래 명령어를 사용하면 됩니다.
SHOW ENGINE INNODB STATUS;
현재 활성화된 트랜잭션이 없을 경우, "not started"라고 표시됩니다.
반면, 트랜잭션이 활성화된 경우 "ACTIVE" 상태로 표시되며, 이때 몇 개의 행(row)에 Lock이 걸렸는지도 확인할 수 있습니다.
트랜잭션 상태 (PostgreSQL)
PostgreSQL에서는 트랜잭션 상태를 5가지로 구분할 수 있으며, 이를 확인하려면 아래 명령어를 사용하면 됩니다.
SELECT * FROM pg_stat_activity;
위 사진에 있는 컬럼 외에도 다양한 정보를 확인할 수 있지만, 여기서는 대표적인 정보만 표시했습니다.
state 컬럼을 확인하면 현재 쿼리의 진행 상태를 알 수 있습니다. 예를 들어, 쿼리가 진행 중인 상태는 "active", 트랜잭션이 활성화된 상태는 "idle in transaction", 실행 중인 트랜잭션이 없는 상태는 "idle"로 나타납니다.
격리 수준
서론에서 여러 트랜잭션이 동시에 실행될 경우 경쟁 상태나 데이터 충돌 등의 문제가 발생할 수 있기 때문에 이를 해결하기 위한 메커니즘이 필요하다고 언급했습니다. 그 중 하나가 격리 수준입니다.
격리 수준은 트랜잭션 간의 상호작용을 어떻게 제어할지 결정하는 것으로, 이를 통해 트랜잭션의 격리 정도를 정의할 수 있습니다. 트랜잭션의 격리 수준은 총 4가지로 구분됩니다.
트랜잭션 격리 수준은 *Read Uncommitted*에서 *Serializable*로 갈수록 격리 수준이 높아지고, 격리 수준이 높을수록 성능은 저하되고 일관성은 향상됩니다.
MySQL의 기본 격리 수준은 Repeatable Read이며, PostgreSQL의 기본 격리 수준은 Read Committed입니다.
이 두 가지 격리 수준은 혼동하기 쉬우므로, 두 격리 수준의 차이를 좀 더 자세히 알아보겠습니다.
Read Committed는 쓰기 작업이 있는 컬럼에 대해 다른 트랜잭션에서의 쓰기 작업을 차단하지 않기 때문에, 트랜잭션 도중에 다른 트랜잭션에서 데이터 수정 및 커밋을 하면 처음과 다른 수정된 값을 조회할 수 있습니다.
반면, Repeatable Read는 트랜잭션이 종료될 때까지 다른 트랜잭션에서의 쓰기 작업을 차단하여, 반복적인 조회에서 처음과 동일한 값을 보장합니다. 하지만 Repeatable Read에서는 트랜잭션 도중에 다른 트랜잭션이 새로운 데이터를 추가하거나 삭제하는 Phantom Read 문제가 발생할 수 있습니다.
DB LOCK 이란?
앞서 트랜잭션의 격리 수준에 대해 살펴보았습니다. 특히 Read Committed와 Repeatable Read를 비교하면서 "쓰기 작업을 차단한다"는 표현을 사용했는데, 정확히는 트랜잭션에서 데이터를 수정하면 자동으로 해당 데이터에 대해 배타 락(Exclusive Lock)을 획득한다고 이해할 수 있습니다. 즉, 데이터베이스 시스템에서 여러 사용자가 동시에 데이터에 접근할 때 발생할 수 있는 충돌을 방지하기 위해 DB Lock 메커니즘이 사용됩니다.
그럼 공유 락과 배타 락이 무엇인지 살펴보겠습니다.
공유락 (Shared Lock)
공유락(Shared Lock)은 데이터에 대한 읽기 작업을 허용하면서, 쓰기 작업은 차단하는 락입니다. 즉, 공유락을 가진 트랜잭션은 다른 트랜잭션이 데이터를 읽는 것을 허용하지만, 데이터를 수정하거나 삭제하는 것은 막습니다. 이러한 공유락은 여러 트랜잭션이 동시에 가질 수 있다는 장점이 있습니다. 그러나 이미 공유락이 걸려 있는 데이터에는 다른 트랜잭션이 배타락(Exclusive Lock)을 걸 수 없습니다.
공유락은 주로 데이터의 일관성을 유지하기 위해 사용되며, 주로 SELECT 쿼리에서 활용됩니다.
SELECT * FROM table WHERE id = 1 LOCK IN SHARE MODE;
MySQL에서는 LOCK IN SHARE MODE 명령어를 사용하면 읽기 작업에 대해 공유 락을 획득할 수 있습니다.
반면, PostgreSQL은 MVCC(Multi-Version Concurrency Control) 방식을 사용하여, 트랜잭션이 데이터를 읽을 때마다 자신의 스냅샷을 읽기 때문에 별도의 락을 필요로 하지 않습니다. 그러나 명시적으로 FOR SHARE 명령어를 사용하면 공유 을 획득할 수 있습니다.
배타락 (Exclusive Lock)
배타락(Exclusive Lock)은 데이터에 대한 읽기와 쓰기 작업을 모두 차단하는 락입니다. 배타락을 가진 트랜잭션은 해당 데이터에 대해 다른 트랜잭션의 읽기와 쓰기 작업을 모두 막으며, 배타락을 획득한 트랜잭션만이 데이터를 수정하거나 삭제할 수 있습니다. 데이터 수정 시 무결성을 보장하기 위해 사용되며, 주로 UPDATE나 DELETE 쿼리에서 발생합니다. 공유락과 달리 별도의 명령어 없이도 데이터가 수정될 때 자동으로 배타락을 획득하게 됩니다.
하지만 MVCC(Multi-Version Concurrency Control)를 사용하는 PostgreSQL에서는 배타락이 자동으로 적용되지 않습니다. PostgreSQL에서는 데이터가 변경되면 새로운 버전이 생성되고, 이전 버전은 다른 트랜잭션이 읽을 수 있도록 읽기 전용 상태로 유지됩니다. MVCC 덕분에 여러 트랜잭션이 동시에 데이터를 읽을 수 있으며, 락을 걸지 않고 독립적인 "스냅샷"을 읽는 방식으로 동시성을 유지합니다.
SELECT * FROM table WHERE id = 1 FOR UPDATE;
추가적으로 위와 같이 읽기 작업(SELECT)에도 FOR UPDATE 키워드를 사용하면 해당 작업을 쓰기 작업으로 간주하여 배타락을 획득할 수 있습니다.
낙관적락, 비관적락
위에서 다룬 공유락과 배타락은 데이터베이스의 기본적인 락 메커니즘입니다.
그렇다면, 낙관적락(Optimistic Lock)과 비관적락(Pessimistic Lock)은 무엇일까요?
낙관적 락과 비관적 락은 데이터베이스의 락 메커니즘이 아니라, 애플리케이션에서 트랜잭션 충돌을 처리하는 방식입니다.
》 낙관적락 (Optimistic Lock)
낙관적 락은 데이터 충돌이 발생할 가능성이 적다고 가정하고, 트랜잭션을 시작할 때 락을 걸지 않습니다. 대신, 각 레코드에 버전 번호나 타임스탬프를 추가하여 트랜잭션이 데이터를 수정하고 커밋하기 전에 해당 버전이나 타임스탬프가 다른 트랜잭션에 의해 변경되지 않았는지 확인합니다. 만약 데이터가 변경되었다면, 충돌이 발생한 것으로 보고 다시 시도해야 합니다.
낙관적 락은 락을 미리 걸지 않기 때문에 성능상 유리하지만, 충돌이 발생할 경우 다시 시도해야 하므로 비효율적일 수 있습니다. 이러한 방식은 주로 조회 작업이 많고 데이터 충돌이 드물며, 트랜잭션의 동시성이 높은 경우에 적합합니다.
스프링 JPA에서는 @Version 어노테이션을 사용하여 버전 관리를 통해 낙관적 락을 구현할 수 있습니다. 아래와 같이 엔티티 클래스에 @Version을 적용하면, 데이터베이스에서 해당 엔티티의 버전이 자동으로 관리됩니다.
@Entity
public class Product {
@Id
private Long id;
private String name;
@Version
private int version;
}
이와 같은 방식으로 버전 관리를 설정하면, 데이터의 변경이 발생할 때마다 버전 번호가 증가하고, 충돌이 발생하면 예외가 발생하여 처리할 수 있습니다.
》 비관적락 (Pessimistic Lock)
비관적 락(Pessimistic Locking)은 데이터베이스의 기본적인 락 메커니즘을 활용하여, 데이터가 수정되는 동안 배타 락을 걸어 다른 트랜잭션이 해당 레코드를 접근하지 못하게 합니다. 이를 통해 데이터 충돌 가능성을 최소화하고 동시성 문제를 확실히 예방할 수 있습니다.
하지만 항상 락을 걸기 때문에 다른 트랜잭션들이 해당 데이터를 기다려야 하며, 이로 인해 성능 저하가 발생할 수 있습니다. 비관적 락은 성능보다는 데이터의 일관성이 중요한 경우에 적합합니다.
스프링 JPA에서는 @Lock 어노테이션을 사용하여 비관적 락을 구현할 수 있습니다. LockModeType.PESSIMISTIC_WRITE를 사용하면, 해당 데이터에 대해 쓰기 잠금을 걸 수 있습니다.
아래와 같이 Repository 인터페이스에 @Lock을 적용하면 됩니다.
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Product findByIdForUpdate(Long id);
}
이 방식으로 비관적 락을 적용하면, 다른 트랜잭션이 해당 데이터를 수정하려 할 때까지 기다려야 하므로 데이터의 일관성을 보장할 수 있습니다.
마치며
이번 글에서는 데이터의 일관성을 보장하는 ACID 원칙과 동시성 문제를 해결하기 위한 DB 락에 대해 간단히 살펴보았습니다. 이러한 지식을 바탕으로 프로젝트를 진행할 때, 데이터베이스의 일관성과 성능을 고려하여 상황에 맞는 전략을 신중하게 선택하는 것이 중요하다고 생각합니다.
'Database' 카테고리의 다른 글
[Database] DB 검색 성능 개선을 위한 INDEX와 FTS (0) | 2025.02.16 |
---|---|
[Database] MySQL과 PostgreSQL, 어느 것을 사용할까요? (0) | 2025.02.12 |