와탭 모니터링
2025-10-31
Android 앱 로딩이 느릴 때, OkHttp·Glide 내부까지 원인 찾는 방법
Android 앱 로딩이 느릴 때, OkHttp·Glide 내부까지 원인 찾는 방법
"MainActivity가 2초 걸리는데... OkHttp 때문인가? Glide 때문인가? 어디서 시간을 쓰는 거지?"

화면 로딩이 느릴 때, 무엇 때문에 느린지 정확히 찾아내는 방법을 안내합니다. 라이브러리 내부까지 완전히 추적하여 성능 병목을 한눈에 파악합니다.

1. 화면이 느린데, 원인을 모르겠어요

전형적인 상황

앱 성능 리뷰 회의:

팀장: "상품 상세 화면이 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 후 수정 필요!
→ 수정하면 버전 관리는 어떻게?
→ 라이브러리 업데이트는 어떻게 추적?

우리가 정말 원하는 것

"내 코드든 라이브러리 코드든, 코드 수정 없이한 번 실행하면 모든 메서드의 시간이 한눈에 보였으면 좋겠어요!"

특히:

  • 내 코드도, 라이브러리 코드도 똑같이 추적
  • 한 번 실행으로 전체 호출 스택 확인
  • 코드 수정 없이 자동으로 수집
  • 라이브러리 업데이트해도 추적 계속 유지

이게 가능할까요? 가능합니다. 바로 성능 차트로.

2. 왜 원인을 찾기 어려운가?

"System.currentTimeMillis()는 시작일 뿐입니다"

기존 방식의 한계

한계 1: 로그 추가/제거의 무한 반복

// 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")

    // ... 계속 추가 😫
}

문제:

  • 로그 코드가 실제 코드보다 많아짐
  • 테스트 → 로그 추가 → 재빌드 → 재실행 반복
  • 한 번에 하나의 메서드만 확인 가능

한계 2: 호출 트리를 볼 수 없음

onCreate()
├─ initializeViews()
│  ├─ setupRecyclerView()  ← 이게 느림
│  │  ├─ createAdapter()
│  │  │  └─ loadItemData() ← 실제 병목은 여기!
│  │  └─ setLayoutManager()
│  └─ loadImages()
└─ loadProductData()

→ 로그만으로는 이 구조를 한눈에 볼 수 없음!

Logcat 출력:

setupRecyclerView: 2500ms
createAdapter: 2300ms
loadItemData: 2100ms  ← 진짜 범인

→ 어떤 메서드가 어떤 메서드를 호출했는지?
→ 전체 호출 흐름은?
→ 알 수 없음!

한계 3: 스레드별 분석 불가

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초
        }

        // 어떤 스레드가 병목인지?
        // 동시에 실행되는 건지?
        // 순차적인지?
        // 로그만으로는 알 수 없음!
    }
}

한계 4: 시간의 누적과 중첩 파악 불가

A 메서드: 1000ms
├─ B 메서드: 700ms
│  └─ C 메서드: 600ms
└─ D 메서드: 200ms

실제 A 메서드만의 시간: 1000 - 700 - 200 = 100ms

→ 로그만으로는 이 계산이 복잡함!
"누적 시간 vs 실제 시간" 구분 불가

우리에게 필요한 것

✅ 자동으로 모든 메서드 추적
✅ 호출 트리를 시각적으로 표시
✅ 시간을 직관적으로 비교
✅ 병목 지점을 즉시 파악
✅ 스레드별 실행 흐름 분석

그것이 바로 성능 차트입니다.

3. 해결 - 라이브러리 내부까지 추적하기

시작하기 전에 - 중요한 제약사항

Plugin이 적용되지 않는 케이스

WhatapAndroidPlugin을 사용할 때 반드시 알아야 할 제약사항이 있습니다:

❌ Plugin 적용이 불가능한 경우:

1. 이미 컴파일된 JAR/AAR 파일

// ❌ 이렇게 JAR/AAR 파일로 추가하면 내부 추적 불가
dependencies {
    implementation(files("libs/some-library.jar"))
    implementation(files("libs/some-library.aar"))
}
  • Plugin은 컴파일 시점에 바이트코드를 수정
  • 이미 컴파일된 파일은 수정 불가능

2. Maven/Gradle dependency로 추가한 라이브러리

// ❌ 이렇게 추가하면 라이브러리 내부 메서드는 추적 안 됨
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.github.bumptech.glide:glide:4.16.0")
}
  • InstrumentationScope.PROJECT 제한으로 현재 모듈만 instrumentation
  • 앱 코드에서 호출하는 부분만 추적됨
  • 라이브러리 내부 메서드는 블랙박스

3. 소스코드가 공개되지 않은 라이브러리

  • 상용 라이브러리
  • 회사 내부 라이브러리 (소스 접근 불가)

