시스템이 복잡해질수록 동시에 여러 작업을 처리해야 할 필요가 늘어나게 됩니다. 이 때 동시에 접근하는 여러 작업 간의 충돌을 방지하기 위해 Lock이라는 개념을 사용하게 됩니다. 여기서 SQL DBMS에서의 Lock과 Java에서의 Lock을 알아보고, 이를 언제 어떻게 사용해야 하는지 살펴보겠습니다.
Lock이란?
Lock(락)은 특정 자원에 대해 여러 작업이 동시에 접근하는 것을 방지하기 위한 메커니즘입니다.
Lock을 활용하지 않은 동시성 문제 예시
동시성 문제로 하나 예를 들어보자면, A와 B라는 사람한테 화이트보드에 적혀있는 숫자를 하나씩 증가시켜 달라고 요청을 합니다.
그럼 A라는 사람이 화이트 보드에 있는 3이라는 값을 읽어서 머리속으로 계산을 합니다.
계산을 끝내고 화이트보드를 지우고 다시 적으려 할 때 마침 B도 화이트보드에 있는 3이라는 값을 읽어서 머리속으로 계산을 시작하게됩니다.
A는 화이트 보드를 지우고 계산이 끝난 값 4를 적습니다.
B도 화이트 보드를 지우고 계산이 끝난 값 4를 적습니다.
A와 B가 모두 값을 올려 5를 원했지만 결과는 4가 적혀있고 이것이 멀티 프로세스(또는 쓰레드) 환경에서 발생할 수 있는 동시성 문제입니다.
이를 해결하기 위해서 A, B 둘중 한명이 값을 읽으면 계산이 끝나기 전까지 읽을 수 없게 헝겁으로 덮는 것을 Lock이라고 합니다.
DB에서의 Lock
데이터베이스에서 Lock은 동시성 제어를 위해 사용됩니다. 여러 트랜잭션이 동시에 같은 데이터를 수정하려 할 때, 데이터 무결성을 보장하기 위해 Lock을 걸고 관리합니다. 일반적으로 데이터베이스에서 Lock을 거는 방식은 설명하겠습니다.
- 공유 락(Shared Lock): 여러 트랜잭션이 동시에 읽기 작업을 수행할 수 있지만, 쓰기 작업은 불가능합니다.
- 배타적 락(Exclusive Lock): 데이터를 수정하는 동안 다른 트랜잭션이 읽기와 쓰기 모두 차단됩니다.
Lock을 어디에 거는지는 크게 세가지로 나뉩니다.
- 행 락(Row Lock): 특정한 하나의 행(row)에 대해서만 Lock을 겁니다. 이 방법은 동시성 제어에 효과적이며, 대부분의 DBMS는 기본적으로 지원합니다.
- 테이블 락(Table Lock): 테이블 전체에 Lock을 겁니다. 다수의 트랜잭션이 테이블의 여러 행을 동시에 수정하는 것을 방지하기 위해 사용됩니다.
- 페이지 락(Page Lock): 테이블의 특정 페이지(데이터 블록 단위)에 Lock을 겁니다.
데드락(Dead Lock)이란?
- 데드락이란 두 개 이상의 트랜잭션이 서로 상대방이 보유한 락을 기다리면서 무한정 대기하는 상황을 말합니다.
- 데드락은 발생하지 못하게 설계해야 하며, 만약 발생한다면 보상 처리를 하지 않고 Timeout으로 실패 처리 하는 방법이 가장 대중적으로 보입니다.
트랜잭션 격리 수준
DB Lock은 트랜잭션의 격리 수준에 따라 다르게 작동합니다. 높은 숫자일수록 더 엄격한 격리 수준입니다.
- Read Uncommitted: 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있습니다. 충돌 가능성이 높습니다.
- Read Committed: 트랜잭션이 커밋된 데이터만 읽을 수 있습니다. 대부분의 DBMS의 기본 격리 수준입니다.
- Repeatable Read: 트랜잭션이 시작된 이후로 다른 트랜잭션이 해당 데이터를 변경할 수 없습니다.
- Serializable: 가장 엄격한 수준으로, 읽기 작업에도 Lock이 걸릴 수 있습니다.
일반적으로 숫자가 높은수록 문제 발생 가능성이 줄어들고, 숫자가 낮을수록 빠른 성능을 기대할 수 있습니다.
Java 객체에서의 Lock
Java에서의 Lock을 알아보겠습니다.
아래 메서드는 하나의 쓰레드만 접근할 수 있고 동시에 두개의 쓰레드가 접근한다면 하나의 쓰레드는 앞 쓰레드가 메서드를 빠져나올 때 까지 BLOCKED 상태로 변경됩니다.
public synchronized MyInstance getInstance() {
// 이 메서드에 대해 동시에 하나의 스레드만 접근 가능
}
위 메서드는 접근에 대하여 모두 Lock을 걸고있으며, 만약 이미 있는 값을 가져올 경우는 선택적으로 Lock을 발생시킬 수 있습니다.
아래와 같은 방법을 Double Checked Lock이라고 합니다.
private static volatile MyInstance instance;
public static MyInstance getInstance() {
if (instance == null) {
synchronized (MyInstance.class) {
if (instance == null) {
instance = new MyInstance();
}
}
}
return instance;
}
synchronized는 객체 단위로 락을 걸기 때문에 간단한 동시성 제어에는 매우 유용합니다. 하지만, 더 정교한 제어가 필요한 경우
ReentrantLock과 같은 Lock 클래스를 사용할 수 있습니다.
# volatile은 CPU 명령어 재정렬을 방지하기 위해 사용됩니다. 인스턴스가 초기화 되지 않은 상태에서 참조 변수가 설정되는 경우를 방지합니다.
ReentrantLock
ReentrantLock은 synchronized보다 유연한 동시성 제어를 제공합니다. 락을 명시적으로 획득하고 해제할 수 있기 때문에, 락을 걸어두고 특정 조건에 따라 락을 해제하거나 다시 걸 수 있습니다.
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
ReentrantLock은 인스턴스 메소드를 보면서 설명하는게 더 좋은 것 같습니다.
- lock(): thread가 락을 획득할 때까지 대기합니다. 대기할 때 Thread State는 BLOCKED이며 Interrupt 시 이를 무시합니다.
- lockInterruptibly(): thread가 락을 획들할 때까지 대기합니다. lock()과의 차이점은 Interrupt가 가능하다는 것 입니다.
- tryLock(): 락을 시도하고 즉시 결과를 반환합니다. return type은 boolean입니다.
- tryLock(time, unit): 지정된 시간 동안만 락을 시도합니다.
- unlock(): 락을 해제합니다.
ReentrantLock의 장점은 락 대기 시간 제한, 인터럽트 가능 락 획득, 공평한(lock fairness) 락 획득 전략 등을 지원한다는 점입니다. 하지만, 사용자가 명시적으로 unlock()을 호출해야 하기 때문에, 반드시 try-finally 블록을 사용하여 락 해제를 보장해야 합니다.
ReadWriteLock
ReadWriteLock은 읽기와 쓰기 작업을 구분하여 동시성 제어를 최적화하는 데 사용됩니다. 여러 스레드가 데이터를 읽을 수 있지만, 쓰기 작업이 발생할 때는 하나의 스레드만 접근할 수 있습니다.
- 읽기 락(Read Lock): 모든 쓰레드에서 읽기가 가능합니다. 쓰기는 불가능합니다.
- 쓰기 락(Write Lock): 자신이 아닌 쓰레드에서 읽기와 쓰기가 불가능합니다.
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
readLock.lock();
try {
} finally {
readLock.unlock();
}
writeLock.lock();
try {
} finally {
writeLock.unlock();
}
Lock 전략
- 낙관적 락(Optimistic Lock): 데이터 충돌이 발생하면 버전 정보나 타임스탬프를 사용해 트랜잭션을 롤백하고 다시 시도합니다. 충돌 가능성이 낮은 환경에서 유용합니다.
- 비관적 락(Pessimistic Lock): 트랜잭션이 데이터를 읽거나 수정할 때 다른 트랜잭션이 그 데이터에 접근하지 못하도록 차단합니다.
- 분산 락(Distributed Lock): 분산 환경에서 여러 노드가 동일한 자원에 접근할 때 사용하는 락입니다. 대표적으로 Kafka에서 사용하는 Zookeeper가 Distributed Lock을 활용합니다.
Lock은 동시성 제어를 위해 꼭 필요한 개념이지만, 잘못 사용하면 데드락이나 성능 저하와 같은 문제가 발생할 수 있습니다.
꼭 필요한 곳에만 사용해야 하며, MySQL InnoDB에서 지원해주는 멀티버전 동시성 제어(MVCC)같은 대안도 고려해보는 것이 좋을 것 같습니다.
'Database' 카테고리의 다른 글
MySQL 8에서 인덱스 성능 극대화: 알고리즘과 실전 사례 (0) | 2023.12.26 |
---|---|
데이터베이스 프로시저, 함수, 뷰: 각각의 역할과 사용법 (2) | 2023.12.26 |
트랜잭션 격리 수준의 종류와 성능 최적화 전략 (0) | 2023.09.02 |
Redis 트랜잭션: MULTI부터 EXEC까지 트랜잭션 처리 과정 (0) | 2023.08.04 |
Redis에서 데이터 영구 보존: 스냅샷과 AOF로 안전한 복구 시스템 구축 (0) | 2023.08.04 |