[Android] 자연의 이치와 함께 이해해보는 안드로이드 생명주기 활용 (2)

Kame·2025년 9월 14일

Android

목록 보기
5/9
post-thumbnail

들어가며

이 시리즈에서는 안드로이드의 Lifecycle Architecture Component의 원리와 활용법을 알아봅니다.

이전 편을 읽고 오시는 것을 권장드립니다.

안드로이드 개발에서 생명주기 관리는 메모리 효율성과 앱 안정성을 위해 매우 중요합니다. 이 글에서는 LifecycleObserver를 통해 컴포넌트 간의 생명주기 연동을 효과적으로 구현하는 방법을 알아보겠습니다.


생명주기에 따른 상호작용

안드로이드 컴포넌트의 생명주기가 바뀔 때 호출되는 콜백 함수를 이용하면, 그 상태에 따라 다른 객체가 동작하도록 제어할 수 있습니다.

생명주기 콜백 함수 활용하기

별도의 수단 없이 한 객체가 안드로이드 컴포넌트의 생명주기에 따라 동작하도록 직접 구현해보겠습니다.
예를 들어, 위치 기반 시스템을 구현한다고 가정해 보겠습니다. 먼저 위치 정보 관련 작업을 수행하는 LocationService 클래스를 살펴보겠습니다. 생성자에서 Context를 받아 내부적으로 보관하고, 위치 업데이트를 위한 locationCallback을 등록합니다. 이후 위치 정보를 받아오고 그 정보를 토대로 필요한 작업을 실행합니다.

class LocationService(private val context: Context) {
    private var locationClient: FusedLocationProviderClient? = null
    private var locationCallback: LocationCallback? = null

    fun start() {
        // 위치 서비스 클라이언트 초기화
        locationClient = LocationServices.getFusedLocationProviderClient(context)
        
        // 위치 콜백 생성
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                locationResult.lastLocation?.let { location ->
                    handleLocationUpdate(location)
                }
            }
        }
        
        // 권한 확인
        if (ActivityCompat.checkSelfPermission(context, 
                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            // 위치 업데이트 시작
            val locationRequest = LocationRequest.create()
            locationClient?.requestLocationUpdates(locationRequest, locationCallback!!, null)
        }
    }
    
    fun stop() {
        locationCallback?.let { callback ->
            locationClient?.removeLocationUpdates(callback)
        }
    }
    
    fun clear() {
        stop()
        locationCallback = null
        locationClient = null
    }
    
    private fun handleLocationUpdate(location: Location) {
        // 위치 업데이트 발생 시 관련 작업 수행
        Log.d("LocationService", "위치: ${location.latitude}, ${location.longitude}")
        // context를 사용한 추가 작업들...
    }
}

생명주기 의존 컴포넌트의 메모리 누수

안드로이드에서 메모리 누수(memory leak)는 대표적으로 안드로이드 컴포넌트의 생명주기가 종료된 이후에도 다른 객체가 여전히 해당 컴포넌트를 참조하는 상황에서 발생합니다.

LocationService 객체에서는 주입받은 Context를 활용해 필요한 작업을 구현하므로, 메모리 누수에 유의해 작업을 수행해야 합니다. 즉 해당 클래스가 의존하고 있는 Context가 더 이상 유효하지 않다면, 그것을 참조하는 모든 객체들을 명시적으로 해제하는 과정이 필요합니다. 여기서는 clear 함수를 두어 내부에서 등록된 콜백과 클라이언트를 null로 할당하여 정리함으로써 LocationService와 Context 간 불필요한 연결고리를 제거하고 있습니다.

이 작업을 해주지 않을 때의 문제는 안드로이드 컴포넌트의 생명주기가 종료될 때 발생합니다. 예를 들어 해당 Context가 액티비티의 Context인 경우, 액티비티의 생명주기가 종료되었음에도 LocationServiceLocationCallback가 계속 등록이 되어있다면 문제가 발생할 수 있습니다. 해당 콜백은 호출되는 과정에서 LocationService와 그 안의 Context(= Activity)를 참조합니다. 이렇게 되면 다음과 같은 참조 체인에 따라 GC는 액티비티 인스턴스를 회수할 수 없게 되고, 결과적으로 액티비티가 메모리에 남아 있게 됩니다.

안드로이드 위치 시스템locationCallback 참조

locationCallbackLocationService 참조 (내부 객체)

LocationService → 액티비티(Context) 참조

