StrictMode는 Android SDK에 내장된 개발자 도구로, 애플리케이션의 메인 스레드에서 우발적인 I/O 작업 및 네트워크 호출을 실시간으로 감지하고 보고합니다. 오류를 수정하지는 않지만 탐지기 역할을 하여 구성된 정책을 위반할 때 예외를 throw하거나 LogCat에 기록합니다. Google, 2024에 따르면 적절한 StrictMode 구성을 통해 앱 출시 전에 최대 80%의 성능 문제를 감지할 수 있습니다.
핵심 사항
StrictMode는 API Level 9(Android 2.3 Gingerbread)부터 Android SDK에 포함된 API입니다. 그 역할은 런타임에 메인(UI) 스레드에서 UI 렌더링을 차단할 수 있는 무거운 작업의 우발적 실행을 감지하는 것입니다. 메인 스레드는 사용자 입력 처리, 레이아웃 계산 및 렌더링을 담당하며 16ms를 초과하는 모든 차단은 프레임 드롭을 유발합니다.
StrictMode는 “빠르게 실패하기” 원칙을 따릅니다. 즉, 문제를 가능한 한 빨리, 이상적으로는 처음 나타나는 순간에 감지합니다. 사용자의 느려짐 불만을 기다리는 대신 개발자는 개발 단계에서 직접 신호(로그, 대화상자 또는 충돌)를 받습니다. 이 도구는 추가 라이브러리나 Gradle 구성이 필요하지 않으며 Application.onCreate에 몇 줄의 코드만 추가하면 모든 기기에서 자동으로 작동합니다.
StrictMode는 경험에 관계없이 모든 Android 개발자를 위해 설계되었습니다. 초보자에게는 좋은 습관(UI 스레드에서 네트워크 요청을 하지 않음)을 형성하는 데 도움이 되고, 숙련된 개발자에게는 CI/CD 파이프라인에서 품질 관리를 자동화합니다. 대규모 프로젝트(Google, Uber, Spotify)는 디버그 빌드에서 penaltyDeath와 함께 StrictMode를 포함하고 BuildConfig.DEBUG 검사를 통해 릴리스 빌드에서는 비활성화합니다.
StrictMode는 스레드를 차단할 수 있는 시스템 호출을 가로채서 활성 정책 집합과 비교합니다. 호출이 정책과 일치하고 메인 스레드에서 실행되면 StrictMode는 지정된 패널티를 적용합니다. 가로채기 메커니즘은 프로세스 내 훅을 통해 구현되며 리플렉션을 사용하지 않고 최소한의 오버헤드로 작동합니다.
StrictMode 정책이 활성화되면 시스템 호출(FileInputStream, FileOutputStream, Socket, URLConnection)의 진입점에 핸들러를 주입합니다. 애플리케이션이 메인 스레드에서 예를 들어 URLConnection.openStream을 호출하면 StrictMode는 현재 스레드를 확인하고 메인 스레드인 경우 도구가 작동합니다. Android 6.0+에서는 메커니즘이 강화되어 메인 스레드의 네트워크 호출이 StrictMode 없이도 NetworkOnMainThreadException을 생성하지만 StrictMode는 디스크 I/O도 제어할 수 있습니다.
각 정책은 고유한 패널티 유형 또는 조합을 가질 수 있습니다: penaltyLog — 스택 추적과 함께 LogCat에 기록, penaltyDialog — 사용자에게 대화상자 표시(디버그 전용), penaltyDeath — 예외를 throw하고 앱 충돌, penaltyDropBox — 이후 분석을 위해 DropBoxManager에 데이터 저장. CI/CD 파이프라인의 경우 penaltyDeath가 권장됩니다. 위반 사항이 있는 병합이 눈에 띄지 않도록 보장합니다.
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.penaltyDeath()
.build()
)
}
}
}
StrictMode는 정책을 두 가지 수준으로 나눕니다: ThreadPolicy(스레드 수준 — 메인 스레드에서 수행할 수 없는 작업) 및 VmPolicy(가상 머신 — 메모리 및 리소스 누수). 두 수준 모두 독립적으로 구성되며 병렬로 작동합니다.
스레드 수준에서 StrictMode는 네 가지 유형의 위반을 제어합니다: 디스크 읽기(detectDiskReads), 디스크 쓰기(detectDiskWrites), 네트워크 작업(detectNetwork) 및 사용자 정의 느린 호출(detectCustomSlowCalls). disk_read는 메인 스레드에서 SharedPreferences, SQLite 또는 파일을 읽을 때 작동합니다. network는 HTTP 요청, WebSocket 및 Socket 연결 시 작동합니다. Android 11+에서는 버퍼링되지 않은 I/O 감지를 위해 detectUnbufferedIO가 추가되었습니다.
VmPolicy는 ART 가상 머신 수준에서 누수를 제어합니다: detectActivityLeaks(소멸되지 않은 활동), detectLeakedClosableObjects(닫히지 않은 Cursor, Stream, Socket), detectLeakedRegistrationObjects(등록되지 않은 BroadcastReceiver, ServiceConnection). VmPolicy가 Activity가 생성되었지만 onDestroy 호출 후 소멸되지 않았음을 감지하면 전체 스택 추적을 출력하여 메모리 누수 디버깅 시간을 절약합니다.
| 정책 | 수준 | 감지 내용 |
|---|---|---|
| detectDiskReads | Thread | UI 스레드에서 SharedPrefs, SQLite, 파일 읽기 |
| detectDiskWrites | Thread | UI 스레드에서 SharedPrefs, SQLite, 파일에 쓰기 |
| detectNetwork | Thread | UI 스레드에서 모든 네트워크 작업 |
| detectActivityLeaks | VM | onDestroy에서 살아남은 활동 |
| detectLeakedClosableObjects | VM | 닫히지 않은 Cursor, Stream, Socket |
detectCustomSlowCalls를 통해 자신의 메서드를 “의심스러운” 것으로 표시하고 지정된 임계값을 초과할 때 경고를 받을 수 있습니다. 예를 들어 loadUserProfile() 메서드가 일반적으로 5ms 걸리지만 때로는 200ms 걸리는 경우 StrictMode.noteSlowCall(“loadUserProfile”)로 래핑합니다. 지속 시간이 임계값(기본 2000ms)을 초과하면 StrictMode가 패널티를 생성합니다. 임계값은 setSlowCallDurationThreshold를 통해 구성됩니다.
기본 StrictMode 구성은 10줄의 코드로 이루어지며 사용자 정의 Application 클래스의 onCreate 메서드에서 수행됩니다. 주요 규칙: StrictMode는 디버그 빌드에서만 활성화됩니다. 릴리스 빌드에서는 앱 속도를 늦추고 오탐지를 생성할 수 있습니다.
Application을 확장하는 클래스를 만들고 android:name 속성을 통해 AndroidManifest.xml에 등록한 후 StrictMode 구성을 추가합니다. ThreadPolicy.Builder에는 모든 감지기와 모든 패널티 유형이 포함됩니다(대화상자 제외 — 디버거가 연결된 경우에만 작동). VmPolicy.Builder는 Activity 누수 및 Closable 객체에 대한 감지기를 추가합니다. 대규모 프로젝트(100+ 화면)의 경우 Activity 누수 감지기에 penaltyDeath를 설정한 VmPolicy를 구성하는 것이 좋습니다. 엄격하지만 효과적입니다.
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectActivityLeaks()
.detectLeakedClosableObjects()
.detectLeakedRegistrationObjects()
.penaltyLog()
.penaltyDeath()
.build()
)
}
}
}
CI/CD에서 자동 제어를 위해 penaltyDeath를 사용합니다. 테스트가 정책을 위반하면 앱이 예외와 함께 충돌합니다. Android Test Orchestrator와 결합하여 각 테스트가 깨끗한 프로세스에서 실행되도록 합니다. UI 테스트(Espresso, Compose Test)의 경우 StrictMode 위반을 가로채서 assertion 실패로 변환하는 사용자 정의 TestRule을 작성합니다. 예: @Before에서 StrictMode를 활성화하고 @After에서 위반이 없었는지 확인합니다.
기본적으로 customSlowCall의 임계값은 2000ms이며 disk_read 및 disk_write에는 임계값이 없습니다(모든 작업이 작동). setSlowCallDurationThreshold 및 setSlowIoDurationThreshold를 통해 밀리초 단위로 자체 값을 설정할 수 있습니다. 앱이 메인 스레드에서 SharedPreferences를 정당하게 읽는 경우(작은 구성) 임계값을 10–20ms로 늘리면 빠른 읽기는 필터링되고 느린 읽기는 유지됩니다.
StrictMode는 강력하지만 까다로운 도구입니다. 잘못된 구성은 수백만 개의 오탐지로 이어져 개발자들이 주의를 기울이지 않게 만듭니다. 다음은 대규모 Android 팀의 경험에서 수집된 검증된 사례입니다.
이것은 철칙입니다: StrictMode는 릴리스 빌드에서 절대 활성화되어서는 안 됩니다. BuildConfig.DEBUG 플래그 또는 사용자 정의 buildConfigField를 사용하십시오. 릴리스 빌드에서는 많은 타사 라이브러리가 메인 스레드에서 정당하게 작업(SDK 초기화, 캐시 쓰기)을 수행하며 StrictMode가 오탐지를 생성합니다. 또한 릴리스 빌드에서 penaltyDialog는 최종 사용자에게 대화상자를 표시하므로 허용되지 않습니다.
소규모 프로젝트(1–10개 화면)의 경우 penaltyLog를 구성합니다. 수동 분석에는 로그로 충분합니다. 중간 규모 프로젝트(10–50개 화면)의 경우 네트워크 및 customSlowCalls에 penaltyDeath를 추가합니다. 대규모 프로젝트(50개 이상 화면)의 경우 CI/CD에서 penaltyDeath를 포함한 전체 정책 세트를 활성화하고 로컬 개발에는 penaltyLog를 사용합니다. 이러한 단계적 접근 방식은 파이프라인에서 품질을 엄격하게 제어하면서 개발자에게 잘못된 충돌을 과도하게 로드하는 것을 방지합니다.
일부 라이브러리(Firebase, Crashlytics, Adjust)는 StrictMode가 잘못 감지할 수 있는 백그라운드 작업을 정당하게 수행합니다. 해결책: penaltyListener를 통해 라이브러리를 허용 목록에 추가하거나, 라이브러리를 명시적으로 백그라운드 스레드로 전환하는 버전으로 업데이트하거나, StrictMode.vmPolicy를 사용합니다. Android 11+에서는 스택 추적으로 위반을 프로그래밍 방식으로 필터링하기 위한 StrictMode.OnVmViolationListener가 도입되었습니다.
// penaltyListener를 통한 오탐지 필터링
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyListener { violation ->
val stack = violation.stackTraceToString()
if ("com.google.firebase" !in stack) {
logViolation(violation)
}
}
.build()
)
StrictMode는 Android 생태계에서 유일한 품질 관리 도구가 아닙니다. 그 위치를 이해하기 위해 주요 기준(검사 시간, 분석 깊이, 자동화)으로 Android Lint, Android Profiler 및 Perfetto와 비교해 보겠습니다.
| 기준 | StrictMode | Android Lint | Profiler / Perfetto |
|---|---|---|---|
| 검사 시간 | 런타임(앱 실행 중) | 컴파일 시간(시작 전) | 런타임(사후 분석) |
| 검사 내용 | 디스크, 네트워크, 누수 | XML, 코드, 리소스 | CPU, 메모리, 네트워크, 전력 |
| 자동화 | penaltyDeath를 통한 CI/CD | Gradle 태스크 + lint-baseline | 수동 분석 필요 |
| 깊이 | UI 스레드 및 누수만 | 정적 코드 분석 | 전체 성능 그림 |
| 오탐지 | 중간(라이브러리에 따라 다름) | 낮음(구성된 규칙) | 없음(실제 측정) |
최상의 전략은 세 가지 접근 방식을 모두 결합하는 것입니다: Android Lint는 컴파일 시간에 명백한 오류(예: 잊혀진 IdleHandler)를 잡고, StrictMode는 런타임에 문제를 감지하며, Android Profiler / Perfetto는 처음 두 도구가 답을 제공하지 않을 때 심층 분석에 사용됩니다. 실제 프로젝트(Google Maps, Instagram)에서는 기본 아키텍처를 설정한 직후인 개발 2주차에 StrictMode가 도입됩니다.
StrictMode가 성능 문제를 감지하고 수정하는 데 도움이 되는 두 가지 실제 시나리오를 살펴보겠습니다: 메인 스레드에서 SharedPreferences 읽기와 등록되지 않은 콜백을 통한 Activity 누수입니다.
앱 시작 시 detectDiskReads 정책을 사용하는 StrictMode가 메인 스레드에서 SharedPreferences 읽기를 감지합니다. 해결책: CoroutineScope를 통해 구성을 비동기적으로 로드하거나 시작 시 메모리에 캐시합니다. SharedPreferences는 디스크에서 XML 파일을 동기적으로 읽습니다. 작은 파일(1–2KB)의 경우에도 작업에 1–5ms가 소요되고 저가형 기기에서는 최대 20ms까지 소요되어 프레임 드롭이 발생할 수 있습니다.
// ❌ 문제가 있는 코드 — UI 스레드에서 SharedPrefs 읽기
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// StrictMode detectDiskReads → 위반!
prefs = getSharedPreferences("config", MODE_PRIVATE)
}
}
// ✅ 수정된 코드 — Coroutine을 통한 읽기
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadConfigAsync()
}
}
VmPolicy.detectActivityLeaks를 사용하는 StrictMode는 스택에서 나갔지만(finish가 호출됨) 정적 참조 또는 등록되지 않은 콜백으로 인해 Activity 객체가 메모리에 남아 있는 것을 감지합니다. 일반적인 시나리오: onPause에서 unregister를 호출하지 않고 onResume에서 EventBus 또는 LocationListener를 등록합니다. VmPolicy는 참조가 생성된 줄을 나타내는 스택 추적을 출력합니다.
// ❌ 누수 — 콜백이 취소되지 않음
private var locationCallback: LocationCallback? = null
override fun onResume() {
super.onResume()
locationCallback = LocationCallback(this::onLocationUpdated)
locationManager.register(locationCallback) // StrictMode → 누수!
}
override fun onPause() {
super.onPause()
// 잊었습니다: locationManager.unregister(locationCallback)
}
자주 묻는 질문
StrictMode는 실제로 약간의 오버헤드를 추가합니다 — 모든 시스템 호출이 정책에 대해 확인됩니다. 성능 영향은 디버그 빌드에서 1–3%이며 릴리스 빌드(StrictMode가 비활성화된 경우)에는 없습니다. 오래된 기기(Android 6–8)에서 detectAll을 활성화하면 오버헤드가 5%에 도달할 수 있으므로 필요한 정책만 구성하는 것이 좋습니다.
네, StrictMode는 Jetpack Compose와 완벽하게 호환됩니다. 디스크 및 네트워크 정책은 UI 프레임워크와 독립적으로 프레임워크 수준에서 작동합니다. 또한 Compose에서는 UI 차단의 중요성이 더 높습니다. Compose는 높은 재생률 기기에서 120FPS로 프레임을 다시 그리므로 파일 읽기에 추가 5ms가 더 눈에 띄게 됩니다.
Android 8.1(API 27)부터 SharedPreferences는 메모리 캐싱을 사용할 수 있습니다. 파일을 이미 읽은 경우 다시 읽어도 StrictMode가 작동하지 않습니다. getSharedPreferences를 처음 호출하는지(콜드 리드)와 detectDiskReads 정책이 활성화되어 있는지 확인하세요. 또한 상위(parent)가 없는 Fragment에서 StrictMode가 재정의되지 않았는지 확인하세요.
JUnit 테스트에서는 @Before에서 StrictMode.allowThreadDiskReads() 및 StrictMode.allowThreadDiskWrites()를 사용하고 @After에서 StrictMode.enableDefaults()를 통해 설정을 복원합니다. 계측 테스트의 경우 원래 정책을 임시로 보존하는 사용자 정의 TestRunner를 사용합니다. Espresso 테스트에서는 StrictMode에 민감한 코드를 IdlingResource로 래핑하는 것이 편리합니다.
StrictMode는 Android SDK를 통해 Android 플랫폼에서만 작동합니다. Kotlin Multiplatform(KMP)에서 commonMain 코드는 StrictMode를 사용할 수 없지만 androidMain에서는 평소처럼 추가할 수 있습니다. iOS 부분의 경우 메인 스레드용 DispatchQueue.main.async 어설션이라는 유사한 기능을 사용합니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.