왜 소스코드가 필요한가?

[Maven dependency 방식]
Maven Repository → JAR 다운로드 → 앱에 포함
                   이미 컴파일 완료 (수정 불가)

[직접 빌드 방식]
GitHub 소스 클론 → WhatapAndroidPlugin 적용 → 컴파일 → Instrumented JAR 생성
                    컴파일 시점에 instrumentation 적용!

핵심: 완벽한 성능 추적을 위해서는 소스코드가 필수입니다.

라이브러리 종류별 적용 가능 여부

⚠️ 중요: 소스코드 없이는 라이브러리 내부 추적이 불가능합니다.

핵심 아이디어

라이브러리도 소스에서 직접 빌드하면서 WhatapAndroidPlugin을 적용하면 된다!

대부분의 오픈소스 라이브러리는 GitHub에 소스가 공개되어 있습니다:

이 소스를 클론해서 WhatapAndroidPlugin을 적용한 뒤 다시 빌드하면, 라이브러리 내부 메서드에도 instrumentation이 적용됩니다!

⚠️ 중요: 이 방법은 소스코드가 있는 오픈소스 라이브러리에만 적용 가능합니다.

3.1 OkHttp 적용하기

실제로 OkHttp 라이브러리를 instrumentation해봅시다.

1단계: 소스 클론

cd ~/your-workspace
git clone <https://github.com/square/okhttp.git>
cd okhttp

2단계: WhatapAndroidPlugin 적용

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 적용

3단계: 빌드

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 완료!

4단계: Instrumented JAR 복사

# 빌드된 JAR을 앱 프로젝트로 복사
cp build/libs/okhttp-5.0.0-alpha.jar \\
   /path/to/your-app/app/libs/okhttp-instrumented.jar

5단계: 앱에서 사용

// 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 내부가 보입니다! 🎉

3.2 Glide 적용하기

Glide도 동일한 방식으로 적용할 수 있습니다.

1단계: 소스 클론

git clone <https://github.com/bumptech/glide.git>
cd glide

2단계: Plugin 적용

// library/build.gradle
buildscript {
    dependencies {
        classpath(files("/path/to/WhatapAndroidPlugin-2.1.0.jar"))
    }
}

apply plugin: 'com.android.library'
apply plugin: 'io.whatap.android'

3단계: 빌드

./gradlew clean :library:assembleDebug

4단계: AAR 복사 및 사용

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)

3.3 Retrofit 적용하기 - Plugin으로 직접 빌드

Retrofit도 OkHttp, Glide처럼 Plugin을 적용해서 직접 빌드할 수 있습니다!

1단계: Retrofit 클론

git clone <https://github.com/square/retrofit.git>
cd retrofit

2단계: Plugin 적용

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'  // ← 추가

3단계: 빌드

# Retrofit은 multi-release JAR을 사용하므로 특정 Java 버전 컴파일 스킵
./gradlew :retrofit:jar -x compileJava14Java -x compileJava16Java

4단계: JAR 복사 및 사용

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)

3.4 Gson 수동 Instrumentation 가이드

Gson은 Gradle이 아닌 Maven (pom.xml) 빌드 시스템을 사용하기 때문에, WhatapAndroidPlugin을 직접 적용할 수 없습니다.

하지만 걱정하지 마세요! Claude Code나 CLI 도구를 활용해서 수동으로 trace 코드를 삽입할 수 있습니다.

왜 수동 Instrumentation이 필요한가?

WhatapAndroidPlugin의 제약:

variant.instrumentation.transformClassesWith(
    SimpleCallStackTraceVisitorFactory::class.java,
    InstrumentationScope.PROJECT  // ← 현재 모듈의 소스코드만!
) {}

의미:

  • Maven dependency로 추가한 라이브러리는 내부 메서드가 추적되지 않음
  • ✅ 앱 코드에서 라이브러리 호출: 추적됨
  • ❌ 라이브러리 내부 메서드: 추적 안 됨

예시:

// ✅ 이 호출은 추적됨 (앱 코드)
val todo = gson.fromJson(jsonString, TodoResponse::class.java)

// ❌ gson 내부 메서드는 추적 안 됨
//    - JsonReader.beginObject()
//    - TypeAdapter.read()
//    - 등등...

수동 Instrumentation 절차

1단계: Gson 소스 클론

git clone <https://github.com/google/gson.git>
cd gson/gson/src/main/java/com/google/gson

2단계: 핵심 메서드 식별

Gson의 성능 병목이 발생하는 핵심 메서드들:

  1. Gson.fromJson() - JSON 파싱 진입점
  2. Gson.toJson() - JSON 직렬화 진입점
  3. TypeAdapter.read() - 실제 파싱 로직
  4. TypeAdapter.write() - 실제 직렬화 로직
  5. JsonReader.beginObject(), JsonReader.nextName() - JSON 읽기

