Compose Stability Analyzer 0.10.0: Stability Doctor와 Trace-All 모드
Compose Stability Analyzer 0.10.0: Stability Doctor와 Trace-All 모드
Compose Stability Analyzer는 그동안 Jetpack Compose 리컴포지션(Recomposition)을 위한 여러 진단 도구의 모음으로 발전해 왔습니다. 거터 아이콘(gutter icon)과 툴팁(tooltip)은 컴파일러가 무엇을 예측하는지 보여 주고, Recomposition Cascade는 리컴포지션이 어디에 영향을 미칠지 보여 주며, Live Heatmap은 실제 기기에서 무엇이 리컴포지션되는지 보여 줍니다. Reality Check는 각 리컴포지션이 정당했는지 등급을 매기고, Recomposition Blame은 그 리컴포지션을 유발한 값이 어디에서 왔는지 추적합니다. 이 도구들은 저마다 자신만의 질문에 답하지만, 한 가지 질문만큼은 여러분의 마음속에 계속 남아 있습니다. 바로 "이 모든 신호를 눈앞에 두고도, 정작 무엇부터 고쳐야 하고, 그래서 무엇을 얻게 되는가?"라는 질문입니다.
0.10.0 버전은 바로 그 질문에 답합니다. Stability Doctor는 플러그인이 수집하는 모든 신호를 종합하여, 원클릭 수정이 딸린 하나의 우선순위 처방 목록으로 통합합니다. 그리고 새로운 trace-all 모드는 어노테이션을 단 하나도 달지 않고도 모듈 전체의 런타임 데이터를 이 목록에 공급합니다.
이 글에서는 0.10.0 버전에 도입된 두 가지 새로운 기능, Stability Doctor와 trace-all 모드를 살펴봅니다.
Stability Doctor
Doctor는 프로젝트를 스캔한 뒤, 세 가지 입력을 결합하여 모든 컴포저블(composable)에 점수를 매깁니다. 세 가지 입력은 정적 안정성 판정(어떤 매개변수가 왜 불안정한지), 하류(downstream) 연쇄의 영향 범위(blast radius, 리컴포지션이 몇 개의 컴포저블을 함께 끌고 가는지), 그리고 Reality Check가 측정한 런타임 낭비(순전히 인스턴스 동일성 변화 때문에 발생한 리컴포지션이 몇 번이고, 또 얼마나 오래 걸렸는지)입니다. 그 결과로 우선순위가 매겨진 목록이 만들어지며, 맨 위 항목은 측정된 효과가 가장 큰 수정안입니다.

