[AI, Android] 프로덕트를 터뜨리며 느낀, 대딸깍시대에도 아직까지는 우리가 할 수 있는 일들

Kame·3일 전

AI

목록 보기
1/1
post-thumbnail

들어가며

클로드 코드를 활용하여 기능 개발을 진행한 이후, 릴리즈 후보(Release Candidate, 이하 RC) 빌드에서 일부 기기에 크래시가 발생했습니다. 디버그 빌드에서는 정상적으로 동작했지만, 프로덕션과 유사한 환경의 RC 빌드에서만 문제가 발생했습니다. 다행히 문제의 원인을 찾아낼 수 있었고, 본격적인 배포 전 수정을 진행하여 프로덕션에서의 이슈 발생은 막아낼 수 있었습니다.

하지만 이 글에서 다루고자 하는 것은 단순한 버그 해결 과정이 아닙니다. AI 에이전트가 코드 생성 속도를 크게 높여주고 있는 시대에, AI가 고려하지 못하는 영역은 무엇인지, 그리고 (안드로이드) 엔지니어가 특히 어떤 부분을 고려해야 하는지를 이번 경험을 통해 정리해보고자 합니다.


문제 상황

디버그에서만 멀쩡히 돌아가던 코드

화면 전환 시 필요한 데이터를 Intent에 담아 다음 Activity로 전달해야 했습니다. 전달하려는 데이터는 내부에 다른 Parcelable 객체를 포함하는 중첩 구조를 갖고 있었습니다.

@Parcelize
data class SomeData(
    val id: String,
    val detail: SomeDetail,
) : Parcelable

@Parcelize
data class SomeDetail(
    val title: String,
    val description: String,
) : Parcelable

이 데이터를 수신하는 쪽에서는 Android 13에서 도입된 새로운 type-safe API를 사용하여 꺼내도록 작성했습니다.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    val data = intent.getParcelableExtra("key", SomeData::class.java)
    // ...
}

디버그 환경에서 여러 디바이스를 활용해 테스트했을 때는 아무 문제가 없었습니다. 하지만 이 코드가 중첩된 Parcelable 구조와 만났을 때 위험할 수 있다는 것을 당시에는 인지하지 못했고, 그대로 릴리즈를 시도하던 중 문제가 발생했습니다.

이슈 발견

현재 속한 팀에서는 본격적인 프로덕션 배포 전, 내부적으로 RC 빌드를 사전 배포하여 자체적으로 회귀 테스트를 진행하고 있습니다. 그런데 테스트 중 Android 13 디바이스를 갖고 있던 한 팀원이 해당 화면을 열자마자 크래시를 경험했습니다.

Fatal Exception: <package_name>$<SomeCustomizedException>:
Failed to deserialize <SomeData> from intent extras.
...
Caused by java.lang.NullPointerException:
Attempt to invoke virtual method 'boolean java.lang.Class.isInterface()' on a null object reference

만약 이 팀원이 없었다면, 또는 모든 테스터가 최신 디바이스만 사용했다면, 이 버그는 그대로 프로덕션에 나갔을 것입니다.

다행이었던 점은 RC 버전에도 크래시 로깅 도구(e.g Firebase Crashlytics)가 설정되어 있어, 에러 트레이스를 원격으로 확인할 수 있었다는 것입니다.

RC 빌드는 별도 툴(e.g Firebase App Distribution)을 통해 배포하도록 되어있었기 때문에 디버그 빌드처럼 로그캣을 직접 확인할 수 없는 환경이었지만, Crashlytics 덕분에 원인 파악이 가능했습니다.

한편 서비스하고 있던 앱에서는 Android 13 사용자 비중이 여전히 상당했기 때문에, 이 버그가 프로덕션에 나갔다면 상당수의 사용자가 앱을 사용하지 못하는 심각한 상황이 벌어질 수 있었습니다.


원인

배경: Android 13의 Parcelable API 변경

Android 13(API 33)부터 기존의 Intent.getParcelableExtra(String) 메서드를 deprecated 처리하고, type-safe한 새 API를 도입했습니다.

// Deprecated (API 33 이전 방식)
val data = intent.getParcelableExtra<MyData>("key")

// 새 API (API 33+)
val data = intent.getParcelableExtra("key", MyData::class.java)

당시 클로드가 생성해주던 해당 코드를 확인하고, 크게 문제가 없을 것이라 생각하고 넘겼습니다.

당시 상황: 새 API + 중첩 Parcelable + 난독화 조합

R8이 코드를 난독화할 때, 중첩된 Parcelable 객체가 있는 경우, 새 API의 내부 동작인 Parcel.readParcelableCreatorInternal()에서 클래스를 올바르게 해석하지 못해 NullPointerException이 발생했던 것입니다.

