멀티모듈로 구성되어 있는 프로젝트의 테스트코드를 실행해 보았더니 

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 파일이 존재하는 위치이다.

이는 compileClasspathruntimeClasspath로 나뉜다.

 

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

https://cantcoding.tistory.com/59

https://giron.tistory.com/101