Shared Internals: 코틀린의 크로스 모듈 가시성을 위한 새 제안서
Shared Internals: 코틀린의 크로스 모듈 가시성을 위한 새 제안서
코틀린의 internal 가시성 수정자(visibility modifier)는 모듈 내부의 구현 세부 사항을 숨기면서 깔끔한 공개 API만 노출할 수 있도록 해 주는 유용한 메커니즘입니다. 하지만 코드베이스가 성장하고 라이브러리가 점차 모듈화되면서, API의 논리적 경계와 모듈의 컴파일 경계가 항상 일치하지 않는다는 문제가 발생합니다. 테스트 모듈은 프로덕션 코드의 internal 멤버에 접근해야 하고, kotlinx.coroutines 같은 라이브러리 패밀리는 여러 아티팩트(artifact) 간에 구현 세부 사항을 공유하면서도 외부 소비자에게는 노출하지 않기를 원합니다. 현재의 해결책인 "friend modules"는 문서화되지 않은 컴파일러 기능에 불과하며, 언어 수준의 설계가 부재한 상태입니다.
KEEP-0451은 바로 이러한 문제를 해결하기 위한 제안서입니다. 핵심은 shared internal이라는 새로운 가시성 수정자를 도입하는 것으로, 이 수준은 internal과 public 사이에 위치하여 모듈이 어떤 내부 요소를 누구와 공유할지 명시적으로 선언할 수 있게 합니다. 이 글에서는 해당 제안서의 등장 배경, 설계 과정에서의 주요 결정 사항, 전이적 공유(transitive sharing)가 복잡한 의존성 그래프를 어떻게 단순화하는지, 그리고 JVM에서 크로스 모듈 가시성을 구현할 때 직면하는 기술적 과제에 대해 살펴보겠습니다.
근본적인 문제: 모듈 경계 vs. 논리적 경계
일반적인 라이브러리 구조를 살펴보겠습니다.
kotlinx-coroutines/
├── kotlinx-coroutines-core/
├── kotlinx-coroutines-test/
├── kotlinx-coroutines-reactive/
└── kotlinx-coroutines-android/
이 아티팩트들은 하나의 응집력 있는 라이브러리 패밀리를 구성합니다. 내부적으로 디스패처(dispatcher) 관련 세부 구현, Continuation 관련 매커니즘, 테스트 유틸리티 등을 서로 공유합니다. 그런데 코틀린의 관점에서 보면 각 아티팩트는 별도의 모듈입니다. kotlinx-coroutines-core에서 internal로 선언된 멤버는, 같은 팀이 관리하며 함께 배포되는 kotlinx-coroutines-test에서도 접근할 수 없습니다. 이는 실질적인 개발 현장에서 상당한 불편함을 야기합니다.
현재 가능한 우회 방법은 모두 만족스럽지 못합니다.
방법 1: 모든 것을 public으로 만들기. 동작은 하지만 API 표면을 오염시킵니다. 소비자(개발자)에게 사용해서는 안 되는 구현 세부 사항이 노출되고, 유지보수 측면에서도 호환성을 깨뜨리지 않으면서 내부 구현을 변경할 자유를 잃게 됩니다.
방법 2: 문서화되지 않은 friend modules 기능 사용. 코틀린 컴파일러는 -Xfriend-paths 플래그를 통해 특정 모듈이 다른 모듈의 internal 멤버에 접근할 수 있도록 지원합니다. 하지만 이는 컴파일러의 내부 구현 세부 사항일 뿐 언어 기능이 아닙니다. 별도의 문법도 없고, IDE 지원도 없으며, 안정성에 대한 보장도 없습니다.
방법 3: 모듈 병합. 관련 모듈을 하나의 컴파일 단위로 합친 뒤 배포 시에만 분리하는 방법이 있습니다. 그러나 빌드 설정이 복잡해지고, 의존성 그래프가 복잡한 경우에는 확장성이 떨어집니다.
KEEP-0451은 friend modules를 명시적인 문법과 명확한 의미론(semantics)을 갖춘 일급 언어 기능으로 승격시켜 이 격차를 해소합니다.
shared internal 수정자
해당 제안서는 shared internal이라는 새로운 가시성 수정자를 도입합니다. 이 수정자가 적용된 선언은 지정된 의존 모듈에서는 접근할 수 있지만, 일반 소비자에게는 보이지 않습니다.
// kotlinx-coroutines-core 모듈 내
shared internal fun internalDispatcherHelper() {
// 다른 coroutines 모듈과 공유되는 구현
}
shared internal class ContinuationImpl {
// 공유 구현 클래스
}
internal과의 핵심 차이점은 다음과 같습니다.
internal: 동일 모듈 내에서만 접근 가능합니다.shared internal: 동일 모듈 내에서 접근 가능하며, 명시적으로 지정된 의존 모듈에서도 접근할 수 있습니다.public: 모든 곳에서 접근 가능합니다.
이를 통해 중간 지대가 만들어집니다. 라이브러리 저자는 모듈 패밀리 전체에서 구현 세부 사항을 공유하면서도, 외부 소비자에게는 해당 세부 사항을 노출하지 않을 수 있습니다.
4단계 공유 수준
해당 제안서는 각각 점진적으로 더 많은 접근을 허용하는 4가지 공유 수준을 정의합니다.
수준 0: 공유 없음
기본 동작입니다. 의존 모듈은 공개 API만 볼 수 있으며, 오늘날 대부분의 라이브러리 의존성이 이 방식으로 동작합니다.
// 라이브러리 모듈
internal fun helper() { ... } // 의존 모듈에서 접근 불가
public fun api() { ... } // 의존 모듈에서 접근 가능
수준 1: 안정성 공유(Stability Sharing)
이 수준은 선언을 노출하지 않지만, 모듈 경계를 넘어 스마트 캐스트가 동작할 수 있도록 보장합니다. 다음 예시를 살펴보겠습니다.
// 라이브러리 모듈
public sealed class Result {
public class Success(val value: Any) : Result()
public class Failure(val error: Throwable) : Result()
}
// 소비자 모듈
fun process(result: Result) {
when (result) {
is Result.Success -> println(result.value) // 스마트 캐스트 동작
is Result.Failure -> println(result.error) // 스마트 캐스트 동작
}
}
스마트 캐스트는 컴파일러가 값의 타입이 안정적(stable)임을, 즉 타입 검사 시점과 사용 시점 사이에 타입이 변경되지 않음을 증명해야 동작합니다. 다른 모듈에서 정의된 값의 경우 라이브러리가 구현을 변경할 수 있으므로, 컴파일러가 통상적으로는 이 보장을 해줄 수 없습니다. 안정성 공유는 바로 이러한 보장을 명시적으로 제공하는 역할을 합니다.
수준 2: Shared Internals
해당 제안서의 핵심 기능입니다. 의존 모듈이 shared internal로 표시된 선언에 접근할 수 있게 됩니다.
// 코어 모듈 (테스트 모듈과 공유)
shared internal fun createTestDispatcher(): CoroutineDispatcher { ... }
// 테스트 모듈 (코어로부터 공유를 받음)
fun runTest(block: suspend () -> Unit) {
val dispatcher = createTestDispatcher() // 접근 가능!
// ...
}
일반 internal 선언은 여전히 숨겨진 상태를 유지합니다. shared internal로 명시적으로 표시된 선언만 접근 가능해집니다.
수준 3: 모든 Internals
최대 공유 수준이며, 현재 friend modules의 동작과 동일합니다. 의존 모듈이 shared internal로 표시된 선언뿐 아니라 모든 internal 선언을 볼 수 있습니다. 이 수준은 주로 프로덕션 코드에 대한 완전한 접근이 필요한 테스트 모듈을 위해 존재합니다.
// 프로덕션 모듈 (테스트 모듈과 all-internals 공유)
internal fun privateHelper() { ... }
shared internal fun sharedHelper() { ... }
// 테스트 모듈
fun testBehavior() {
privateHelper() // 수준 3에서 접근 가능
sharedHelper() // 역시 접근 가능
}
전이적 공유: 복잡한 계층 구조의 단순화
해당 제안서의 핵심 설계 결정 중 하나는 공유가 전이적(transitive)이라는 점입니다. 모듈 A가 모듈 B와 공유하고, 모듈 B가 모듈 C와 공유하면, C는 자동으로 A의 shared internals에 접근할 수 있습니다.
A ──shares──▶ B ──shares──▶ C
C는 A의 shared internals에 접근 가능
언뜻 의문이 들 수 있습니다. 왜 각 단계에서 명시적인 선언을 요구하지 않을까요? 해당 제안서는 상속 시나리오를 통해 그 이유를 설명합니다.
전이성이 없을 경우의 다이아몬드 문제
모듈에 걸쳐 존재하는 클래스 계층 구조를 살펴보겠습니다.
// 모듈 A
shared internal open class Base {
shared internal fun helper() { ... }
}
// 모듈 B (A에 의존)
open class Middle : Base() {
// helper()를 상속
}
// 모듈 C (B에 의존하지만, A에 직접 의존하지는 않음)
class Derived : Middle() {
fun doWork() {
helper() // C가 이 메서드를 호출할 수 있을까?
}
}
전이성이 없다면, 모듈 C는 Middle은 볼 수 있지만(public이므로) Base.helper()에는 접근할 수 없습니다(A로부터 공유 관계가 없으므로). 이로 인해 상속받은 멤버에 접근할 수 없는 모순된 상황이 발생합니다.
전이성이 적용되면 공유 관계가 의존성 그래프를 따라 자연스럽게 흐릅니다. C가 B에 의존하고, B가 A와 공유 관계를 맺고 있으므로, C도 A의 shared internals에 접근할 수 있게 됩니다. 따라서 상속 계층 구조가 별도의 추가 설정 없이도 자연스럽게 동작합니다.
유효 공유 수준의 계산
두 모듈 간의 유효 공유 수준(effective sharing level)은 의존성 그래프의 모든 경로를 조사하여 계산합니다.
- 소비자에서 제공자까지의 각 경로에 대해, 해당 경로상의 최소 공유 수준을 취합니다.
- 모든 경로에 대해, 이 최솟값들 중 최댓값을 취합니다.
이 "경로 내 최솟값, 경로 간 최댓값" 알고리즘은 다음을 보장합니다.
- 공유되지 않는 단일 홉이 있으면 해당 경로를 차단합니다.
- 일부 경로가 차단되더라도 여러 다른 경로를 통해 접근이 가능합니다.
- 가장 강력한 가용 경로가 유효 수준을 결정합니다.
해당 제안서는 이 계산에 변형된 다익스트라 알고리즘(Dijkstra's algorithm) 사용을 제안하며, 공유 수준을 간선 가중치로 취급하여 가장 높은 공유 수준의 경로를 찾는 방식입니다.
모듈 식별 및 선언
공유가 동작하려면 모듈에 안정적인 식별자가 필요합니다. 해당 제안서는 배포되는 아티팩트에 대해 Maven 좌표를 권장합니다.
group:artifact:version
가령, org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0과 같습니다.
그러나 모듈 식별은 몇 가지 시나리오에서 복잡해집니다.
Main 모듈 vs. 테스트 모듈
대부분의 빌드 시스템에서 프로젝트의 메인 소스와 테스트 소스는 별도의 모듈로 컴파일됩니다. 테스트 모듈은 메인 모듈에 의존하며, 일반적으로 all-internals 접근이 필요합니다. 하지만 두 모듈은 동일한 Maven 좌표를 공유합니다.
해당 제안서는 이러한 경우를 구분하기 위해 보조 식별자(secondary identifier)를 도입합니다.
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0#main
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0#test
미배포 모듈
모든 모듈이 Maven 좌표를 갖고 있지는 않습니다. 내부 프로젝트 모듈, 로컬 개발 빌드, 멀티 모듈 프로젝트에서는 배포 없이도 모듈을 식별해야 합니다. 해당 제안서는 이를 빌드 도구 구현에 위임하며, 파일 경로, 프로젝트 이름, 합성 식별자 등을 활용할 수 있도록 열어 두고 있습니다.
버전 범위와 호환성
한 가지 까다로운 질문이 있습니다. kotlinx-coroutines-core:1.7.0과 공유를 선언했을 때, 버전 1.7.1도 포함되는 것일까요? 해당 제안서는 버전 의미론(version semantics)을 강제하지 않으며, 빌드 도구가 적절한 정책을 구현할 수 있도록 유연성을 부여하고 있습니다.
상속 규칙: 전부 아니면 전무(All or Nothing)
해당 제안서는 상속 계층 구조에 대해 엄격한 규칙을 적용합니다. 클래스는 상위 타입 체인에 있는 모든 internal 멤버를 볼 수 있거나, 아무것도 볼 수 없어야 합니다. 부분적인 가시성은 허용되지 않습니다.
이 규칙이 왜 중요한지 살펴보겠습니다.
// 모듈 A
open class Base {
internal open fun hook() { defaultBehavior() }
}
// 모듈 B (A와 공유)
open class Middle : Base() {
override fun hook() { customBehavior() }
}
// 모듈 C (A와는 공유하지 않지만, B와는 공유)
class Derived : Middle() {
// Derived가 hook()을 오버라이드할 수 있을까?
}
만약 C가 Middle.hook()은 볼 수 있지만 Base.hook()은 볼 수 없다면, 오버라이드 체인이 불일치하게 됩니다. C는 일반적인 메서드라고 생각하고 오버라이드할 수 있지만, 실제로는 A에 정의된 internal 확장 지점의 일부라는 사실을 알지 못하는 상황이 벌어집니다.
전부 아니면 전무 규칙은 이러한 혼란을 방지합니다. C가 해당 상속 계층 구조에 참여하려면, 전체 internal 구조에 대한 가시성을 확보하거나, 해당 클래스를 불투명(opaque)하게 취급해야 합니다.
JVM 구현의 기술적 과제
JVM에서 크로스 모듈 가시성을 구현하는 데에는 몇 가지 기술적 과제가 존재합니다.
이름 맹글링(Name Mangling)
코틀린의 internal 가시성은 이름 맹글링(name mangling)을 통해 부분적으로 적용됩니다. internal 선언의 이름에 모듈 식별자를 포함시켜 다른 모듈에서 접근할 수 없도록 만드는 방식입니다.
internal fun helper() { ... }
// 컴파일 결과: helper$moduleName()
shared internal의 경우, 맹글링 전략은 지정된 모듈에서는 접근을 허용하면서 나머지 모듈은 차단해야 합니다. 해당 제안서는 기존 friend modules 인프라를 기반으로 구축하되, 적절한 언어 수준의 의미론으로 확장하는 방식을 채택하고 있습니다.
브릿지 메서드(Bridge Methods)
모듈 B의 클래스가 모듈 A의 internal 멤버를 오버라이드하면, 컴파일러는 호출 사이트를 연결하기 위한 브릿지 메서드를 생성합니다. 이 브릿지 메서드는 가시성이 모듈 경계를 넘어 확장되는 경우에도 올바르게 생성되어야 합니다.
// 모듈 A
open class Base {
internal open fun process(): String = "base"
}
// 모듈 B (A로부터 공유를 받음)
class Derived : Base() {
override fun process(): String = "derived"
}
컴파일러는 양쪽 모듈에서 process()를 호출할 때 오버라이드된 메서드로 올바르게 디스패치되도록 적절한 브릿지를 생성해야 합니다.
리플렉션 및 도구 지원
리플렉션 API는 shared internal 가시성을 인식해야 합니다. IDE 도구는 인가된 모듈에서는 코드 완성 기능에 shared internals를 표시하되, 인가되지 않은 모듈에서는 숨겨야 합니다. 디버그 도구 역시 적절한 접근 권한이 필요합니다. 해당 제안서는 이러한 통합 지점을 인정하면서도, 구체적인 구현 방식까지 강제하지는 않습니다.
빌드 도구 통합
해당 제안서에는 빌드 도구가 공유 설정을 노출할 수 있는 방법에 대한 예시가 포함되어 있습니다.
Gradle DSL (가상)
kotlin {
sharing {
// 같은 프로젝트의 모든 모듈과 공유
allInternalsTo(project(":app"))
// 배포된 아티팩트와 특정 internals 공유
sharedInternalsTo("org.example:other-library")
}
}
실제 DSL 문법은 빌드 도구 관리자가 결정하겠지만, 해당 제안서는 이러한 도구가 지원해야 할 의미론에 대한 가이드라인을 제공하고 있습니다.
모듈 간 스마트 캐스트 안정성
공유 시스템의 한 가지 미묘하면서도 중요한 이점은 스마트 캐스트 동작의 개선입니다. 코틀린의 스마트 캐스트는 값이 안정적임을, 즉 타입 검사와 사용 사이에 값이 변경되지 않음을 증명해야 동작합니다.
다른 모듈에서 가져온 값에 대해 컴파일러는 보수적으로 판단합니다. 라이브러리가 val을 연산 프로퍼티(computed property)로 변경하여 안정성 가정을 깨뜨릴 수 있기 때문입니다. 안정성 공유(수준 1 이상)가 적용되면, 선언 모듈이 그러한 변경을 하지 않겠다는 보장을 제공하므로, 그렇지 않았다면 실패했을 스마트 캐스트가 정상적으로 동작하게 됩니다.
// 라이브러리 모듈 (소비자에게 안정성 보장을 제공)
class Container {
val item: Any = computeItem()
}
// 소비자 모듈 (안정성 공유가 적용된 상태)
fun process(container: Container) {
if (container.item is String) {
// 안정성 보장 덕분에 String으로 스마트 캐스트가 동작
println(container.item.length)
}
}
공유 관계가 없다면, 컴파일러가 이 스마트 캐스트를 거부하여 명시적 캐스트나 로컬 변수를 사용해야 할 수 있습니다.
설계 트레이드오프와 대안
해당 제안서에는 여러 명시적인 설계 선택이 담겨 있으며, 각각 대안에 대한 신중한 고려를 반영하고 있습니다.
Friend modules를 언어 기능으로 승격
기존 friend modules 메커니즘은 -Xfriend-paths 플래그를 통해 컴파일러 수준에서 동작하지만, 사용자 대상 기능으로 설계된 적은 없습니다. 어떤 멤버를 공유할지 선언하기 위한 소스 수준의 문법이 없고, "공유 없음"과 "전부 공유" 사이의 단계 구분도 없으며, 상속 계층 구조를 위한 전이성 의미론도 정의되어 있지 않고, 모듈 경계를 넘는 스마트 캐스트를 위한 안정성 보장도 제공하지 않습니다.
이러한 한계점을 내부 컴파일러 기능에 덧붙이는 대신, 해당 제안서는 shared internal을 적절한 언어 구성 요소(language construct)로 도입합니다. 이 접근 방식은 개발자가 추론할 수 있는 명확한 의미론을 제공하고, 코드 완성 및 오류 강조를 위한 IDE 지원을 가능케 하며, 향후 개선을 위한 기반을 마련합니다. 언어 복잡성이 추가되는 비용이 있지만, 실제 라이브러리 아키텍처에 확장 가능한 일관된 가시성 모델을 얻게 되는 것이 더 큰 이점입니다.
전이적 공유의 근거
대안 설계로는 통신이 필요한 모든 모듈 쌍 사이에 명시적인 공유 선언을 요구하는 방법이 있을 수 있습니다. 모듈 A가 B와의 공유를, C와의 공유를, 그리고 다른 모든 소비자와의 공유를 각각 개별적으로 선언하는 방식입니다. 이러한 명시적 접근은 라이브러리 저자에게 세밀한 제어 권한을 부여하지만, 모든 가능한 의존성 구성을 사전에 예측해야 한다는 부담도 발생합니다.
전이적 모델은 다른 입장을 취합니다. 공유 관계가 의존성 그래프를 따라 자동으로 흐르는 것입니다. A가 B와 공유하고 B가 C와 공유하면, C가 A의 shared internals에 대한 접근 권한을 자동으로 얻게 됩니다. 이 설계는 앞서 논의한 상속 문제에서 비롯되었습니다. 클래스가 모듈 경계에 걸쳐 있을 때, internal 멤버는 상속 계층 구조 전체에서 일관되게 보여야 합니다. 전이성은 복잡한 선언 매트릭스 없이도 상속 체인에 참여하는 것만으로 필요한 가시성을 부여받을 수 있도록 보장합니다.
트레이드오프는 제어력이 줄어든다는 점입니다. 라이브러리 저자가 B와는 공유하면서 B의 의존자가 접근하는 것을 차단할 수는 없습니다. 하지만 실제로 이는 라이브러리 패밀리가 운영되는 방식과 일치합니다. 모듈에 내부 구현을 공유할 만큼 신뢰한다면, 해당 모듈의 의존자도 일반적으로 신뢰하기 때문입니다.
단계적 가시성 수준
더 단순한 설계라면 공유 여부를 이분법적으로만 제공할 수 있습니다. 그러나 이 이진적 선택은 실제 사용 사례의 미묘한 차이를 담아내지 못합니다. 스마트 캐스트 안정성은 전체 internal 접근보다 약한 보장이며, 테스트 모듈은 모든 것이 필요하지만 형제(sibling) 라이브러리 모듈은 특정 공유 유틸리티만 필요할 수 있습니다.
4단계 구분은 이러한 차이를 해결합니다. 수준 0은 특별한 접근이 없는 표준 의존성을 나타냅니다. 수준 1은 선언을 노출하지 않으면서 스마트 캐스트를 위한 안정성 보장만 제공합니다. 수준 2는 shared internal 멤버를 노출하되 일반 internal 멤버는 숨깁니다. 수준 3은 현재 테스트 모듈의 동작과 동일하게 모든 것을 개방합니다.
이 수준을 합쳐 버리면 불편한 선택을 강요받게 됩니다. 수준 1이 없다면 라이브러리가 구현 세부 사항을 노출하지 않고는 스마트 캐스트 안정성을 제공할 수 없게 됩니다. 수준 2와 3 사이에 구분이 없다면 일부 내부 구현만 공유하고 나머지는 보호하는 것이 불가능해집니다. 이러한 세분화는 멘탈 모델에 복잡성을 더하지만, 라이브러리 저자가 실제로 필요로 하는 단계적 구분을 반영하고 있습니다.
shared internal을 통한 명시적 옵트인
해당 제안서는 라이브러리 저자가 인가된 의존 모듈에서 접근할 수 있도록 선언에 shared internal을 명시적으로 표시하도록 요구합니다. 대안으로는 공유 관계가 있는 모듈에 모든 internal 선언을 자동으로 노출하여, 공유를 선언별 선택이 아닌 모듈 수준의 스위치로 취급하는 방법이 있을 수 있습니다.
명시적 표시 방식은 캡슐화에 대한 기대를 보존합니다. 개발자가 internal을 작성할 때, 해당 수정자가 가시성 경계를 강제한다고 기대합니다. 모듈 관계에 따라 internal 멤버가 자동으로 노출된다면 이 기대가 훼손됩니다. shared internal 수정자는 의식적인 결정을 나타냅니다. 즉, "이 특정 선언은 신뢰하는 의존 모듈과 공유되는 확장 API 표면의 일부이고, 다른 internal 멤버는 진정한 의미에서 내부적으로 유지된다"는 의미입니다.
이 설계는 점진적 도입도 가능하게 합니다. 라이브러리 저자가 모든 internal 선언을 감사(audit)할 필요 없이 공유 관계를 도입할 수 있습니다. 명시적으로 표시된 멤버만 접근 가능해지므로, 호환성 고려가 필요한 표면적이 제한됩니다.
현재 상태 및 향후 방향
KEEP-0451은 현재 진행 중인 상태입니다. 관련 논의는 KEEP-469에서 진행되고 있으며, 관련 이슈는 YouTrack(KT-76146, KT-62688)에서 추적되고 있습니다.
주요 미결 사항은 다음과 같습니다.
- 빌드 도구 통합을 위한 정확한 문법
- 공유 선언에 대한 버전 호환성 의미론
- IDE 및 도구 지원 요구 사항
- 기존 friend modules 사용에서의 마이그레이션 경로
해당 제안서는 코틀린의 가시성 시스템에 대한 의미 있는 개선안으로, 라이브러리 개발에서의 실질적인 고충을 해결하면서도 명시적이고 이해하기 쉬운 의미론에 대한 코틀린의 철학을 유지하고 있습니다.
마무리
코틀린의 internal 가시성은 항상 트레이드오프를 수반해 왔습니다. 모듈 내에서는 강력한 캡슐화를 제공하지만, 모듈 간에 구현 세부 사항을 공유해야 할 때는 불편함을 감수해야 했습니다. KEEP-0451의 shared internal 수정자는 명시적인 공유 선언, 4단계 가시성 수준, 의존성 그래프를 따르는 전이적 전파를 통해 이 간극을 체계적으로 해소합니다.
해당 제안서는 라이브러리 저자가 실제로 직면하는 문제를 해결합니다. 프로덕션 내부 구현에 접근해야 하는 테스트 모듈, 아티팩트 간에 구현을 공유하는 라이브러리 패밀리, 모듈 경계에서의 스마트 캐스트 제한 등이 바로 그것입니다. friend modules를 컴파일러 내부 기능에서 언어 기능으로 승격함으로써, 프로덕션 환경에서 요구하는 문법, 의미론, 도구 통합을 제공하게 됩니다.
이 제안서를 이해하면 코틀린 라이브러리 개발이 앞으로 어떻게 발전할지 예측하는 데 도움이 됩니다. 멀티 모듈 라이브러리를 관리하시든, 공개 API를 설계하시든, 언어 설계 트레이드오프에 관심이 있으시든, shared internal의 설계 철학을 통해 언어 설계자가 캡슐화와 실용적 유연성 사이에서 어떻게 균형을 잡는지 엿볼 수 있습니다. 전부 아니면 전무의 상속 규칙, 전이적 공유 계산, 4단계 구분 모두 모듈 시스템에서 경계를 어디에 설정할 것인지에 대한 신중한 결정의 산물입니다.

