수명 주기 인식 구성요소로 수명 주기 처리   Android Jetpack의 구성요소

수명 주기 인식 구성요소는 활동과 프래그먼트 같은 다른 구성요소의 수명 주기 상태 변경에 따라 작업을 실행합니다. 이러한 구성요소를 사용하면 잘 구성된 경량의 코드를 만들고 더욱 쉽게 유지할 수 있습니다.

일반적인 패턴은 활동과 프래그먼트의 수명 주기 메서드에 종속 구성요소의 작업을 구현합니다. 하지만 이 패턴으로 인해 코드 구성이 나빠지고 오류가 증가하게 됩니다. 수명 주기 인식 구성요소를 사용하면, 수명 주기 메서드에서 구성요소 자체로 종속 구성요소의 코드를 옮길 수 있습니다.

androidx.lifecycle 패키지는 수명 주기 인식 구성요소(활동이나 프래그먼트의 현재 수명 주기 상태를 기반으로 동작을 자동 조정할 수 있는 구성요소)를 빌드할 수 있는 클래스 및 인터페이스를 제공합니다.

Android 프레임워크에 정의된 대부분의 앱 구성요소에는 수명 주기가 연결되어 있습니다. 수명 주기는 운영체제 또는 프로세스에서 실행 중인 프레임워크 코드에서 관리합니다. 또한 Android 작동 방식의 핵심으로, 애플리케이션은 수명 주기를 고려해야 하며, 그렇게 하지 않으면 메모리 누수 또는 애플리케이션 비정상 종료가 발생할 수 있습니다.

화면에 기기 위치를 표시하는 활동이 있다고 가정해 보겠습니다. 일반적인 구현은 다음과 같을 수 있습니다.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Java

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

이 샘플은 괜찮아 보이지만, 실제 앱에서는 수명 주기의 현재 상태에 따라 UI와 다른 구성요소를 관리하는 호출이 너무 많이 발생하게 됩니다. 여러 구성요소를 관리하면 onStart()onStop()과 같은 수명 주기 메서드에 상당한 양의 코드를 배치하게 되어 유지하기 어려워집니다.

게다가 활동이나 프래그먼트가 중지되기 전에 구성요소가 시작된다는 보장도 없습니다. onStart()의 일부 구성 확인과 같은 장기 실행 작업을 진행해야 하는 경우 특히 그렇습니다. 이로 인해 onStop() 메서드가 onStart() 전에 종료되어 구성요소가 필요 이상으로 오래 유지되는 경합 상태가 발생할 수 있습니다.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

androidx.lifecycle 패키지는 이러한 문제를 탄력적이고 단독적인 방법으로 처리하는 데 도움이 되는 클래스와 인터페이스를 제공합니다.

Lifecycle

Lifecycle은 활동이나 프래그먼트와 같은 구성요소의 수명 주기 상태 관련 정보를 포함하며 다른 객체가 이 상태를 관찰할 수 있게 하는 클래스입니다.

Lifecycle은 두 가지 기본 열거를 사용하여 연결된 구성요소의 수명 주기 상태를 추적합니다.

이벤트
프레임워크 및 Lifecycle 클래스에서 전달되는 수명 주기 이벤트입니다. 이러한 이벤트는 활동과 프래그먼트의 콜백 이벤트에 매핑됩니다.
상태
Lifecycle 객체가 추적한 구성요소의 현재 상태입니다.
수명 주기 상태 다이어그램
그림 1. Android 활동 수명 주기를 구성하는 상태 및 이벤트

그래프에서 상태를 노드(node)로, 이벤트를 노드 사이의 간선(edge)으로 생각하세요.

클래스는 DefaultLifecycleObserver를 구현하고 onCreate, onStart 등의 상응하는 메서드를 재정의하여 구성요소의 수명 주기 상태를 모니터링할 수 있습니다. 그러면 다음 예에 나와 있는 것처럼 Lifecycle 클래스의 addObserver() 메서드를 호출하고 관찰자의 인스턴스를 전달하여 관찰자를 추가할 수 있습니다.

Kotlin

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

Java

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

위 예에서 myLifecycleOwner 객체는 LifecycleOwner 인터페이스를 구현합니다. 이 내용은 다음 섹션에서 설명합니다.

LifecycleOwner

LifecycleOwner는 클래스에 Lifecycle이 있음을 나타내는 단일 메서드 인터페이스입니다. 이 인터페이스에는 클래스에서 구현해야 하는 getLifecycle() 메서드가 하나 있습니다. 대신 전체 애플리케이션 프로세스의 수명 주기를 관리하려는 경우 ProcessLifecycleOwner를 참고하세요.

