implementation과 api, compile
build.gradle에서 사용하는 implementation과 api는 모두 라이브러리를 적용시키는 키워드이다. 오래된 Gradle 버전에는 implementation이 없어서 compile을 사용했고, 새로운 버전(6.x 이후 버전)에서 compile이 deprecated 되면서 compile 대신에 api 키워드가 사용되게 되었다. 이 때문에 오래된 프로젝트들의 build.gradle 파일들을 보면 api와 compile을 implementation 대신 쓰는 것을 볼 수 있다.
즉, api와 compile은 같은 역할을 한다. 하지만 Gradle은 api나 compile를 사용하는 것을 권장하지 않는다. api를 통해 라이브러리를 가져올 경우 라이브러리가 적용되는 범위 때문이다.
*compile은 작성 시점의 Gradle version에서는 사용 가능하지만 deprecated 되었으므로 차후 버전에서 제거될 것이다.
api, compile의 라이브러리 적용 범위
모듈에서 api 나 compile을 사용해 라이브러리를 가져오게 되면, 해당 라이브러리는 해당 모듈을 의존하는 모듈에도 가져와진다. 예를 들어 ModuleA에서 api를 사용해 Library를 가져온 다음 ModuleB에서 api를 사용해 ModuleA를 가져올 경우 Library도 같이 가져와진다.
이것은 프로그램 유지보수성 측면에서 매우 좋지 않다. Module을 사용할 때는 Module의 인터페이스만이 외부에 노출되어야 하는데, Library의 인터페이스까지 같이 노출되기 때문이다. 또한 Leak이 일어나는데, ModuleA를 빌드하면서 이미 들어간 Library가 중복으로 ModuleB에도 추가되기 때문이다.
*라이브러리도 패키징된 모듈이다.
특히 클린아키텍처적인 측면에서 Entity 모듈은 비즈니스 로직을 만드는 어플리케이션의 Usecase 모듈에서만 사용되고, Usecase 모듈은 Controller 모듈에서만 사용되는 구조로 이루어져 있어 각 레이어별로 분리가 확실하게 일어나는데, 만약 Controller에서 Entity 모듈을 알게된다면 모듈 계층을 건너뛴 참조가 일어날 수 있게 되어 레이어를 나눈 이유가 없어지게 된다.
따라서 이를 해결하기 위해 한 모듈에서 사용하는 라이브러리는 그 모듈에서만 사용될 수 있도록 implementation을 써야 한다.
implementation의 적용 범위
모듈에서 implementation을 사용해 가져오는 라이브러리는 해당 모듈을 의존하는 모듈에는 가져와지지 않는다. 예를 들어 ModuleA에서 implementation을 이용해 Library를 가져온다음 ModuleB에서 implementation을 사용해 moduleA를 가져오면 Library는 가져와지지 않는다. Library는 ModuleA에 캡슐화되어 ModuleB에 노출되지 않는다.
이 방식을 이용하면 모듈 수준에서 공개되는 코드를 모듈의 인터페이스로 제한할 수 있어 모듈간 의존성을 줄이게 된다. 위 그림1에서는 ModuleB가 moduleA와 Library에 모두 의존했지만, 그림2에서는 ModuleB가 ModuleA에만 의존하는 것을 볼 수 있다. 프로그래밍 유지보수의 핵심은 모듈간 의존성을 줄이는 것이 1순위이다. 수많은 라이브러리(모듈)들이 범람하고, 하나의 프로그램에 코드가 수십만 줄이 넘어가는 현대에는 모듈간 의존성을 줄여야 프로그램을 유지보수하기 편해지기 때문이다.
api는 절대 쓰지 말아야 한다. implementation만 쓰자.