디버그 빌드에서는 보통 R8 난독화를 적용하지 않으므로 클래스 해석에 문제가 없었습니다. RC 빌드에서는 프로덕션과 동일한 난독화가 적용되었으므로 이 문제가 발생하였던 것입니다.

나중에 조사해 보니, 이 이슈는 Google Issue Tracker에도 보고되어 있었습니다. 즉 이미 알려져 있는 이슈였던 것입니다.

Deprecated API를 새 API로 교체하는 것은 너무나 자연스러운 작업이고, Google이 공식적으로 권장하는 마이그레이션이었습니다. 자기변호로 보일 수 있지만, 공식 권장 API로 바꾸고 나서 특정 버전에서 크래시가 발생할 수도 있을 것이라 예상할 수 있는 사람은 얼마 없을 것이라 생각합니다.


해결

해결 방법은 크게 아래 두 방식 중 하나를 선택하거나 둘을 조합하는 것이었습니다.

  1. 중첩된 Parcelable 구조를 평탄화 시키기
  2. AndroidX의 IntentCompat을 사용하여 버전별 분기를 자동 처리하도록 하기
// Before : 직접 버전 분기 (Android 13에서 문제 발생)
val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    intent.getParcelableExtra("key", SomeData::class.java)
} else {
    intent.getParcelableExtra("key") // @Suppress("DEPRECATION") 필요
}

// After : IntentCompat (권장)
val data = IntentCompat.getParcelableExtra(intent, "key", SomeData::class.java)

IntentCompat은 내부적으로 Android 버전과 알려진 이슈들을 고려하여 안전한 방식으로 Parcelable을 역직렬화합니다. 당시 빠른 해결이 필요했기에 IntentCompat을 선택했습니다.


AI는 모든 것을 알지만, 맥락을 알려줘야 적용한다

다른 글들과 달리, 이번 글은 단순히 특정 이슈의 원인과 해결 과정을 다루고자 하는 것이 아닙니다.

이 사건을 돌아보면서 현재 AI 에이전트가 갖고 있는 흥미로운 한계를 느끼게 되었습니다.

물론 개인적으로는 이 한계 역시 머지않아 깨질 것이라 생각합니다.

AI는 분명 코드 레벨의 문제를 잘 해결합니다. AI 에이전트를 활용한지 얼마 되지 않았음에도 불구하고, 요즘 인간이 해오던 상당수의 일을 이미 훨씬 빠르고 정확하게 처리하고 있음을 느끼고 있습니다.

실제로 이 이슈가 발생했을 때 담당했던 작업이, 기존에 다른 레포지토리에 있던 SDK 코드를 호스트 프로젝트로 마이그레이션하는 대규모의 작업이었는데, 이 작업을 AI 덕분에 작업 자체는 매우 빠르게 진행할 수 있었습니다.

맥락 없이는 경고도 안 하는 AI

코드는 완벽해도 실행 환경은 아닐 수 있다.

여기서 한 가지 아이러니한 지점이 있습니다. 사실 이 이슈는 2022년부터 Google Issue Tracker에 보고된, 알려진 문제였습니다. AI의 학습 데이터 속에는 이미 이 정보가 포함되어 있었을 가능성이 매우 높습니다. 즉, 이론적으로 이 문제를 알고 있었을 것입니다.

문제는 AI가 코드를 생성하는 순간, 어떠한 경고도 남기지 않았다는 점입니다.

지금껏 경험한 AI는 명확한 맥락 없이는 먼저 주의를 주지 않았습니다. 특정 기능을 구현해달라는 요청에 AI는 돌아가는 코드를 생성할 뿐, "혹시 R8 난독화를 적용하시나요?"라든가 "Android 13 버전에서의 호환성을 고려하셨나요?"라고 되묻지 않았습니다.

결국 개발자가 "이 코드에 잠재적인 위험 요소가 있는가?"라고 명시적으로 질문하지 않는 한, AI가 자발적으로 위험을 감지하고 그 위험을 제시하는 경우는 드뭅니다. 그런데 재밌게도 그런 질문을 던지려면 개발자가 이미 문제의 실체를 인지하고 있어야만 합니다. (닭이 먼저냐, 달걀이 먼저냐의 문제인 것 같습니다.)

따라서 일단 AI가 준 코드가 컴파일 및 CI/CD 파이프라인을 통과하고, 심지어 디버그 빌드에서 완벽하게 동작할 때 느껴지는 막연한 확신은 위험하다는 것은 직시해야 할 것입니다. 이번 사례처럼 문제는 코드 그 자체가 아니라, 코드가 실행되는 특수한 환경에 숨어있기 때문입니다.

AI가 생성한 코드가 프로덕션에 미칠 영향의 책임은 여전히 인간의 몫임을 다시금 깨닫게 됩니다.


안드로이드 개발자로서 얻은 교훈

이 경험에서 얻은 교훈들을 정리하면 다음과 같습니다.