이 인터페이스는 FragmentAppCompatActivity와 같은 개별 클래스에서 Lifecycle의 소유권을 추출하고, 함께 작동하는 구성요소를 작성할 수 있게 합니다. 모든 맞춤 애플리케이션 클래스는 LifecycleOwner 인터페이스를 구현할 수 있습니다.

관찰자가 관찰을 위해 등록할 수 있는 수명 주기를 소유자가 제공할 수 있으므로, DefaultLifecycleObserver를 구현하는 구성요소는 LifecycleOwner를 구현하는 구성요소와 원활하게 작동합니다.

위치 추적 예에서는 MyLocationListener 클래스에서 DefaultLifecycleObserver를 구현하도록 한 후 onCreate() 메서드에서 활동의 Lifecycle로 클래스를 초기화할 수 있습니다. 이렇게 하면 MyLocationListener 클래스가 자립할 수 있습니다. 즉, 수명 주기 상태의 변경에 반응하는 로직이 활동 대신 MyLocationListener에서 선언됩니다. 개별 구성요소가 자체 로직를 저장하도록 설정하면 활동과 프래그먼트 로직을 더 쉽게 관리할 수 있습니다.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

일반적인 사용 사례에서는 Lifecycle이 현재 정상 상태가 아닌 경우 특정 콜백 호출을 피합니다. 예를 들어 활동 상태가 저장된 후 콜백이 프래그먼트 트랜잭션을 실행하면 비정상 종료를 트리거할 수 있으므로 콜백을 호출하지 않는 것이 좋습니다.

이러한 사용 사례를 쉽게 만들 수 있도록 Lifecycle 클래스는 다른 객체가 현재 상태를 쿼리할 수 있도록 합니다.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

Java

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

이 구현으로 LocationListener 클래스는 수명 주기를 완전히 인식합니다. 다른 활동이나 프래그먼트의 LocationListener를 사용해야 한다면 클래스를 초기화하기만 하면 됩니다. 모든 설정과 해제 작업은 클래스 자체에서 관리합니다.

라이브러리에서 Android 수명 주기와 작동하는 데 필요한 클래스를 제공한다면 수명 주기 인식 구성요소를 사용하는 것이 좋습니다. 클라이언트 측에서 직접 수명 주기를 관리하지 않아도 라이브러리 클라이언트는 이러한 구성요소를 쉽게 통합할 수 있습니다.

맞춤 LifecycleOwner 구현

지원 라이브러리 26.1.0 이상의 프래그먼트 및 활동에서는 이미 LifecycleOwner 인터페이스가 구현되어 있습니다.

LifecycleOwner를 만들려는 맞춤 클래스가 있다면 LifecycleRegistry 클래스를 사용할 수 있지만 다음 코드 예와 같이 이 클래스에 이벤트를 전달해야 합니다.

Kotlin

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

수명 주기 인식 구성요소의 권장사항

  • UI 컨트롤러(활동과 프래그먼트)를 가능한 한 가볍게 유지하세요. 이러한 컨트롤러는 자체 데이터를 확보하려고 해서는 안 됩니다. 대신 ViewModel을 사용하여 데이터를 확보하고 LiveData 객체를 관찰하여 변경사항을 다시 뷰에 반영해야 합니다.
  • 데이터 기반 UI를 작성해 보세요. 여기서 데이터 변경에 따라 뷰를 업데이트하거나 사용자 작업을 다시 ViewModel에 알리는 것은 UI 컨트롤러의 책임입니다.
  • ViewModel 클래스에 데이터 로직을 배치하세요. ViewModel은 UI 컨트롤러와 앱 나머지 부분 간의 커넥터 역할을 해야 합니다. 하지만 데이터를 가져오는 것(예: 네트워크에서 데이터 가져오기)은 ViewModel의 책임이 아닙니다. 대신 ViewModel은 적절한 구성요소를 호출하여 데이터를 가져온 후 결과를 다시 UI 컨트롤러에 제공해야 합니다.
  • 데이터 결합을 사용하여 뷰와 UI 컨트롤러 사이의 인터페이스를 깔끔하게 유지하세요. 이렇게 하면 뷰를 더욱 선언적으로 만들고, 활동과 프래그먼트에 작성해야 하는 업데이트 코드를 최소화할 수 있습니다. Java 프로그래밍 언어로 이 작업을 하려면 Butter Knife와 같은 라이브러리를 사용하여 상용구 코드를 피하고 더 나은 추상화를 구현하세요.
  • UI가 복잡하다면 UI 수정을 처리할 수 있는 presenter 클래스를 만드는 것이 좋습니다. 힘든 작업이지만, 이렇게 하면 UI 구성요소를 더 쉽게 테스트할 수 있습니다.
  • ViewModel에서 View 또는 Activity 컨텍스트를 참조하지 마세요. ViewModel이 활동보다 오래 지속되면(구성이 변경되는 경우) 활동이 누출되고 가비지 컬렉터가 활동을 제대로 처리하지 못합니다.
  • Kotlin 코루틴을 사용하여 장기 실행 작업 및 비동기적으로 실행될 수 있는 기타 작업을 관리하세요.

