
"MainActivity가 2초 걸리는데... OkHttp 때문인가? Glide 때문인가? 어디서 시간을 쓰는 거지?"
화면 로딩이 느릴 때, 무엇 때문에 느린지 정확히 찾아내는 방법을 안내합니다. 라이브러리 내부까지 완전히 추적하여 성능 병목을 한눈에 파악합니다.
앱 성능 리뷰 회의:
팀장: "상품 상세 화면이 2초나 걸린다는 불만이 많아요."
개발자: "네, Profiler로 확인했는데 MainActivity.onCreate()가 1,950ms 걸리네요."
팀장: "그래서 어디가 느린 거죠? OkHttp? Glide? 어디 코드?"
개발자: "그게... 안에서 뭐가 느린지는 모르겠어요.
OkHttpClient.newCall().execute() 호출하는데...
그 안에서 뭐가 느린지는..."
팀장: "로그 찍어서 찾아보면 되지 않나요?"
개발자: "OkHttp 라이브러리 내부 코드까지 로그를 찍을 순 없어서요...
소스코드가 없으니까..."
핵심 문제: 화면이 느린 건 알겠는데, 왜 느린지 원인을 찾을 수 없습니다.
우리는 보통 이렇게 성능 문제를 찾습니다:
class ProductDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val start = System.currentTimeMillis()
super.onCreate(savedInstanceState)
Log.d("Performance", "super.onCreate: ${System.currentTimeMillis() - start}ms")
val layoutStart = System.currentTimeMillis()
setContentView(R.layout.activity_product_detail)
Log.d("Performance", "setContentView: ${System.currentTimeMillis() - layoutStart}ms")
val initStart = System.currentTimeMillis()
initializeViews()
Log.d("Performance", "initializeViews: ${System.currentTimeMillis() - initStart}ms")
val dataStart = System.currentTimeMillis()
loadProductData()
Log.d("Performance", "loadProductData: ${System.currentTimeMillis() - dataStart}ms")
// ... 계속 로그를 추가해야 함 😫
}
private fun initializeViews() {
// 여기도 느린 것 같은데...
// 또 로그를 찍어야 하나? 🤔
}
}
Logcat 결과:
D/Performance: super.onCreate: 45ms
D/Performance: setContentView: 120ms
D/Performance: initializeViews: 2850ms ← 범인 발견!
D/Performance: loadProductData: 180ms
"좋아, initializeViews가 문제구나!"
하지만...
private fun initializeViews() {
setupRecyclerView() // 여기?
loadImages() // 여기?
initializeViewPager() // 여기?
setupListeners() // 여기?
// 이 중 어디가 2850ms를 소비하는 거지? 🤔
// 또 로그를 찍어야 하나...?
}
문제 발견 → 원인 추적 → 다시 로그 추가 → 재실행
↓
또 다른 메서드가 문제 → 또 로그 추가 → 재실행
↓
그 안에서 또... → 또또 로그 추가 → 재실행
↓
😫😫😫 끝이 없다!
위 상황도 힘들지만, 진짜 문제는 따로 있습니다.
화요일 오전, 또 다른 긴급 회의:
팀장: "어제 OkHttp로 바꿨는데 왜 네트워크 요청이 더 느려졌죠?"
개발자: "음... 확인해보니 OkHttp 내부에서 2초가 걸리네요."
팀장: "어디서요? Interceptor? Connection Pool? DNS 조회?"
개발자: "그게... OkHttp는 외부 라이브러리라서...
내부 코드에 로그를 추가할 수가 없어요... 😰"
팀장: "그럼 어떻게 찾죠?"
개발자: "소스코드를 직접 받아서... fork해서... 로그 추가하고...
로컬 빌드해서... 교체하고... 테스트하고... 🤯"
이게 현실입니다:
// 내 코드 - 로그 추가 가능 ✅
fun loadUserData() {
val start = System.currentTimeMillis()
val response = apiService.getUser() // ← 여기서 2초 걸림!
Log.d("Perf", "API call: ${System.currentTimeMillis() - start}ms")
}
// OkHttp 내부 (외부 라이브러리) - 로그 추가 불가능 ❌
class RealCall : Call {
override fun execute(): Response {
// Connection Pool 체크?
// DNS 조회?
// SSL Handshake?
// 여기 어디가 느린 건지 알 수가 없다!
return getResponseWithInterceptorChain()
}
}
오픈소스 라이브러리 성능 디버깅의 현실:

