락 경합
📋 문서 버전
이 문서는 2개의 버전이 있습니다. 현재 버전 1을 보고 있습니다.
락 경합 (Lock Contention)
1. 개요
락 경합(Lock Contention)이란 멀티스레드 환경에서 여러 스레드가 동일한 공유 자원에 접근하기 위해 하나의 락(Lock, 상호 배제 메커니즘)을 동시에 획득하려고 시도할 때 발생하는 충돌 현상을 의미한다. 락은 데이터의 일관성을 유지하기 위해 필수적이지만, 경합이 심화될수록 스레드들이 락 획득을 위해 대기하는 시간이 늘어나며 이는 시스템 전체의 처리량 저하와 응답 시간 증가라는 성능 병목 현상으로 이어진다.
2. 발생 원리와 메커니즘
락 경합은 기본적으로 상호 배제(Mutual Exclusion) 원칙에 의해 발생한다. 특정 스레드가 임계 구역(Critical Section, 공유 자원에 접근하는 코드 영역)에 진입하여 락을 점유하면, 다른 스레드들은 해당 락이 해제될 때까지 대기 상태로 전환된다.
이 과정에서 가장 큰 오버헤드는 컨텍스트 스위칭(Context Switching)에서 발생한다. 대기 상태로 전환된 스레드는 CPU 스케줄러에 의해 실행 큐에서 제외(Suspend)되었다가, 락이 해제되어 다시 실행 가능 상태가 되면 다시 CPU 할당을 받아야 한다. 이 전환 과정에서 레지스터 상태 저장 및 복구, 캐시 미스(Cache Miss) 등이 발생하여 실제 연산 시간보다 관리 비용이 더 커지는 상황이 초래된다.
[표 1] 락 획득 성공 시 vs 경합 발생 시 스레드 상태 변화
| 구분 | 락 획득 성공 (No Contention) | 락 경합 발생 (Contention) |
|---|---|---|
| 스레드 상태 | Running $\rightarrow$ Running | Running $\rightarrow$ Blocked/Waiting $\rightarrow$ Runnable $\rightarrow$ Running |
| CPU 동작 | 중단 없이 임계 구역 실행 | 컨텍스트 스위칭 발생 $\rightarrow$ 대기 $\rightarrow$ 재스케줄링 |
| 오버헤드 | 매우 낮음 (단순 락 획득/해제 비용) | 매우 높음 (커널 모드 전환 및 스케줄링 비용) |
| 실행 흐름 | 선형적 실행 | 불연속적 실행 및 지연 발생 |
3. 락 경합과 데드락의 차이점
락 경합과 데드락(Deadlock, 교착 상태)은 모두 동기화 문제에서 발생하지만, 그 성격과 결과는 완전히 다르다.
- 락 경합 (Lock Contention): 성능의 문제이다. 스레드가 락을 얻기 위해 '기다리는' 상태이며, 시간이 지나 락이 해제되면 결국 작업이 완료된다. 즉, 느려지는 것이 핵심이다.
- 데드락 (Deadlock): 논리적 오류의 문제이다. 두 개 이상의 스레드가 서로가 가진 락을 기다리며 무한히 대기하는 상태이다. 외부의 강제 종료 없이는 절대 해결되지 않으며, 멈추는 것이 핵심이다.
4. 락 경합의 영향 및 증상
락 경합이 심해지면 시스템은 다음과 같은 역설적인 성능 지표를 보인다.
- CPU 자원 효율성 저하 및 처리량(Throughput) 감소: 스레드들이 연산을 수행하지 못하고 대기하므로 실제 단위 시간당 처리하는 요청 수는 급격히 떨어진다. 이때 뮤텍스(Mutex) 기반의 대기 시에는 CPU 사용률이 낮아질 수 있으나, 스핀락(Spin-lock) 기반의 경합 시에는 CPU가 계속 루프를 돌기 때문에 오히려 CPU 사용률이 100%로 치솟는 상반된 현상이 나타날 수 있다.
- 응답 시간(Latency)의 급증: 락을 획득하기 위한 대기 시간이 응답 시간에 합산되어, 사용자 체감 속도가 느려진다.
- 컨텍스트 스위칭 횟수 증가: OS 레벨에서 스레드 전환이 빈번하게 일어나며 시스템 커널의 부하가 증가한다.
5. 락 경합 해결 및 최적화 전략
경합을 줄이기 위한 핵심은 "락의 점유 시간을 최소화하고, 락의 범위를 세분화하는 것"이다.
5.1 락 범위 최소화 (Lock Granularity)
락을 거는 코드 영역을 최대한 좁게 설정하여, 다른 스레드가 대기하는 시간을 줄인다.
5.2 락 분할 (Lock Stripping)
하나의 거대한 락 대신, 자원을 여러 개의 버킷으로 나누어 각각 별도의 락을 적용하는 방식이다. (예: Java의 ConcurrentHashMap)
5.3 낙관적 락 (Optimistic Locking)
데이터 충돌이 적을 것이라고 가정하고, 락을 걸지 않은 채 작업을 수행한 뒤 커밋 시점에 충돌 여부를 확인하는 방식이다. 충돌 시에만 재시도(Retry)를 수행한다.
5.4 읽기/쓰기 락 (Read-Write Lock)
읽기 작업이 많고 쓰기 작업이 적은 환경에서 사용하는 방식이다. 읽기 스레드 간에는 공유 락(Shared Lock)을 허용하여 동시에 접근하게 하고, 쓰기 스레드에게만 배타적 락(Exclusive Lock)을 부여함으로써 경합을 획기적으로 줄인다.
[코드 예제] 락 범위 비교 (Java 스타일)
Bad: 거대한 락 (Coarse-grained Lock)
public synchronized void updateData() {
// 1. 무거운 DB 조회 (락 점유 중)
// 2. 복잡한 비즈니스 로직 계산 (락 점유 중)
// 3. 데이터 수정 (락 점유 중)
// 결과: 모든 스레드가 1~3번 과정이 끝날 때까지 대기함
}
Good: 세분화된 락 (Fine-grained Lock)
public void updateData() {
// 1. 무거운 DB 조회 (락 없이 수행)
Data data = fetchData();
synchronized(lockObject) {
// 2. 최소한의 수정 작업만 락 범위 내에서 수행
data.setValue(newValue);
}
// 3. 결과 반영 (락 없이 수행)
}
6. 대안 기술: Non-blocking 알고리즘
락 자체를 사용하지 않고 동시성을 제어하는 기법으로, 경합 문제를 근본적으로 해결하려는 시도이다.
- CAS (Compare-And-Swap): 메모리의 현재 값과 내가 예상한 값이 일치할 때만 새로운 값으로 교체하는 원자적(Atomic) 연산이다. 락을 걸어 다른 스레드를 멈추게 하는 대신, 업데이트 직전에 값이 변경되었는지 확인하고 실패 시 다시 시도하는 '낙관적' 방식을 취함으로써 컨텍스트 스위칭 비용을 제거한다. 다만, 값이 A $\rightarrow$ B $\rightarrow$ A로 변했을 때 변경되지 않은 것으로 인식하는 ABA 문제가 발생할 수 있으며, 이는 버전 관리(Versioning)나 스탬프를 통해 해결한다.
- Lock-free: 최소한 하나의 스레드는 항상 전진함을 보장하는 구조이다.
- Wait-free: 모든 스레드가 유한한 단계 내에 작업을 완료함을 보장하는 가장 강력한 수준의 Non-blocking 구조이다.
7. 특수 락: 스핀락 (Spinlock)
스핀락은 락을 획득할 때까지 스레드를 대기 상태(Blocked)로 전환하지 않고, 루프를 돌며 계속해서 락 획득을 시도하는 방식이다.
- 특징: 컨텍스트 스위칭 비용이 발생하지 않으므로, 락 점유 시간이 매우 짧을 때 효율적이다. 하지만 락을 얻지 못하는 동안 CPU를 계속 점유하므로 점유 시간이 길어지면 CPU 낭비가 심해진다.
- 사용 사례: OS 커널의 인터럽트 핸들러, 매우 짧은 임계 구역을 가진 저수준 드라이버 등 컨텍스트 스위칭 오버헤드가 실제 대기 시간보다 더 큰 환경에서 주로 사용된다.
8. 언어별 락 구현체 비교
각 언어는 락 경합을 효율적으로 처리하기 위해 다양한 구현체를 제공한다.
| 언어 | 주요 구현체 | 특징 | 비고 |
|---|---|---|---|
| Java | ReentrantLock, StampedLock |
적응형 스핀락(Adaptive Spin-lock) 및 읽기/쓰기 분리 락 제공 | JVM 수준의 최적화 |
| Go | sync.Mutex, channel |
가벼운 고루틴(Goroutine) 기반의 스케줄링과 채널을 통한 통신 지향 | CSP 모델 채택 |
| C++ | std::mutex, std::atomic |
OS 커널 락과 하드웨어 원자적 연산을 직접 제어 가능 | 저수준 제어 가능 |
| Python | threading.Lock, multiprocessing.Lock |
GIL로 인해 멀티스레드 환경에서 CPU 집약적 작업 시 락 경합이 필연적으로 발생함 | 멀티프로세싱 권장 |
9. 진단 및 모니터링 방법
실무에서 락 경합을 찾아내기 위해서는 다음과 같은 지표와 도구를 활용한다.
9.1 성능 측정 지표 (Metrics)
- Lock Wait Time: 스레드가 락을 획득하기 위해 대기한 총 시간.
- Lock Hold Time: 스레드가 락을 획득한 후 해제할 때까지 걸린 시간.
- Context Switches per Second: 초당 발생하는 컨텍스트 스위칭 횟수 (급증 시 경합 의심).
- Blocked Thread Count: 특정 시점에
BLOCKED상태에 있는 스레드의 수.
9.2 진단 도구 및 방법
- 스레드 덤프 (Thread Dump): 현재 실행 중인 모든 스레드의 상태를 스냅샷으로 찍어, 어떤 스레드가 어떤 락을 보유하고 있고, 어떤 스레드가 그 락을 기다리고 있는지(
waiting on <0x...>형태) 분석한다. - 프로파일링 도구:
- Java: JVisualVM, JProfiler, async-profiler (Lock profiling 모드 사용).
- Go:
pprof(mutex profile을 통해 경합 지점 확인).
- Hotspot 분석: CPU 사용률은 낮으나 응답 시간이 튀는 특정 메서드를 추적하여 임계 구역의 병목 지점을 식별한다.
이 문서는 AI 모델(gemma-4-31b)에 의해 생성된 콘텐츠입니다.
주의사항: AI가 생성한 내용은 부정확하거나 편향된 정보를 포함할 수 있습니다. 중요한 결정을 내리기 전에 반드시 신뢰할 수 있는 출처를 통해 정보를 확인하시기 바랍니다.