각 처방에는 다음 두 가지 배지(badge) 중 하나와 함께 점수가 표시됩니다.
- ESTIMATED(추정): 정적 분석만으로 계산한 점수입니다. 기기를 연결하지 않아도 동작하므로, 탭을 여는 순간부터 Doctor를 유용하게 활용하실 수 있습니다.
- MEASURED(측정): 실행 중인 앱에서 들어온 실시간 히트맵 데이터로 뒷받침되는 점수입니다. 측정 점수는 추정 점수보다 상한이 높게 설정되어 있어, 낭비가 실제로 관찰되어 확인된 컴포저블은 추측에 불과한 컴포저블보다 항상 더 높은 순위에 놓입니다. 그 반대도 중요합니다. 컴파일러는 불안정하다고 표시했지만 런타임에서는 문제없이 스키핑되는 컴포저블은 목록 아래쪽으로 가라앉는데, 측정 결과가 그 경고를 거짓 경보로 판명했기 때문입니다.
처방을 펼치면 문제가 되는 매개변수들이 나타나며, 각 매개변수에는 정적 분석이 제시한 원인(가령 "변경 가능한(var) 프로퍼티가 2개 있음"), Reality Check 등급, 그리고 각 호출 지점(call site)에서 그 값이 어디에서 비롯되는지가 Blame 분석을 재활용하여 함께 표시됩니다. 각 원인 아래에서 Doctor는 더블 클릭으로 적용할 수 있는 수정안을 제시합니다.
- 매개변수 클래스에서
var를val로 변경: 이 수정은 먼저 프로젝트 전체에서 쓰기 사용처를 검색하고, 할당이 하나라도 존재하면 적용을 거부하므로 컴파일을 절대 깨뜨리지 않습니다. - 클래스에
@Immutable또는@Stable붙이기: 확인 대화 상자에는 이 어노테이션이 검증이 아니라 컴파일러에 대한 약속일 뿐이라는 주의 문구가 함께 표시됩니다. - 안정성 설정 파일에 타입 추가하기: 직접 수정할 수 없는 라이브러리 타입을 위한 방법입니다.
- 호출 지점의 인자를
remember\(keys\) { ... }로 감싸기: Silent waste 매개변수를 위한 방법입니다.equals기준으로는 동일한 값인데도 리컴포지션마다 새로운 인스턴스로 도착하는 값을 바로잡습니다. Doctor는 일련의 안전 규칙이 이 변환이 유효함을 증명할 때만 이 수정을 제시합니다. 즉, 인자가 컴포지션에서 직접 평가되고, 컴포저블 호출을 포함하지 않으며, 모든 입력이 매개변수나 앞서 선언된 지역val로 해석되는 경우입니다. 키는 그 입력들로부터 자동으로 도출되며, 미리보기 대화 상자가 무엇이든 바뀌기 전에 정확한 교체 결과를 보여 줍니다.
이 기능을 실행하시려면, Compose Stability Analyzer 도구 창에서 Doctor 탭을 열고(또는 Code 메뉴에서 Run Stability Doctor를 선택하고) 새로 고침을 누르시면 됩니다. 기기가 없어도 추정 순위가 곧바로 표시됩니다. 리컴포지션 히트맵을 시작하고 앱을 직접 조작하시면, 세션이 진행되는 동안 각 행이 측정 점수로 격상되면서 자동으로 순위가 다시 매겨집니다. 수정을 적용하고 나면 영향을 받은 처방이 다시 분석되므로, 해당 처방이 목록 아래로 내려가거나 사라지는 모습을 확인하실 수 있습니다.
Trace-all 모드
지금까지 런타임 데이터는 @TraceRecomposition을 하나씩 직접 붙인 컴포저블에 대해서만 흘러나왔습니다. 특정 대상을 겨냥한 디버깅에는 이것으로 충분하지만, Doctor는 모듈 전체에 순위를 매기므로, 어노테이션을 붙인 컴포저블 세 개로 만든 순위는 나머지 전부를 놓치게 됩니다. Trace-all 모드는 이 간극을 메웁니다. 즉, 컴파일러 플러그인이 모듈 안의 모든 재시작 가능한 컴포저블(restartable composable)을 마치 저마다 어노테이션을 단 것처럼 계측합니다.
composeStabilityAnalyzer {
traceAll {
enabled.set(true)
threshold.set(2)
variants.set(listOf("debug"))
}
}
이 모드는 옵트인 방식이며 기본적으로 디버그 빌드를 겨냥합니다. 변형 이름(variant name)이 설정된 토큰 중 하나와 일치하는 컴파일만 계측되므로, debug는 debug, stagingDebug, fullDebug를 포함하는 반면 릴리스 빌드는 건드리지 않으며, 테스트 컴파일은 절대 계측되지 않습니다. 자동으로 추적되는 컴포저블은 기본적으로 두 번째 리컴포지션부터 로깅을 시작하는데, 이렇게 하면 화면이 처음 컴포지션될 때 쏟아지는 로그를 잠재울 수 있습니다. 한 번도 리컴포지션되지 않는 컴포저블은 로그를 한 줄도 남기지 않으며, 바로 이런 컴포저블이야말로 Doctor가 순위를 매길 필요가 없는 대상입니다. 명시적으로 붙인 @TraceRecomposition 어노테이션은 자신만의 태그와 임계값을 그대로 유지하고, 프리뷰(preview), 인라인 컴포저블(inline composable), readonly 컴포저블, 프로퍼티 게터(property getter)는 자동으로 제외됩니다.
Trace-all은 런타임 데이터를 코드와 다시 연결하는 방식 또한 더 정교하게 다듬습니다. 이제 로그 헤더에는 컴포저블의 정규화된 이름(fully qualified name)이 담깁니다.
[Recomposition #2] UserProfile (1.20ms) (fq: com.example.profile.UserProfile) (auto)
끝에 붙는 토큰들은 구버전 파서와도 호환되며, IDE는 이 토큰을 사용하여 이벤트를 정확히 귀속시킵니다. 덕분에 패키지가 서로 다르면서 단순 이름이 같은 두 컴포저블이 더 이상 같은 인레이(inlay)를 공유하지 않습니다. ComposeStabilityAnalyzer.setEnabled(false)로 분석기를 비활성화하면, 컴포지션당 계측 비용은 맵 조회 한 번과 곧바로 반환되는 호출 정도에 그치므로, trace-all을 켠 디버그 빌드라도 측정을 하지 않을 때는 무리 없이 사용할 수 있습니다.
한데 모아 사용하기
이번 릴리스의 기능들은 플러그인이 그동안 쌓아 온 고리를 완성합니다. 정적 분석은 무엇이 리컴포지션될 수 있는지 알려 주고, 히트맵은 무엇이 실제로 리컴포지션되는지 알려 주며, Reality Check는 그 리컴포지션이 낭비였는지 아닌지를, Blame은 그 이유를 알려 줍니다. Doctor는 이 모두를 읽어 들여 남은 질문에 답합니다. 즉, 어떤 수정이 시간을 들일 가치가 있는지, 어떤 순서로 해야 하는지, 그리고 그 수정 자체를 더블 클릭 한 번으로 끝낼 수 있는지를 답합니다.
실제 작업 흐름은 다음과 같습니다. 먼저 디버그 빌드에서 trace-all을 활성화하고, 기기 없이 Doctor를 한 번 실행하여 추정 기준선을 얻으세요. 그런 다음 히트맵을 시작하고 앱을 직접 조작하면서, 측정된 낭비가 들어오는 대로 순위가 다시 정렬되는 모습을 살펴보세요. 맨 위부터 차례로 작업하시면 됩니다. 제시된 수정을 적용하거나, 수정에 사람의 판단이 필요한 경우 해당 컴포저블로 이동한 다음, 처방이 사라지는 모습을 확인하세요. 목록이 잠잠해지면, 남아 있는 리컴포지션은 측정 결과가 정당하다고 판정한 것들입니다.
0.10.0 버전은 이 밖에도 대규모 프로젝트에서 히트맵을 시작할 때 Android Studio가 멈추던 문제를 수정하고, Gradle 플러그인이 AGP 9를 사용자(consumer)의 클래스패스(classpath)로 흘려보내던 문제를 막았으며(Valerio Pilo 님께 감사드립니다), nullable 타입을 안정성 설정 파일과 대조합니다(David Kurzica 님께 감사드립니다).
0.10.0 버전은 JetBrains Marketplace에서 설치하실 수 있습니다.
공식 문서를 통해 자세한 내용을 확인해 주세요.