1. 사전 빌드(RC/Release 등) 생성하여 테스트하기

디버그 빌드만으로는 프로덕션 환경을 재현할 수 없습니다. R8 난독화, 리소스 축소, 코드 최적화 등은 릴리즈 빌드에서만 적용됩니다.

특히 이번 사례처럼 Parcelable, Reflection, Serialization 관련 코드를 추가한다면, 이것들은 난독화에 민감하므로 반드시 RC 빌드에서 검증해야 할 것입니다.

2. 최대한 다양한 OS 버전 커버하기

현실은 녹록지 않다는 것을 알고 있습니다.

"다양한 버전을 테스트하라"고 말하기는 쉽습니다. 하지만 현실은 쉽지 않습니다. Android는 현재 활성 사용자가 있는 버전만 해도 10개 이상입니다. 예를 들어 프로젝트에서 API 24(Android 7)부터 API 35(Android 15)까지 지원하고 있다면, 이 모든 버전을 매번 수동으로 테스트하는 것은 현실적으로 불가능합니다.

게다가 이번처럼 특정 조합에서만 발생하는 버그는, 단순히 여러 버전에서 돌려보는 것만으로는 부족합니다. 사실 배경지식이 있지 않는 한 이걸 예측할 수 있는 사람이 과연 얼마나 있을지 모르겠습니다.

따라서 이 사안에 대한 답은 아직 개인적으로 잘 모르겠지만, 하지만 다음 항목과 같은 대응은 가능할 것이라 생각합니다.

3. 리스크 관리하기

아직까지는 인간이 잘 할 수 있는 영역 아닌가 생각해봅니다.

1) 배포 전 프로덕션 환경과 가장 비슷한 앱 버전으로 테스팅 혹은 QA 진행하기

2) 릴리즈 빌드에 반드시 크래시 모니터링 도구 활성화
이번에 원인을 빠르게 파악할 수 있었던 것은 전적으로 로깅 툴 덕분이었습니다. 모든 버전에서 테스트할 수는 없더라도, 최소한 문제가 발생했을 때 원인을 추적할 수 있는 인프라는 갖추고 있어야 할 것입니다.

3) 팀 내 테스트 디바이스의 OS 버전 분포 의도적으로 분산시키기
모두가 최신 버전 기기를 쓰고 있다면, 구버전 이슈는 영원히 발견되지 않을 것입니다. 거창한 자동화 인프라 없이도, 테스트 진행 시 의식적으로 버전을 나눠서 테스트하는 것만으로도 커버리지가 달라집니다.

4) 피쳐 플래그로 killswitch를 만들어 둘 것
모든 버전을 테스트할 수 없다면, 최소한 문제가 생겼을 때 빠르게 롤백할 수 있는 장치는 있어야 합니다. 서버 기반 피쳐 플래그(e.g Firebase Remote Config)를 사용하면, 문제가 발견되었을 때 핫픽스 빌드를 배포하고 심사를 기다릴 필요 없이 서버 설정 변경 하나로 해당 코드를 즉시 비활성화할 수 있습니다. 피쳐 플래그는 버그를 예방하는 도구가 아니라, 버그가 프로덕션에 나갔을 때 피해를 최소화하는 도구라는 점은 고려해야 합니다.

4. AI가 생성한 코드의 안전성 다시 체크하기

특히 모든 환경에서 안전한지 여부를 확인해야 합니다. 이를 위해서는 플랫폼 릴리즈 노트, 알려진 이슈, 마이그레이션 가이드를 읽는 과정이 여전히 필요할 것입니다. 혹은 이 과정을 AI에게 맡기는 것도 좋을 것입니다.

5. 컨텍스트 엔지니어링

실패에서 추출한 교훈을 시스템에 적용하는 것은 아직은 인간의 몫인 것 같습니다.

이번 문제의 본질은 AI가 몰랐던 것이 아니라, AI에게 판단에 필요한 맥락을 주지 않았다는 데 있습니다. "deprecated API를 새 API로 바꿔 줘"라는 요청에는 빌드 환경도, 지원 버전 범위도, 난독화 여부도 담겨 있지 않습니다. AI 입장에서는 주어진 코드만 보고 최선의 답을 낼 수밖에 없습니다.

프로젝트 실정에 맞는 코드를 생성하기 위해서는, AI가 올바른 판단을 내릴 수 있도록, 프로젝트의 런타임 환경과 제약 조건을 구조적으로 제공하는 것이 필요합니다.

대화에서 직접 맥락 제공하기

가장 단순한 방법은 요청할 때 환경 정보를 함께 명시하는 것입니다.

"이 앱은 minSdk xx, targetSdk yy의 Android 앱이고, 프로덕션 앱은 디버그 때와는 달리 R8 난독화가 적용된 릴리즈 빌드에서 실행된다. deprecated된 getParcelableExtra를 새 API로 전환하려 하는데, 알려진 호환성 이슈나 주의사항이 있는가?"

