DEV-STUDY/Spring

[Gradle] java-library 플러그인, implementation()과 api() 차이

HwangJerry 2023. 7. 17. 11:19

java-library와 api()란 무엇인가요?

java-library 플러그인은 java 플러그인을 확장하여 추가적인 기능을 제공합니다. 확장 버전이므로 java-library 플러그인만 추가하면 java 플러그인에서 제공하는 모든 method, configuration 등도 활용할 수 있습니다.

 

 

java-library를 사용함으로써 가장 먼저 얻을 수 있는 것이 바로 api() configuration입니다.

  • api : 모든 소비자의 compile classpath와 runtime classpath 모두에 포함 (다른 모듈에 노출 O)
  • implementation : 내부에서만 사용되는 의존성으로 선언 (다른 모듈에 노출 X)

 

이는 우리가 함수나 필드를 설정할 때 public이나 private처럼 scope를 설정하는 개념이라고 이해해볼 수 있겠습니다.

 

api와 implementation의 차이를 이해해봅시다.

멀티모듈을 구성한다고 할 때, module-api와 module-consumer가 있다고 가정해봅시다. 아래는 module-api의 build.gradle 설정 중 일부입니다.

plugins {
    id 'java-library'
}

dependencies {
    api 'org.apache.httpcomponents:httpclient:4.5.7'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

그리고 module-consumer는 아래와 같이 의존성을 선언해 줍니다.

plugins {
    id 'java'
}

dependencies {
    implementation project(':module-api')
}

 

위처럼 설정한 뒤 빌드를 진행하면 다음과 같은 결과가 됩니다.

  • module-consumer는 module-api에 의존적인 모듈입니다.
  • consumer의 compile classpath와 runtime classpath에 httpClient는 존재 O
  • consumer의 compile classpath에 commons-lang은 존재 X (runtime classpath에는 존재 O)

 

언제 api를 사용해야 할까요?

우리가 private과 public과 같은 접근 제어자를 사용할 때, 기본적으로 안정적인 애플리케이션 동작을 위해 private을 기본적으로 사용하는 것과 같이, 기본적으로는 implementation을 사용하는 것이 좋습니다.

 

그렇다면, 언제 api를 사용해야 할지를 이해해야 합니다. 앞서 api와 implementation을 구분해볼 때 private과 public을 떠올리며 이해해보자고 한 것을 다시 떠올리면 됩니다. 이해하기 쉽도록 위 의존성을 예시로 들어보겠습니다.

 

현재 module-api와 module-consumer가 존재하고, module-consumer가 module-api에 의존하고 있는 상태입니다. 그리고 module-api에는 아래와 같은 클래스가 정의되어 있다고 해 봅시다.

public class HttpClientWrapper {

    private final HttpClient client; // private member로 사용됩니다.

    // HttpClient 는 public 메서드의 파라미터로 사용됩니다.
    public HttpClientWrapper(HttpClient client) {
        this.client = client;
    }

    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); // ExceptionUtils는 메서드 내부에서만 사용됩니다.
        } finally {
            request.releaseConnection();
        }
        return null;
    }

    // HttpGet 와 HttpEntity 는 private 메서드에서만 사용되므로, 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 타입의 파라미터를 선언하고 있습니다. public으로 선언한 메서드이니만큼 module-consumer에서도 이 메서드를 사용할 수 있을텐데요. 이 때, HttpClient 라이브러리를 api로 선언해 두었기 때문에 module-consumer에서도 compile classpath를 인식할 수 있어 컴파일타임에서 문제가 발생하지 않습니다. 만약 HttpClient 라이브러리 의존성을 module-api의 build.gradle에서 implement로 선언해줬다면 api에서 해당 메서드를 사용하려고 할 때 컴파일 에러가 발생했을 겁니다.

 

다른 예시로 public doRawGet 메서드를 확인해봅시다. doRawGet 메서드는 파라미터로는 일반 String 타입을 사용하고 있고, return 값으로도 byte 배열을 반환합니다. 따라서 이 메서드를 사용하는 쪽에서 별다른 라이브러리를 알고 있지 않아도 컴파일에 전혀 문제가 되지 않으니 해당 메서드 내부에서 사용하는 라이브러리 의존성은 implementation으로 선언해줘도 전혀 무방합니다. 위 예시에서는 ExceptionUtils 라는 라이브러리가 적용되었는데, 이를 module-api의 build.gradle에서 implementation으로 선언해줘도 내부 메서드 로직에서만 사용하므로 큰 문제가 발생하지 않는다는 겁니다.

 

마지막으로 private doGet를 볼까요? 사실 private으로 제어해뒀기 때문에 module-consumer에서 사용할 수 없으므로 볼 것도 없습니다. 해당 부분은 module-api 내부에서만 사용할 예정이기 때문에 해당 메서드 내부 로직상에서 사용하는 의존성은 implementation으로 선언해도 전혀 무방합니다.

 

출처: 

 

[Gradle] api와 implementation의 차이 (feat. java-libraly 플러그인)

이전 글에서 살펴본 자바 플러그인에 이어, 이번 글에서는 자바 라이브러리 플러그인에 대해 알아보도록 하겠습니다. 🧐 java-library 플러그인 java-library 플러그인은 java 플러그인을 확장하여 추

ttl-blog.tistory.com