통제되지 않은 사이드 이펙트 방지
사이드 이펙트란 호출된 함수의 제어를 벗어나 발생할 수 있는 예상치 못한 모든 동작을 의미한다.
로컬 캐시의 데이터 읽기, 네트워크 요청 작업, 전역 변수 설정과 같은 것들 모두 사이드 이펙트라고 할 수 있다.
즉 함수는 결과를 생성하기 위해 입력값에만 의존하게 되는 것이 아닌, 외부 요인에도 의존하게 된다.
Compose Runtime은 Composable 함수가 예측 가능하도록 기대하므로 사이드 이펙트가 포함된 Composable 함수는 예측이 어려워지고, Compose의 개념에 상반된다.
즉, 사이드 이펙트는 Compose 내에서 아무 통제를 받지 않고 여러 번 실행될 수 있고, 함수가 호출될 때마다 새로운 프로그램 상태를 생성할 수 있으므로 멱등성을 따르지 않게 된다.
만약 Composable 함수 내에서 직접 네트워크 요청을 실행한다 생각해보자.
@Composable
fun getData(networkService: NetworkService) {
val data = networkService.load()
LazyColumn {
items(data) { event ->
Text(text = event.name)
}
}
}
위와 같이 작성하게 되면 매우 좋지 않은 안티 패턴이다.
왜냐하면 Composable 함수는 Compose Runtime에 의해 짧은 시간 내에 여러 번 호출 될 수 있으며, 이로 인해 네트워크 요청이 여러번 수행되어 제어를 벗어나거나 부하를 줄 수 있다.
또한 Compose Runtime은 Composable 함수에 대한 실행 전략을 선택할 권한이 있으므로 하드웨어의 멀티 코어의 장점을 활용하기 위해 recomposition 작업을 다른 스레드로 이전시키거나 우선순위에 따라 임의의 순서(현재 화면에 보이지 않는 Composable은 낮은 우선순위로 작업 진행)로 실행할 수 있다.
이에 따라 Composable 함수 내에서 요청한 네트워크 요청 작업이 아무 조건 없이 다른 스레드에서 실행될 수도 있다는 것이다.
다른 안티 패턴으로는 Composable 함수를 다른 Composable 함수의 결과에 의존하도록 만들고, 관계를 부여하는 것이다.
@Composable
fun MainScreen() {
Header()
ProfileDetail()
DataList()
}
위 코드와 같이 Header, Profile, DataList과 같은 Composable 함수는 Compose Compiler에 의해 어떤 순서로든, 또는 병렬로 실행될 수 있다.
그렇기에 만약 Header에서 외부 변수를 업데이트하고 ProfileDetail에서 그 외부 변수를 참조하거나 하는 특정 실행 순서를 기대하고 작성하면 안 된다.
따라서 사이드 이펙트는 Composable 함수와 어울이지 않으므로, 모든 Composable 함수를 stateless 하게 만드는 게 좋다.
단순히 주어진 입력값으로 결과를 생성하는 형태로 단조로워야 높은 재사용성을 갖게 된다.
만약 Stateful 하게 만들면 사이드 이펙트가 필요하므로 Jetpack Compose는 Composable 함수에서 안전하게 이펙트를 호출할 수 있는 이펙트 핸들러와 같은 메커니즘을 제공한다.
이펙트 핸들러는 Composable의 라이프사이클에 종속되어 있으며 이펙트에 주어진 입력값이 변경되면 재실행시키거나, 동일한 이펙트 recomposition 과정에서 유지시키거나 한 번만 호출되게 할 수 있다.
재시작
Composable 함수는 recomposition 때문에 단 한 번만 호출되는 일반 표준 함수와는 다르다.
표준 함수들의 콜 스택은 한 번씩 호출되며, 하나 또는 여러 개의 다른 함수들을 호출할 수 있다.
반면 Composable 함수는 위에서 말했듯이 recomposition으로 여러 번 다시 시작될 수 있다. 그래서 Compose Runtime은 각 함수들에 대한 참조를 유지하고 있다.
Compose는 메모리에 저장된 데이터를 항상 최신 상태로 유지하기 위해 트리의 어떤 노드를 recomposition 할지 선택적으로 판단한다.
Compose Compiler는 일부 상태를 읽는 모든 Composable 함수를 찾아 Compose Runtime에게 재시작하는 방법을 제공하는 코드를 생성한다.
상태를 읽지 않는 Composable의 경우엔 재시작이 필요 없으므로 코드를 생성하지 않는다.
빠른 실행
Composable 함수는 UI를 그리거나 반환하지 않고 단순히 인메모리 구조를 구축하고 업데이트에 필요한 데이터를 방출한다.
이 메커니즘을 통해 Composable은 더욱 빠르고 런타임 환경에서 해당 함수를 여러 번 실행하더라도 별 문제가 없도록 해준다.
따라서 Composable 코드를 작성할 때는 비용이 큰 계산 작업들은 코루틴으로 처리해야 하며, 해당 작업들은 라이프사이클에 대응할 수 있는 이펙트 핸들러 내부에서 다루어져야 한다.
위치 기억
위치 기억업은 함수 메모이제이션의 한 형태로 메모이제이션이란 함수가 입력값에 기반하여 결과를 캐싱하는 기법을 의미한다.
따라서 동일한 입력값에 한해서는 함수를 다시 호출하여 계산을 할 필요가 없다.
메모이제이션은 함수형 프로그래밍 패러다임에서 널리 알려진 기법으로 순수한 함수에 대해서만 적용 가능한 기법이다.
함수 메모제이션에서 함수 호출은 그 이름, 타입 및 매개변수의 조합으로 식별될 수 있다.
이러한 요소들을 사용하여 고유한 키를 생성하고, 나중에 호출될 때 캐싱된 결과에 접근(읽기, 쓰기 등) 하기 위해 사용된다.
Compose의 경우에는 Composable 함수는 소스 코드 내 호출 위치에 대한 불변의 정보를 가지고 있다.
Compose Runtime은 동일한 함수가 동일한 매개변수 값으로 다른 위치에서 호출될 때, 동일한 Composable 부모 트리 내에서 고유한 다른 ID를 생성한다.
@Composable
fun Composable() {
Text("abc") //id 1
Text("abc") //id 2
Text("abc") //id 3
}
메모리상의 트리에는 해당 함수들을 세 개의 서로 다른 인스턴스로 저장하고, 각각 고유한 정체성을 가지게 된다.
여기서 만들어진 정체성은 recomposition을 거쳐도 동일하게 유지되며 이를 근거로 Compose Runtime이 해당 Composable이 이전에 호출되었는지 여부를 파악하고 상황에 따라 생략될 수 있다.
Compose Runtime 입장에서 예외적으로 Composable 함수에 고유한 정체성을 부여하는 것이 어려운 경우도 있다.
반복문에서 생성된 Composable의 List형태가 그러하다.
@Composable
fun AScreen(datas: List) {
Column(
for (data in datas) {
A(data)
}
)
}
이 경우에 A(data)은 물리적으로 매번 같은 위치에서 호출되고 있지만, 리스트 내에서는 서로 다른 항목으로 구분되고 트리 내에서도 서로 다른 노드로 구성된다.
이와 같은 경우엔 Compose Runtime은 고유한 ID를 생성하고 서로 다른 Composable 함수를 구별할 수 있도록 호출 순서에 의존한다.
하지만, 이런 리스트 상단이나 중간에 요소를 추가하게 되면 Compose Runtime은 요소 삽입이 발생하는 지점 아래의 모든 A Composable 함수에 대해 recomposition을 발생시킨다.
왜냐하면 Composable 함수들의 위치가 변경되었기 때문에 입력값이 그대로여도 무시하고 recomposition 된다.
이러한 현상은 매우 비효율 적이고 리스트가 크면 클수록 부하도 큰데, 이런 문제를 해결하기 위해서는 Composable 함수에게 명시적으로 고유한 key값을 지정해 주면 해결할 수 있다.
@Composable
fun AScreen(datas: List) {
Column(
for (data in datas) {
key(data.id) { //Unique한 Key
A(data)
}
}
)
}
이렇게 Composable을 작성하면 Composable 함수의 위치와 상관없이 리스트에 속한 모든 항목의 정체성을 유지할 수 있다.
위치 기억법과 관련하여 Compose Runtime은 remember 함수를 제공한다.
remember는 트리의 상태를 유지하는 인메모리 구조에서 값을 메모리에 읽고 쓰는 역할을 수행하는 Composable 함수이다.
'Android' 카테고리의 다른 글
[Android] Compose 컴파일러 - (1) (0) | 2024.11.13 |
---|---|
[Android] Compose Composable 함수들 - (3) (0) | 2024.11.12 |
[Android] Compose Composable 함수들 - (1) (0) | 2024.11.11 |
[Android] TelephonyManager를 통해 USIM 데이터 추출하기 (0) | 2024.10.31 |
[Android] API KEY 안전하게 관리하기 (Kotlin DSL) (0) | 2024.09.05 |