실제로 이 질문을 사후에 테스트해 봤을 때, AI는 Android 13의 Parcelable 이슈와 IntentCompat 사용을 권장하는 답변을 내놓았습니다. 맥락 하나가 결과를 완전히 바꾼 셈입니다.

하지만 매번 이런 정보를 수동으로 입력하는 것은 현실적이지 않습니다.

프로젝트 인스트럭션 파일 활용

요즘 대부분의 AI 코딩 도구는 프로젝트 루트에 인스트럭션 파일을 두면, 매 세션마다 자동으로 해당 내용을 컨텍스트로 로딩하는 기능을 제공합니다.
예를 들어, 클로드에서는 아래와 같은 파일들을 만들어 맥락을 제공할 수 있습니다.

인스트럭션 파일목적
CLAUDE.md (프로젝트 루트)세션 시작 시 자동 로딩
.claude/rules/*.md경로별 규칙 — 특정 파일 작업 시에만 로딩

여기에 프로젝트의 핵심 환경 정보를 적어두면, 매번 수동으로 맥락을 제공하지 않아도 AI가 자동으로 참고하게 됩니다.

하지만 중요한 제약이 있습니다. AI 도구마다 한 번에 읽을 수 있는 컨텍스트 용량에 제한이 있습니다. 모든 규칙을 하나의 파일에 장황하게 적으면 오히려 중요한 정보가 묻히게 됩니다. 그래서 두 가지 전략이 필요합니다.

프로젝트 전체에 항상 적용되는 핵심 규칙은 짧게

프로젝트 루트의 인스트럭션 파일(CLAUDE.md 등)에는 매 세션마다 로딩되므로, 정말 중요한 환경 정보만 간략히 적습니다.

# 빌드 환경
- minSdk: xx, targetSdk: yy
- R8 난독화 적용 (릴리즈 빌드)

# 알려진 호환성 이슈
- Parcelable 전달 시 IntentCompat/BundleCompat 필수 (Android 13 이슈)
- deprecated API 전환 시 대상 API의 버전별 알려진 이슈 확인 필요

특정 상황에만 필요한 규칙은 경로별로 분리

Claude Code의 .claude/rules/나 Cursor의 .cursor/rules/는 특정 파일 패턴에 매칭될 때만 로딩되는 규칙을 지원합니다. 예를 들어, Parcelable 관련 주의사항을 별도 파일로 분리하면 관련 코드를 작업할 때만 로딩되어 컨텍스트 용량을 아낄 수 있습니다.

# .claude/rules/android-compat.md
---
paths:
  - "**/*Parcelable*"
  - "**/*Activity*"
  - "**/*Fragment*"
---
- Intent/Bundle에서 Parcelable 꺼낼 때 반드시 IntentCompat/BundleCompat 사용
- Android 13에서 getParcelableExtra(String, Class)는 R8 난독화 시 NPE 발생
- 중첩 Parcelable 구조는 가능한 피하거나, 사용 시 RC 빌드에서 반드시 검증

이렇게 하면 모든 세션에 불필요한 정보를 로딩하지 않으면서도, 관련 코드를 작업할 때는 정확한 가이드가 자동으로 제공됩니다.

물론 이것이 모든 엣지 케이스를 커버하지는 못합니다. 개발자가 이런 문제가 있을 수 있다는 것 자체를 모르면 인스트럭션 파일에 적을 수도 없기 때문입니다. 하지만 한 번 겪은 문제를 기록해 두면, 같은 유형의 실수가 반복되는 것은 확실히 막을 수 있습니다.

이번 이슈도, 인스트럭션 파일에 기록해 둔 이후로는 AI가 관련 코드를 작성할 때 자동으로 IntentCompat 사용을 권장하게 되었습니다. 첫 번째 버그를 막는 것은 어렵지만, 같은 버그를 두 번 겪는 것은 막을 수 있기 때문입니다.


마치며

AI 에이전트는 개발 속도를 높여주고, 반복 작업을 줄여주고, 코드 품질을 끌어올려 줍니다. 하지만 AI가 아무리 발전하더라도, 코드가 실행되는 환경에 대한 이해와 검증은 여전히 개발자의 몫입니다.

AI가 답을 제시하는 물리적 시간을 단축했을지라도, 그 답들의 가치를 판단하고 최종적으로 결정하는 주체는 결국 프로덕트 팀과 엔지니어이므로 신중한 고민이 필요할 것입니다. 많은 사람들이 말하듯, 엔지니어가 단순 코드 작성자에서 벗어나 시스템 설계자이자 검증자로 진화해야 하는 시기임을 느끼는 요즘입니다.

profile
Software Engineer

0개의 댓글