3단계: Claude Code로 Trace 코드 삽입

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() 추가해줘"

4단계: Instrumented 코드 예시

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"
    );
  }
}

5단계: Gson 빌드

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

6단계: 앱에서 사용

// 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)
└─ ... (완전한 호출 스택)

3.5 우리 샘플 앱

4개 라이브러리를 모두 적용한 실제 샘플 앱을 만들었습니다!

적용 방식 요약:

  • OkHttp: Plugin으로 직접 빌드 (JAR) ✅
  • Glide: Plugin으로 직접 빌드 (AAR) ✅
  • Retrofit: Plugin으로 직접 빌드 (JAR) ✅
  • Gson: Maven dependency (실용적 선택) ⚠️ 또는 수동 instrumentation

프로젝트 구조:

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개 라이브러리 테스트!

3.6 확인 - 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 사용으로 내부까지 추적됨)

핵심 성과

1. OkHttp (직접 빌드 방식)

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 설정 개선 검토

2. Glide (직접 빌드 방식)

Before:

Glide.load() - ???ms
  ??? - 블랙박스

After:

Glide.with() - 7ms
├─ GlideBuilder.build (4ms)
│  ├─ GlideExecutor.newSourceExecutor (2ms) ← 스레드 풀 생성
│  └─ MemorySizeCalculator.build (1ms)     ← 메모리 계산
└─ Glide.getRetriever (1ms)

발견: 초기화가 매우 빠름 (7ms) → 문제 없음

3. Gson (수동 instrumentation)

Before:

Gson.fromJson() - ???ms
  ??? - 블랙박스

After (수동 instrumentation):

Gson.fromJson() - 25ms
├─ (내부 메서드 추적)
└─ CallStackTracer 코드를 수동으로 추가하여 내부까지 완전 추적

특징:

  • Maven 프로젝트이므로 Plugin 직접 적용 불가
  • 소스 코드에 수동으로 CallStackTracer 코드 삽입
  • 내부 메서드까지 완전 추적 가능 (섹션 3.4 참조)
4. Retrofit (직접 빌드 방식)

Before:

Retrofit.create() - ???ms
  ??? - 블랙박스

After (Plugin으로 직접 빌드):

Retrofit.create() - 15ms
├─ Retrofit$Builder.build (8ms)      ← 내부까지 보입니다!
│  └─ Platform.findPlatform (2ms)
├─ ServiceMethod.parseAnnotations (5ms)
└─ RequestFactory.parseAnnotations (2ms)

발견:

  • ServiceMethod 파싱이 5ms 소비 → 정상
  • Retrofit이 OkHttp와 Gson을 잘 활용
  • 전체 HTTP 요청 흐름 투명 (OkHttp도 instrumented JAR 사용)

4. 성능 차트로 원인 한눈에 보기

"1000개의 로그보다 하나의 차트가 더 명확합니다"

실제 Whatap 대시보드 화면

실제로 수집된 화면 로딩 데이터를 확인해보세요!

이 샘플 앱을 실행한 결과, MainActivity가 로딩되는데 7.597초가 걸렸습니다. Whatap 대시보드에서 정확한 시간과 함께 모든 리소스 목록을 한눈에 볼 수 있습니다.

화면에서 확인 가능한 정보:

  • 전체 경과 시간: 7.597s (MainActivity 로딩에 걸린 총 시간)
  • 시작/종료 시간: 정확한 타임스탬프로 기록
  • 리소스 목록: MainActivity와 WebView 리소스들의 개별 경과 시간
  • 디바이스 정보: Samsung SM-A356N, Android 15, WiFi 등

이제 "성능" 탭을 클릭하면 더 상세한 FLAME 차트를 볼 수 있습니다.

실제 FLAME 차트 화면

성능 분석 탭에서 확인할 수 있는 FLAME 차트입니다. FLAME 차트를 통해 어떤 메서드가 어느 스레드에서 얼마나 오래 실행되었는지 한눈에 파악할 수 있습니다.

FLAME 차트에서 확인 가능한 정보:

main 스레드:

  • io.whatap.stacktrace.MainActivity.onCreate - 전체 화면 초기화

DefaultDispatcher-worker 스레드들:

  • Kotlin Coroutines로 실행되는 비동기 작업들
  • OkHttp 네트워크 요청과 응답 처리

OkHttp Dispatcher & TaskRunner:

  • okhttp3/internal/connection - 실제 네트워크 연결 처리
  • okhttp3/internal/http2 - HTTP/2 프로토콜 통신
  • 각 메서드의 정확한 실행 시간과 호출 순서

시간 축(X축): 0ms ~ 2491ms까지의 타임라인으로 각 메서드가 언제 실행되었는지 확인

