티스토리 뷰
build.gradle에 의존성을 추가하다 보면 다음과 같은 설정을 자주 보게 됩니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.testcontainers:mysql'
}
그런데 다음과 같은 의문이 생깁니다.
- 왜 Spring MVC는
implementation일까요? - MySQL 드라이버는 왜
runtimeOnly일까요? - Testcontainers는 왜
testImplementation일까요? compileOnly는 정확히 언제 사용하는 걸까요?
이 글에서는 Gradle configuration을 단순히 외우기보다, 어떤 기준으로 선택하는지 이해해보겠습니다.
1. 컴파일과 런타임의 차이
먼저 컴파일과 런타임을 구분해야 합니다.
컴파일
Java 소스 코드를 JVM이 실행할 수 있는 바이트코드로 변환하는 과정입니다.
.java 파일 → 컴파일러 → .class 파일
이때 소스 코드에서 사용하는 클래스와 인터페이스를 컴파일러가 알아야 합니다.
예를 들어 다음 코드가 있습니다.
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TicketController {
}
컴파일러는 RestController가 어떤 타입인지 알아야 합니다. 따라서 Spring MVC 라이브러리가 컴파일 classpath에 있어야 합니다.
런타임
컴파일된 애플리케이션을 실제로 실행하는 시점입니다.
./gradlew bootRun
런타임에는 다음과 같은 작업이 발생할 수 있습니다.
- 데이터베이스 연결
- JDBC 드라이버 탐색
- 리플렉션
- ServiceLoader 기반 구현체 탐색
- Spring Boot 자동 구성
- 설정 파일을 통한 구현체 선택
컴파일할 때 직접 타입을 사용하지 않았더라도 실행 중에 필요한 라이브러리가 있을 수 있습니다.
2. Gradle configuration은 의존성의 출입증입니다
implementation, runtimeOnly, testImplementation은 단순한 라벨이 아닙니다.
Gradle에 다음 정보를 알려주는 설정입니다.
- 언제 필요한가?
- 어떤 소스에서 사용하는가?
- 다른 프로젝트에 공개해야 하는가?
Gradle은 선언된 configuration을 바탕으로 작업별 classpath를 만듭니다.
의존성 선언
↓
Gradle이 작업별 classpath 계산
↓
컴파일·실행·테스트가 classpath 사용
대표적인 classpath는 다음과 같습니다.
| Classpath | 사용 시점 |
|---|---|
compileClasspath |
main 코드 컴파일 |
runtimeClasspath |
애플리케이션 실행 |
testCompileClasspath |
테스트 코드 컴파일 |
testRuntimeClasspath |
테스트 실행 |
Configuration과 실제 classpath의 관계를 단순화하면 다음과 같습니다.
implementation
├─ compileClasspath
└─ runtimeClasspath
runtimeOnly
└─ runtimeClasspath
testImplementation
├─ testCompileClasspath
└─ testRuntimeClasspath
3. implementation
implementation은 현재 애플리케이션의 컴파일과 실행에 필요한 기본 configuration입니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
}
다음과 같이 애플리케이션 코드에서 직접 Spring 타입을 사용한다고 가정해보겠습니다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TicketController {
@GetMapping("/tickets")
public String tickets() {
return "tickets";
}
}
이 코드를 컴파일하려면 Spring MVC가 필요합니다.
또한 애플리케이션을 실행하려면 Spring MVC의 실제 구현체도 필요합니다.
컴파일: Spring 타입을 직접 사용
실행: Spring MVC 애플리케이션 실행
따라서 implementation을 사용합니다.
implementation을 선택하는 기준
다음 질문에 “예”라고 답할 수 있다면 implementation을 우선 고려하면 됩니다.
- main 코드에서 직접 import하는가?
- 어노테이션이나 인터페이스로 사용하는가?
- 컴파일할 때 필요한가?
- 애플리케이션 실행에도 필요한가?
현재처럼 하나의 Spring Boot 애플리케이션을 만드는 경우에는 대부분의 일반적인 라이브러리가 implementation으로 들어갑니다.
4. runtimeOnly
runtimeOnly는 컴파일할 때 직접 타입을 사용할 필요는 없지만, 애플리케이션 실행 중에는 필요한 의존성입니다.
대표적인 예가 MySQL JDBC 드라이버입니다.
dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
}
애플리케이션 코드에서 보통 다음과 같은 코드를 직접 작성하지 않습니다.
import com.mysql.cj.jdbc.Driver;
대신 Spring Boot와 JDBC가 설정된 JDBC URL을 바탕으로 실행 중에 드라이버를 찾습니다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/ticketing
MySQL 드라이버의 사용 시점은 다음과 같습니다.
컴파일: 애플리케이션 코드에서 드라이버 타입을 직접 사용하지 않음
실행: MySQL 연결을 만들기 위해 드라이버 필요
따라서 runtimeOnly가 적절합니다.
implementation으로 넣으면 안 되는가?
다음과 같이 선언해도 애플리케이션은 동작할 수 있습니다.
implementation 'com.mysql:mysql-connector-j'
하지만 runtimeOnly가 의도를 더 정확하게 표현합니다.
이 애플리케이션 코드는 MySQL 드라이버 구현 타입을 직접 사용하지 않지만, 실행할 때는 드라이버가 필요합니다.
즉, runtimeOnly는 의존성의 사용 범위를 더 좁게 선언하는 방법입니다.
5. testImplementation
testImplementation은 테스트 코드의 컴파일과 실행에만 필요한 의존성입니다.
dependencies {
testImplementation 'org.testcontainers:mysql'
}
Testcontainers를 사용하는 테스트는 다음과 같이 작성할 수 있습니다.
MySQLContainer<?> mysql =
new MySQLContainer<>("mysql:8.4");
이 타입은 운영 애플리케이션 코드가 아니라 테스트 코드에서만 사용합니다.
운영 애플리케이션: Testcontainers 불필요
테스트 코드: MySQLContainer 타입 필요
따라서 testImplementation을 사용합니다.
중요한 점
testImplementation은 테스트 컴파일에만 사용된다는 뜻이 아닙니다.
테스트 코드에서 직접 타입을 사용해야 하므로 다음 두 시점에 필요합니다.
테스트 컴파일: MySQLContainer 타입을 알아야 함
테스트 실행: 실제 컨테이너를 실행해야 함
다만 운영 애플리케이션의 main runtime classpath에는 포함되지 않습니다.
6. compileOnly
compileOnly는 컴파일할 때는 필요하지만, 애플리케이션 실행 시에는 해당 실행 환경이 제공한다고 가정하는 의존성입니다.
dependencies {
compileOnly 'jakarta.servlet:jakarta.servlet-api:버전'
}
의미는 다음과 같습니다.
컴파일: 필요
애플리케이션 실행 classpath: 포함하지 않음
예를 들어 애플리케이션 서버가 Servlet API를 제공한다고 가정할 수 있다면, 소스 코드를 컴파일하기 위한 타입 정보만 필요할 수 있습니다.
compileOnly를 잘못 사용하면?
실행할 때 실제로 필요한 라이브러리를 compileOnly로 선언하면 문제가 발생합니다.
컴파일 성공
↓
애플리케이션 실행
↓
ClassNotFoundException
또는 NoClassDefFoundError
따라서 compileOnly를 사용할 때는 반드시 다음을 확인해야 합니다.
실행 환경이 정말 이 라이브러리를 제공하는가?
실행 환경이 제공하지 않는다면 implementation을 사용해야 합니다.
7. Lombok은 왜 compileOnly인가?
Lombok은 조금 특수한 예시입니다.
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
다음과 같은 코드를 작성한다고 가정해보겠습니다.
@Getter
public class Ticket {
private Long id;
}
Lombok은 컴파일 과정에서 getter 메서드를 생성합니다.
public Long getId() {
return id;
}
실행 시점에는 이미 getter 코드가 생성되어 있으므로 Lombok 라이브러리 자체가 필요하지 않습니다.
각 설정의 역할은 다음과 같습니다.
| 설정 | 역할 |
|---|---|
compileOnly |
컴파일러가 Lombok 어노테이션 타입을 알 수 있게 함 |
annotationProcessor |
컴파일 중 실제 코드를 생성 |
즉 Lombok은 다음과 같습니다.
컴파일: 필요
코드 생성: 필요
실행: 불필요
8. testRuntimeOnly
testRuntimeOnly는 테스트 소스 코드에서 타입을 직접 사용하지 않지만, 테스트 실행 순간에 필요한 의존성입니다.
dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
테스트 코드에서 직접 junit-platform-launcher의 타입을 import하지는 않지만, 테스트 실행 엔진이 동작하려면 필요할 수 있습니다.
테스트 코드 컴파일: 불필요
테스트 실행: 필요
이런 경우 testRuntimeOnly를 사용합니다.
9. api와 implementation
api는 여러 프로젝트가 함께 사용하는 라이브러리 모듈에서 중요합니다.
plugins {
id 'java-library'
}
dependencies {
api 'com.fasterxml.jackson.core:jackson-databind:버전'
implementation 'org.apache.commons:commons-lang3:버전'
}
내 라이브러리의 public API에 외부 라이브러리 타입이 등장한다면 api를 고려합니다.
public interface TicketPolicy {
ExternalTicketType findTicket(Long ticketId);
}
ExternalTicketType이 외부 라이브러리의 타입이라면, 이 라이브러리를 사용하는 프로젝트도 해당 타입을 알아야 컴파일할 수 있습니다.
이때 api를 사용합니다.
반대로 외부 라이브러리를 내부 구현에서만 사용한다면 implementation이 적절합니다.
현재처럼 하나의 Spring Boot 애플리케이션을 만드는 단계에서는 api보다 다음 설정을 먼저 이해하는 것이 좋습니다.
implementationruntimeOnlytestImplementationcompileOnly
10. configuration 선택 순서
새로운 의존성을 추가할 때는 다음 순서로 판단하면 됩니다.
1. main 코드가 라이브러리 타입을 직접 사용하는가?
├─ 실행에도 필요하다
│ └─ implementation
└─ 실행 환경이 제공한다
└─ compileOnly
2. main 코드에서 직접 사용하지 않지만
실행 중 프레임워크가 찾는가?
└─ runtimeOnly
3. src/test에서만 사용하는가?
├─ 테스트 코드가 타입을 직접 사용한다
│ └─ testImplementation
└─ 테스트 실행 순간에만 필요하다
└─ testRuntimeOnly
더 쉽게 요약하면 다음과 같습니다.
| 질문 | 선택 |
|---|---|
| 애플리케이션 코드가 직접 사용하고 실행에도 필요한가? | implementation |
| 컴파일에는 필요하지만 실행 환경이 제공하는가? | compileOnly |
| 코드에서 직접 사용하지 않지만 실행 중 필요한가? | runtimeOnly |
| 테스트 코드에서 직접 사용하는가? | testImplementation |
| 테스트 실행 순간에만 필요한가? | testRuntimeOnly |
애매할 때는 implementation을 기본값으로 선택하고, 실제 사용 범위가 명확해졌을 때 configuration을 좁히면 됩니다.
11. Gradle classpath 직접 확인하기
Gradle이 실제로 어떤 의존성을 넣었는지 확인할 수 있습니다.
컴파일 classpath
./gradlew dependencies --configuration compileClasspath
애플리케이션 실행 classpath
./gradlew dependencies --configuration runtimeClasspath
테스트 실행 classpath
./gradlew dependencies --configuration testRuntimeClasspath
특정 의존성이 왜 포함됐는지 확인하려면 dependencyInsight를 사용합니다.
./gradlew dependencyInsight \
--dependency mysql-connector-j \
--configuration runtimeClasspath
확인할 때는 다음 질문을 던져보면 됩니다.
- MySQL 드라이버가
compileClasspath에는 없는가? - MySQL 드라이버가
runtimeClasspath에는 있는가? - Testcontainers가 운영용
runtimeClasspath에는 없는가? - 테스트 의존성이
testRuntimeClasspath에는 포함되어 있는가? - 예상하지 못한 전이 의존성이 추가되지는 않았는가?
마무리
Gradle configuration은 의존성을 아무 곳에나 넣는 문법이 아닙니다.
누가, 언제, 어디까지 이 라이브러리를 볼 수 있어야 하는지를 선언하는 규칙입니다.
새로운 의존성을 추가할 때는 다음 순서로 생각하면 됩니다.
1. main 코드가 타입을 직접 사용하는가?
2. 실행에도 필요한가?
3. 실행 환경이 대신 제공하는가?
4. 테스트에서만 사용하는가?
5. 테스트 코드가 직접 타입을 사용하는가?
최종적으로 다음처럼 기억하면 됩니다.
직접 사용 + 실행에도 필요
→ implementation
컴파일에는 필요하지만 실행 환경이 제공
→ compileOnly
직접 사용하지 않지만 실행 중 필요
→ runtimeOnly
테스트 코드에서 직접 사용
→ testImplementation
테스트 실행 순간에만 필요
→ testRuntimeOnly
참고 자료
'개발 > Spring & Spring Boot' 카테고리의 다른 글
| 초기 세팅시 스프링부트 메인을 실행했는데 꺼지는 이유 (1) | 2025.04.10 |
|---|---|
| 메소드 파라미터마다 Final 하는 이유 (0) | 2023.10.21 |
| [Spring] 프록시 객체를 이용하는 이유 (0) | 2023.05.29 |
| API 응답값으로 MAP을 지양하는이유 (3) | 2022.08.21 |
| @WebMvcTest & @AutoConfigureMockMvc (1) | 2022.08.20 |
- Total
- Today
- Yesterday
- 3Way Handshake
- 라우팅
- 프로그래머스
- ec2
- 스프링
- 자바
- 회고
- 프로토콜
- 네트워크
- 스위치
- tcp
- dto
- 삽질
- Docker
- 개발자
- java
- spring
- rds
- aws
- osi7계층
- Spring Boot
- 회고록
- s3
- 계층
- 라우터
- 요금
- 개발
- lambda
- SpringBoot
- 초보
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |