데이터베이스 동시성 제어: 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) 실무 비교
공유 자원에 대한 동시 수정 요청이 충돌할 때 데이터의 무결성을 유지하기 위해 데이터베이스 엔진 단에서 지원하는 비관적 락(Pessimistic Locking)과 애플리케이션 수준의 버전 비교 방식인 낙관적 락(Optimistic Locking)을 분석합니다.
1. 비관적 락 (Pessimistic Lock)
트랜잭션 충돌 가능성이 매우 높다고 '비관적'으로 가정하고, 데이터를 조회할 때부터 행에 물리적인 배타 락(Exclusive Lock)을 선제적으로 취득하는 방식입니다.
- 작동 원리: 데이터베이스 SQL 쿼리에 `SELECT ... FOR UPDATE` 구문을 작성하여 락을 취득하며, 다른 트랜잭션의 수정 및 읽기를 원천 차단합니다.
- 적합한 환경: 잔액 변경, 재고 차감 등 자원에 대한 쓰기 빈도가 매우 잦아 롤백(Rollback) 비용이 매우 크고 데이터 정확도가 최우선인 비즈니스.
2. 낙관적 락 (Optimistic Lock)
트랜잭션 충돌 가능성이 거의 없을 것이라고 '낙관적'으로 가정하고, 락을 따로 획득하지 않고 자원을 자유롭게 수정한 뒤 커밋 시점에 정합성이 어긋나지 않았는지 검증하는 방식입니다.
- 작동 원리: 테이블에 `version` 컬럼을 두고, `UPDATE ... SET version = version + 1 WHERE id = 1 AND version = {현재버전}` 처럼 변경 대상이 조회 시점과 같은 버전을 유지하고 있을 때만 업데이트에 성공하도록 보장합니다.
- 적합한 환경: 동시 수정 충돌이 가끔만 발생하고 데이터베이스 잠금 오버헤드와 물리적인 데드락을 철저히 회피하고 싶을 때.
3. Go, NestJS, Spring Boot 구현 예제
Go (Golang) - GORM Pessimistic & Optimistic
Go ORM인 GORM에서 비관적 락(`FOR UPDATE`)과 낙관적 락 버전 제어를 수행하는 구문 비교입니다.
package main
import (
"context"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Account struct {
ID uint `gorm:"primaryKey"`
Balance int
Version int // 낙관적 락을 위한 버전 컬럼
}
// 1. 비관적 락 구현 (SELECT FOR UPDATE)
func DeductPessimistic(ctx context.Context, db *gorm.DB, id uint, amount int) error {
return db.Transaction(func(tx *gorm.DB) error {
var account Account
// Clause를 이용해 SELECT ... FOR UPDATE 실행
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, id).Error; err != nil {
return err
}
account.Balance -= amount
return tx.Save(&account).Error
})
}
// 2. 낙관적 락 구현 (Version 검증)
func DeductOptimistic(ctx context.Context, db *gorm.DB, id uint, amount int) error {
var account Account
if err := db.First(&account, id).Error; err != nil {
return err
}
oldVersion := account.Version
account.Balance -= amount
account.Version += 1
// 데이터 수정 당시 버전이 조회 당시 버전과 일치하는지 조건 확인
result := db.Model(&Account{}).
Where("id = ? AND version = ?", id, oldVersion).
Updates(map[string]interface{}{
"balance": account.Balance,
"version": account.Version,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("concurrent update conflict: check version failed")
}
return nil
}
Node.js (NestJS / Prisma) - Row locking & Retries
Prisma ORM을 이용해 비관적 락을 수행하거나, 낙관적 락 충돌 시 애플리케이션에서 재시도 루프를 도는 패턴입니다.
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Injectable()
export class ConcurrencyService {
constructor(private prisma: PrismaService) {}
// 1. Prisma 비관적 락 (Prisma는 SELECT FOR UPDATE를 queryRaw로 지원)
async deductPessimistic(id: number, amount: number) {
return this.prisma.$transaction(async (tx) => {
const [account] = await tx.$queryRaw`
SELECT * FROM "Account" WHERE id = ${id} FOR UPDATE
`;
if (!account) throw new Error('Account not found');
const newBalance = account.balance - amount;
await tx.$executeRaw`
UPDATE "Account" SET balance = ${newBalance} WHERE id = ${id}
`;
});
}
// 2. Prisma 낙관적 락 및 3회 재시도 처리
async deductOptimistic(id: number, amount: number, retries = 3): Promise {
for (let i = 0; i < retries; i++) {
const account = await this.prisma.account.findUnique({ where: { id } });
if (!account) throw new Error('Account not found');
try {
await this.prisma.account.update({
where: {
id,
version: account.version, // 버전 매칭
},
data: {
balance: account.balance - amount,
version: { increment: 1 },
},
});
return; // 성공 시 함수 탈출
} catch (err) {
if (i === retries - 1) throw new Error('Optimistic lock transaction failed after retries');
// 다음 루프에서 재시도 (Back-off 잠시 대기할 수 있음)
await new Promise((res) => setTimeout(res, 50));
}
}
}
}
Kotlin (Spring Boot) - Spring Data JPA Annotations
Spring Data JPA 환경에서는 에노테이션 지정만으로 비관적 락(`@Lock`)과 낙관적 락(`@Version`)을 간편하게 처리할 수 있습니다.
@Entity
class Account(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
var balance: Int,
@Version
var version: Int = 0 // @Version 애노테이션으로 낙관적 락 지정
)
interface AccountRepository : JpaRepository {
// 1. 비관적 배타 락을 명시하는 쿼리 메서드
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
fun findByIdWithPessimisticLock(id: Long): Optional
}
@Service
class AccountService(
private val accountRepository: AccountRepository
) {
// 비관적 락 호출 서비스
@Transactional
fun deductPessimistic(id: Long, amount: Int) {
val account = accountRepository.findByIdWithPessimisticLock(id)
.orElseThrow { RuntimeException("Account not found") }
account.balance -= amount
account.balance // 트랜잭션 종료 시 더티 체킹에 의해 자동 반영 및 락 해제
}
// 낙관적 락 호출 서비스 (버전 불일치 시 ObjectOptimisticLockingFailureException 발생)
@Transactional
fun deductOptimistic(id: Long, amount: Int) {
val account = accountRepository.findById(id)
.orElseThrow { RuntimeException("Account not found") }
account.balance -= amount
}
}
Database Concurrency Control: Pessimistic vs. Optimistic Locking Guide
When multiple concurrent transactions attempt to write to the same record, you must configure either Pessimistic Locking (database-level row locking) or Optimistic Locking (application-level version checks) to maintain absolute database integrity.
1. Pessimistic Locking
Assumes that write conflicts are highly probable, immediately locking targeted database rows upon SELECT execution to block other transactions.
- How it works: Appends a `FOR UPDATE` suffix to RDBMS queries, preventing concurrent read-for-writes or updates on targeted rows.
- Ideal usage: Highly competitive transactional write paths (e.g., banking ledgers, stock checkout queues) where rollbacks are costly.
2. Optimistic Locking
Assumes that conflicts are unlikely to occur, allowing transactions to modify rows freely and only validating record versions during the commit stage.
- How it works: Integrates a `version` column. Updates match on `WHERE id = ? AND version = ?`, failing if another process updated the version first.
- Ideal usage: Low-to-medium contention writes, avoiding lock overheads and physical RDBMS deadlock risks.
3. Go, NestJS, and Spring Boot Implementations
Go (Golang) - GORM Pessimistic & Optimistic
Implement locking using GORM clauses for `SELECT FOR UPDATE` or conditional update loops evaluating old versions.
// Refer to the KO tab code for Go implementation details.
// GORM query builders allow specifying locks or version-matching parameters.
Node.js (NestJS / Prisma) - Row locking & Retries
Implement pessimistic raw queries or use programmatic optimistic retry loops that query, verify version fields, and loop under failure.
// Refer to the KO tab code for NestJS implementation details.
// Promise retries loop up to a customizable max count to handle conflicts.
Kotlin (Spring Boot) - Spring Data JPA Annotations
Spring Data JPA natively implements pessimistic locking via the `@Lock` annotation and optimistic locking via the JPA standard `@Version` field annotation.
// Refer to the KO tab code for Kotlin/Spring Boot implementation details.
// LockModeType.PESSIMISTIC_WRITE signals JDBC to issue database-level locking.