더 큰 문제들:
❌ Retrofit 내부에서 JSON 파싱이 느린가?
❌ Glide 이미지 로딩의 어느 단계가 병목인가?
❌ Room Database의 어떤 쿼리가 문제인가?
❌ Gson vs Jackson vs Moshi - 정확히 어디서 차이가 나는가?
→ 전부 외부 라이브러리라 코드 수정 불가!
→ 로그 찍으려면 fork 후 수정 필요!
→ 수정하면 버전 관리는 어떻게?
→ 라이브러리 업데이트는 어떻게 추적?
"내 코드든 라이브러리 코드든, 코드 수정 없이한 번 실행하면 모든 메서드의 시간이 한눈에 보였으면 좋겠어요!"
특히:
이게 가능할까요? 가능합니다. 바로 성능 차트로.
"System.currentTimeMillis()는 시작일 뿐입니다"
// 1차 시도: onCreate 레벨 측정
override fun onCreate(savedInstanceState: Bundle?) {
val start = System.currentTimeMillis()
super.onCreate(savedInstanceState)
Log.d("Perf", "onCreate: ${System.currentTimeMillis() - start}ms")
// 결과: 3200ms
}
// 2차 시도: initializeViews 안 측정
private fun initializeViews() {
val start = System.currentTimeMillis()
setupRecyclerView()
Log.d("Perf", "setupRecyclerView: ${System.currentTimeMillis() - start}ms")
// 결과: 2500ms - 범인 발견!
}
// 3차 시도: setupRecyclerView 안 측정
private fun setupRecyclerView() {
val start1 = System.currentTimeMillis()
createAdapter()
Log.d("Perf", "createAdapter: ${System.currentTimeMillis() - start1}ms")
val start2 = System.currentTimeMillis()
setLayoutManager()
Log.d("Perf", "setLayoutManager: ${System.currentTimeMillis() - start2}ms")
// ... 계속 추가 😫
}
문제:
onCreate()
├─ initializeViews()
│ ├─ setupRecyclerView() ← 이게 느림
│ │ ├─ createAdapter()
│ │ │ └─ loadItemData() ← 실제 병목은 여기!
│ │ └─ setLayoutManager()
│ └─ loadImages()
└─ loadProductData()
→ 로그만으로는 이 구조를 한눈에 볼 수 없음!
Logcat 출력:
setupRecyclerView: 2500ms
createAdapter: 2300ms
loadItemData: 2100ms ← 진짜 범인
→ 어떤 메서드가 어떤 메서드를 호출했는지?
→ 전체 호출 흐름은?
→ 알 수 없음!
class ProductDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Main Thread
super.onCreate(savedInstanceState)
// Background Thread 1
lifecycleScope.launch(Dispatchers.IO) {
loadProductData() // 1.5초
}
// Background Thread 2
lifecycleScope.launch(Dispatchers.Default) {
processImages() // 2.0초
}
// 어떤 스레드가 병목인지?
// 동시에 실행되는 건지?
// 순차적인지?
// 로그만으로는 알 수 없음!
}
}
A 메서드: 1000ms
├─ B 메서드: 700ms
│ └─ C 메서드: 600ms
└─ D 메서드: 200ms
실제 A 메서드만의 시간: 1000 - 700 - 200 = 100ms
→ 로그만으로는 이 계산이 복잡함!
→ "누적 시간 vs 실제 시간" 구분 불가
✅ 자동으로 모든 메서드 추적
✅ 호출 트리를 시각적으로 표시
✅ 시간을 직관적으로 비교
✅ 병목 지점을 즉시 파악
✅ 스레드별 실행 흐름 분석
그것이 바로 성능 차트입니다.
WhatapAndroidPlugin을 사용할 때 반드시 알아야 할 제약사항이 있습니다:
❌ Plugin 적용이 불가능한 경우:
1. 이미 컴파일된 JAR/AAR 파일
// ❌ 이렇게 JAR/AAR 파일로 추가하면 내부 추적 불가
dependencies {
implementation(files("libs/some-library.jar"))
implementation(files("libs/some-library.aar"))
}
2. Maven/Gradle dependency로 추가한 라이브러리
// ❌ 이렇게 추가하면 라이브러리 내부 메서드는 추적 안 됨
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.github.bumptech.glide:glide:4.16.0")
}
InstrumentationScope.PROJECT 제한으로 현재 모듈만 instrumentation3. 소스코드가 공개되지 않은 라이브러리
[Maven dependency 방식]
Maven Repository → JAR 다운로드 → 앱에 포함
↑
이미 컴파일 완료 (수정 불가)
[직접 빌드 방식]
GitHub 소스 클론 → WhatapAndroidPlugin 적용 → 컴파일 → Instrumented JAR 생성
↑
컴파일 시점에 instrumentation 적용!
핵심: 완벽한 성능 추적을 위해서는 소스코드가 필수입니다.