class MainActivity : AppCompatActivity() {
    private lateinit var locationService: LocationService
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        locationService = LocationService(context = this)
    }
    
    override fun onStart() {
        super.onStart()
        locationService.start()
    }
    
    override fun onStop() {
        super.onStop()
        locationService.stop()
    }
    
    override fun onDestroy() {
        super.onDestroy()
        locationService.clear() // 참조 해제
    }    
}

문제점

앞서 살펴본 방식으로 안전하게 원하는 기능을 구현할 수 있었습니다. 하지만 개발자가 직접 컴포넌트의 생명주기에 따라 수동적으로 객체의 동작을 정의하는 방식에는 다음과 같은 한계점이 존재합니다.

1. 가독성 저하로 인한 실수 가능성, 메모리 누수 위험

특히 한 컴포넌트에 의존하는 컴포넌트의 수가 많아질수록 문제가 심각해집니다. 액티비티에 수많은 컴포넌트들이 생명주기에 따라 동작해야 한다고 가정해보겠습니다.

 class MainActivity : AppCompatActivity() {
    private lateinit var locationService: LocationService
    private lateinit var networkMonitoring: NetworkMonitoring
    private lateinit var sensorService: SensorService
    // 더 많은 객체들...
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        locationService = LocationService(this)
        networkMonitoring = NetworkMonitoring(this)
        sensorService = SensorService(this)
        // 모든 객체 초기화...
    }
    
    override fun onStart() {
        super.onStart()
        locationService.start()
        networkMonitoring.start()
        sensorService.start()
        // 다른 객체 동작 시작
    }
    
    override fun onResume() {
        super.onResume()
        networkMonitoring.resume()
        sensorService.resume()
        // 다른 객체...
    }
    
    override fun onPause() {
        super.onPause()
        networkMonitoring.pause()
        sensorService.pause()
        // 다른 객체...
    }
    
    override fun onStop() {
        super.onStop()
        locationService.stop()
        networkMonitoring.stop()
        sensorService.stop()
        // 다른 객체...
    }
    
    override fun onDestroy() {
        super.onDestroy()
        locationService.clear()
        networkMonitoring.clear()
        sensorService.clear()
        **// ... 모든 객체의 Context 참조 해제 작업**
    }
}

이처럼 서비스가 많아질수록 각 생명주기 메서드에서 호출해야 할 작업들이 늘어나고, 코드 자체의 복잡성도 높아집니다. 이로 인해 개발자가 실수할 가능성이 커지고, 그 결과 메모리 누수가 발생할 위험도 증가합니다.

2. 강한 결합도

만약 다른 안드로이드 컴포넌트에서 동일한 기능을 사용하려면 모든 생명주기 코드를 다시 작성해야 할 것입니다. 즉 안드로이드 컴포넌트와 비즈니스 로직을 담당하는 객체들이 강하게 결합되어, 재사용성이 떨어지고 유지보수가 어려워집니다.

class MapActivity : AppCompatActivity() {
    private lateinit var locationService: LocationService
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        locationService = LocationService(this) *// 동일 코드*
    }
    
    override fun onStart() {
        super.onStart()
        locationService.start() *// 동일 코드*
    }
    
    override fun onStop() {
        super.onStop()
        locationService.stop() *// 동일 코드*
    }
    
    override fun onDestroy() {
        super.onDestroy()
        locationService.clear() *// 동일 코드*
    }
}

3. 테스트의 어려움

단위 테스트가 어려워집니다. 가짜 혹은 모의 객체를 생성하고, 실제 생명주기를 시뮬레이션해야 하기 때문입니다.

LifecycleObserver

필요성

지난 편에서, 안드로이드 컴포넌트의 Lifecycle 변화는 다른 객체에 의해 관찰이 가능함을 알 수 있었습니다. Lifecycle의 메서드인 addObserver, removeObserver를 활용하여 관찰자를 등록하고 해제할 수 있습니다. 액티비티나 프래그먼트 같은 안드로이드 컴포넌트는 LifecycleOwner를 구현하고 있으므로, 내부의 lifecycle 인스턴스를 통해 이러한 기능을 제공합니다.

@MainThread public abstract fun addObserver(observer: LifecycleObserver)

@MainThread public abstract fun removeObserver(observer: LifecycleObserver)

하지만 LifecycleObserver 인터페이스를 우리가 직접 구현하여 활용하는 것은 권장되지 않습니다. 실제로 인터페이스를 살펴보면 아무런 멤버 함수도 없는 것을 확인할 수 있습니다.

