모바일 개발에서의 Livelock: 개념, 상호 배제와의 차이 및 작동 원리

저자: IT Sectr 게시일: 2026-03-18 읽는 시간: 10 분

Livelock(활성 잠금)은 멀티스레드 프로그래밍에서 스레드가 차단되지는 않았지만 서로의 동작에 무한히 반응하여 유용한 작업을 수행하지 못하는 상황입니다. Baeldung(Java Concurrency Guide, 2024)에 따르면, Livelock에서 스레드는 이웃 스레드의 상태에 응답하여 지속적으로 상태를 변경하지만 어떤 스레드도 목표를 달성하지 못합니다. Deadlock과 달리 Livelock은 CPU를 100% 소비하여 모바일 기기의 배터리를 빠르게 소모시킵니다.

핵심 사항

  • Livelock은 스레드가 활성 상태이지만 진행하지 못하고 충돌에 무한히 반응하는 상태입니다
  • Deadlock과 달리 Livelock에서는 스레드가 차단되지 않으며 상태 사이를 지속적으로 전환합니다
  • 활성 잠금은 CPU 시간과 에너지를 소비하여 애플리케이션 성능을 저하시킵니다
  • 재시도 제한(retry limit)은 무한 Livelock을 방지하는 가장 간단한 방법입니다
  • 무작위 지연(exponential backoff)은 스레드 간의 동기적 반응 주기를 차단합니다

Livelock이란?

Livelock(활성 잠금)은 멀티스레드 시스템에서 스레드가 차단되지는 않았지만 유용한 작업을 수행하지 못하는 상황입니다. 각 스레드는 계속할 수 없음을 감지하고 이를 해결하려고 시도하지만, 그 동작이 다른 스레드에서 동일한 반응을 유발합니다. 결과적으로 시스템은 진행되지 않은 채 무한히 상태 사이를 전환합니다.

Livelock의 전형적인 비유는 좁은 복도에서 두 사람이 마주치는 것입니다. 각자가 상대방을 지나치게 한쪽으로 비켜서려고 하지만, 둘 다 동시에 같은 움직임을 하여 다시 서로 앞에 서게 됩니다. 그들은 가만히 서 있지 않습니다(그것은 Deadlock입니다). 오히려 활발히 움직이지만 결코 지나칠 수 없습니다. 프로그래밍에서 이는 스레드가 리소스를 지속적으로 해제하고 다시 획득하는 것에 해당합니다.

모바일 개발에서 Livelock은 특히 위험합니다. 사용자에게 눈에 띄지 않기 때문입니다. 앱이 멈추지 않고 인터페이스가 차단되지 않지만, 백그라운드 스레드의 100% CPU 부하로 인해 배터리가 2~3배 더 빨리 소모됩니다. Google 테스트(Android Battery Optimization, 2023)에 따르면, 백그라운드 Service의 Livelock은 기기 배터리 수명을 40%까지 단축시킬 수 있습니다.

Livelock의 발생 원인

충돌에 대한 동기적 반응

Livelock은 여러 스레드가 동일한 충돌 대응 전략을 사용할 때 발생합니다. 스레드 A가 리소스를 획득할 수 없어 현재 리소스를 해제하고, 스레드 B가 동시에 같은 작업을 수행하면, 둘 다 주기를 반복하며 상황이 무한히 지속됩니다. 이는 특히 TryLock과 실패 시 자동 해제를 사용하는 알고리즘에서 일반적입니다.

재시도의 무작위성 부족

스레드가 재시도 전에 고정된 지연을 사용하면 동기 주기에 빠질 수 있습니다. 두 스레드가 같은 시간을 기다리면 동시에 다시 리소스를 획득하려 시도하고 동시에 해제합니다. 이 문제는 Ethernet의 CSMA/CD 알고리즘처럼 무작위 구성 요소(지터)가 있는 exponential backoff을 사용하여 해결됩니다.

잘못된 큐 설계

모바일 개발에서 Livelock은 종종 작업 큐의 잘못된 구현으로 인해 발생합니다. 예를 들어, 워커 스레드가 메시지 처리를 완료했지만 우선순위 로직 때문에 동일한 작업을 수행하는 다른 워커에게 지속적으로 제어권을 넘기는 경우입니다. 이러한 상황은 비표준 RejectedExecutionHandler 정책을 가진 사용자 정의 ThreadPoolExecutor에서 일반적입니다.

Kotlin 코드의 Livelock 예제

두 스레드가 TryLock을 사용하고 실패 시 리소스를 해제하는 상황을 고려해 보겠습니다. 두 스레드가 동일한 로직을 적용하고 동기적으로 재시도하기 때문에 활성 잠금이 발생합니다.

kotlin
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit

