Gradle에서 의존성을 구분하는 핵심 이유는 “언제 필요한 라이브러리인가?”를 명확히 해서 빌드/실행/배포를 최적화하기 위해서다.
.class로 컴파일할 때 필요한 라이브러리(코드에서 직접 참조하는 경우가 많음)이 구분이 되면 아래 효과가 생긴다.
implementation은 컴파일 시점과 런타임 시점 모두 필요한 라이브러리에 쓴다.
예시
spring-boot-starter-webspring-boot-starter-data-jpacom.fasterxml.jackson.core:jackson-databindcompileOnly는 컴파일 시점에만 필요하고 런타임에는 필요하지 않은 의존성을 선언한다.
대표 사례가 Lombok이다.
@Getter, @Setter 같은 어노테이션은 “런타임에 동작하는 기능”이라기보다, 컴파일 타임에 코드를 생성/변환하도록 돕는 성격이 강하다.주의할 점
compileOnly + annotationProcessor를 같이 쓴다.Gradle(특히 5 이후)에서는 어노테이션 기반 코드 생성/처리를 명확히 하기 위해 annotationProcessor를 많이 사용한다.
예시(가장 흔한 형태)
compileOnly + annotationProcessorruntimeOnly는 컴파일에는 필요 없고, 실행 시에만 필요한 의존성을 선언한다.
대표 예시는 JDBC 드라이버다.
com.mysql.cj.jdbc.Driver 클래스를 직접 import해서 쓰기보다는,예시
추가로 “로그 의존성”은 케이스가 조금 갈린다.
실무에서는 위 3개만큼이나 테스트 스코프도 중요해서 같이 알아두는 걸 추천한다.
testImplementation: 테스트 코드 컴파일 + 실행에 필요한 라이브러리(JUnit, Mockito 등)testRuntimeOnly: 테스트 실행에만 필요한 라이브러리dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
내 프로젝트에서의 dependencies scope 예시
| 스코프 | 언제 필요? | 포함 범위(클래스패스) | 주로 쓰는 상황 | 예시 |
|---|---|---|---|---|
implementation | 컴파일 + 런타임 | main 컴파일 O / main 런타임 O | 애플리케이션 코드에서 직접 사용하는 라이브러리 | Spring Web, Spring Data JPA, Jackson |
compileOnly | 컴파일만 | main 컴파일 O / main 런타임 X | 컴파일 때만 타입/어노테이션이 필요하고 실행 시엔 필요 없는 라이브러리 | Lombok(보통 compileOnly로 둠) |
annotationProcessor | 컴파일(어노테이션 처리) | main 컴파일 단계에서 프로세서 동작 / 런타임 X | 어노테이션 기반 코드 생성/변환이 필요할 때 | Lombok, MapStruct, (케이스에 따라) QueryDSL |
runtimeOnly | 런타임만 | main 컴파일 X / main 런타임 O | 실행 시점에만 필요한 구현체/드라이버 | MySQL/PostgreSQL JDBC 드라이버 |
testImplementation | 테스트 컴파일 + 실행 | test 컴파일 O / test 런타임 O | 테스트 코드에서 직접 사용하는 라이브러리 | JUnit, Mockito, spring-boot-starter-test |
testRuntimeOnly | 테스트 런타임만 | test 컴파일 X / test 런타임 O | 테스트 실행 환경에서만 필요한 구성요소 | JUnit 플랫폼 런처 등(상황에 따라) |
Lombok을 compileOnly + annotationProcessor로 같이 두는 이유는 역할이 분리돼 있기 때문이야. compileOnly는 컴파일할 때 Lombok 관련 타입/어노테이션을 인식하게 해주고, annotationProcessor는 실제로 컴파일 과정에서 @Getter, @Builder 같은 어노테이션을 처리해서 보이지 않는 코드(메서드 등)를 생성해준다.
JDBC 드라이버를 runtimeOnly로 두는 이유는 보통 애플리케이션 코드에서 드라이버 클래스를 직접 호출하지 않고, 실행 시점에 DB 연결을 위해 드라이버가 로딩되기 때문이야. 즉 컴파일 자체에는 필요 없고, 서버가 뜨고 DB에 붙는 순간에만 필요하니까 런타임 의존성으로 분리하는 게 자연스럽다.
추가로 @Transactional(readOnly = true) 같은 최적화나, JPA/Hibernate 설정에 따라 “실제로 flush가 언제 일어나는지”가 성능에 영향을 줄 수 있어서, 조회 전용/쓰기 작업을 스코프와 트랜잭션 전략으로 분리해두면 디버깅이 훨씬 쉬워진다.