Gradle 의존성 설정 - api, implementation, compileOnly 차이
왜 알아야 하는가
Gradle에서 의존성을 추가할 때 api, implementation, compileOnly 중 어떤 걸 써야 하는지 헷갈린다.
단독 애플리케이션이면 사실 큰 차이가 없는데, 라이브러리를 만들어서 다른 프로젝트에 제공하는 순간 이 차이가 중요해진다.
핵심 개념: 의존성 전이 (Transitive Dependency)
A 모듈이 B 라이브러리를 의존하고, 내 프로젝트가 A 모듈을 의존한다고 하자.
내 프로젝트 → A 모듈 → B 라이브러리
이때 B 라이브러리가 내 프로젝트에서도 보이는가? 이것이 api와 implementation의 차이다.
api
dependencies {
api("com.example:B:1.0")
}
A 모듈이 B를 api로 선언하면, A를 사용하는 내 프로젝트에서도 B의 클래스를 직접 사용할 수 있다.
내 프로젝트 → A 모듈 --api--> B 라이브러리
↑ ↑
└── B의 클래스를 직접 사용 가능 ──┘
언제 쓰는가: A 모듈의 public API(메서드 파라미터, 리턴 타입 등)에 B의 타입이 노출될 때.
실제 예시를 보면:
// commons/build.gradle.kts
dependencies {
api(libs.kotlin.reflect)
api(libs.jackson.module.kotlin)
}
commons 라이브러리가 kotlin-reflect와 jackson-module-kotlin을 api로 선언했다.
commons를 의존하는 downtime, card 등 모든 프로젝트에서 이 두 라이브러리를 별도 선언 없이 바로 쓸 수 있다.
// commons-web-client/build.gradle.kts
dependencies {
api(project(":kotlin:commons"))
}
commons-web-client가 commons를 api로 선언했다.
downtime 프로젝트가 commons-web-client만 의존하면 commons의 클래스도 자동으로 사용 가능하다.
downtime → commons-web-client --api--> commons --api--> kotlin-reflect
└--> jackson-module-kotlin
전부 전이되어 downtime에서 다 쓸 수 있다.
implementation
dependencies {
implementation("com.example:B:1.0")
}
A 모듈이 B를 implementation으로 선언하면, B는 A 내부에서만 사용된다. 내 프로젝트에서는 B를 직접 import할 수 없다.
내 프로젝트 → A 모듈 --impl--> B 라이브러리
↑ ↑
└── B의 클래스를 사용 불가 ────┘
언제 쓰는가: A 모듈이 내부적으로만 B를 사용하고, public API에 B의 타입이 노출되지 않을 때.
// commons-web-client/build.gradle.kts
dependencies {
implementation(libs.spring.boot.starter)
implementation(libs.spring.web)
implementation(libs.spring.security.oauth2.jose)
}
Spring 관련 의존성을 implementation으로 선언했다. 이유는:
- commons-web-client 내부에서 WebClient를 만들 때 Spring을 쓰지만
- 최종 사용처(downtime 등)는 어차피 자체적으로 Spring 의존성을 갖고 있으므로
- 굳이 전이시킬 필요가 없다
implementation의 장점:
- 빌드 속도 - B가 변경되어도 A만 다시 컴파일하면 된다.
api면 내 프로젝트까지 다시 컴파일해야 한다. - 의존성 충돌 방지 - 내 프로젝트에 불필요한 라이브러리가 classpath에 올라오지 않는다.
- 캡슐화 - 내부 구현을 숨길 수 있다.
compileOnly
dependencies {
compileOnly("com.example:B:1.0")
}
컴파일할 때만 classpath에 포함되고, 런타임(실행 시)에는 포함되지 않는다.
컴파일 시: A 모듈 → B 라이브러리 (있음)
런타임 시: A 모듈 → B 라이브러리 (없음!)
언제 쓰는가:
- 선택적 기능 - “사용처가 B를 가져오면 동작하고, 안 가져오면 해당 기능은 비활성”
- 컴파일에만 필요한 경우 - 어노테이션 프로세서(Lombok 등)
// commons-web-server/build.gradle.kts
dependencies {
// Spring Servlet (compileOnly - 사용처에서 Servlet/Reactive 선택)
compileOnly(libs.spring.boot.starter.webmvc)
// Spring Reactive (compileOnly - 사용처에서 Servlet/Reactive 선택)
compileOnly(libs.spring.boot.starter.webflux)
compileOnly(libs.kotlinx.coroutines.core)
// jOOQ (optional - for JooqQueryBuilder)
compileOnly(libs.jooq)
// MyBatis (optional - for MybatisSqlProvider)
compileOnly(libs.mybatis.spring.boot.starter)
}
commons-web-server는 Servlet(WebMVC)과 Reactive(WebFlux) 둘 다 지원하는 라이브러리다.
두 가지를 전부 compileOnly로 선언해서, 사용처가 둘 중 하나를 선택하도록 했다.
- downtime 프로젝트는 WebFlux를 선택 →
spring-boot-starter-webflux를 자체 의존성에 추가 - 다른 프로젝트가 WebMVC를 선택 →
spring-boot-starter-webmvc를 자체 의존성에 추가
jOOQ와 MyBatis도 마찬가지다. commons-web-server에 JooqQueryBuilder와 MybatisSqlProvider가 둘 다 있지만, 사용처가 어떤 DB 접근 기술을 쓰느냐에 따라 필요한 것만 가져간다.
비교 요약
컴파일 시 런타임 시 사용처에 전이
api O O O
implementation O O X
compileOnly O X X
판단 기준
의존성을 추가할 때 이렇게 질문하면 된다:
1단계: 런타임에 필요한가?
- 아니오 →
compileOnly(Lombok, 선택적 기능)
2단계: 내 public API에 이 타입이 노출되는가?
- 예 →
api(메서드 파라미터/리턴 타입에 사용되는 경우) - 아니오 →
implementation(내부에서만 사용)
헷갈리면 implementation을 쓰자. 나중에 사용처에서 “이 타입을 직접 못 쓰겠다”는 컴파일 에러가 나면 그때 api로 바꾸면 된다. 반대로 api를 남용하면 의존성이 과도하게 퍼져서 돌이키기 어렵다.
참고: 일반 애플리케이션에서는?
라이브러리가 아닌 최종 실행 애플리케이션(downtime-api 같은)에서는 api와 implementation의 차이가 거의 없다. 어차피 이 프로젝트를 의존하는 다른 프로젝트가 없기 때문이다. 그래서 애플리케이션 모듈에서는 보통 implementation만 사용한다.
댓글