dependencies
implementation & api
컴파일 타임과 런타임 모두에 걸쳐서 사용자에게 의존성을 부여하는 지시어이다.
api
컴파일 타임과 런타임에 사용자에게 의존성을 노출시킨다.
의존 라이브러리가 수정되는 경우 해당 라이브러리를 의존하는 모든 모듈들을 재빌드한다.
implementation
내부적으로만 사용되고 사용자에게는 의존성을 노출시키지 않게 선언한다.
의존 라이브러리를 수정해도 직접적으로 사용하는 모듈까지만 재빌드한다.
클래스 A, B, C가 있다.
B는 A를 호출한다. (B는 A를 의존)
C는 B를 호출한다. (C는 A, B를 의존)
즉, A > B > C 와 같은 의존성을 보인다.
api를 사용해 A에 의존성을 부여했다고 하자.
만약 A가 수정된다면 A를 직접적으로 참조하는 B와, 간접적으로 참조하는 C 모두 새롭게 빌드되어야 한다.
implementaion을 사용해서 A에 의존성을 부여했다면,
A를 직접적으로 참조하는 B만 새롭게 빌드하게 된다.
따라서 implementation을 사용해 의존성을 부여하면 빌드시간도 빨라지고 결과물도 가벼워지는 장점이 있다.
무엇보다 중요한 문제는 api를 사용해 의존성을 부여하면 classPath를 통해 의존성이 모두 노출되기 때문에 보안상의 문제가 있을 수 있다고 한다.
반면에 implementation은 의존성을 외부로 노출시키지 않기 때문에 안전하다고 한다.
공식문서 비교 예제
// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class HttpClientWrapper {
private final HttpClient client; // private member: implementation details
// HttpClient is used as a parameter of a public method
// so "leaks" into the public API of this component
public HttpClientWrapper(HttpClient client) {
this.client = client;
}
// public methods belongs to your API
public byte[] doRawGet(String url) {
HttpGet request = new HttpGet(url);
try {
HttpEntity entity = doGet(request);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
entity.writeTo(baos);
return baos.toByteArray();
} catch (Exception e) {
ExceptionUtils.rethrow(e); // this dependency is internal only
} finally {
request.releaseConnection();
}
return null;
}
// HttpGet and HttpEntity are used in a private method, so they don't belong to the API
private HttpEntity doGet(HttpGet get) throws Exception {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
System.err.println("Method failed: " + response.getStatusLine());
}
return response.getEntity();
}
}
HttpClientWrapper의 public 생성자는 매개변수로 HttpClient를 사용한다.
따라서 외부로 노출되기 때문에 api에 속할 수 있다.
반면에 ExceptionUtils에서 가져온 commons-lang은 doRawGet 매서드의 내부에서만 사용되기 때문에 implementation을 생각할 수 있다.
(외부에서의 접근 여부에 따라 달라지는 듯하다.)
따라서 다음과 같이 선언할 수 있다.
dependencies {
api 'org.apache.httpcomponents:httpclient:4.5.7'
implementation 'org.apache.commons:commons-lang3:3.5'
}
compileOnly
lombok과 같이 컴파일시에만 필요한 의존성을 명시할 수 있다.
runtimeOnly
h2, mysql 같이 런타임시에만 필요한 의존성을 명시한다.
주로 DB, LOG 와 관련된 라이브러리가 해당 지시어를 사용한다.
컴파일시 해당 의존성을 포함하지 않도록 하면 컴파일 시간을 줄일 수 있다.
annotationProcessor
lombok과 같은 어노테이션을 사용할 때 필수로 추가해야 하는 지시어이다.
만약 추가하지 않는다면 컴파일러는 lombok에서 제공하는 어노테이션을 인식하지 못한다. (@getter, @setter)
이는 컴파일러가 어노테이션을 확인할 때 lombok 라이브러리를 확인하라고 요청하는 것이다.
Gradle에서는 컴파일 classPath와 어노테이션 classPath를 분리해서 성능을 향상시키기 때문에
기본적으로 포함되어 있는 어노테이션이 아니라면 annotationProcessor를 사용해 명시적으로 추가해줘야 한다.
testImplementation & testCompileOnly & testRuntimeOnly
Test 할 때만 적용되며, 기본 작동 방식은 위와 동일하다.
Spring 프로젝트를 할 때, 항상 lombok, springfox 같은 외부 라이브러리를 사용하게 된다.
지금까지는 블로그에 적힌대로, 책에 나와있는대로, 강의 자료에 나와있는대로.. 무의식적으로 복붙해서 사용했다.
Gradle dependencies를 이해하지 못하고 막 사용하고 있었다.
이제라도 이해 해보려 해서 다행이지만 지금까지 아무 생각없이 썼다는 사실은 반성해야겠다.
Ref