수명 주기 인식 구성요소의 사용 사례

수명 주기 인식 구성요소를 사용하면 다양한 사례의 수명 주기를 훨씬 쉽게 관리할 수 있습니다. 몇 가지 예는 다음과 같습니다.

  • 대략적인 위치와 세분화된 위치 업데이트 간 전환. 수명 주기 인식 구성요소를 사용하여 위치 앱이 공개 상태이면 세분화된 위치 업데이트를 사용하고, 앱이 백그라운드에 있으면 대략적인 위치 업데이트로 전환할 수 있습니다. 수명 주기 인식 구성요소인 LiveData를 사용하면 사용자가 위치를 변경할 때 앱에서 자동으로 UI를 업데이트할 수 있습니다.
  • 동영상 버퍼링 중지와 시작. 수명 주기 인식 구성요소를 사용하면 동영상 버퍼링을 최대한 빨리 시작하지만, 앱이 완전히 시작될 때까지 재생을 연기합니다. 또한 수명 주기 인식 구성요소를 사용하여 앱이 제거될 때 버퍼링을 종료할 수 있습니다.
  • 네트워크 연결 시작과 중지. 수명 주기 인식 구성요소를 사용하면 앱이 포그라운드에 있는 동안 네트워크 데이터를 실시간으로 업데이트(스트리밍)할 수 있으며, 앱이 백그라운드로 이동하면 실시간 업데이트를 자동으로 일시중지할 수도 있습니다.
  • 애니메이션 드로어블 일시중지와 재개. 수명 주기 인식 구성요소를 사용하면 앱이 백그라운드에 있는 경우 애니메이션 드로어블 일시중지를 처리하고, 앱이 포그라운드로 이동한 후 드로어블을 재개할 수 있습니다.

중지 이벤트 처리

LifecycleAppCompatActivity 또는 Fragment에 속하면 Lifecycle의 상태가 CREATED로 변경되고 AppCompatActivity 또는 FragmentonSaveInstanceState()가 호출되면 ON_STOP 이벤트가 전달됩니다.

onSaveInstanceState()를 통해 Fragment 또는 AppCompatActivity의 상태를 저장하면 ON_START가 호출될 때까지 UI는 변경할 수 없는 것으로 간주됩니다. 상태를 저장한 후 UI를 수정하려고 하면 애플리케이션의 탐색 상태에 불일치가 나타날 수 있습니다. 따라서 상태가 저장된 후 앱에서 FragmentTransaction을 실행하면 FragmentManager가 예외를 발생시킵니다. 자세한 내용은 commit()을 참고하세요.

LiveData는 관찰자의 관련 Lifecycle이 적어도 STARTED 상태가 아니라면 관찰자를 호출하지 않게 하여 이러한 에지 케이스(edge case)를 처음부터 방지합니다. 하지만 이면에서는 관찰자 호출을 결정하기 전에 isAtLeast()를 호출합니다.

아쉽게도 AppCompatActivityonStop() 메서드는 onSaveInstanceState() 이후에 호출되어 UI 상태 변경이 허용되지 않지만 Lifecycle이 아직 CREATED 상태로 전환되지 않는다는 차이가 생깁니다.

이 문제를 방지하기 위해 beta2 버전 이하의 Lifecycle 클래스는 이벤트를 전달하지 않아도 상태를 CREATED로 표시하여, 시스템에서 onStop()을 호출할 때까지 이벤트가 전달되지 않았더라도 현재 상태를 확인하는 모든 코드가 실제 값을 받도록 합니다.

하지만 이 솔루션에는 다음 두 가지 중대한 문제가 있습니다.

  • API 수준 23 이하에서 Android 시스템은 다른 활동으로 인해 활동이 일부 가려진 경우에도 활동의 상태를 실제로 저장합니다. 즉, Android 시스템은 onSaveInstanceState()를 호출하지만 반드시 onStop()을 호출하지는 않습니다. 이로 인해 UI 상태를 수정할 수 없는 경우에도 관찰자는 수명 주기가 계속 활성 상태라고 생각하는 긴 주기가 발생할 수 있습니다.
  • LiveData 클래스에 유사한 동작을 노출하려는 모든 클래스는 Lifecycle 버전 beta 2 이하에서 제공되는 해결 방법을 구현해야 합니다.

추가 리소스

수명 주기 인식 구성요소로 수명 주기 처리에 관해 자세히 알아보려면 다음 추가 리소스를 참고하세요.

샘플

  • Sunflower - 아키텍처 구성요소 사용의 권장사항을 보여주는 데모 앱

Codelabs

블로그