원문 - Declaring Capabilities of a Library


  • 첫 번째 개념으로서의 기능(Capabilities as first-level concept)
  • 외부 모듈에 대한 기능 선언(Declaring capabilities for external modules)
  • 로컬 컴포넌트에 대한 추가 기능 선언(Declaring additional capabilities for a local component)

라이브러리의 기능 선언(Declaring Capabilities of a Library)

첫 번째 개념으로서의 기능(Capabilities as first-level concept)

컴포넌트는 기능을 제공하는 데 사용되는 소프트웨어 아키텍처와는 독립적인 여러 기능을 제공한다. 예를 들어 라이브러리는 단일 아티팩트에 여러 기능을 포함할 수 있다. 그러나 이러한 라이브러리는 단일 GAV(그룹, 아티팩트 및 버전) 좌표로 게시된다. 이는 단일 좌표에서 컴포넌트의 다양한 “기능”이 잠재적으로 공존한다는 것을 의미한다.

그레이들을 사용하면 컴포넌트가 제공하는 기능을 명시적으로 선언하는 것이 흥미로워진다. 이를 위해 그레이들은 기능(capability)이라는 개념을 제공한다.

기능(feature)은 종종 다양한 기능(capabilities)들을 결합하여 구축된다.

이상적으로는 컴포넌트가 명시적으로 GAV에 대한 의존성을 선언해서는 안 되며, 기능 측면에서 요구 사항을 표현해야 한다.

  • “로깅을 제공하는 컴포넌트를 제공.”
  • “스크립팅 엔진을 제공.”
  • “그루비를 지원하는 스크립팅 엔진을 제공.

기능을 모델링함으로써, 의존성 관리 엔진은 더 똑똑해지고 의존성 그래프에 호환되지 않는 기능이 있을 때마다 알려주거나 그래프의 다른 모듈이 동일한 기능을 제공할 때마다 선택하도록 요청할 수 있다.

외부 모듈에 대한 기능 선언(Declaring capabilities for external modules)

그레이들은 빌드한 컴포넌트에 대한 기능 선언을 지원하지만, 그렇지 않은 경우 외부 컴포넌트를 지원한다는 점에 주목해야 한다.

예제는 빌드 파일에 의존성이 포함된 경우를 보여준다.

예제 1. 로깅 프레임워크의 암시적 충돌이 있는 빌드 파일

build.gradle.kts

dependencies {
    // 이 의존성은 log4:log4j를 전이적으로 가져온다.
    implementation("org.apache.zookeeper:zookeeper:3.4.9")

    // 우리는 slf4j보다 log4j를 사용한다
    implementation("org.slf4j:log4j-over-slf4j:1.7.10")
}

build.gradle

dependencies {
    // 이 의존성은 log4:log4j를 전이적으로 가져온다.
    implementation 'org.apache.zookeeper:zookeeper:3.4.9'

    // 우리는 slf4j보다 log4j를 사용한다
    implementation 'org.slf4j:log4j-over-slf4j:1.7.10'
}

현재로서는 클래스패스에 두 개의 로깅 프레임워크가 있다는 사실을 파악하기 어렵다. 실제로 주키퍼(zookeeper)는 log4j를 가져올 것이며, 여기서 우리가 사용하려는 것은 log4j-over-slf4j다. 두 로깅 프레임워크가 동일한 기능을 제공한다고 선언하는 규칙을 추가하여 충돌을 사전에 감지할 수 있다.

예제 2. 로깅 프레임워크의 암시적 충돌이 있는 빌드 파일

build.gradle.kts

dependencies {
    // "LoggingCapability" 규칙 활성화
    components.all(LoggingCapability::class.java)
}

class LoggingCapability : ComponentMetadataRule {
    val loggingModules = setOf("log4j", "log4j-over-slf4j")

