동우님 상황 설명(“에뮬/개발환경에선 재현 X, 실기기에서 잠금화면+동시 타이머에서 간헐적으로 메인 타이머 알림이 -00:xx로 남음”)을 기준으로 코드를 더 촘촘히 손봤습니다.
핵심 원인은 보조 타이머 이벤트(START/STOP/ALARM)와 메인 타이머 종료 이벤트가 실기기에서 거의 동시에 들어오면서, 서비스가 일부 인텐트를 “스킵(버림)” 처리해 버리는 케이스였습니다.
그 결과 메인 타이머 종료 처리(알림 취소/교체)가 누락되어, SystemUI에 남아있는 Chronometer RemoteViews가 0을 지나 음수로 계속 흘러가는 현상이 나옵니다. (잠금/Doze/제조사 절전에서 더 잘 터짐)
추가로, Foreground 알림 ID가 하나(42)로 고정되어 있으면 리더(메인/보조)가 바뀌는 시점에 startForeground 업데이트가 드롭될 경우, “이전 타이머 RemoteViews”가 그대로 남아 음수로 흘러갈 가능성이 있어요.
그래서 Foreground ID를 리더별로 분리해서 이전 리더 알림을 강제 cancel할 수 있게 바꿨습니다.
✅ 이번 수정 내용 (실기기 음수 알림 잔존 방지)
1) ClockService 인텐트 “스킵” 제거 → Mutex로 순차 처리
- 기존: 처리 중이면 return START_STICKY로 들어온 인텐트를 버림
- 변경: Mutex.withLock { handleAction(intent) } 로 모든 인텐트를 순서대로 처리
- 효과: 메인 종료 이벤트가 보조 이벤트에 밀려 사라지지 않음 → 종료 정리 누락 방지
2) Foreground 알림 ID를 리더별로 분리
- FOREGROUND_ID_TIMER = 42
- FOREGROUND_ID_EXTRA = 43
- FOREGROUND_ID_STOPWATCH = 44
- FOREGROUND_ID_STOPWATCH2 = 45
그리고 startOrSwitchForeground()에서
- 리더가 바뀌면 이전 Foreground ID 알림을 cancel → “옛 타이머 RemoteViews” 잔존 차단
3) 메인 타이머 종료 시, 타이머 Foreground(42)도 추가로 정리
- cancelSafe(NID_TIMER) 뿐 아니라
- cancelSafe(FOREGROUND_ID_TIMER)도 같이 수행
변경된 파일
- app/src/main/java/com/krdonon/timer/ClockService.kt
- app/src/main/java/com/krdonon/timer/ExtraTimerAlarmReceiver.kt
수정본 ZIP
아래 파일을 받아서 기존 프로젝트에 그대로 덮어 빌드하시면 됩니다.
실기기에서 확인 포인트 (재현 시나리오 그대로)
동우님이 말한 시나리오 그대로 테스트하면 됩니다.
- 메인 01분 설정
- “추가”로 보조 타이머 생성
- 메인 10초로 재설정 후 메인 시작
- 보조도 시작
- 잠금화면 → 기다렸다가 켜기
이제는 메인이 울린 뒤에도 -00:xx로 남아있는 “타이머(메인)” 알림이 계속 카운트다운 하거나 버튼이 먹통처럼 보이는 상태가 훨씬 안정적으로 사라져야 합니다. (이 케이스의 뿌리가 “이벤트 스킵/업데이트 드롭”이었어서요)
원하시면, 다음 단계로는
- “알림바에 찍히는 타이머 항목을 1개로 정리(그룹/요약 정책 재정리)”
- “제조사(삼성 등) 절전 정책에서 서비스가 죽더라도 상태 복구를 더 강하게”
이런 쪽까지도 안정화할 수 있어요.
좋습니다. 실기기에서까지 마이너스 알림 잔존이 완전히 사라졌다면, 이번 케이스는 사실상 “알림/포그라운드 서비스/알람 동시성” 쪽의 고질적인 레이스 컨디션을 제대로 잡은 겁니다. (특히 Doze/잠금에서만 간헐 재현되는 게 전형적인 패턴이에요.)
정리해서 “이번에 무엇이 문제였고, 왜 해결됐는지”만 깔끔히 남겨드릴게요. 나중에 유지보수할 때 큰 도움이 됩니다.
이번 문제의 핵심 원인
- 메인/보조 타이머 이벤트가 거의 동시에 들어올 때
- 서비스에서 “처리 중이면 인텐트/갱신을 스킵”하거나,
- 같은 Foreground ID를 공유하면서 startForeground() 업데이트가 드롭되면,
- SystemUI에 남아있는 Chronometer(RemoteViews)가 0을 지나 -00:xx로 계속 흘러가는 상태가 됩니다.
- 보조 타이머가 끝나거나 중지되면 서비스가 정리되며 같이 사라져서 “범인이 보조 같아 보이는” 현상이 나타났던 거고요.
해결 포인트(이번 패치가 한 일)
- 들어오는 액션을 버리지 않고 순차 처리(Mutex) → 종료 정리 누락 방지
- Foreground 알림을 리더별로 ID 분리 → 이전 RemoteViews 잔존 차단
- 종료 시 관련 알림 ID를 강제 cancel → 음수 카운트다운이 남을 여지 제거
혹시 다음에 또 “실기기에서만 간헐” 문제가 나오면,
- 배터리 최적화(절전) 예외 처리 안내 UI
- 디버그 모드(알림 하단에 endAt/state 한 줄 표시)
이 두 개만 넣어도 로그 없이 재현 원인을 거의 잡아낼 수 있어요.
필요할 때 언제든지 말씀 주세요.
그때 우리가(그리고 동우님이) 그렇게 고생한 이유는, 이 버그가 “코드 한 줄 잘못” 같은 단순한 게 아니라 안드로이드 시스템 동작 + 타이밍 레이스가 겹친 “전형적인 악성 케이스”였기 때문이에요. 정리하면 이렇습니다.
1) 재현 조건이 ‘희귀 조합’이었음
문제는 메인 타이머 + 보조 타이머 동시 실행 + 잠금화면(Doze/절전) + 알림 갱신이 겹칠 때만 “간헐적으로” 터졌죠.
- PC/에뮬레이터: Doze가 약하거나 타이밍이 다르게 흘러서 잘 안 터짐
- 실기기: 잠금/절전에서 스레드/코루틴/타이머 콜백이 늦게 실행되거나 누락될 수 있음
이런 타입은 “두 번째 실행부터” 같은 조건도 흔합니다. (상태가 남아있거나, 알림/서비스가 이전 실행의 흔적을 갖고 들어오는 경우)
2) 알림(RemoteViews Chronometer)이 “앱 로직과 별개로” 계속 돈다
알림에서 Chronometer는 SystemUI가 자체적으로 계속 갱신합니다.
즉, 앱 코드에서 “끝났으니 알림을 제거/교체”를 정확한 타이밍에 못 하면,
알림은 0을 지나 -00:xx로 계속 진행할 수 있어요.
이건 개발자가 흔히 착각하는 부분인데:
- 앱 타이머가 멈췄어도
- 알림의 크로노는 계속 흐를 수 있습니다.
그래서 “메인 타이머가 끝났는데 마이너스로 감” 같은 현상이 보이는 거고요.
3) 원인이 “보조”처럼 보이지만, 실제론 “동시성 충돌”이었다
동우님이 관찰한 것처럼,
- 보조를 중지하면 마이너스가 사라지고,
- 보조가 끝나면 같이 정리되는 것처럼 보였죠.
이건 “보조가 메인에 기억을 주입”이라기보단,
- 보조 타이머 이벤트(START/STOP/ALARM)와
- 메인 타이머 종료 이벤트가
거의 동시에 들어오면서
서비스가 일부 이벤트를 **스킵(버림)**하거나 알림 갱신이 드롭되는 레이스가 발생했고,
그 결과 “메인 종료 처리(알림 교체/취소)”가 빠진 상태로 예전 RemoteViews가 남아 음수로 흘러간 거예요.
즉 범인은 “보조 로직 자체”라기보다 보조가 트리거가 되는 레이스 컨디션이었던 셈입니다.
4) 로그가 없으면 ‘원인’이 아니라 ‘현상’만 보인다
실기기에서 로그를 못 보면 디버깅이 체감상 5배는 어려워져요.
왜냐면 이 문제는
- “끝났다” 상태는 내부적으로 맞는데
- “알림 업데이트/취소 이벤트”가 누락된 상태
처럼 겉과 속이 다른 상태가 되기 때문입니다.
스크린샷만 보면 “메인 타이머가 이상” “보조가 간섭”처럼 보이지만,
실제로는 어느 이벤트가 누락됐는지(또는 순서가 뒤집혔는지)를 봐야 확정이 되거든요.
5) 최종적으로 해결된 이유는 ‘구조적 해결’이었음
우리가 마지막에 잡은 포인트는 “증상 땜질”이 아니라 구조를 바꾼 거였죠:
- 인텐트/액션 처리 스킵하지 않게 (Mutex로 순차 처리)
- Foreground 알림 ID를 리더별로 분리해서
이전 알림(RemoteViews) 잔존 자체를 차단 - 종료 시 관련 알림 ID를 강제 cancel
그래서 Doze/잠금 같은 불리한 환경에서도 누락/드롭이 나도 살아남는 구조가 된 거고, 결과적으로 “간헐적”이 사라진 겁니다.
원하면, 이 경험을 코드에 “재발 방지 장치”로 남길 수도 있어요:
- 디버그 토글: 알림 하단에 mode=timer/extra, endAt, lastAction 같은 1줄 표시
- 배터리 최적화 상태 표시: “제외됨/적용됨”을 화면에서 바로 확인 + 설정 이동 버튼
- 알림 업데이트 rate-limit: 알림 갱신 주기를 더 보수적으로 제한
이런 걸 넣어두면 다음번엔 같은 급의 버그가 와도 훨씬 빨리 잡힙니다.
동우님도 진짜 고생 많으셨습니다. 🙏
이건 “버그 하나”가 아니라 실기기·잠금·Doze·포그라운드 알림·동시 타이머가 한꺼번에 엮이는 레이스 컨디션이라, 재현도 어렵고 원인도 한 번에 안 잡히는 타입이었어요.
그래도 끝까지 증상/재현 시나리오를 정확히 공유해주셔서(“두 번째 실행부터”, “보조 중지하면 사라짐”, “잠금 후 복귀”) 결정적인 단서를 잡을 수 있었고, 결과적으로 구조적으로 재발 가능성을 없애는 방식으로 마무리됐습니다.
이제 남은 건 간단히 두 가지만 추천드려요(선택):
- 릴리즈 빌드로 실기기 장시간 테스트
- 설정/도움말에 “배터리 최적화 제외 방법” 한 화면만 추가(사용자 문의 줄어듦)
필요하면 그 UI 문구/화면도 같이 만들어드릴게요.
'앱개발' 카테고리의 다른 글
| 내일 타이머 사소한 문제 일수 있는 것들? (0) | 2026.02.25 |
|---|---|
| 내일 타이머 리포터 폴더 정리 하기 (0) | 2026.02.25 |
| 내일 타이머 나중에 고쳐야 할 부분? (0) | 2026.02.19 |
| 실패노트 adb 환경변수 등록 하기 (0) | 2026.02.19 |
| 내일 타이머 고쳐야 하는 부분 (0) | 2026.02.11 |