누군가(어쩌면 동료)가 만든 모듈을 참조하기로 결정하면 다음과 같은 오류를 만나는 경우가 있습니다.

Cannot access class 'retrofit2.Response'. Check your module classpath 
for missing or conflicting dependencies

도입

관련 의존성을 찾을 수 없는 이유는 프로젝트의 참조로 설정된 의존성의 API 노출 영역에 서드파티 의존성이 노출되었기 때문입니다. 다시 말하면, API 노출 영역에 implementation 의존성과 같은 다른 의존성이 있고 클라이언트가 해당 API에 호출하면 위와 같이 의존성 문제가 생기게 됩니다.

샘플 프로젝트로 저장소 서비스가 있는 라이브러리를 사용하는 컨트롤러가 있다고 해봅시다. 사용하려는 메서드는 retrofit의 Response 클래스를 의존하고 있습니다.

classDiagram RepositoryService ..> Response UserController o-- RepositoryService class RepositoryService{ <<dependency>> +findRepositories() Response~RepoList~ } class UserController{ <<our_project>> listAllRepositoryNames(String username) } <<transitive_dependency>> Response
fun findRepositories(username: String): retrofit2.Response<List<Repo>>

혹할만한 해결책

먼저, 클라이언트에서 참조하는 의존성의 코드를 수정할 수 없다면, 해당 의존성의 의존성 (영어로는 Transitive dependency라고 표현합니다)을 직접 의존성으로 추가해서 클래스 오류를 피할 수 있기는 합니다.

// your_project/build.gradle
dependencies {
    // ...
    implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}"
}

그러나 몇 가지 문제가 있습니다. 일단 주먹구구식으로 해결하는 방식입니다. 해당 의존성에 어떤 의존성이 얼마나 필요한지 정확히 알 수가 없기 떄문에 의존성 문제가 없어질 때까지 계속해서 의존성을 추가해야 합니다. 그리고 클라이언트 입장에서 관련된 모든 의존성을 알아야 하므로 좋지 않은 모듈 설계에 해당한다. 결국 관리해야 하는 의존성이 많아질수록, 프로젝트를 관리하기가 힘들어지고, 이는 품질을 해치고 복잡도를 높이게 됩니다.

두 번째 방법으로, 해당 의존성을 수정할 수 있으면, 참조하는 의존성을 Gradle에서 api로 추가할 수도 있습니다. implementationapi의 차이에 대해 자세히 알고 싶으면 공식 설명서가 읽기 가장 좋습니다.

// dependency/build.gradle
plugins {
    id 'java-library'
}

dependencies {
    // ...
    api "com.squareup.retrofit2:retrofit:${retrofitVersion}"
}

데메테르1 법칙

데메테르 법칙에 대한 글은 인터넷에 수도 없이 많습니다. 간단히 설명하면, 클래스 간에 낮은 결합도를 달성하는 설계 가이드라인입니다. 이 가이드라인은 다음과 같은 특성이 있습니다.

  • 각 단위는 다른 단위(현재 단위와 “밀접하게” 관련된 단위)에 대해 제한된 지식만 가지고 있어야 한다.
  • 각 단위는 친구하고만 이야기해야 한다. 모르는 사람과 이야기하지 않는다.
  • 가까운 친구하고만 이야기한다. 2

위 모듈 문제는 데메테르 법칙 위반이라고 할 수 있습니다. 이는 다시 클린 코드에서 정확히 설명된 부분입니다3.

G36: 추이적 탐색을 피하라

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다. 좀 더 구체적으로, A가 B를 사용하고 B가 C를 사용한다 하더라도 A가 C를 알아야 할 필요는 없다는 뜻이다.

진정한 해결책

다시 말하자면, 라이브러리는 통제권이 있으면 좋은 클래스 설계로 개선할 수 있습니다. 가이드라인을 따르면 샘플 프로젝트의 클라이언트 모듈은 retrofit의 어떤 클래스도 알 필요가 없습니다. 대상 메서드(API)의 목적을 다시 생각해보시길 바랍니다. 클라이언트에게 꼭 필요한 것이 무엇인지를 고민해보세요.

샘플 프로젝트에서는 클라이언트가 응답의 내용(레파지토리 리스트)에만 관심이 있습니다. 응답 상태에 대해서는 신경쓰지 않습니다. 결과를 성공적으로 얻었는지 여부를 결정하기 위해서만 응답을 사용하고 있습니다.

따라서 메소드는 레파지토리 리스트를 리턴하거나, 잘못되면 예외를 던지기만 하면 됩니다. 간단합니다.

fun findRepositories(username: String): List<Repo>

이제 클라이언트는 관심있는 구조에 집중할 수 있습니다. 샘플 프로젝트의 전체 소스 코드가 궁금하시면, Github를 참고해주세요.

다른 방법으로, retrofit2.Response<List<Repo>> 대신 List<Repo>을 래핑한 어떤 클래스를 만들 수도 있습니다. 응답 중에서 라이브러리가 노출해도 괜찮은 정보를 클라이언트가 원하는 경우, 이 방법이 적절합니다. 예를 들어 위와 같은 목표(정보 획득이 성공적인지 여부)라면 단순히 리턴 타입을 Optional<List<Repo>>로 사용할 수도 있습니다.

  1. ‘디미터’라고 표기하기도 하지만, 국립국어원 외래어 표기법 한국어 어문 규범에 따라 ‘데메테르’로 표기하겠습니다. 

  2. 원문 위키피디아 

  3. 로버트 C. 마틴의 『클린 코드』(인사이트, 2013) 중 17장의 G36 참조