[Spring] Gradle 의존성 스코프

이지연·2026년 1월 22일

Gradle 의존성 스코프를 나누는 이유

Gradle에서 의존성을 구분하는 핵심 이유는 “언제 필요한 라이브러리인가?”를 명확히 해서 빌드/실행/배포를 최적화하기 위해서다.

  • 컴파일 타임(compile-time) 의존성: 소스 코드를 .class로 컴파일할 때 필요한 라이브러리(코드에서 직접 참조하는 경우가 많음)
  • 런타임(runtime) 의존성: 애플리케이션 실행 중 필요한 라이브러리(코드에서 직접 참조하지 않더라도 실행에 필요할 수 있음)

이 구분이 되면 아래 효과가 생긴다.

  • 불필요한 라이브러리가 배포 산출물/클래스패스에 섞이는 것을 줄임(배포 크기, 충돌 가능성 감소)
  • 빌드 속도 및 의존성 해석 비용 감소
  • “컴파일은 되는데 실행이 안 됨” 같은 문제를 더 빨리 원인 파악 가능

implementation: 컴파일 + 런타임 모두 필요

implementation컴파일 시점과 런타임 시점 모두 필요한 라이브러리에 쓴다.

  • 프로젝트 코드에서 직접 사용하는 라이브러리(예: Spring Web, Jackson, Validation 등)
  • 컴파일러가 타입을 알아야 하고, 실행 시에도 실제 구현이 필요함

예시

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • com.fasterxml.jackson.core:jackson-databind

compileOnly: 컴파일에만 필요(런타임에는 제외)

compileOnly컴파일 시점에만 필요하고 런타임에는 필요하지 않은 의존성을 선언한다.

대표 사례가 Lombok이다.

  • @Getter, @Setter 같은 어노테이션은 “런타임에 동작하는 기능”이라기보다, 컴파일 타임에 코드를 생성/변환하도록 돕는 성격이 강하다.
  • 그래서 런타임 클래스패스에 Lombok이 꼭 필요하진 않다(일반적인 사용 기준)

주의할 점

  • compileOnly로만 두면 “어노테이션 처리”가 실제로 수행되지 않을 수 있다.
  • 그래서 Lombok은 보통 compileOnly + annotationProcessor를 같이 쓴다.

annotationProcessor: 컴파일 시점 코드 생성/처리 담당

Gradle(특히 5 이후)에서는 어노테이션 기반 코드 생성/처리를 명확히 하기 위해 annotationProcessor를 많이 사용한다.

  • 컴파일 시점에 어노테이션 프로세서가 동작하도록 등록
  • Lombok, MapStruct, QueryDSL(설정 방식은 케이스별로 다름) 등이 여기에 해당

예시(가장 흔한 형태)

  • Lombok: compileOnly + annotationProcessor

runtimeOnly: 실행할 때만 필요한 의존성

runtimeOnly컴파일에는 필요 없고, 실행 시에만 필요한 의존성을 선언한다.

대표 예시는 JDBC 드라이버다.

  • 보통 애플리케이션 코드에서 com.mysql.cj.jdbc.Driver 클래스를 직접 import해서 쓰기보다는,
  • 설정을 통해 자동 로딩/연결이 이루어지고 실행 시점에 드라이버가 필요해진다.

예시

  • MySQL 드라이버, PostgreSQL 드라이버 등

추가로 “로그 의존성”은 케이스가 조금 갈린다.

  • 스프링 부트 스타터를 쓰면 로깅은 대개 starter에 같이 묶여 들어오기도 해서, 무조건 runtimeOnly라고 일반화하긴 애매하다.
  • 다만 “실행 환경에서만 붙이는 로깅 구현체” 같은 맥락이라면 runtimeOnly로 분리하는 전략을 쓰기도 한다.

(보충) 테스트 전용: testImplementation / testRuntimeOnly

실무에서는 위 3개만큼이나 테스트 스코프도 중요해서 같이 알아두는 걸 추천한다.

  • testImplementation: 테스트 코드 컴파일 + 실행에 필요한 라이브러리(JUnit, Mockito 등)
  • testRuntimeOnly: 테스트 실행에만 필요한 라이브러리

많이 쓰는 예시 조합(Lombok + DB 드라이버)

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가 언제 일어나는지”가 성능에 영향을 줄 수 있어서, 조회 전용/쓰기 작업을 스코프와 트랜잭션 전략으로 분리해두면 디버깅이 훨씬 쉬워진다.

profile
Eazy하게

0개의 댓글