멀티모듈로 구성되어 있는 프로젝트의 테스트코드를 실행해 보았더니
Caused by: java.lang.IllegalStateException: Failed to introspect Class [AccountService] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7]
Caused by: java.lang.NoClassDefFoundError: repository/AccountRepository
Caused by: java.lang.ClassNotFoundException: repository.AccountRepository
이렇게 다른 모듈로 구분되어 있는 클래스파일을 읽어오지 못한다는 에러가 발생했다.
우선 내 build.gradle 파일의 문제가 된 부분은 이렇게 되어있었다.
project(':app') {
dependencies {
compileOnly project(':core')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}
project(':core') {
bootJar.enabled = false
jar.enabled = true
dependencies {
}
}
여기서 저 compileOnly가 문제였다..!
이를 implementation으로 바꾸니 해결되었다.
compileOnly와 implementation은 정확히 어떤 차이가 있을까?
Intellij에서 우측 gradle 탭을 누르면 볼 수 있는 Classpath는 클래스나 jar 파일이 존재하는 위치이다.
이는 compileClasspath와 runtimeClasspath로 나뉜다.
compileClasspath
- 에러 없이 컴파일을 하기 위해 필요한 클래스와 jar들의 경로를 나타낸다.
- 따라서 해당 부분만 잘 설정했다고 해서 애플리케이션이 잘 작동하는 것을 보장하지는 않는다.
런타임에서 필요한 다른 클래스와 jar가 필요할 수 있기 때문이다.
runtimeClasspath
- 애플리케이션이 정상적으로 실행하기 위해 필요한 클래스들과 jar들의 경로를 나타낸다.
🌱 의존성 옵션
implementation
의존 라이브러리 수정 시 본 모듈까지만 재빌드한다.
본 모듈을 의존하는 모듈은 해당 라이브러리의 api 사용 X
api
의존 라이브러리 수정시 본 모듈을 의존하는 모듈들도 재빌드
본 모듈을 의존하는 모듈들도 해당 라이브러리의 api 사용 O
compileOnly
이름에서 유추할 수 있듯이 compile시에만 빌드하고 빌드 결과물에는 포함하지 않는다.
gradle dependency의 comlileClassPath에만 추가된다.
빌드 결과물의 사이즈가 줄어드는 장점이 있다.
runtime 시 필요 없는 라이브러리인 경우(runtime 환경에 이미 라이브러리가 제공되고 있는 경우 등) 사용한다.
ex) lombok: getter, setter 등 필요한 것들을 생성시킨 후 런타임 때에는 사용하지 않음
-> gradle5부터 compileOnly 대신 annotationprocessor 사용
annotationProcessor
annotation processor 명시 (ex:lombok)
runtimeOnly
컴파일 타임에는 필요 없지만 런타임에서는 의존하는 경우
gradle dependency의 runtimeClassPath에만 추가된다.
해당 클래스에서 코드 변경이 발생해도 컴파일을 다시 할 필요가 없다는 장점이 있다.
ex) DB나 로그 관련 dependency
testImplementation
테스트 코드를 수행할 때만 적용.
✅ 예시
class A {
public static void main(String[] args) {
B b = new B();
}
}
class B {
public B() {
return new C().sum();
}
}
class C {
public int sum() {
return 5;
}
}
위 코드에서 내가 정의한 A는 외부 라이브러리 B를 의존하고 있다. 외부 라이브러리 B는 C를 의존하고 있다.
컴파일을 위해서는 사용자(=나)는 A와 B에 대한 경로만 가지고 있으면 된다.
하지만 런타임을 위해서는 C에 대한 정보도 갖고 있어야 한다.
-> 이는 A와 B에 대한 컴파일 타임 의존성을 갖고 있는 것이며 A, B, C에 대해 런타임 의존성을 갖고 있는 것이다.
1. B에 대한 dependency를 implementation 로 설정하는 경우
compile path에는 B만 들어가기 때문에 컴파일 타임에서 사용자는 C를 알 수가 없으며 C가 수정되어도 A를 다시 컴파일할 필요가 없다.
A 모듈 수정 시 A를 직접적으로 의존하는 모듈(B)까지만 rebuild 한다. = 빠르다
직접적으로 의존하는 모듈만 노출되기 때문에 사용자에게 필요이상의 API 노출을 막는다.
2. api로 설정한 경우
compile path에 B와 C가 들어간다.
컴파일 타임에서 사용자는 C를 알고 있으며 C가 수정이 돼 재빌드 해야 하는 상황이 오면 A 또한 재빌드 해야 한다.
A모듈 수정 시 A를 의존하는 모든 모듈(B, C)이 rebuild 된다. = 시간이 오래 걸린다.
configuration | 사용가능한 시간 | 사용자의 컴파일 시점에 노출되는가? | 사용자의 런타임에 노출되는가? |
implementation | 컴파일 런타임 |
X | O |
api | 컴파일 런타임 |
O | O |
compileOnly | 컴파일 | X | X |
runtimeOnly | 런타임 | X | O |
출처
https://bepoz-study-diary.tistory.com/372
'Back-end' 카테고리의 다른 글
JPA Entity @Builder 등 어노테이션 주의점 (0) | 2023.03.06 |
---|---|
[Gradle] Spring boot 3.0.0 이상 Querydsl build.gradle + 멀티 모듈 프로젝트 (0) | 2023.02.27 |
[Spring Boot] DB Schema 및 Data 초기화 schema.sql data.sql (0) | 2023.02.19 |
[AWS] SpringBoot + gradle 프로젝트(jar) EC2 서버 배포 (0) | 2023.02.11 |
[Spring Security] CSRF disable? (0) | 2023.01.26 |