    override fun execute(context: ComponentMetadataContext) = context.details.run {
        if (loggingModules.contains(id.name)) {
            allVariants {
                withCapabilities {
                    // log4j와 log4j-over-slf4j가 모두 동일한 기능을 제공한다고 선언한다.
                    addCapability("log4j", "log4j", id.version)
                }
            }
        }
    }
}

build.gradle

dependencies {
    // "LoggingCapability" 규칙 활성화
    components.all(LoggingCapability)
}

@CompileStatic
class LoggingCapability implements ComponentMetadataRule {
    final static Set<String> LOGGING_MODULES = ["log4j", "log4j-over-slf4j"] as Set<String>

    void execute(ComponentMetadataContext context) {
        context.details.with {
            if (LOGGING_MODULES.contains(id.name)) {
                allVariants {
                    it.withCapabilities {
                        // log4j와 log4j-over-slf4j가 모두 동일한 기능을 제공한다고 선언한다.
                        it.addCapability("log4j", "log4j", id.version)
                    }
                }
            }
        }
    }
}

이 규칙을 추가하면, 그레이들이 충돌을 감지하고 제대로 실패하는지 확인할 수 있다.

> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve org.slf4j:log4j-over-slf4j:1.7.10.
     Required by:
         project :
      > Module 'org.slf4j:log4j-over-slf4j' has been rejected:
           Cannot select module with conflict on capability 'log4j:log4j:1.7.10' also provided by [log4j:log4j:1.2.16(compile)]
   > Could not resolve log4j:log4j:1.2.16.
     Required by:
         project : > org.apache.zookeeper:zookeeper:3.4.9
      > Module 'log4j:log4j' has been rejected:
           Cannot select module with conflict on capability 'log4j:log4j:1.2.16' also provided by [org.slf4j:log4j-over-slf4j:1.7.10(compile)]

기능 충돌을 해결하는 방법을 알아보려면 문서의 기능 절을 참고하자.

로컬 컴포넌트에 대한 추가 기능 선언(Declaring additional capabilities for a local component)

모든 컴포넌트에는 동일한 GAV 좌표에 해당하는 암시적 기능이 있다. 그러나 컴포넌트에 대한 추가 명시적 기능을 선언하는 것도 가능하다. 이는 서로 다른 GAV 좌표에 게시된 라이브러리가 동일한 API의 대체 구현일 때 편리하다.

예제 3. 컴포넌트의 기능 선언

build.gradle.kts

configurations {
    apiElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
    runtimeElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
}

build.gradle

configurations {
    apiElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
    runtimeElements {
        outgoing {
            capability("com.acme:my-library:1.0")
            capability("com.other:module:1.1")
        }
    }
}

기능(Capabilitie)은 컴포넌트의 소모성(consumable) 구성인 아웃고잉(outgoing) 구성에 있어야 한다.

이 예제에서는 두 가지 기능을 선언하는 방법을 보여준다.

  1. com.acme:my-library:1.0, 이는 라이브러리의 암시적 기능에 해당한다.
  2. com.other:module:1.1, 이 라이브러리의 또 다른 기능에 해당한다.

1을 수행해야 한다는 점이 주목할만 하다. 명시적 기능 선언을 하자마자 암시적 기능을 포함하여 모든 기능을 선언해야 하기 때문이다.

두 번째 기능은 이 라이브러리에만 해당되거나, 외부 컴포넌트에서 제공하는 기능에 해당할 수 있다. 이 경우 com.other:module이 동일한 의존성 그래프에 나타나면 빌드가 실패하고 소비자는 사용할 모듈을 선택해야 한다.

기능은 그레이들 모듈 메타데이터에 게시된다. 그러나 POM 또는 Ivy 메타데이터 파일에는 이에 상응하는 항목이 없다. 결과적으로 이러한 컴포넌트를 게시할 때 그레이들은 이 기능이 그레이들 소비자에게만 해당된다는 경고를 표시한다.

Maven publication 'maven' contains dependencies that cannot be represented in a published pom file.
  - Declares capability com.acme:my-library:1.0
  - Declares capability com.other:module:1.1