⚠️ 중요: 소스코드 없이는 라이브러리 내부 추적이 불가능합니다.
라이브러리도 소스에서 직접 빌드하면서 WhatapAndroidPlugin을 적용하면 된다!
대부분의 오픈소스 라이브러리는 GitHub에 소스가 공개되어 있습니다:
이 소스를 클론해서 WhatapAndroidPlugin을 적용한 뒤 다시 빌드하면, 라이브러리 내부 메서드에도 instrumentation이 적용됩니다!
⚠️ 중요: 이 방법은 소스코드가 있는 오픈소스 라이브러리에만 적용 가능합니다.
실제로 OkHttp 라이브러리를 instrumentation해봅시다.
cd ~/your-workspace
git clone <https://github.com/square/okhttp.git>
cd okhttp
okhttp/okhttp/build.gradle 파일을 열어서 Plugin을 추가합니다:
// okhttp/okhttp/build.gradle
buildscript {
dependencies {
// 👉 Whatap Plugin 추가
classpath(files("/path/to/WhatapAndroidPlugin-2.1.0.jar"))
}
}
apply plugin: 'com.android.library'
apply plugin: 'io.whatap.android' // 👉 Plugin 적용
cd okhttp/okhttp
./gradlew clean assembleDebug
빌드 로그에서 instrumentation이 잘 적용되었는지 확인:
[Whatap][Instrumentation] okhttp3/OkHttpClient$Builder.build - CallStackTrace applied
[Whatap][Instrumentation] okhttp3/internal/http2/Http2Reader.nextFrame - CallStackTrace applied
[Whatap][Instrumentation] okhttp3/internal/connection/RealConnection.connect - CallStackTrace applied
✅ 120개 클래스 instrumentation 완료!
# 빌드된 JAR을 앱 프로젝트로 복사
cp build/libs/okhttp-5.0.0-alpha.jar \\
/path/to/your-app/app/libs/okhttp-instrumented.jar
// app/build.gradle.kts
dependencies {
// ❌ 기존 Maven 라이브러리 대신
// implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha")
// ✅ Instrumented JAR 사용
implementation(files("libs/okhttp-instrumented.jar"))
implementation("com.squareup.okio:okio:3.6.0") // 의존성은 별도 추가
}
이제 앱을 실행하고 Logcat을 확인하면:
[CallStackTracer] okhttp3/OkHttpClient$Builder.build (3ms)
[CallStackTracer] okhttp3/internal/platform/Platform.connectSocket (313ms)
[CallStackTracer] okhttp3/internal/http2/Http2Reader.nextFrame (384ms)
[CallStackTracer] okhttp3/internal/connection/Exchange.readResponseHeaders (353ms)
드디어 OkHttp 내부가 보입니다! 🎉
Glide도 동일한 방식으로 적용할 수 있습니다.
git clone <https://github.com/bumptech/glide.git>
cd glide
// library/build.gradle
buildscript {
dependencies {
classpath(files("/path/to/WhatapAndroidPlugin-2.1.0.jar"))
}
}
apply plugin: 'com.android.library'
apply plugin: 'io.whatap.android'
./gradlew clean :library:assembleDebug
cp library/build/outputs/aar/library-debug.aar \\
/path/to/your-app/app/libs/glide-instrumented.aar
// app/build.gradle.kts
dependencies {
implementation(files("libs/glide-instrumented.aar"))
// Glide 의존성들
implementation("androidx.fragment:fragment:1.6.1")
implementation("androidx.exifinterface:exifinterface:1.3.6")
}
[CallStackTracer] com/bumptech/glide/Glide.with (7ms)
[CallStackTracer] com/bumptech/glide/GlideBuilder.build (4ms)
[CallStackTracer] com/bumptech/glide/load/engine/executor/GlideExecutor.newSourceExecutor (2ms)
Retrofit도 OkHttp, Glide처럼 Plugin을 적용해서 직접 빌드할 수 있습니다!
git clone <https://github.com/square/retrofit.git>
cd retrofit
Root build.gradle에 Plugin 추가:
// retrofit/build.gradle
buildscript {
dependencies {
classpath libs.androidPlugin
classpath libs.kotlin.gradlePlugin
// WhatapAndroidPlugin for CallStackTrace instrumentation
classpath files('WhatapAndroidPlugin-2.1.0.jar') // ← 추가
}
}
Retrofit 모듈에 Plugin 적용:
// retrofit/retrofit/build.gradle
apply plugin: 'java-library'
apply plugin: 'org.jetbrains.kotlin.jvm'
apply plugin: 'com.vanniktech.maven.publish'
apply plugin: 'io.whatap.android' // ← 추가
# Retrofit은 multi-release JAR을 사용하므로 특정 Java 버전 컴파일 스킵
./gradlew :retrofit:jar -x compileJava14Java -x compileJava16Java
cp retrofit/build/libs/retrofit.jar \\
/path/to/your-app/app/libs/retrofit-instrumented.jar
// app/build.gradle.kts
dependencies {
// ✅ Retrofit - Instrumented JAR (내부 메서드까지 완전 추적!)
implementation(files("libs/retrofit-instrumented.jar"))
// Retrofit Gson converter (Maven)
implementation("com.squareup.retrofit2:converter-gson:2.9.0") {
exclude(group = "com.squareup.okhttp3", module = "okhttp") // 중복 방지
exclude(group = "com.squareup.retrofit2", module = "retrofit") // instrumented JAR 사용
}
}
[CallStackTracer] retrofit2/Retrofit.create (12ms)
[CallStackTracer] retrofit2/ServiceMethod.invoke (8ms)
[CallStackTracer] retrofit2/RequestFactory.create (5ms)
Gson은 Gradle이 아닌 Maven (pom.xml) 빌드 시스템을 사용하기 때문에, WhatapAndroidPlugin을 직접 적용할 수 없습니다.
하지만 걱정하지 마세요! Claude Code나 CLI 도구를 활용해서 수동으로 trace 코드를 삽입할 수 있습니다.
WhatapAndroidPlugin의 제약:
variant.instrumentation.transformClassesWith(
SimpleCallStackTraceVisitorFactory::class.java,
InstrumentationScope.PROJECT // ← 현재 모듈의 소스코드만!
) {}
의미:
예시:
// ✅ 이 호출은 추적됨 (앱 코드)
val todo = gson.fromJson(jsonString, TodoResponse::class.java)
// ❌ gson 내부 메서드는 추적 안 됨
// - JsonReader.beginObject()
// - TypeAdapter.read()
// - 등등...
git clone <https://github.com/google/gson.git>
cd gson/gson/src/main/java/com/google/gson
Gson의 성능 병목이 발생하는 핵심 메서드들:
Gson.fromJson() - JSON 파싱 진입점Gson.toJson() - JSON 직렬화 진입점TypeAdapter.read() - 실제 파싱 로직TypeAdapter.write() - 실제 직렬화 로직JsonReader.beginObject(), JsonReader.nextName() - JSON 읽기Claude Code Prompt 예시:
Gson 라이브러리 성능 모니터링을 위해 instrumentation이 필요합니다.
다음 메서드들에 CallStackTracer 코드를 추가해주세요:
1. 파일: com/google/gson/Gson.java
메서드: public <T> T fromJson(String json, Class<T> classOfT)
메서드 시작 부분에 추가:
io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer.onEnter("com/google/gson/Gson", "fromJson");
return 전에 try-finally로 추가:
try {
// 기존 코드
} finally {
io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer.onExit("com/google/gson/Gson", "fromJson");
}
2. 파일: com/google/gson/Gson.java
메서드: public String toJson(Object src)
동일한 패턴으로 적용
3. 파일: com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
메서드: public T read(JsonReader in)
동일한 패턴으로 적용
위 패턴을 모든 메서드에 적용해주세요.
또는 Cursor CLI를 사용한 빠른 편집:
# Cursor CLI로 파일 열기
cursor /path/to/gson/Gson.java
# 에디터에서 Ctrl+K (AI command palette):
"fromJson 메서드 시작 부분에 CallStackTracer.onEnter() 추가하고, return 전에 CallStackTracer.onExit() 추가해줘"
Before (원본 Gson.java):
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
Object object = fromJson(json, (Type) classOfT);
return Primitives.wrap(classOfT).cast(object);
}
After (Instrumented):
public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer.onEnter(
"com/google/gson/Gson", "fromJson"
);
try {
Object object = fromJson(json, (Type) classOfT);
return Primitives.wrap(classOfT).cast(object);
} finally {
io.whatap.android.agent.instrumentation.stacktrace.CallStackTracer.onExit(
"com/google/gson/Gson", "fromJson"
);
}
}
Gson은 Maven을 사용하므로:
cd gson
mvn clean package -DskipTests
# 생성된 JAR 복사
cp gson/target/gson-2.10.1.jar \\
/path/to/your-app/app/libs/gson-instrumented.jar
// app/build.gradle.kts
dependencies {
// ✅ Gson - Manually Instrumented JAR
implementation(files("libs/gson-instrumented.jar"))
}
[CallStackTracer] com/google/gson/Gson.fromJson (25ms)
├─ [CallStackTracer] com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.read (18ms)
│ ├─ [CallStackTracer] com/google/gson/stream/JsonReader.beginObject (3ms)
│ └─ [CallStackTracer] com/google/gson/stream/JsonReader.nextName (2ms)
└─ ... (완전한 호출 스택)
4개 라이브러리를 모두 적용한 실제 샘플 앱을 만들었습니다!
적용 방식 요약:
프로젝트 구조:
stacktrace-library-sample/
├── app/
│ ├── libs/
│ │ ├── okhttp-instrumented.jar ← Plugin 직접 빌드
│ │ ├── glide-instrumented.aar ← Plugin 직접 빌드
│ │ ├── retrofit-instrumented.jar ← Plugin 직접 빌드
│ │ └── whatap-agent-bom.aar ← Whatap Agent
│ └── src/main/java/.../MainActivity.kt
└── build.gradle.kts
MainActivity.kt 핵심 코드:
class MainActivity : AppCompatActivity() {
private lateinit var okHttpClient: OkHttpClient
private lateinit var glide: RequestManager
private lateinit var gson: Gson
private lateinit var retrofit: Retrofit
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 라이브러리 초기화
okHttpClient = OkHttpClient.Builder().build()
glide = Glide.with(this)
gson = Gson()
retrofit = Retrofit.Builder()
.baseUrl("<https://jsonplaceholder.typicode.com/>")
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
// ✅ onCreate에서 자동으로 테스트 실행 (onResume 전)
testNetworkLibrary() // OkHttp
testImageLibrary() // Glide
testGsonLibrary() // Gson
testRetrofitLibrary() // Retrofit
}
override fun onResume() {
super.onResume()
// onResume에 3초 대기로 확실히 수집
log("⏸️ onResume - 3초 대기 중...")
Thread.sleep(3000)
log("✅ onResume - 대기 완료")
}
private fun testNetworkLibrary() {
log("\\n━━━━ OkHttp 네트워크 테스트 ━━━━")
log("🌐 OkHttp 라이브러리 내부 추적 중...")
CoroutineScope(Dispatchers.IO).launch {
val request = Request.Builder()
.url("<https://httpbin.org/get>")
.build()
val response = okHttpClient.newCall(request).execute()
withContext(Dispatchers.Main) {
log("✅ HTTP 요청 완료: ${response.code}")
log("📊 성능 차트에서 다음 OkHttp 내부 메서드 확인 가능:")
log(" - OkHttpClient.newCall()")
log(" - RealCall.execute()")
log(" - Interceptor.intercept()")
}
}
}
private fun testImageLibrary() {
log("\\n━━━━ Glide 이미지 로딩 테스트 ━━━━")
log("🖼️ Glide 라이브러리 내부 추적 중...")
val imageView = findViewById<ImageView>(R.id.testImageView)
glide.load("<https://via.placeholder.com/300x300.png>")
.into(imageView)
log("📊 성능 차트에서 다음 Glide 내부 메서드 확인 가능:")
log(" - Glide.with() - RequestManager 생성")
log(" - RequestManager.load() - 요청 시작")
}
private fun testGsonLibrary() {
log("\\n━━━━ Gson JSON 파싱 테스트 ━━━━")
val jsonString = """{"userId": 1, "id": 1, "title": "Test"}"""
val todo = gson.fromJson(jsonString, TodoResponse::class.java)
val serialized = gson.toJson(todo)
log("✅ JSON 파싱 완료: ${todo.title}")
log("📊 성능 차트에서 다음 Gson 내부 메서드 확인 가능:")
log(" - Gson.fromJson() - JSON 파싱")
log(" - TypeAdapter.read()")
}
private fun testRetrofitLibrary() {
log("\\n━━━━ Retrofit HTTP 클라이언트 테스트 ━━━━")
CoroutineScope(Dispatchers.IO).launch {
val api = retrofit.create(JsonPlaceholderApi::class.java)
val todo = api.getTodo()
withContext(Dispatchers.Main) {
log("✅ Retrofit 요청 완료: ${todo.title}")
log("📊 성능 차트에서 다음 Retrofit 내부 메서드 확인 가능:")
log(" - Retrofit.create() - 프록시 생성")
log(" - ServiceMethod.invoke()")
}
}
}
}
빌드 & 실행:
cd /Users/devload/whatap/android_workspace/stacktrace-library-sample
./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk
# 앱 실행하면 자동으로 4개 라이브러리 테스트!
앱을 실행하면 다음과 같은 완전한 호출 스택이 성능 차트에 표시됩니다:
MainActivity.onCreate (171ms)
│
├─ [1] OkHttp 네트워크 테스트 (Plugin 직접 빌드 ✅)
│ OkHttpClient.newCall().execute() (1,150ms)
│ ├─ okhttp3/internal/platform/Platform.connectSocket (313ms) ← 내부까지 보입니다!
│ │ └─ TLS Handshake...
│ ├─ okhttp3/internal/http2/Http2Reader.nextFrame (384ms) ← 이것도!
│ └─ okhttp3/internal/connection/Exchange.readResponseHeaders (353ms) ← 완벽!
│
├─ [2] Glide 이미지 로딩 테스트 (Plugin 직접 빌드 ✅)
│ Glide.with() (7ms)
│ ├─ Glide.get() (6ms)
│ │ └─ Glide.checkAndInitializeGlide (6ms)
│ │ └─ GlideBuilder.build (4ms) ← 내부까지!
│ │ ├─ GlideExecutor.newSourceExecutor (2ms)
│ │ └─ MemorySizeCalculator.Builder.build (1ms)
│ └─ Glide.getRetriever (1ms)
│
├─ [3] Gson JSON 파싱 테스트 (수동 instrumentation ✅)
│ Gson.fromJson() (25ms)
│ │ └─ (수동으로 CallStackTracer 코드 추가하여 내부까지 추적)
│
└─ [4] Retrofit HTTP 클라이언트 테스트 (Plugin 직접 빌드 ✅)
Retrofit.create() (15ms)
├─ retrofit2/Retrofit$Builder.build (8ms) ← 내부까지!
│ └─ retrofit2/Platform.findPlatform (2ms)
├─ retrofit2/ServiceMethod.parseAnnotations (5ms) ← 보입니다!
└─ retrofit2/RequestFactory.parseAnnotations (2ms) ← 완벽!
(실제 HTTP 요청은 OkHttp instrumented JAR 사용으로 내부까지 추적됨)
Before:
OkHttpClient.newCall().execute() - 1,150ms
??? - 블랙박스
After:
OkHttpClient.newCall().execute() - 1,150ms
├─ Platform.connectSocket (313ms) ← DNS + TCP 연결
├─ Http2Reader.nextFrame (384ms) ← HTTP/2 프레임 읽기
└─ Exchange.readResponseHeaders (353ms) ← 헤더 파싱
발견된 병목: TCP 연결에 313ms 소비 → Connection Pool 설정 개선 검토
Before:
Glide.load() - ???ms
??? - 블랙박스
After:
Glide.with() - 7ms
├─ GlideBuilder.build (4ms)
│ ├─ GlideExecutor.newSourceExecutor (2ms) ← 스레드 풀 생성
│ └─ MemorySizeCalculator.build (1ms) ← 메모리 계산
└─ Glide.getRetriever (1ms)
발견: 초기화가 매우 빠름 (7ms) → 문제 없음
Before:
Gson.fromJson() - ???ms
??? - 블랙박스
After (수동 instrumentation):
Gson.fromJson() - 25ms
├─ (내부 메서드 추적)
└─ CallStackTracer 코드를 수동으로 추가하여 내부까지 완전 추적
특징:
Before:
Retrofit.create() - ???ms
??? - 블랙박스
After (Plugin으로 직접 빌드):
Retrofit.create() - 15ms
├─ Retrofit$Builder.build (8ms) ← 내부까지 보입니다!
│ └─ Platform.findPlatform (2ms)
├─ ServiceMethod.parseAnnotations (5ms)
└─ RequestFactory.parseAnnotations (2ms)
발견:
"1000개의 로그보다 하나의 차트가 더 명확합니다"
실제로 수집된 화면 로딩 데이터를 확인해보세요!
이 샘플 앱을 실행한 결과, MainActivity가 로딩되는데 7.597초가 걸렸습니다. Whatap 대시보드에서 정확한 시간과 함께 모든 리소스 목록을 한눈에 볼 수 있습니다.
.webp)
화면에서 확인 가능한 정보:
이제 "성능" 탭을 클릭하면 더 상세한 FLAME 차트를 볼 수 있습니다.
성능 분석 탭에서 확인할 수 있는 FLAME 차트입니다. FLAME 차트를 통해 어떤 메서드가 어느 스레드에서 얼마나 오래 실행되었는지 한눈에 파악할 수 있습니다.
.webp)
FLAME 차트에서 확인 가능한 정보:
✅ main 스레드:
io.whatap.stacktrace.MainActivity.onCreate - 전체 화면 초기화✅ DefaultDispatcher-worker 스레드들:
✅ OkHttp Dispatcher & TaskRunner:
okhttp3/internal/connection - 실제 네트워크 연결 처리okhttp3/internal/http2 - HTTP/2 프로토콜 통신시간 축(X축): 0ms ~ 2491ms까지의 타임라인으로 각 메서드가 언제 실행되었는지 확인
호출 깊이(Y축): 메서드 호출 스택을 계층적으로 표시 - 아래로 갈수록 더 깊게 호출된 메서드
색상 구분: 각 스레드와 모듈별로 다른 색상으로 구분되어 시각적으로 명확함
FLAME 차트를 보면 다양한 패턴을 통해 성능 병목 지점을 즉시 발견할 수 있습니다.
████████████████ (넓은 막대)
→ 병목! 최적화 필요
의미: 해당 메서드가 실행 시간의 대부분을 차지함
조치: 우선적으로 최적화 대상
█ █ █ █ █ (여러 개 작은 막대)
→ 반복 호출! 캐싱 검토
의미: 같은 메서드가 여러 번 호출됨
조치: 캐싱이나 배치 처리 검토
깊이 5 이상
→ 과도한 중첩! 리팩토링 검토
의미: 메서드 호출이 너무 깊게 중첩됨
조치: 코드 구조 개선 또는 리팩토링 필요
Main Thread 블록 크기 > 16ms
→ UI 버벅임 가능성!
의미: 메인 스레드가 16ms(60fps 기준) 이상 블로킹됨
조치: Background 스레드로 작업 이동
FLAME 차트를 보면 왜 CPU가 높은지, 왜 MEMORY가 높은지 정확히 알 수 있습니다.
FLAME 차트에서 보이는 것:
0ms 500ms 1000ms 1500ms 2000ms
|━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|
[Main Thread] ████████████████████████
[Worker-1] ████████████████████
[Worker-2] ████████████████
[Worker-3] █████████████
[OkHttp Dispatcher] █████████
→ 여러 스레드가 동시에 일하고 있음
→ CPU 사용률이 높아짐
원인:
FLAME 차트로 확인:
FLAME 차트에서 보이는 것:
0ms 500ms 1000ms 1500ms 2000ms
|━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|
[Main Thread]
└─ loadImage() ███████████████████████████
└─ decode() ████████████████████████ ← 긴 시간, 하지만 CPU는 낮음
→ 하나의 작업이 오래 걸리지만 CPU를 많이 쓰지 않음
→ MEMORY 사용률이 높아짐
원인:
FLAME 차트로 확인:
InputStream.read(), Bitmap.decode(), Response.body() 같은 메서드가 넓게 표시됨예시 1: CPU 높음 + FLAME 차트
상황: 앱 실행 시 CPU 80%
FLAME 차트 확인:
[Main Thread] ████████████████
[DefaultDispatcher-1] ███████████████
[DefaultDispatcher-2] ████████████
[DefaultDispatcher-3] ██████████
[OkHttp Dispatcher] ████████
→ 원인: 4개 스레드가 동시에 JSON 파싱/API 호출 처리
→ 해결: 필요 없는 병렬 작업 줄이기, 순차 처리로 변경
예시 2: MEMORY 높음 + FLAME 차트
상황: 앱 실행 시 MEMORY 300MB
FLAME 차트 확인:
[Main Thread]
└─ Glide.load() ████████████████████████████
└─ BitmapFactory.decode() ████████████████ ← 매우 넓음, 단일 스레드
→ 원인: 고해상도 이미지 로딩으로 큰 Bitmap 객체 생성
→ 해결: 이미지 리사이징, downsampling 적용
ANR (Application Not Responding)이 발생하면, FLAME 차트로 정확한 원인을 찾을 수 있습니다.
ANR 조건:
FLAME 차트로 ANR 분석:
ANR 발생 시나리오:
0ms 1000ms 2000ms 3000ms 4000ms 5000ms 6000ms
|━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|
[Main Thread]
onCreate() ███████████████████████████████████████████████████ (5500ms)
└─ loadUserData() ██████████████████████████████████████████ (5300ms)
└─ database.query() ████████████████████████████████████ (5200ms) ← ANR 원인!
→ 메인 스레드가 5.5초 블로킹
→ 사용자가 3초쯤에 화면 터치 → ANR 발생
→ 원인: database.query()가 메인 스레드에서 5.2초 실행
FLAME 차트가 없다면:
ANR Stack Trace:
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow
at android.database.sqlite.SQLiteConnection.executeForCursorWindow
at com.example.MainActivity.loadUserData
at com.example.MainActivity.onCreate
→ "어디서 느린지"는 알 수 있지만
→ "왜 5초나 걸렸는지"는 모름
→ 다른 스레드와의 관계도 모름
FLAME 차트로 분석하면:
✅ database.query()가 5.2초 걸린 것을 정확히 확인
✅ 해당 메서드가 Main Thread에서 실행된 것 확인
✅ 다른 스레드들은 idle 상태였음 (병렬 처리 안 함)
✅ 해결 방법: Dispatchers.IO로 이동 또는 Room의 suspend 함수 사용
Whatap은 ANR을 이렇게 수집합니다:
Whatap Android Agent는 ANR 발생 시 해당 시점의 모든 정보를 자동으로 수집합니다:
수집되는 정보:
1. StackTrace ANR → ANR 전용 대시보드에 표시
2. CallStack 데이터 → FLAME 차트로 시각화
3. 디바이스 정보 → 기기별 ANR 발생 패턴 분석
4. 화면 로딩 시간 → 전체 성능 맥락 파악
→ ANR이 발생한 순간의 모든 정보를 완벽하게 재현 가능