public interface LifecycleObserver

이는 LifecycleObserver마커 인터페이스(Marker Interface) 역할을 하기 때문입니다. 마커 인터페이스는 특정 기능을 구현하기보다는, 해당 클래스가 특정 용도로 사용될 수 있다는 것을 표시하는 역할을 합니다. 따라서 실제 생명주기 이벤트를 처리하기 위해서는 LifecycleObserver를 구현하는 구체적인 인터페이스들을 사용하는 것이 좋으며 개발자 입장에서 더욱 편리합니다.

DefaultLifecycleObserver

LifecycleObserver를 구현하는 또 다른 인터페이스 중 하나로, 생명주기의 각 단계에 대응하는 기본 메서드들을 제공합니다. LifecycleOwner(예: Activity, Fragment)의 생명주기 이벤트에 맞춰 특정 로직을 실행할 수 있습니다.

public interface DefaultLifecycleObserver : LifecycleObserver {

    public fun onCreate(owner: LifecycleOwner) {}

    public fun onStart(owner: LifecycleOwner) {}

    public fun onResume(owner: LifecycleOwner) {}

    public fun onPause(owner: LifecycleOwner) {}

    public fun onStop(owner: LifecycleOwner) {}

    public fun onDestroy(owner: LifecycleOwner) {}
}
  • onCreate
    • ON_CREATE 이벤트가 발생했음을 알리는 콜백
    • LifecycleOwner의 onCreate()반환된 이후 실행
    • 초기화 작업이나 한 번만 수행되는 설정 로직을 실행할 때 사용
  • onStart
    • ON_START 이벤트 발생 시 호출
    • LifecycleOwner의 onStart()반환된 이후 실행
  • onResume
    • ON_RESUME 이벤트 발생 시 호출
    • LifecycleOwner의 onResume()반환된 이후 실행
  • onPause
    • ON_PAUSE 이벤트 발생 시 호출
    • LifecycleOwner의 onPause()호출되기 직전에 실행
  • onStop
    • ON_STOP 이벤트 발생 시 호출
    • LifecycleOwner의 onStop()호출되기 직전에 실행
  • onDestroy
    • ON_DESTROY 이벤트 발생 시 호출
    • LifecycleOwner의 onDestroy()호출되기 직전에 실행
    • 주로 메모리 해제, 콜백 제거 등 최종 정리 작업을 수행할 때 사용

여기서 주목할 만한 점은, 다음과 같이 콜백 별로 호출 시점의 차이를 보인다는 것입니다. 이러한 설계는 안전한 리소스 관리가 가능하도록 도와줍니다.

  • Forward Events (활성화되는 방향)
    • onCreate, onStart, onResume
    • LifecycleOwner의 해당 메서드가 완료된 후 호출 : LifecycleOwner가 완전히 해당 상태에 진입한 후 추가 작업을 수행할 수 있도록 보장
  • Backward Events (소멸되는 방향)
    • onPause, onStop, onDestroy
    • LifecycleOwner의 해당 메서드가 호출되기 직전에 호출 : LifecycleOwner가 상태를 변경하기 전에 필요한 정리 작업을 미리 수행할 수 있도록 함

예시

이제 앞서 작성한 LocationService를 DefaultLifecycleObserver를 사용하여 개선해보겠습니다.

class LocationService(private val context: Context) : DefaultLifecycleObserver {
    private var locationClient: FusedLocationProviderClient? = null
    private var locationCallback: LocationCallback? = null

    override fun onCreate(owner: LifecycleOwner) {
        // 생명주기 소유자가 생성될 때 초기화 작업
        locationClient = LocationServices.getFusedLocationProviderClient(context)
        
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                locationResult.lastLocation?.let { location ->
                    handleLocationUpdate(location)
                }
            }
        }
    }
    
    override fun onStart(owner: LifecycleOwner) {
        // 기존 start 함수
        if (ActivityCompat.checkSelfPermission(context,
                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            val locationRequest = LocationRequest.create()
            locationClient?.requestLocationUpdates(locationRequest, locationCallback!!, null)
        }
    }
    
    override fun onStop(owner: LifecycleOwner) {
        // 기존 stop 함수
        stopLocationUpdates()
    }
    
    override fun onDestroy(owner: LifecycleOwner) {
        // 기존 clear 함수
        stopLocationUpdates()
        locationCallback = null
        locationClient = null
    }
    
    private fun stopLocationUpdates() {
        locationCallback?.let { callback ->
            locationClient?.removeLocationUpdates(callback)
        }
    }
    
    private fun handleLocationUpdate(location: Location) {
        Log.d("LocationService", "위치: ${location.latitude}, ${location.longitude}")
    }
}

