안드로이드 아키텍처 용어 사전
모듈화전이 의존성 · 컨벤션 플러그인

implementation vs api

의존성을 감추느냐 내보내느냐. 이 한 줄이 재컴파일 범위를 정한다.

모듈 의존성을 선언하는 두 가지 방식. 겉보기엔 같은 일을 하지만 변경이 퍼지는 범위가 다르다.

dependencies {
    implementation(project(":core:domain"))   // 감춘다 — 전이되지 않는다
    api(project(":core:ui"))                  // 내보낸다 — 쓰는 쪽도 함께 본다
}

:feature:detail:core:domainimplementation 으로 쓰면, :feature:detail 을 쓰는 다른 모듈은 :core:domain 을 보지 못한다.

왜 빌드 속도가 갈리나

핵심은 재컴파일 범위다.

api 로 선언           implementation 으로 선언
:core:domain 변경     :core:domain 변경
   ↓                    ↓
:feature:detail 재컴파일   :feature:detail 재컴파일
   ↓                    ↓
그것을 쓰는 모듈 전부      여기서 멈춘다
   ↓                    (공개 API 가 안 바뀌었으면)
또 그것을 쓰는 모듈…

api 는 의존성을 공개 서명의 일부로 만든다. 그래서 바뀌면 그것을 보는 모든 모듈이 다시 컴파일된다. implementation 은 벽을 세워 파급을 그 모듈에서 끊는다. 모듈이 수십 개인 프로젝트에서 이 차이가 빌드 시간을 몇 배로 가른다.

그래서 기본은 implementation 이다

api 가 필요한 경우는 좁다 — 그 타입이 내 공개 API 에 등장할 때다.

// :core:ui 의 Theme 을 반환한다면 → 쓰는 쪽도 Theme 을 알아야 한다 → api
fun appTheme(): Theme

// :core:domain 의 UseCase 를 안에서만 쓴다면 → 감춰도 된다 → implementation

판단 기준은 취향이 아니라 "이 타입이 밖에서 보이는가" 다. 안 보이는데 api 로 두면 불필요한 재컴파일만 늘고, 보이는데 implementation 으로 두면 쓰는 쪽이 컴파일 에러를 만난다.

캡슐화의 도구이기도 하다

빌드 속도만의 문제가 아니다. implementation다른 모듈이 내 내부 의존성에 손대지 못하게 막는다. 감추지 않으면 시간이 지나며 모두가 :core:domain 을 직접 참조하게 되고, 모듈 경계가 이름만 남는다. 언어의 접근 제어자가 클래스 안에서 하는 일을 모듈 수준에서 하는 셈이다.

나머지 두 가지

compileOnly("javax.annotation:jsr305:3.0.2")   // 컴파일에만 필요, 패키징 안 함
runtimeOnly("com.h2database:h2:2.2.224")       // 실행에만 필요, 컴파일 땐 안 보임
  • compileOnly — 애너테이션처럼 컴파일할 때만 필요하고 실행 중엔 없어도 되는 것. 결과물에 안 들어가 크기가 준다
  • runtimeOnly — 드라이버처럼 코드에서 직접 부르지 않는데 실행 시 필요한 것. 컴파일 때 안 보이므로 실수로 직접 참조하는 것을 막아 준다

네 가지를 정리하면 축이 둘이다 — 컴파일에 필요한가, 그리고 밖으로 내보내는가.

어긋났는지 어떻게 아나

./gradlew :feature:detail:dependencies 로 의존성 트리를 보면 무엇이 전이되는지 드러난다.

빌드가 느려졌다면 한 모듈을 고쳤을 때 몇 개가 다시 컴파일되는지를 먼저 본다. 예상보다 넓게 퍼진다면 대개 어딘가에서 api 를 남발한 것이다. 특히 :core 계열이 api 로 열려 있으면 거의 모든 모듈이 그 변경을 따라 다시 컴파일된다 — 쪼갠 효과가 통째로 사라지는 지점이다.

설정 중복은 컨벤션 플러그인으로

모듈이 수십 개가 되면 각 build.gradle.kts 의 설정이 그대로 복사된다. 코틀린 버전을 올리면 수십 곳을 고쳐야 한다.

컨벤션 플러그인은 공통 빌드 로직을 build-logic 한 곳에 모으고, 각 모듈은 id("myapp.android.feature") 한 줄로 가져다 쓴다. 모듈 파일이 짧아지고 규칙이 한 곳에서 강제된다.

면접 함정

  • "api 가 더 편하니 그것만 쓰면 된다" → 전이가 퍼져 재컴파일 범위가 커지고 모듈 경계가 무너진다.
  • "implementation 이면 런타임에도 안 보인다" → 컴파일 시점의 가시성 문제다. 실행 시에는 클래스패스에 있다.
  • "모듈을 쪼개면 무조건 빨라진다"api 로 다 열어 두면 쪼갠 효과가 사라진다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 멀티 모듈화 — 빌드 속도·경계·재사용