집중 공부를 위한 뮤직 플레이어 + 포모도로 타이머 Android 앱
| 포모도로 타이머 | 음악 선택 | 플레이리스트 |
|---|---|---|
![]() |
![]() |
![]() |
- 로컬 음악 재생 — MediaStore 기반으로 기기 내 음악 파일을 탐색하고 재생
- 포모도로 타이머 — 집중/휴식 사이클을 관리하는 내장 타이머
- 백그라운드 재생 — Foreground Service + MediaSession으로 잠금 화면·블루투스 컨트롤 지원. 오디오 포커스 처리로 전화 수신 시 자동 일시정지
- 세션 완료 알림 — 집중/휴식 세션 종료 시 소리 + 진동 알림
- 셔플 / 반복 재생 — 랜덤 재생 및 단곡·전체 반복 지원
- 플레이리스트 — 원하는 곡을 묶어 플레이리스트를 구성하고 재생
| 분류 | 기술 |
|---|---|
| UI | Jetpack Compose, Material3 |
| 아키텍처 | ViewModel, StateFlow, Hilt |
| 미디어 | ExoPlayer (Media3) |
| 로컬 저장 | Room, DataStore |
| 테스트 | Kotest (BehaviorSpec), Turbine (Flow 테스트) |
| 정적 분석 | detekt, ktlint |
| 빌드 | Gradle KTS |
app/
├── core/ # 앱 전역 인프라 (DI, DB, Navigation, Service)
├── features/
│ ├── music/ # 로컬 음악 스캔
│ ├── player/ # 재생 엔진 (ExoPlayer 래퍼)
│ ├── playlist/ # 플레이리스트 관리
│ └── timer/ # 포모도로 타이머
└── zen/ # 메인 화면 (슬라이스 조립)
VSA(Vertical Slice Architecture)
기능(feature) 단위로 코드를 수직으로 묶는 구조입니다.
music, player, playlist, timer 4개의 슬라이스가 각자 UI·ViewModel·Repository를 모두 포함하며, 슬라이스 간 직접 참조는
금지됩니다.
기능을 추가하거나 삭제할 때 다른 슬라이스에 영향을 주지 않는다는 점이 핵심입니다.
모든 사용자 이벤트는 onAction() 단일 진입점을 통해 처리하고, 여러 StateFlow를 combine으로 묶어 단일 UiState로 통합합니다. UI는 상태를
구독만 하므로 부수 효과 없이 화면을 재현할 수 있습니다.
Channel 기반 Actor 패턴
포모도로 타이머는 start, pause, resume, skip 같은 사용자 명령과 타이머 완료(COMPLETED) 이벤트가 동시에 발생할 수 있습니다.
이를 단일 Channel에 직렬화해 race condition 없이 순서를 보장합니다.
사용자: skip() ──┐
타이머: COMPLETED ──┤─→ Channel ─→ Actor(단일 코루틴) ─→ 순차 처리
사용자: pause() ──┘
각 명령은 CompletableDeferred를 포함해 호출자가 처리 완료를 await()으로 대기할 수 있습니다.
테스트
실무에서 테스트 코드 부재로 인한 버그와 사이드 이펙트를 반복적으로 경험했습니다. 아이러니하게도, AI와 협업하면서 테스트의 필요성을 다시 깨달았습니다. AI가 생성한 코드는 확률 모델의 결과물이라 본질적으로 불안정합니다. 결정론적인 테스트가 없으면 AI가 만든 코드를 신뢰할 수 없습니다. AI는 컨텍스트가 한정적이라 외부 환경을 전부 고려하지 못합니다. 테스트가 그 간극을 잡습니다.
외부 의존성(MediaStore, Room, DataStore)은 Fake 구현체로 대체해 Android 런타임 없이 단위 테스트가 실행됩니다.
Kotest의 BehaviorSpec으로 Given-When-Then 구조로 시나리오를 서술합니다.
커버리지 숫자보다 타이머·플레이어 같은 핵심 기능이 충분히 커버되는지를 우선합니다.
타이머 세션 전환·종료 조건, 반복/셔플 모드별 재생 흐름 등 핵심 기능의 시나리오가 테스트로 커버됩니다.
커밋 전 Python 스크립트로 전체 검증을 단일 명령으로 실행합니다.
python scripts/check.py format # ktlint 포맷팅 (커밋 전 별도 실행)
python scripts/check.py full # lint → arch → build → test 순차 실행
python scripts/check.py arch # 아키텍처 규칙 검사만각 단계가 실패하면 즉시 중단됩니다. GitHub Actions 없이 로컬에서 동일한 품질 게이트를 강제합니다.
슬라이스 간 의존성을 강제하는 가장 확실한 방법은 멀티모듈입니다. 그러나 이 프로젝트 규모에서 멀티모듈은 빌드 설정 복잡도 대비 실익이 없다고 판단했습니다.
대신 scripts/arch_check.py가 모든 .kt 파일의 import 구문을 파싱해 슬라이스 간 직접 참조를 정적으로 검출합니다.
외부 라이브러리 없이 단일 모듈에서도 아키텍처 규칙을 코드로 강제합니다.
규칙 1 (슬라이스 격리): features/player → features/playlist ❌ 슬라이스 간 직접 참조 금지
규칙 2 (단방향 의존): features/* → core ✅ core만 공유 인프라로 사용
규칙 3 (글루 레이어): zen → features/* ✅ zen이 슬라이스를 조립
이 프로젝트는 Claude Code를 단순 코드 생성 도구가 아닌 개발 루프의 게이트키퍼로 활용합니다.
훅은 Claude Code의 PreToolUse/Stop 이벤트에 연결되어 AI의 행동 자체를 제약합니다.
| Hook | 시점 | 역할 |
|---|---|---|
guard-bash |
모든 Bash 명령 전 | git push --force, git reset --hard 등 위험 명령 차단 |
pre-commit-gate |
AI가 git commit 실행 전 |
테스트 실패 시 AI의 커밋 차단 |
post-check |
작업 완료 후 | app/ 변경 감지 시 format → lint 순차 실행 (실패 시 즉시 중단) |
단일 AI가 모든 역할을 맡는 대신, 역할별로 분리된 에이전트를 운용합니다.
| Agent | 역할 |
|---|---|
project-manager |
현재 상태 분석 → 다음 작업 우선순위 결정 |
code-reviewer |
git diff 기반 품질·보안·아키텍처 검토 |
designer |
Material Design 3 기반 UI/UX 설계 |
doc-manager |
코드-문서 간 불일치 감지 및 수정 |
work-on, write-test, work-on-prd 등의 커스텀 스킬로 기능 구현부터 테스트·커밋까지 일관된 워크플로우를 유지합니다.
모든 작업은 dev/active/에 계획을 먼저 작성하고 사용자 승인 후 구현을 시작합니다.
프로젝트 전용 스킬 외에 재사용 가능한 공통 스킬은 별도 플러그인으로 분리해 관리합니다.
| 플러그인 | 내용 | 대표 스킬 |
|---|---|---|
| common | 범용 워크플로우 | commit, create-pr, apply-review |
| android | Android 특화 | write-test, code-review |
요구 사항
| 항목 | 버전 |
|---|---|
| JDK | 11 이상 |
| Android Studio | Meerkat 이상 권장 |
| AGP | 9.0.0 |
| Kotlin | 2.0.21 |
| compileSdk / targetSdk | 36 |
| minSdk | 24 (Android 7.0) |
git clone https://github.com/gagip/zenplayer.git
cd zenplayer
./gradlew assembleDebug # debug APK 빌드
./gradlew testDebugUnitTest # 단위 테스트 실행검증 파이프라인 전체 실행:
python scripts/check.py format # 포맷팅
python scripts/check.py full # lint → arch → build → test