호출 깊이(Y축): 메서드 호출 스택을 계층적으로 표시 - 아래로 갈수록 더 깊게 호출된 메서드

색상 구분: 각 스레드와 모듈별로 다른 색상으로 구분되어 시각적으로 명확함

패턴 인식으로 성능 병목 빠르게 발견하기

FLAME 차트를 보면 다양한 패턴을 통해 성능 병목 지점을 즉시 발견할 수 있습니다.

넓은 막대 → 병목 지점!
████████████████ (넓은 막대)
→ 병목! 최적화 필요

의미: 해당 메서드가 실행 시간의 대부분을 차지함
조치: 우선적으로 최적화 대상

여러 개 작은 막대 → 반복 호출!
█ █ █ █ █ (여러 개 작은 막대)
→ 반복 호출! 캐싱 검토

의미: 같은 메서드가 여러 번 호출됨
조치: 캐싱이나 배치 처리 검토

호출 깊이 5 이상 → 과도한 중첩!
깊이 5 이상
→ 과도한 중첩! 리팩토링 검토

의미: 메서드 호출이 너무 깊게 중첩됨
조치: 코드 구조 개선 또는 리팩토링 필요

Main Thread 블록 > 16ms → UI 버벅임!
Main Thread 블록 크기 > 16ms
→ UI 버벅임 가능성!

의미: 메인 스레드가 16ms(60fps 기준) 이상 블로킹됨
조치: Background 스레드로 작업 이동

CPU와 MEMORY가 말해주는 것

FLAME 차트를 보면 왜 CPU가 높은지, 왜 MEMORY가 높은지 정확히 알 수 있습니다.

CPU가 높을 때
FLAME 차트에서 보이는 것:

0ms     500ms    1000ms   1500ms   2000ms
|━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|

[Main Thread]        ████████████████████████
[Worker-1]              ████████████████████
[Worker-2]                 ████████████████
[Worker-3]                    █████████████
[OkHttp Dispatcher]              █████████

→ 여러 스레드가 동시에 일하고 있음
→ CPU 사용률이 높아짐

원인:

  • ✅ 여러 스레드가 동시에 작업 중
  • ✅ 복잡한 연산 작업 (JSON 파싱, 암호화, 이미지 처리 등)
  • 병렬 처리로 CPU 자원을 최대한 활용

FLAME 차트로 확인:

  • 같은 시간대에 여러 스레드가 활성화되어 있음
  • Coroutines Dispatcher나 OkHttp Dispatcher의 worker 스레드들이 동시 실행
MEMORY가 높을 때
FLAME 차트에서 보이는 것:

0ms     500ms    1000ms   1500ms   2000ms
|━━━━━━━|━━━━━━━━|━━━━━━━━|━━━━━━━━|

[Main Thread]
└─ loadImage() ███████████████████████████
   └─ decode() ████████████████████████  ← 긴 시간, 하지만 CPU는 낮음

→ 하나의 작업이 오래 걸리지만 CPU를 많이 쓰지 않음
→ MEMORY 사용률이 높아짐

원인:

  • IO 작업으로 데이터 버퍼링 (파일 읽기/쓰기, 네트워크)
  • 큰 객체 생성/처리 (대용량 이미지, JSON 데이터)
  • 메모리 캐싱으로 데이터 보관 (Glide 캐시, OkHttp 캐시)

FLAME 차트로 확인:

  • 특정 메서드가 오래 걸리지만 스레드는 1~2개만 활성화
  • 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 분석에도 필수

ANR (Application Not Responding)이 발생하면, FLAME 차트로 정확한 원인을 찾을 수 있습니다.

ANR 조건:

  • 메인 스레드가 5초 이상 블로킹되고 사용자가 터치
  • BroadcastReceiver가 10초 이상 블로킹
  • Service가 20초 이상 블로킹

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 발생 시 해당 시점의 모든 정보를 자동으로 수집합니다:

  • StackTrace ANR을 별도로 수집하여 ANR 전용 분석 대시보드 제공
  • ANR이 발생한 화면 로딩의 전체 FLAME 차트를 확인 가능
  • 메인 스레드 블로킹 원인을 정확히 파악하여 재발 방지
수집되는 정보:
1. StackTrace ANR → ANR 전용 대시보드에 표시
2. CallStack 데이터 → FLAME 차트로 시각화
3. 디바이스 정보 → 기기별 ANR 발생 패턴 분석
4. 화면 로딩 시간 → 전체 성능 맥락 파악

→ ANR이 발생한 순간의 모든 정보를 완벽하게 재현 가능

🆚 로그 vs 성능 차트 비교

5. 마무리 - 이제 원인을 찾을 수 있습니다

"측정할 수 없으면 개선할 수 없습니다"

우리가 달성한 것

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! 여러분의 앱이 더 빨라지길 바랍니다!

와탭 모니터링을 무료로 체험해보세요!