java.lang.NoSuchMethodError / ClassNotFoundException / NoSuchFieldError 해결 방법
릴리즈 빌드에서만 터지는 ProGuard / R8 오류 완전 정리
안드로이드 앱을 개발할 때 디버그에서는 잘 동작하는데, 릴리즈 빌드로 올리면 갑자기 크래시가 나는 경우가 많다. 특히 minifyEnabled true 로 코드 난독화/최적화를 켜면 java.lang.NoSuchMethodError, ClassNotFoundException, NoSuchFieldError 같은 오류가 대표적이다. 이 글에서는 릴리즈에서 자주 터지는 ProGuard / R8 관련 오류가 언제/왜 발생하는지, 어떻게 해결/예방하는지를 실제 예제 코드와 함께 정리한다.

1. 릴리즈에서만 터지는 ProGuard / R8 오류는 왜 발생할까?
디버그 빌드는 보통 minifyEnabled false 이기 때문에, 코드 난독화나 최적화가 거의 없다. 반면 릴리즈 빌드는 minifyEnabled true + shrinkResources true 를 켜서 크기를 줄이는데, 이 과정에서 다음과 같은 일이 발생한다.
- 클래스/메서드/필드 이름이 난독화되어 원래 이름과 달라진다.
- 사용하지 않는다고 판단된 코드가 제거(shrink)된다.
- 리플렉션(Reflection), 직렬화, Annotation 기반 프레임워크 등은 런타임에 문자열로 이름을 찾기 때문에, 난독화/제거되면
NoSuchMethodError,ClassNotFoundException등이 발생한다.
1.1 build.gradle 릴리즈 설정 예시
android {
buildTypes {
debug {
minifyEnabled false
shrinkResources false
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'
), 'proguard-rules.pro'
}
}
}
위와 같이 설정하면 디버그에서는 괜찮다가, 릴리즈에서만 “갑자기” 터질 가능성이 생긴다. 특히 Retrofit, Gson, Hilt, Glide, Firebase, Room 같이 리플렉션/코드 생성에 의존하는 라이브러리에서 자주 발생한다.
2. 대표적인 오류 사례와 해결 방법
실제로 릴리즈에서 자주 보는 오류 패턴과, 각각을 어떻게 해결하는지 예제를 통해 정리해보자.
2.1 Retrofit + Gson: NoSuchFieldError / Json 파싱 실패
Gson 은 기본적으로 필드 이름을 그대로 사용해서 JSON 을 매핑한다. 그런데 ProGuard / R8 이 DTO 의 필드명을 난독화해버리면, Gson 이 해당 필드를 찾지 못해 NPE, IllegalStateException, 파싱 실패 등이 발생할 수 있다.
// 서버 응답 DTO
data class UserResponse(
val id: String,
val name: String,
val age: Int
)
디버그 때는 잘 동작하지만, 릴리즈에서 ProGuard 가 id, name, age 필드를 a, b, c 처럼 바꿔버리면 Gson 이 JSON 키와 매칭하지 못한다.
// ProGuard / R8 난독화 후 (개념적으로)
data class UserResponse(
val a: String, // 원래 id
val b: String, // 원래 name
val c: Int // 원래 age
)
해결 방법 1: DTO 에 @Keep 또는 @SerializedName 사용
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep // 이 클래스와 필드는 난독화/제거 금지
data class UserResponse(
@SerializedName("id")
val id: String,
@SerializedName("name")
val name: String,
@SerializedName("age")
val age: Int
)
해결 방법 2: proguard-rules.pro 에 keep 규칙 추가
# Gson 모델 클래스는 난독화하지 않기
-keepclassmembers class com.example.app.data.remote.model.** {
<fields>;
}
# 또는 패키지 단위로 통째로 유지
-keep class com.example.app.data.remote.model.** { *; }
2.2 리플렉션 기반 프레임워크: ClassNotFoundException / NoSuchMethodError
리플렉션을 직접 쓰거나, 프레임워크가 내부적으로 리플렉션을 사용하는 경우에도 문제가 많이 생긴다. 대표적으로 Hilt, Dagger, EventBus, Glide, Firebase, Room, DataBinding 등이 있다.
// 리플렉션 예시
val clazz = Class.forName("com.example.app.feature.DeepLinkHandler")
val method = clazz.getDeclaredMethod("handle", String::class.java)
method.invoke(clazz.newInstance(), "myapp://home")
ProGuard / R8 이 DeepLinkHandler 클래스를 삭제하거나, handle 메서드 이름을 바꿔버리면 아래와 같은 에러가 릴리즈에서만 발생한다.
java.lang.ClassNotFoundException: com.example.app.feature.DeepLinkHandler
java.lang.NoSuchMethodException: handle [class java.lang.String]
해결 방법: 리플렉션 대상은 무조건 keep
# 리플렉션으로 사용하는 클래스/메서드는 유지
-keep class com.example.app.feature.DeepLinkHandler {
public *;
}
라이브러리마다 공식 문서에 ProGuard / R8 설정 예제가 있으니, Hilt, Retrofit, Glide, Firebase, Room 등을 쓴다면 반드시 “라이브러리명 + proguard” 로 검색해서 기본 룰을 먼저 복붙해 두는 것이 좋다.
2.3 릴리즈에서만 앱이 바로 죽는 경우: Logcat / mapping.txt 확인
릴리즈 APK 를 설치했더니 앱이 바로 죽어서 어디서 터지는지 모를 때는 다음 순서로 확인한다.
- 테스트 기기에서 USB 디버깅 켜고,
adb logcat으로 크래시 로그 확인 - 릴리즈 빌드에 대해 Crashlytics 를 붙여서 실제 사용자 환경 로그 수집
- 난독화된 스택트레이스를
mapping.txt로 디코딩해서 원래 클래스/메서드 이름 확인
# mapping.txt 를 사용해서 스택트레이스 디코딩 (예시)
retrace.bat mapping.txt stacktrace.txt
mapping.txt 를 보관해두면, 나중에 특정 버전에서 발생한 크래시를 분석할 때 큰 도움이 된다.3. 처음부터 예방하는 ProGuard / R8 설정 전략
릴리즈 직전에 “minify 켰다가” 대량의 크래시를 맞는 것보다, 개발 초기부터 ProGuard / R8 을 켜둔 상태에서 개발하는 것이 훨씬 안전하다. 아래 체크리스트를 기준으로 설정해보자.
3.1 개발 단계부터 minifyEnabled 켜두기
buildTypes {
debug {
minifyEnabled true // 개발 단계에서부터 켜두기
shrinkResources false // 리소스 shrink 는 나중에 켜도 됨
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'
), 'proguard-rules.pro'
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'
), 'proguard-rules.pro'
}
}
이렇게 해두면, 개발 도중에 이미 ProGuard / R8 관련 문제를 발견할 수 있어서 “출시 직전 릴리즈에서만 터지는 버그”를 미리 잡을 수 있다.
3.2 DTO, 리플렉션 대상, 엔티티 패키지에 공통 keep 룰 적용
# 서버 통신 DTO
-keep class com.example.app.data.remote.model.** { *; }
# Room Entity / DAO
-keep class androidx.room.** { *; }
-keep class * extends androidx.room.RoomDatabase
# Hilt / Dagger / Glide / Gson / Retrofit / Firebase 등
# => 각 라이브러리 공식 문서의 ProGuard 예제를 그대로 가져다 쓴다.
3.3 테스트 전략: 릴리즈 빌드로 QA 하기
- CI 에서
assembleRelease를 빌드하고 테스트 기기에 설치 - QA / 스테이징 환경은 항상 릴리즈 빌드 기준으로 테스트
- Crashlytics 를 스테이징에도 붙여서 ProGuard 관련 크래시를 조기에 발견
마무리
디버그에서는 잘 되는데 릴리즈에서만 터지는 오류는 대부분 ProGuard / R8 난독화/최적화와 관련이 있다. 리플렉션, Gson/Retrofit, Room, Hilt 등 동적으로 클래스를 사용하는 라이브러리를 쓸수록 이런 문제는 더 자주 나타난다.
이 글에서 소개한 DTO / 엔티티 keep 규칙, 리플렉션 대상 keep, 개발 단계부터 minify 켜두기 전략을 적용하면, “출시 직전에 릴리즈에서만 터지는” 악몽 같은 버그를 미리 방지할 수 있다.
- 릴리즈에서만 터지는
NoSuchMethodError,ClassNotFoundException은 대부분 ProGuard / R8 이 원인이다. - 서버 DTO, Room Entity, 리플렉션 대상 클래스는
@Keep또는 proguard-rules.pro 에-keep규칙을 반드시 추가한다. - 개발 초기부터
minifyEnabled true로 개발하면 출시 직전 대형 사고를 줄일 수 있다. - 라이브러리별 공식 ProGuard 룰을 꼭 적용하고, Crashlytics + mapping.txt 로 크래시를 분석한다.

카카오톡 오픈채팅 링크