이렇게 DefaultLifecycleOwner를 구현한다면 액티비티 측에서 다음과 같이 간결하게 LocationService를 활용할 수 있게 됩니다. LocationService는 액티비티의 생명주기를 자동으로 관찰하고, 적절한 시점에 자신의 메서드들을 호출하게 됩니다. 즉 개발자는 더 이상 각 생명주기 메서드에서 수동으로 LocationService의 메서드를 호출할 필요가 없습니다.

class MainActivity : AppCompatActivity() {
    private lateinit var locationService: LocationService
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        locationService = LocationService(this)
        
        // Activity(LifecycleOwner)가 가지고 있는 lifecycle을 통해 관찰자 등록
        lifecycle.addObserver(locationService)
    }   
}

만약 특정 조건이나 시점에서, onDestroy 호출 이전에 LocationService의 생명주기 관찰을 중단하고 싶다면, 다음과 같이 removeObserver를 호출하여 해제할 수 있습니다.

class MainActivity : AppCompatActivity() {

    // ...
    
		fun stopLocationServiceEarly() {
		    lifecycle.removeObserver(locationService)
		    locationService.clear()  // (필요한 경우) 리소스 해제
		}
}

LifecycleEventObserver

LifecycleEventObserverLifecycleObserver를 구현하는 또 다른 인터페이스로, DefaultLifecycleObserver와 달리 단일 메서드로 모든 생명주기 이벤트를 처리합니다.

public fun interface LifecycleEventObserver : LifecycleObserver {
    public fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event)
}

onStateChanged 메서드 하나만 제공되며, LifecycleOwner와 발생한 Lifecycle.Event를 함께 전달받습니다. 따라서 개발자는 조건문(if, when 문)을 사용하여 필요한 이벤트만 선별적으로 처리할 수 있습니다.

이제 앞서 살펴본 LocationServiceLifecycleEventObserver를 구현하도록 변경해보겠습니다. DefaultLifecycleObserver에서는 생명주기 단계별로 메서드를 각각 오버라이드했지만, 이번에는 단일 메서드 안에서 이벤트를 분기 처리하도록 변경할 수 있습니다.

class LocationService(private val context: Context) : LifecycleEventObserver {
    private var locationClient: FusedLocationProviderClient? = null
    private var locationCallback: LocationCallback? = null
    
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        when (event) {
            Lifecycle.Event.ON_START -> start()
            Lifecycle.Event.ON_STOP -> stop()
            Lifecycle.Event.ON_DESTROY -> clear()
            else -> { /* 필요 없는 이벤트 */ }
        }
    }

    fun start() {
        // 위치 서비스 클라이언트 초기화
        // ...
    }
    
    fun stop() {
        // ...
    }
    
    fun clear() {
        stop()
        locationCallback = null
        locationClient = null
    }
    
    private fun handleLocationUpdate(location: Location) {
        // 위치 업데이트 발생 시 관련 작업 수행
        // ...
    }
}

해당 객체는 앞선 DefaultLifecycleObserver와 동일하게 관찰하고자 하는 Lifecycle에 등록하여 활용할 수 있습니다.

class MainActivity : AppCompatActivity() {
    private lateinit var locationService: LocationService
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        locationService = LocationService(this)
        
        // Activity(LifecycleOwner)가 가지고 있는 lifecycle을 통해 관찰자 등록
        lifecycle.addObserver(locationService)
    }   
}

유의 사항

DefaultLifecycleOwner vs LifecycleEventObserver?

하나의 클래스가 DefaultLifecycleObserverLifecycleEventObserver를 동시에 구현할 경우, DefaultLifecycleObserver 측의 구현이 먼저 호출되며, 이어서 LifecycleEventObserver의 구현이 호출됩니다.

class MixedObserver : DefaultLifecycleObserver, LifecycleEventObserver {
    
    // DefaultLifecycleOwner
    override fun onCreate(owner: LifecycleOwner) {
        Log.d("Mixed", "DefaultLifecycleObserver.onCreate 호출")
    }
    
    // DefaultLifecycleOwner    
    override fun onStart(owner: LifecycleOwner) {
        Log.d("Mixed", "DefaultLifecycleObserver.onStart 호출")
    }
    