class LivelockWorker(private val name: String,
                     private val lock1: ReentrantLock,
                     private val lock2: ReentrantLock) {

    fun execute() {
        while (true) {
            if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
                if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
                    println("$name — 완료!")
                    lock2.unlock()
                    lock1.unlock()
                    return
                } else {
                    lock1.unlock()  // 해제 및 재시도
                }
            }
            Thread.sleep(50)  // 동일한 지연 — Livelock의 주요 요인
        }
    }
}

LivelockWorker의 두 인스턴스가 lock1과 lock2를 획득하는 순서를 다르게 하여 실행되면 활성 잠금에 빠집니다. 각각 첫 번째 리소스를 획득하고, 두 번째를 얻지 못하며, 첫 번째를 해제하고, 50ms를 기다린 후 재시도하는 것을 CPU를 소비하며 무한히 반복합니다. 수정 방법은 지연에 무작위 요소(지터)를 추가하고 재시도 횟수를 제한하는 것입니다.

수정된 버전은 무작위 지터가 있는 exponential backoff을 사용합니다. 실패할 때마다 무작위 승수와 함께 대기 시간이 증가하여 스레드 간의 동기화를 차단합니다.

kotlin
fun executeWithBackoff() {
    var delay = 10L
    var attempts = 0

    while (attempts < 5) {
        if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
            if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
                println("성공!")
                lock2.unlock(); lock1.unlock()
                return
            }
            lock1.unlock()
        }
        delay = (delay * 2 + (0..50).random())
        attempts++
    }
    println("5회 시도 후 실패")
}

Livelock vs Deadlock: 주요 차이점

겉보기 유사성에도 불구하고 Livelock과 Deadlock은 근본적으로 다른 메커니즘과 결과를 가집니다. Deadlock에서는 스레드가 차단되고 CPU를 소비하지 않습니다. 애플리케이션이 그냥 멈춥니다. Livelock에서는 스레드가 활성 상태이고 CPU를 100% 소비하지만 유용한 작업을 수행하지 않습니다. 해결 전략의 선택은 잠금 유형의 올바른 식별에 달려 있습니다.

파라미터DeadlockLivelock
스레드 상태BLOCKED / WAITINGRUNNABLE
CPU 소비최소높음 (90-100%)
배터리 소비낮음높음
감지Thread DumpCPU Profiler + 시각적 분석
일반적 원인잠금 획득 순서 차이동일한 충돌 대응 전략
수정잠금 계층화재시도 제한 + exponential backoff

모바일 개발에서 실질적인 차이는 엄청납니다. Deadlock은 ANR과 앱 재시작으로 이어지며 Google Play Console을 통해 감지 및 보고됩니다. Livelock은 눈에 띄지 않습니다. 앱이 작동하는 것처럼 보이지만 배터리가 한 시간 안에 방전되고 사용자는 그냥 앱을 삭제합니다. Firebase Analytics(App Retention Report, 2024)에 따르면, 백그라운드에서 과도하게 배터리를 소비하는 앱은 사용자의 68%가 삭제합니다.

Livelock 감지 방법

Livelock 감지는 Deadlock보다 어렵습니다. 시스템이 명백한 신호를 보내지 않기 때문입니다. 예외도, ANR도, 오류 메시지도 없습니다. 주요 진단 방법은 Android Studio의 CPU Profiler입니다. 스레드가 지속적으로 RUNNABLE 상태이지만 유용한 I/O나 계산을 수행하지 않는다면 Livelock이 의심됩니다.

추가 지표는 앱이 유휴 상태일 때의 비정상적인 배터리 소비입니다. Android Battery Historian(Android SDK 도구)은 구성 요소별 에너지 소비 그래프를 만듭니다. CPU Wakelock이 명백한 이유 없이 유지되면 Method Tracing을 실행하고 의심스러운 스레드의 호출 스택을 분석하십시오.

코드 수준에서는 threadId와 타임스탬프가 있는 재시도 로깅이 도움이 됩니다. 로그가 초당 수천 번의 재시도를 단 한 번의 성공 없이 보여준다면 그것은 Livelock입니다. 임계값을 초과하면 작업을 비활성화하고 Crashlytics를 통해 개발자에게 알리는 Hystrix 스타일의 서킷 브레이커 또는 재시도 카운터를 구현하는 것이 좋습니다.

활성 잠금 방지 방법

재시도 제한(Retry Limit)

가장 간단하고 신뢰할 수 있는 방법은 리소스 획득 시도 횟수를 제한하는 것입니다. N번 시도 후에도 작업이 실패하면 스레드는 오류 상태로 전환되고 사용자에게 알립니다. N은 경험적으로 선택됩니다. 모바일 앱의 경우 일반적으로 3~5회 시도입니다. 이는 높은 부하에서 드물게 발생하는 오탐지를 대가로 무한 Livelock을 완전히 제거합니다.

지터가 있는 Exponential Backoff