"측정할 수 없으면 개선할 수 없습니다"
Before: 로그 기반 성능 디버깅
✅ onCreate: 100ms
✅ initViews: 2850ms
✅ loadData: 180ms
→ "initViews가 느린 건 알겠는데...
그 안에서 뭐가 문제지?" 🤔
→ 또 로그 추가 → 재빌드 → 재실행 → 분석
→ 끝이 없는 반복...
After: 성능 차트 기반 성능 분석
📊 성능 차트 한눈에:
onCreate() ████████████████████████ (3200ms)
├─ initViews() ███████████████████ (2850ms)
│ └─ loadThumbnails() ████████████ (2100ms) ← 병목!
│ └─ Bitmap.decode() ████ × 5회
└─ loadData() ████ (180ms)
→ "loadThumbnails의 Bitmap.decode가 문제구나!"
→ Glide로 변경하자!
→ 즉시 최적화 적용 ✅
성능 차트가 제공하는 가치:
✅ Zero Code: 코드 한 줄 추가 없이 모든 메서드 추적
✅ Library Transparency: 내 코드든 라이브러리든 똑같이 추적
✅ Instant Analysis: 클릭 한 번으로 병목 발견
✅ Visual Clarity: 1000개 로그보다 명확한 하나의 차트
✅ Thread-aware: 스레드별 실행 흐름 한눈에
✅ Production Ready: 실제 사용자 환경에서도 사용 가능
이제 여러분의 앱에서:
병목을 찾는 시간: 30분 → 30초
최적화 효과 검증: 즉시
성능 개선: 데이터 기반 의사결정
이제 여러분 차례입니다! 이 방법을 사용하면 여러분의 앱에서 사용하는 모든 라이브러리의 내부 동작을 완전히 추적할 수 있습니다.
더 이상 라이브러리는 블랙박스가 아닙니다. 이제 정확히 어디서 시간을 소비하는지 알 수 있고, 데이터 기반으로 성능을 최적화할 수 있습니다.
Happy Profiling! 여러분의 앱이 더 빨라지길 바랍니다!