    // LifecycleEventObserver    
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("Mixed", "LifecycleEventObserver.onStateChanged: $event")
    }
}

Mixed: DefaultLifecycleObserver.onCreate 호출
Mixed: LifecycleEventObserver.onStateChanged: ON_CREATE

Mixed: DefaultLifecycleObserver.onStart 호출
Mixed: LifecycleEventObserver.onStateChanged: ON_START

이외 두 방식의 차이점은 다음과 같습니다. 이 사항들을 고려하여 상황에 따라 어떤 접근 방식이 좋을지 고려하는 것이 필요할 것입니다.

구분DefaultLifecycleObserverLifecycleEventObserver
콜백 방식생명주기 단계별 메서드 제공(onCreate, onStart, onResume 등)단일 메서드 onStateChanged에서 이벤트 분기 처리
가독성상태별 메서드가 분리 → 가독성 높음모든 이벤트를 한 곳에서 처리 → 가독성 낮음
코드 작성IDE 자동완성 지원 → 실수 방지이벤트 분기(when/switch) 직접 작성 필요
적합한 상황- 상태별 구체적 로직 필요- 리소스 관리, UI 관련 작업- 모든 이벤트 공통 처리- 로깅, 분석, 모니터링
성능내부적으로 Reflection 기반 어노테이션 사용 가능 → 약간의 오버헤드Reflection 없이 직접 이벤트 전달 → 성능 유리
테스트 용이성테스트 코드에서 단계별 콜백 호출 확인 쉬움모든 이벤트를 한 곳에서 확인 가능 → 단위 테스트 단순화

LifecycleEventObserver vs Annotation?

관련 설명은 없었지만, 컴포넌트의 Lifecycle을 감지할 수 있는 다른 방식이 있습니다. 과거에는 LifecycleObserver 인터페이스와 함께 @OnLifecycleEvent 어노테이션을 사용하여 생명주기 이벤트를 처리하는 방식이 사용되었습니다. 앞서 언급하였듯, 현재 LifecycleObserver를 직접 구현한 클래스를 생명주기 감지에 활용하는 방식은 권장되지 않습니다.

class LocationService(private val context: Context) : LifecycleObserver {
    private var locationClient: FusedLocationProviderClient? = null
    private var locationCallback: LocationCallback? = null

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun create() {
        // ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun start() {
        // ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun stop() {
        // ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun clear() {
        // ...
    }
}

만약 동일한 클래스에서 LifecycleEventObserver를 구현하면서 동시에 @OnLifecycleEvent 어노테이션을 사용할 경우, 어노테이션이 붙은 메서드는 무시됩니다.

class LocationService : LifecycleEventObserver {
    
    // 호출 ❌
    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun initializeWithAnnotation() {
        Log.d("Service", "어노테이션 방식 - 호출되지 않음")
    }
    
    // 호출 ❌
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun cleanupWithAnnotation() {
        Log.d("Service", "어노테이션 방식 - 호출되지 않음")
    }
    
    // 실행 ✅ 
    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        when (event) {
            Lifecycle.Event.ON_CREATE -> {
                Log.d("Service", "LifecycleEventObserver 방식 - 정상 호출")
            }
            Lifecycle.Event.ON_DESTROY -> {
                Log.d("Service", "LifecycleEventObserver 방식 - 정상 호출")
            }
        }
    }
}

다음 편에 계속

이번 편에서는 LifecycleObserver의 필요성과 함께, 이를 구현하는 DefaultLifecycleObserver, LifecycleEventObserver를 활용하여 안드로이드 컴포넌트의 생명주기를 안전하고 효율적으로 관찰하는 방법을 알아보았습니다.

다음 편에서는 Custom Lifecycle 구현을 다룰 예정입니다. 기본적으로 ActivityFragmentLifecycleOwner를 내장하고 있지만, 경우에 따라서는 커스텀 뷰, 뷰모델, 혹은 별도의 컴포넌트에서도 직접 생명주기를 관리해야 할 필요가 있습니다. 이때 활용할 수 있는 것이 바로 LifecycleRegistry입니다.

이것을 활용해 안드로이드가 제공하는 컴포넌트 이외의 객체에서 직접 생명주기를 정의하고 제어하는 방법을 살펴보겠습니다.


참고 자료

https://developer.android.com/topic/libraries/architecture/lifecycle?hl=ko

profile
Software Engineer

0개의 댓글