ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DoveLetter interview question 훑어보기1
    IT/android 2024. 12. 9. 16:00
    SMALL
    What is Dove Letter?
    Dove Letter is a private subscription repository where you can learn, discuss, and share new
    knowledge about Android and Kotlin. You can stay updated with the latest information through
    articles and references, tips with code samples that demonstrate best practices, updates from the
    Android Open Source Project (AOSP), new releases, news about the overall Android/Kotlin
    ecosystem, and fresh job posting information for your career. Unlike a typical news or weekly letter,
    it provides useful tips and information frequently and daily from the repository manager, Jaewoong
    Eum (skydoves). If you want to learn more about the details, check out Learn Kotlin and Android
    With Dove Letter.

     

    목차

    • Android 에서 Intent 란 무엇인가요?
    • Serializable vs Parcelable
    • Parcel and Parcelabale
    • 안드로이드에서 ANR 은 무엇이고 어떻게 막을 수 있나요?
    • 구성 변경을 어떻게 다룰 수 있나요?
    • Jetpack Compose 에서 remember vs rememberSaveable
    • Jetpack Compose 에서 어떻게 리컴포지션을 감소시키면서 최적화를 시키나요?
    • Jetpack Compose 에서 상태 호이스팅이란 무엇인가요?
    • Jetpack Compose 에서 side-effect 란 무엇인가요? 또 어떻게 side-effect api를 사용할 수 있나요?
    • Jetpack Compose 에서 SaveableStateHolder 란 무엇인가요?

     

    1. Android 에서 Intent 란 무엇인가요?

    Android 에서 intent 란 수행되어야 할 동작의 추상적 설명 입니다. 메시징을 하는 객체 정도로 표현되는데 이것은 액티비티, 서비스, 브로드캐스트 리시버들이 서로 커뮤니케이션 할 수 있게 합니다. 일반적으로 액티비티를 실행하거나 브로드캐스트를 보내고, 서비스를 실행하는데 사용됩니다. 또한 컴포넌트 사이에서 데이터를 넘겨줌으로써 안드로이드의 컴포넌트 기반의 아키텍쳐의 근본을 만들게 됩니다.

     

    Intent 는 두가지 타입이 있습니다.

    • 명시적(Explicit) Intent
    • 암시적(Implicit) Intent

     

    1. 명시적 Intent

    명시적 인텐트란 호출해야할 대상 컴포넌트를 명확히 지정하는 Intent 입니다.

    명시적 인텐트는 목표 컴포넌트를 정확히 알고있을 경우에 사용됩니다. 예를들면 특정한 액티비티를 앱 내에서 호출하는 경우가 있습니다.

    val intent = Intent(this, TargetActivity::class.java)
    startActivity(intent)

     

    예를들면 앱 내에서 또다른 액티비티로 이동하고자 하는 경우, 명시적 Intent 를 사용할 수 있습니다.

     

    2. 암시적 Intent

    호출할 컴포넌트를 명확히 특정할 수는 없지만, 어떤 것이 수행되어야할지 보편적인 액션을 설정 가능할때 사용합니다.

    시스템은 어떤 컴포넌트가 해당 intent를 수행할 수 있을지를 action, category, 그리고 data 로서 판단합니다.

     

    암시적 intent 는 다른 앱이나 시스템 컴포넌트가 액션을 수행하길 원할때 유용하게 사용할 수 있습니다.

    예를들면 URL 을 오픈하거나 content 를 공유해야할 때 사용할 수 있습니다.

    val intent = Intent(Intent.ACTION_VIEW)
    intent.data = Uri.parse("<https://www.google.com>")
    startActivity(intent)

     

    만약 웹페이지를 브라우저에서 열고싶거나 컨텐츠를 다른 앱과 공유하고 싶은경우 암시적 intent 를 사용하면 좋습니다. 안드로이드 시스템은 intent 를 처리할 적절한 앱을 결정할것입니다.

     

    결론적으로,

    명시적 intent 는 컴포넌트를 명확히 특정할 수 있을경우, 암시적 인텐트는 컴포넌트를 명확하게 특정할 수는 없지만 시스템이나 다른 앱에서 처리하길 원하는경우, 그리고 다른 앱과 데이터를 공유할 경우 사용할 수 있습니다.

     

    2. Serializable vs Parcelable

    Serializable 과 Parcelable 은 서로 다른 컴포넌트들 사이에서 데이터를 전달할 수 있게 하는 메커니즘 입니다. 하지만 이 둘은 퍼포먼스와 동작방식에서 차이가 있습니다.

     

    • Serializable 

    Serializable 은 Java 표준 인터페이스로서 객체를 byte 스트림으로 변환합니다. 그렇기 때문에 액티비티 사이에서 전달가능하고 또한 disk IO 도 가능합니다.

    Java Reflection 을 통해 동작하는데, 시스템이 런타임에 클래스나 필드, 메서드 정보들을 동적으로 확인하고 객체를 직렬화 시킵니다.

    Reflection 을 사용하기 때문에 Parcelable 에 비하여 퍼포먼스가 떨어지는 편이며, 임시 객체를 많이 생성함으로서 메모리 오버헤드를 증가시킵니다.

    Serializable 은 퍼포먼스가 중요하지 않은경우, 또는 안드로이드 기반이 아닌 코드베이스에서 다루기 좋습니다. 왜냐하면 Parcelable 은 안드로이드에 특화된 직렬화 도구이기 때문에 안드로이드가 아닌곳에서는 동작하지 않습니다.

     

    • Parcelable

    Parcleable 은 안드로이드 특화 인터페이스로서 고성능과 IPC(프로세스간 통신) 을 위하여 만들어졌습니다.

    안드로이드에 최적화 되어있고 reflection 에 의존적이지 않으므로 임시 객체를 많이 생성하지않음으로서 GC를 최소화 시키며 퍼포먼스를 증가시킵니다.

    Parcelable 은 퍼포먼스가 중요할 때 사용되며 특히 IPC 나 액티비티나 서비스 사이에서 데이터를 전달할때 사용됩니다.

     

    3. Parcel and parcelable

    * Marshalling: 마샬링은 데이터를 전송하거나 저장하기 위해 한 형식에서 다른 형식으로 변환하는 가정을 말합니다.
    예를들어 Kotlin/Java 객체 -> JSON, 객체 -> 바이트스트림 같은 과정이 포함됩니다.
    마샬링은 주로 다음과 같은 이유로 사용됩니다.
    API 통신 시 데이터 전송을 위해객체를 파일로 저장해야할 때메모리나 디스크에 데이터를 캐시할때IPC 를 수행할때
    * 그럼 직렬화랑 무슨 차이가 있나요?
    직렬화는 마샬링의 한 형태로 볼 수 있으며 주로 데이터를 byte stream 같은 바이너리 형태로 변환합니다.

     

    Parcel 은 안드로이드에서 Container class 이며 고성능과 IPC 를 가능하게 해줍니다. 주로 마샬링과 언 마샬링을 위해 사용되며 이것이 안드로이드 IPC 를 가능하게 만들어 줍니다.

    Parcel 은 일반적인 용도의 직렬화 도구는 아닙니다. 내부 동작때문에 오래된 데이터를 변화시키거나 읽을 수 없게 만들수도 있기때문에 persistent data storage 에 저정하면 안됩니다.

     

    Parcelable 은 안드로이드 특화 인터페이스이며 객체를 직렬화 시키기때문에 Parcel 컨테이터 클래스를 통해 전달되어질 수 있습니다.

    Parcelable 을 implement 한 객체는 Parcle 로 쓰여지거나 복구될 수 있으며, 안드로이드 컴포넌트 사이에서 복잡한 데이터를 전달하는데 적합하게 만듭니다.

     

    즉 Parcel 은 데이터 컨테이너 클래스, Parcelable 은 객체를 Parcel 에 담아 효율적인 데이터 전송을 가능하게 하는 인터페이스 입니다.

     

    4. 안드로이드에서 ANR 은 무엇이고 또, 어떻게 방지할 수 있나요?

    ANR(Application Not Responding) 은 메인 스레드가 너무 오랫동안 블록킹 될 경우 발생하는 에러입니다.

    보통 5초, 그 이상일때 발생합니다. ANR 이 발생하면 안드로이드 시스템은 유저에게 앱을 종료하거나 기다리게끔 하게됩니다. ANR 은 유저 경험을 떨어뜨리며 다양한 이유로 발생될 수 있습니다.

     

    • 메인스레드에서 5초를 넘기는 무거운 작업을 할 경우
    • 오래 지속되는 네트워크 또는 DB 작업 수행시
    • UI 스레드를 블록킹 하는 경우(예를들면 동기작업을 UI 스레드에서 실행하는경우)

    그러면 ANR 을 어떻게 방지할 수 있을까요?

    무거운 작업이나 시간소모가 오래걸리는 작업들을 메인스레드로부터 분리하여 다른 스레드에서 실행하도록 만드는것이 좋습니다.

    Best Practices:

    • 작업을 다른 스레드를 사용하여 메인 스레드로부터 분리시킵니다.
    • 최신 그리고 안전한 방식으로는 코틀린 코루틴을 사용하여 백그라운드 작업을 효율적으로 처리가 가능합니다. with Dispatchers.IO
    • 지속적인 작업인 경우 WorkManager 을 사용할 수 있습니다.
    • 앱이 백그라운드 상태일 경우에도 동작해야하는 작업이라면 WorkManager 을 사용할 수 있습니다. 이 API 는 작업 스케줄링을 다룰 수 있으며 메인스레드 밖에서 작업을 마치는것을 보장할 수 있습니다.
    • 페이징 기법을 사용합니다. 외부로부터 데이터를 받아오는경우 모든 데이터를 받아오는 것 보다 작은 단위로 나누어 처리하면 퍼포먼스가 향상되므로 ANR 을 방지할 수 있습니다.
    • 구성 변경 시 UI 작업을 최소화 시킵니다.
    • ViewModel 을 사용함으로서 UI 관련 데이터를 유지하고 전체 UI 데이터를 구성변경시에 다시 로딩하는것을 방지하는것이 좋습니다.
    • Android Studio 에 내장된 Profile 기능을 사용하여 퍼포먼스를 디버깅하고 적잘한 대응을 할 수 있습니다.
    • 오래 지속되는 루프, sleep, 동기적 네트워크 작업을 메인스레드에서 하지 않아야 합니다.
    • 만약 UI 스레드를 블록킹하지 않고 짧은 delay 가 필요한 경우가 있다면, Handler.postDealyed() 를 사용합니다. Thread.sleep() 은 메인스레드에서 사용하지 않는것이 좋습니다.

    6. Jetpack Compose 에서 remember vs rememberSaveable

    remember 와 rememberSavable 은 둘다 리컴포지션 시에도 스테이트를 유지할 수 있도록 도와줍니다. 하지만 프로세스가 죽거나 구성변경(예를들면 화면전환) 이 발생했을 경우에 동작이 다릅니다.

     

    • remember

    리컴포지션 시에도 메모리에 상태를 저장하기 위허사 사용됩니다. 하지만 구성변경시에는 데이터를 유지하지 못합니다.

    Composable 의 수명주기동안 데이터를 메모리에 저장시키는데 유용합니다.

    하지만 단순히 리컴포지션에만 유지되고 구성변경시에는 유지하지 못하는 단점이 존재합니다.

     

    • rememberSavable

    remember 와 비슷하게 동작하지만 추가적으로 구성변경시에도 데이터가 유지됩니다. SavableHolder 에 state 를 저장함으로서 이를 유지할 수 있습니다.

    구성변경 시에도 유지되면 좋은 데이터들(예를들면 input 값, selection 값)을 저장하는데 유용합니다.

    안드로이드 Bundle 에서 지원되는 데이터만 저장이 가능하지만 Saver 인터페이스를 커스텀함으로서 다른 타입의 데이터 또한 유지할 수 있습니다.

     

    7. Jetpack Compose 에서 어떻게 리컴포지션을 감소시키면서 최적화를 시키나요?

     

    1. @Stable 와 @Immutable 어노테이션을 이용하여 클래스를 안정화 시킬 수 있습니다.
      • @Immutable 어노테이션은 클래스의 public 프로퍼티들을 immutable 하게 유지한다는것을 보장하며 그것은 컴포즈 컴파일러에게 이 클래스는 절대 변하지 않는다는것을 알립니다. 이렇게 함으로써 불필요한 리컴포지션을 방지할 수 있습니다.
      • 반면에 @Stable 어노테이션은 컴포즈 컴파일러에게 안정하다는것을 알려주지만 약간 차이가 있습니다. 몇몇 프로퍼티나 필드의 경우 변할수는 있지만 리컴포지션을 트리거하지는 않습니다.
    2. Immutable collections 을 사용하세요
      • 컴포저블 함수 내에서 안정성을 유지하기 위해서 immutable collection 을 사용하는것이 좋습니다. 이러한 immutable 한 collections 들은 다른 mutable 한 것들과는 다르게 인스턴스가 생성된 이후 변하지 않고 예측가능한 행동을 하며 이는 리컴포지션을 감소시킬 수 있습니다.
    3. 람다 최적화
      • 컴포즈에서 람다는 람다가 capture 하고 있는 값이 있냐 없냐에 따라 최적화를 할 수 있습니다. 만약 람다가 외부 값을 capture 하지 않는다면, 컴포즈는 그것을 싱글톤으로 다루며, 그것은 불필요한 reallocation 을 피함으로서 퍼포먼스를 향상시킬 수 있습니다.
        예를들면

    modifier.clickable{
    	Log.d("LOG", "No captured values here")
    }

     

    하지만 람다가 값을 capture 하고 있다면, 컴포즈는 그것을 효율적으로 처리하기위해 메모리에 저장합니다.

    var sum = 0
    ints.filter {it > 0}.forEach{
    	sum += it
    }

     

    이러한 경우에는 remember api 를 사용하는 것이 값이 변경되었을경우 람다가 제대로 호출되었다는것을 보증할 수 있습니다.

     

      4. Stability Configuration file 을 사용하세요

    • 컴포즈 컴파일러 1.5.5 버전 이상부터 stability configuration file 을 정의할 수 있습니다. 이것은 특정한 클래스들(서드파티 또는 external 클래스) 들을 stable 하다고 마크할 수 있으며, 심지어 직접 그것들을 컨트롤 하지 않아도 됩니다.

     5. Strong Skipping Mode 를 활성화 하세요

    • 컴포즈 컴파일러 1.5.4 버전 에서 Strong Skipping Mode 가 소개되었습니다. 이것은 컴포저블 함수를 skip 할 수 있게 해주는데 심지어 unstable 한 파라미터를 포함하는 경우에도 리컴포지션을 skip 할 수 있습니다. 이 모드는 또한 unstable 값을 caputre 하고있는 람다 또한 효율적으로 momoization 해주며 이것은 중복되는 리컴포지션을 방지함으로서 퍼포먼스 향상에 도움이 됩니다.

     

    8. Jetpack Compose 에서 상태 호이스팅이란 무엇인가요?

    • 이것은 디자인 패턴으로 상태를 호출자나 부모 컴포저블로 올리는것을 의미합니다. 이것은 부모 컴포저블이 자식의 상태를 관리하는것을 도와주며 이렇게 함으로써 자식 컴포저블은 자신의 UI 상태에 더 집중할 수 있게 도와줍니다. 이것은 관심사 분리 라는 목표를 달성하고 UI 컴포넌트들을 Stateless 상태로 만듦으로서 테스터블하고 재사용이 가능한 상태로 만드는것을 도와줍니다.
    • 상태 호이스팅에서...
    • 상태는 부모 컴포저블에서 관리됩니다.
    • 이벤트나 어떤 트리거 들은 콜백 등을 통하여 부모에게 되돌아가고 이것을 통해 상태가 변경됩니다. 그로인해서 다시 자식 컴포넌트의 UI 를 변경할 수 있습니다.
    @Composable
    fun Parent() {
    	var sliderValue by remember { mutableStateOf(0f) }
    	SliderComponent(
    		value = sliderValue,
    		onValueChange = { sliderValue = it }
    	)
    }
    
    @Composable
    fun SliderComponent(value: Float, onValueChange: (Float) -> Unit) {
    	Slider(value = value, onValueChange = onValueChange)
    }

     

      위 예제에서는 sliderValue 를 부모 컴포저블 쪽에서 다루고 SliderComponent 는 stateless 상태를 유지할 수 있습니다. 

      이러한 접근이 더 좋은 구조와 컴포즈의 유지보수성을 향상시킵니다.

     

    Jetpack Compose 에서 상태 호이스팅은 몇가지 benefit 을 제공합니다 :

    • Single source of truth: 상태 호이스팅은 상태를 단일 작업공간에서 다룰 수 있게 도와주는데 이것이 상태의 충돌을 방지하는 역할을 하고 데이터의 일관성을 유지할 수 있습니다.
    • 재사용성: 자식 컴포저블들은 자신의 상태를 직접 다루지 않기때문에 Stateless 하여 앱의 다른 부분에서도 재사용이 가능하게 됩니다. 또한 자식 컴포저블에 다른형태의 state, callback 등을 넘겨줌으로서 더욱 상황에 맞게 변하고 재사용할 수 있습니다.
    • 관심사 분리: 부모 컴포저블로 상태를 올림으로써 자식 컴포넌트의 관심사를 UI 자체에 더욱 집중할 수 있도록 해줍니다. 이것은 컴포넌트들을 더 심플하고 읽기쉽고 유지보수하기 쉽게 만들어 줍니다.
    • Improved testability: Stateless 한 컴포저블들은 내부에 상태를 가지지않기 때문에 테스트하기 용이해집니다. 테스트할때 다른 상태들과 핸들러를 넘김으로서 다양한 상황을 시뮬레이션 해볼 수 있습니다.
    • Unidirectional Data Flow: 단방향 데이터 흐름을 강제할 수 있습니다. 상태는 부모로부터 전달되고 상태변경은 콜백을통해 부모로 다시 올라가기때문에 데이터의 흐름을 더욱 예측 가능하고 디버깅이 용이하게 만듭니다.
    • Lifecycle 에 더 나은 대응: 부모에서 상태를 관리한다면 lifecycle 에 더 적절히 대응할 수 있습니다. 부모는 언제 어떻게 상태 데이터가 변경되어야 하는지 결정하고 이것은 성능 향상과 메모리 같은 자원을 관리하는데 효율성을 높입니다. 

    9. Jetpack Compose 에서 side-effect 란 무엇인가요? 또 어떻게 side-effect api를 사용할 수 있나요?

    • Jetpack Compose 에서 side-effect 는 컴포저블 바깥 범위에서 상태에 영향을 미치는 동작이나 또는 리컴포지션 이후에도 지속되는 작업을 의미합니다. 컴포저블들은 단순히 현재 상태를 기반으로 UI를 렌더링 하기위해 디자인된 순수한 함수형태이기때문에 side effect 는 컴포저블 함수의 수명주기 바깥에서 어떠한 동작수행이 필요할때 사용됩니다. 예를들면 공유 상태 업데이트, 특정한 event 발생, 또는 외부 리소스와의 상호작용 등이 있습니다.
    • Jetpack Compose 는 몇가지 side-effect api 를 제공하는데요 조금전 시나리오들을 안전하고 예측 가능하게 다루는것을 도와줍니다. 예를들면 LaunchedEffect, SideEffect, DisposableEffect 등이 있습니다.
    1. LaunchedEffect: 컴포저블 내에서 코루틴을 실행하기 위해 사용됩니다.
    • LaunchedEffect 는 특정한 key 상태의 변화에 반응하여 코루틴을 실행시킵니다. Composition 내부에서 실행되며 key 가 변경되면 취소되거나 재시작 될 수 있습니다. 데이터를 가져오거나 애니메이션을 핸들링 하는 등의 한번, 반응형 작업 등에 사용됩니다.
    • Example:
    @Composable
    fun MyScreen(userId: String){
    	LaunchedEffect(userId) {
        	// Runs when userId changes, or when entering the composition
            fetchDataForUser(userId)
        }
    }

     

      2. SideEffect: 재시작 불가능한 side-effect 를 실행하기 위하여 사용됩니다.

    • SideEffect 는 컴포저블이 성공적으로 리컴포지션을 했을때 수행됩니다. 가벼운작업, 공유 오브젝트 업데이트, 로깅 등의 다시 시작할수 없는 작업에 사용됩니다.
    • Example:
    @Composable
    fun MyComposable(screenName: String){
    	SideEffect {
        	// Runs after each recomposition, ideal for analytics or loggin
            logScreenView(screenName)
        }
    }

      

      3. DisposableEffect: Clean up 이 필요한 side-effect 가 필요할때 사용됩니다.

    • DisposableEffect 는 셋업과 클린업이 필요한 경우 사용됩니다. 예를들면 listener을 등록하거나 컴포지션이 screen 을 벗어나거나 리컴포지션 되었을때 반드시 메모리에서 해제되어야 하는 자원들의 경우가 있습니다. 이 API 는 onDispose block 을 정의하도록 하는데요, 이것은 컴포저블 함수의 수명주기가 종료될때 호출됩니다.
    • Example:
    @Composable
    fun MyComposableWithListener(listener: SomeListener){
    	DisposableEffect(listener){
        	listener.register() // called when entering the composition
            
            onDispose {
            	listener.unregister() // called when leaving the composition
            }
        }
    }

     

    10. Jetpack Compose 에서 SaveableStateHolder 란 무엇인가요?

    SaveableStateHolder 는 구성 변경시 UI 상태를 보존하고 복구하는것을 도와주는 인터페이스 입니다. 이것은 rememberSaveable 와 비슷하게 동작하는데요, 하지만 다양한 UI element 를 가지고 각각이 독립적인 수명주기를 가질때 사용되도록 디자인 되었습니다. 예를들면 네비게이션이나, 멀티 스크린 셋업의 경우가 있습니다.

     

    주요 기능은 다음과 같습니다.

    • UI 상태를 유지: 컴포저블들의 상태를 리컴포지션이나 재생성시에도 유지합니다.
    • Unique key 인증: 각각 상태는 unique key 에 저장되며 이 유일키가 상태를 복원하거나 관리하는데 도움을 줍니다.
    • 범위 한정된 상태 관리: 컴포저블 계층구조에서 특정한 지역내에서 데이터를 저장 복구하는 것을 컨트롤할 수 있습니다. 이것은 복잡한 UI 나 멀티 스크린, 다이얼로그 등에 적합합니다.
    • Example: 
    @Composable
    fun MyScreen(saveableStateHolder: SaveableStateHolder) {
    	saveableStateHolder.SaveableStateProvider("unique_key") {
    		// Composable content that should retain its state
    		MyComplexComposable()
    	}
    }

     

    어떻게 동작하나요?

    1. SaveableStateProvider: Unique key 를 가지며 컴포저블을 감쌉니다. 이 wrapper 은 유니크 키에 연관된 Jetpack Compose 에게 메모리에 저장하고, 복구하라고 알려주게 됩니다.
    2. State Restoration: SaveableStateProvider 을 가지는 컴포저블이 UI 에서 제거되고 다시 추가될때, UI 상태는 자동으로 복구됩니다.

    실용적인 유즈케이스들

    • 스크린 사이에서 네비게이팅 할때: 멀티 레이아웃에서 각 스크린의 상태를 메모리에 저장할때 SaveableStateHolder 을 사용하는것은 데이터를 다시 셋팅하지 않도록 도와줍니다.
    • 다이얼로그 또는 모달 관리: 일시적으로 화면에서 제거되더라도 다이얼로그나 모달의 상태를 유지합니다 
    LIST
Designed by Tistory.