Android Session Tracking — 시니어 엔지니어의 관점: OS가 아무것도 약속하지 않을 때
Source: Dev.to
해당 글의 본문을 제공해 주시면, 원본 형식과 마크다운을 유지하면서 한국어로 번역해 드리겠습니다.
Introduction
Android는 사용자가 앱을 떠났을 때 알려주는 약속을 하지 않습니다.
이 글은 트릭에 관한 것이 아니라 Android에서 세션 추적을 올바르게 생각하는 방법—프로덕션 및 SDK 수준—에 관한 것입니다.
대부분의 Android 개발자는 어느 시점에서든 다음 중 적어도 하나를 믿어왔습니다:
onStop()은 사용자가 떠났다는 의미onDestroy()는 앱이 종료됐다는 의미- 최근 앱 화면에서 스와이프하면 세션이 종료된다는 의미
이 모든 것은 근본적으로 잘못된 생각입니다. Android는 여러분을 위해 앱을 실행하지 않으며, 여러분의 앱은 손님에 불과합니다. 프로세스는 콜백 없이(SIGKILL / LMK) 강제 종료될 수 있으며, 정리(cleanup)에 대한 보장은 전혀 없습니다. 라이프사이클 이벤트는 프로세스가 살아 있는 동안에만 존재합니다.
Implications
세션 종료를 감지하기 위해 콜백을 기다리고 있다면, Firebase, AdMob, Adjust, AppsFlyer와 같은 서비스는 종료를 “감지”하지 않습니다. 이들은:
- 프로세스가 포그라운드인지 백그라운드인지 추적
- 타임아웃 휴리스틱 적용
- 다음 실행 시 세션 종료를 추론
이벤트도 없고, 보장도 없으며—오직 확률적 모델링만 존재합니다. 이것이 고급 사고 방식입니다: 불확실성을 받아들이고 그에 맞게 설계하십시오.
세션 수명 주기
실제 세션은 다음과 같은 상태를 가집니다:
- Created
- Active
- Background
- Expired
- Restored (추론됨)
앱이 종료되더라도 세션이 바로 끝나는 것은 아니며, 다음 기준을 통해 더 이상 유효하지 않다고 판단합니다:
- 백그라운드 지속 시간
- 다음 실행 시점
- 영속된 메타데이터
ProcessLifecycleOwner가 제공하는 이벤트:
ON_START: 프로세스가 포그라운드로 전환됨ON_STOP: 프로세스가 백그라운드로 전환됨
하지만 다음은 제공하지 않습니다:
- 앱 강제 종료
- 최근 앱 화면에서 스와이프
- 네이티브 크래시
그리고 그래서는 안 됩니다. 시니어 엔지니어는 불가능한 일을 API에 요구하지 않습니다.
비즈니스 규칙으로서의 타임아웃
Timeout은 해킹이 아니라 비즈니스 규칙입니다:
“사용자가 X ms 이상 떠 있으면 세션이 종료된 것으로 간주합니다.”
정확한 값(30 초, 1 분, 5 분 등)은 제품 요구사항에 따라 달라지며, 보편적으로 옳은 값은 없습니다.
앱이 강제 종료될 때:
onSessionEnd콜백이 호출되지 않음- 정리 작업이나 플러시가 이루어지지 않음
남아 있는 정보:
- 마지막 백그라운드 타임스탬프
- 세션 ID
- 추론된 종료 이유
다음에 앱을 다시 실행하면 SDK는 다음을 수행해야 합니다:
- 지속된 상태를 로드한다
- 이전 세션이 종료되었음을 추론한다
- 논리적인 세션 종료를 발생시킨다
이러한 2단계 설계는 시니어 엔지니어에게 익숙한 방식입니다.
종료 사유 모델링
USER_BACKGROUND_TIMEOUTPROCESS_KILLED_INFERREDAPP_UPGRADECRASH_DETECTED
키워드는 inferred입니다. 좋은 SDK는 “사용자가 X를 수행했다”고 주장하지 않습니다.
세션 추적이 경우:
- 활동에 의존함
- 시스템 콜백에 의존함
- 실시간에 의존함
→ 테스트할 수 없으며, 따라서 신뢰할 수 없습니다.
Source:
Proper SessionTracker
올바른 SessionTracker:
- 순수 로직이다
- 클록을 주입한다
- 가짜 라이프사이클 신호를 받아들인다
테스트 가능하다는 것은 이해하기 쉽다는 뜻이다.
SDK 설계 목표
- Activity 의존성을 피한다
- 프로세스가 종료될 때 절대 크래시하지 않는다
- 메인 스레드를 절대 블록하지 않는다
SDK는 Application.onCreate()에서 초기화되어야 한다.
// src/main/kotlin/com/example/App.kt
class App : Application() {
private lateinit var sessionObserver: AndroidSessionObserver
override fun onCreate() {
super.onCreate()
sessionObserver = AndroidSessionObserver(
context = this,
timeoutMs = 30_000L,
callback = object : SessionTracker.Callback {
override fun onSessionStart(session: Session) {
// analytics / ads init
}
override fun onSessionEnd(
session: Session,
reason: ExitReason
) {
// flush analytics / revenue sync
}
}
)
ProcessLifecycleOwner.get()
.lifecycle
.addObserver(sessionObserver)
}
}
핵심 포인트
- Activity 라이프사이클을 사용하지 않는다
- UI 참조가 없다
- 앱이 강제 종료될 경우 콜백이 절대 호출되지 않을 수 있다 (SDK는 이 가정을 전제로 설계되었다)
대신 SDK는 다음과 같이 동작한다:
- 백그라운드에서 세션 상태(타임스탬프 + 세션 ID)를 영구 저장한다
- 다음 실행 시 이전 상태를 복원하고, 세션이 종료된 것으로 추론하여
ExitReason.PROCESS_KILLED_INFERRED를 방출한다
종료 사유 예시
USER_BACKGROUND_TIMEOUT→ 시간 기반 증거PROCESS_KILLED_INFERRED→ 이력서 추론 누락
Analytics 및 백엔드 시스템은 이를 추론된 데이터로 처리해야 합니다. SDK는 이를 숨기지 않습니다.
테스트 가능한 로직
All session logic:
- 프레임워크에 독립적이다
- 시간을 주입한다
- 완전히 테스트 가능하다
// src/test/kotlin/com/example/SessionTrackerTest.kt
@Test
fun `session ends after timeout`() {
val fakeClock = FakeClock()
val tracker = SessionTracker(fakeClock, timeoutMs = 30_000)
tracker.onForeground()
fakeClock.advance(31_000)
tracker.onForeground()
assertTrue(tracker.lastExitReason is ExitReason.Timeout)
}
테스트 가능한 로직은 결정론적 동작을 생성한다.
Repository
The complete SDK—including pure SessionTracker logic, AndroidSessionObserver, ExitReason model, and unit tests—is available on GitHub:
🔗 https://github.com/vinhvox/ViO---Android-Session-Tracker
The repository is designed to:
- Read like documentation
- Be copy‑paste friendly
- Not pretend Android is controllable
시니어 엔지니어를 위한 최종 말씀
Android에서 세션 추적은 절대 완벽하지 않습니다. 좋은 SDK는:
- 제한 사항을 숨기지 않는다
- 제약 조건을 명확히 밝힌다
- 불확실성을 명시적으로 모델링한다
- 비즈니스가 올바른(단순히 보기 좋은 것이 아니라) 결정을 내리도록 돕는다
Android는 절대적인 진실을 제공하지 않지만, 올바르게 설계한다면 추론할 수 있을 만큼 충분한 신호를 제공합니다.