낙관적 락(Optimistic Lock), 비관적 락(Pessimistic Lock)
Series: 데이터베이스
들어가며
데이터베이스를 다루다 보면 동시에 여러 요청이 하나의 데이터를 수정하려는 상황을 자주 마주하게 된다. 이러한 상황에서 데이터의 정합성을 유지하기 위해 사용하는 개념이 바로 락(Lock) 이다. 락은 크게 낙관적 락(Optimistic Lock) 과 비관적 락(Pessimistic Lock) 으로 나눌 수 있으며, 각각은 데이터 충돌을 다루는 방식에서 차이를 가진다. 이번 글에서는 낙관적 락과 비관적 락의 개념과 차이점, 그리고 어떤 상황에서 어떤 방식을 선택하는 것이 적절한지에 대해 정리해보겠다.
1. 개요
1.1. 동시성 문제
예를 들어, 하나의 게시글 조회수를 증가시키는 상황을 생각해보자.
- 사용자 A가 조회수를 읽는다 → 10
- 사용자 B도 조회수를 읽는다 → 10
- 사용자 A가 +1 해서 저장 → 11
- 사용자 B가 +1 해서 저장 → 11
결과적으로 조회수는 12가 되어야 하지만 11이 되어버리는 문제가 발생한다. 이를 Lost Update 문제라고 한다. 이처럼 여러 트랜잭션이 동시에 데이터를 수정할 때 발생하는 문제를 해결하기 위해 락 전략을 사용한다.
1.2. 낙관적 락(Optimistic Lock)
낙관적 락은 이름 그대로 "충돌이 거의 발생하지 않을 것"이라고 가정하는 방식이다. 데이터를 수정할 때 별도의 락을 걸지 않고, 수정 시점에 충돌 여부를 검사한다. 일반적으로는 version 컬럼이나 updatedAt 값을 활용한다.
동작 방식은 다음과 같다.
- 데이터를 조회할 때 version 값을 함께 가져온다.
- 데이터를 수정할 때 where version = 기존 값 조건을 건다.
- 수정된 row 수가 0이면 충돌로 간주한다.
UPDATE post
SET
title = 'new title',
version = version + 1
WHERE id = 1 AND version = 3;만약 다른 트랜잭션이 먼저 수정했다면 version 값이 달라져서 업데이트가 실패한다.
1.3. 비관적 락(Pessimistic Lock)
비관적 락은 "충돌이 발생할 것이다"라고 가정하는 방식이다. 데이터를 조회하는 시점에 락을 걸어 다른 트랜잭션이 해당 데이터를 수정하지 못하도록 막는다. 대표적으로 SELECT ... FOR UPDATE 구문을 사용한다.
SELECT * FROM post WHERE id = 1 FOR UPDATE;2. 낙관적 락 vs 비관적 락
2.1. 동작 방식 차이
- 낙관적 락 : 충돌을 허용하고, 수정 시점에 검증하며, 별도의 DB 락을 사용하지 않는다.
- 비관적 락 : 충돌을 방지하기 위해 조회 시점에 락을 획득하여 DB 레벨에서 동시 접근을 제어한다.
2.2. 성능 관점
낙관적 락은 별도의 락을 걸지 않기 때문에 일반적으로 대기 시간이 없고 처리량이 높다. 다만 충돌이 발생하면 재시도를 해야 하므로 로직이 복잡해질 수 있다. 반면, 비관적 락은 데이터 충돌을 원천적으로 방지할 수 있지만, 락 대기 시간이 발생할 수 있고 트랜잭션이 길어질수록 성능에 영향을 줄 수 있다. 특히 트래픽이 많은 환경에서 비관적 락을 과도하게 사용하면 병목 지점이 될 가능성이 있다.
2.3. 사용 기준
단순하게 정리하면 다음과 같이 볼 수 있다.
- 충돌이 거의 발생하지 않는 경우 → 낙관적 락
- 충돌이 자주 발생하거나, 반드시 순서를 보장해야 하는 경우 → 비관적 락
다만 실제로는 이보다 조금 더 복합적으로 판단해야 한다. 예를 들어, 사용자 프로필 수정은 낙관적 락, 재고 감소나 결제 처리는 비관적 락을 채택하는 것이다. 이와 같이 데이터의 중요도와 충돌 가능성을 함께 고려하는 것이 일반적이다.
2.4. 개인 견해
실무에서는 무조건 한 가지 방식을 고집하기보다는, 상황에 따라 적절히 선택하는 것이 중요하다고 생각한다. 낙관적 락은 구조가 단순하고 확장성이 좋아 대부분의 일반적인 CRUD 상황에서 유용하다. 특히 읽기 비중이 높고, 충돌 가능성이 낮은 서비스에서는 매우 효율적이다. 반면, 재고 감소나 포인트 차감처럼 정확성이 매우 중요한 도메인에서는 비관적 락이나 트랜잭션 제어를 통해 충돌을 사전에 방지하는 것이 더 안전하다. 또한 비관적 락은 잘못 사용하면 데드락이나 성능 저하를 유발할 수 있기 때문에, 트랜잭션 범위를 최소화하는 설계가 중요하다. 결국 핵심은 '어떤 방식이 더 좋은가?'가 아니라, '현재 문제 상황에서 어떤 방식이 더 적절한가?'라고 생각한다.
3. 예시
3.1. 낙관적 락
TypeORM에서는 @VersionColumn을 통해 낙관적 락을 사용할 수 있다.
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@VersionColumn()
version: number;
}이 경우 save 시 version이 자동으로 증가하며, 충돌이 발생하면 에러가 발생한다.
3.2. 비관적 락
QueryBuilder를 통해 비관적 락을 사용할 수 있다.
await dataSource.getRepository(Post).createQueryBuilder('post').setLock('pessimistic_write').where('post.id = :id', { id: 1 }).getOne();이 쿼리는 내부적으로 SELECT ... FOR UPDATE를 수행한다.
마치며
낙관적 락과 비관적 락은 모두 데이터 정합성을 유지하기 위한 중요한 개념이지만, 접근 방식이 완전히 다르다. 낙관적 락은 충돌을 감지하고 처리하는 방식, 비관적 락은 충돌을 사전에 차단하는 방식이라고 볼 수 있다. 실무에서는 데이터의 특성과 트래픽 패턴을 고려하여 적절한 전략을 선택해야 하며, 경우에 따라 두 가지 방식을 혼합해서 사용하는 것도 하나의 방법이 될 수 있다. 락을 어떻게 사용할지 고민하는 과정 자체가 결국 동시성 문제를 어떻게 다룰 것인가에 대한 설계라고 생각한다.