[독일어 학습 앱 개발기 3편] Android 프로젝트 구조 설계 – 처음에 이것만 지켜도 망하지 않는다

2026. 1. 15. 06:21AI Product Building/HalloGerman

반응형

이 글의 핵심:
앱 개발에서 가장 위험한 순간은 “기능이 잘 돌아가기 시작할 때”다. 이때 구조를 대충 잡아두면, 기능이 늘어날수록 수정 비용이 기하급수적으로 커진다. 3편에서는 HalloGerman 프로젝트에서 실제로 사용한 Android 프로젝트 구조와, 1인 개발 기준에서 현실적으로 유지 가능한 설계 기준을 공유한다.


1. 왜 프로젝트 구조가 그렇게 중요한가

초기 개발 단계에서는 구조가 엉망이어도 앱은 잘 돌아간다. 문제는 기능이 늘어날 때 발생한다.

  • 단어장 화면 하나 추가
  • 뉴스 화면 추가
  • 음성 인식, 필기 입력 기능 추가
  • 광고, 설정, 통계 화면 추가

이때 화면 로직, 데이터 로직, 상태 관리가 한 파일에 섞여 있으면 수정이 두려워진다. 그래서 이번 프로젝트에서는 “처음부터 완벽한 아키텍처” 대신, 확장에 버티는 구조를 목표로 잡았다.


2. HalloGerman 프로젝트 구조 설계 원칙 3가지

구조를 설계할 때 세운 원칙은 딱 세 가지였다.

  • 1인 개발자가 이해할 수 있어야 한다
  • 기능 단위로 파일을 찾을 수 있어야 한다
  • 나중에 기능을 통째로 빼거나 추가할 수 있어야 한다

이 기준을 벗어나는 구조는 아무리 “정석”이라도 과감히 버렸다.


3. 전체 패키지 구조 개요

HalloGerman의 기본 패키지 구조는 다음과 같다.

com.myy.hallogerman
 ├─ ui
 │   ├─ vocabulary
 │   ├─ news
 │   ├─ study
 │   ├─ settings
 │   └─ common
 ├─ data
 │   ├─ model
 │   ├─ repository
 │   └─ source
 ├─ speech
 │   ├─ tts
 │   └─ recognition
 ├─ util
 └─ MainActivity.kt

이 구조의 핵심은 “기술 기준”이 아니라 사용자 기능 기준으로 나눈다는 점이다.


4. UI 패키지: 화면은 기능별로 분리한다

ui 패키지는 사용자가 직접 보는 화면 기준으로 나눈다.

  • vocabulary – 단어장, 단어 암기, 입력 화면
  • news – 독일어 뉴스 읽기, 듣기 화면
  • study – 학습 모드 (듣기, 말하기, 쓰기)
  • settings – 설정, 정보 화면
  • common – 버튼, 카드 등 공통 UI 컴포넌트

이렇게 나누면 “단어장 기능 전체를 수정하고 싶다”는 요구가 생겼을 때, vocabulary 패키지만 보면 된다.


5. Data 패키지: UI와 데이터를 분리하는 최소 단위

이번 프로젝트에서는 서버나 DB 없이도 동작해야 했기 때문에, 데이터 구조를 단순하게 유지했다.

  • model – Word, Vocabulary, NewsItem 같은 데이터 클래스
  • repository – UI에서 사용하는 데이터 접근 창구
  • source – JSON, CSV, 로컬 파일 처리

UI에서는 절대 JSON이나 CSV를 직접 다루지 않는다. 항상 repository를 통해 접근하게 설계했다.

예를 들어 단어를 불러오는 구조는 다음과 같은 흐름이다.

UI → VocabularyRepository → LocalCsvSource / JsonSource

이 구조 덕분에 나중에 서버 API를 붙이더라도 UI를 거의 건드리지 않아도 된다.


6. 음성 기능은 반드시 분리한다

TextToSpeech와 SpeechRecognizer는 Android에서 가장 불안정한 영역 중 하나다. 그래서 이 기능은 반드시 별도 패키지로 분리했다.

  • speech.tts – 독일어 발음 처리
  • speech.recognition – 음성 인식 처리

UI에서는 “말하기 시작”, “듣기 재생” 같은 명령만 보내고, 실제 구현은 speech 패키지에서 처리한다. 이 방식은 크래시 대응에도 매우 유리하다.


7. 아직 ViewModel을 과도하게 쓰지 않은 이유

많은 Android 예제에서는 모든 화면에 ViewModel을 붙인다. 하지만 MVP 단계에서는 오히려 복잡도를 높일 수 있다.

HalloGerman에서는 다음 기준으로 ViewModel 사용 여부를 결정했다.

  • 상태가 화면 전환 후에도 유지되어야 하는가?
  • 비동기 로직이 많은가?

이 조건에 해당하지 않는 단순 화면은 Compose state로 처리했다. 덕분에 초기 개발 속도가 크게 빨라졌다.


8. “지금은 필요 없지만, 나중에 살릴 수 있는 구조”

이 구조의 가장 큰 장점은 확장성이다.

  • 문장 학습 기능 → study 패키지에 그대로 확장
  • 서버 연동 → data.source에 API 소스 추가
  • 통계 기능 → data.repository + ui.stats 추가

처음부터 모든 걸 넣지 않아도, “들어갈 자리”만 확보해 두는 것이 핵심이다.

 

 

//모듈 분리로 시작
settings.gradle.kts
rootProject.name = "HalloGerman"
include(":app")
include(":domain")
include(":data")
//도메인 로직과 데이터 접근을 분리하면, UI가 커져도 구조가 무너지지 않는다.

//app에서 domain/data 의존
//build.gradle.kts
dependencies {
    implementation(project(":domain"))
    implementation(project(":data"))
    ...
}
//UI(app)는 domain/data를 소비만 하고, 구현 책임은 data가 가진다.

//데이터 접근은 DI 모듈로 고정
//DatabaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "hallo_german_db"
        )
        .fallbackToDestructiveMigration()
        .build()
    }
}
//DB 생성/제공 책임을 모듈에 묶어두면, 어느 화면에서도 동일한 방식으로 접근 가능해진다.

9. 다음 편 예고

4편. 단어장 데이터 구조 설계 (CSV·뉴스·로컬 저장)
다음 글에서는 CSV 단어장, 뉴스에서 추출한 단어, 앱 내부 데이터가 어떻게 하나의 구조로 관리되는지 구체적으로 다룬다.


참조 라이브러리 및 도구 모음

 

반응형