시도 사이의 고정된 지연 대신 무작위 구성 요소가 있는 기하급수적으로 증가하는 일시 중지를 사용합니다. 공식: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). 이 접근 방식은 스레드 동기화를 차단할 뿐만 아니라 높은 경합 상황에서 시스템 전체 부하도 줄입니다. 네트워크 프로토콜 알고리즘에서 사용되며 Firebase Realtime Database 재시도 로직을 위해 Google에서 권장합니다.

우선순위 및 비대칭 로직

다른 스레드에 다른 전략을 할당하면 Livelock의 근본 원인인 충돌에 대한 동일한 반응을 제거합니다. 예를 들어, 높은 우선순위의 스레드는 해제하지 않고 리소스를 획득하고, 낮은 우선순위의 스레드는 해제하고 기다립니다. 모바일 개발에서 UI 스레드는 잠금 획득 시 우선순위를 가질 수 있고, 백그라운드 워커 스레드는 타임아웃이 있는 TryLock을 사용합니다.

순환 해제 방지

일부 아키텍처에서는 설계 수준에서 Livelock이 방지됩니다. 한 방향으로만 리소스 해제가 이루어집니다. 예를 들어, 스레드 A가 고정 채널을 통해 항상 스레드 B로 제어권을 넘기고 B가 절대 A로 제어권을 반환하려 하지 않으면 반응 주기가 불가능합니다. 단방향 처리 단계가 있는 파이프라인 아키텍처는 Android CameraX 및 MediaPipe에서 인접 단계 간의 Livelock을 완전히 제거합니다.

자주 묻는 질문

Livelock과 무한 루프를 어떻게 구별하나요?

무한 루프는 외부 요인에 의존하지 않으며 다른 스레드와 상호 작용 없이 단일 작업을 반복합니다. Livelock은 항상 다른 스레드의 동작에 대한 반응입니다. 스레드가 이웃 스레드의 상태에 응답하여 동작을 변경하여 폐쇄된 피드백 루프를 만듭니다. Livelock의 경우 Thread Dump는 지속적인 컨텍스트 전환을 보여줍니다.

데이터베이스 맥락에서 Livelock이란?

데이터베이스에서 Livelock은 다른 트랜잭션의 잠금으로 인해 트랜잭션이 지속적으로 연기될 때 발생합니다. 예를 들어, DBMS가 wait-die 알고리즘을 사용하는 경우, 시작 시간이 더 빠른 트랜잭션이 최신 트랜잭션과 충돌하면 롤백되고 다시 시작되지만 매번 동일한 충돌이 발생합니다. 무작위 재시작 지연으로 해결됩니다.

Livelock이 유용한 경우는?

일부 시스템에서는 스레드가 활성 상태를 유지하고 문제를 감지할 수 있으므로 Livelock이 Deadlock보다 선호됩니다. 예를 들어, 낙관적 잠금(optimistic locking) 알고리즘에서는 재시도 제한이 최종 완료를 보장하는 한 livelock 유사 동작이 허용됩니다. 이는 성능과 진행 보장 간의 절충안입니다.

Livelock이 테스트에 어떤 영향을 미치나요?

Livelock은 테스트에서 재현하기 매우 어렵습니다. 스레드의 정확한 타이밍 일치가 필요하기 때문입니다. 단위 테스트는 결정론적으로 실행되며 활성 잠금을 거의 발견하지 못합니다. 부하 상태에서 반복 실행과 프로파일러의 CPU 소비 모니터링을 통한 스트레스 테스트를 사용하는 것이 좋습니다.

Android의 Livelock과 서버의 Livelock은 어떻게 다른가요?

서버에서 Livelock은 성능 저하와 타임아웃을 초래하지만 서버는 수평적으로 확장됩니다. Android에서 Livelock은 배터리를 소모하고 기기를 과열시켜 최악의 사용자 경험을 만듭니다. 또한 모바일 기기는 CPU 코어 수가 제한되어 있으므로 Livelock이 더 빠르게 전체 시스템의 작동 불능 상태로 이어집니다.

요약

  • Livelock은 스레드가 차단되지 않았지만 충돌에 무한히 반응하며 진행하지 못하는 활성 잠금 상태입니다
  • Deadlock과 달리 Livelock에서는 스레드가 CPU를 100% 소비하며 이는 모바일 기기에 치명적입니다
  • 주요 원인은 동일한 충돌 대응 전략과 지연의 무작위성 부족입니다
  • 지터가 있는 Exponential backoff은 동기 주기를 차단하고 활성 잠금을 방지합니다
  • 재시도 제한(3-5회)은 무한 Livelock을 완전히 제거합니다
  • Android Studio의 CPU Profiler와 Battery Historian이 Livelock의 주요 진단 도구입니다
  • 스레드별 비대칭 잠금 획득 로직은 활성 잠금의 가능성 자체를 제거합니다

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기