Kotlin 진화 원칙
Kotlin 진화 원칙
본문
실용적 진화의 원칙
언어 설계는 돌에 새겨진 것이지만,
그 돌은 꽤나 부드러워서,
약간의 노력만 있다면 나중에 다시 빚어낼 수 있다.
— Kotlin 설계 팀
Kotlin은 프로그래머를 위한 실용적인 도구로 설계되었어요. 언어 진화와 관련해 Kotlin의 실용적인 성격은 다음과 같은 원칙에 담겨 있어요.
- 언어를 시간이 지나도 현대적으로 유지한다.
- 사용자와 지속적인 피드백 루프를 유지한다.
- 사용자가 새 버전으로 업데이트하기 쉽고 편안하게 만든다.
이 원칙이 Kotlin이 어떻게 나아가고 있는지 이해하는 핵심이므로, 좀 더 자세히 살펴볼게요.
언어를 현대적으로 유지하기(Keeping the Language Modern). 우리는 시스템이 시간이 지나면서 레거시를 쌓아간다는 것을 알고 있어요. 한때 최첨단 기술이었던 것이 오늘날에는 구식이 되어버릴 수도 있죠. 우리는 언어가 사용자의 필요에 맞게 관련성을 유지하고 기대에 부응하도록 진화시켜야 해요. 여기에는 새 기능을 추가하는 것뿐만 아니라, 더 이상 프로덕션 사용에 권장되지 않아 레거시가 된 옛 기능을 단계적으로 없애는 것도 포함돼요.
편안한 업데이트(Comfortable Updates). 언어에서 무언가를 제거하는 것 같은 비호환 변경은, 제대로 주의를 기울이지 않고 실행하면 한 버전에서 다음 버전으로의 고통스러운 마이그레이션으로 이어질 수 있어요. 우리는 항상 그러한 변경을 미리 잘 공지하고, 변경이 일어나기 전에 것을 deprecate(더 이상 사용하지 않음) 표시하고 자동 마이그레이션 도구를 제공할 거예요. 언어가 변경될 무렵에는 세계 대부분의 코드가 이미 업데이트되어 있어서 새 버전으로 마이그레이션하는 데 문제가 없길 바라요.
피드백 루프(Feedback Loop). deprecation 주기를 거치는 데는 상당한 노력이 필요하므로, 우리는 미래에 만들 비호환 변경의 수를 최소화하고 싶어요. 최상의 판단을 사용하는 것 외에도, 실제 생활에서 시도해보는 것이 설계를 검증하는 가장 좋은 방법이라고 믿어요. 돌에 새기기 전에, 전투에서 검증을 받고 싶어요. 그래서 우리는 설계의 초기 버전을 언어의 프로덕션 버전에서 이용 가능하게 만들 수 있는 모든 기회를 사용하는데, 다만 Experimental, Alpha, Beta 같은 사전 안정 상태 중 하나로 제공해요. 이러한 기능은 안정적이지 않아 언제든 변경될 수 있고, 사용을 선택하는 사용자들은 미래의 마이그레이션 문제를 처리할 준비가 되어 있다는 것을 명시적으로 나타내는 셈이에요. 이 사용자들은 우리가 설계를 반복해서 견고하게 만드는 데 모으는 귀중한 피드백을 제공해줘요.
비호환 변경
한 버전에서 다른 버전으로 업데이트할 때, 예전에 동작하던 코드가 더는 동작하지 않게 된다면, 그것은 언어의 비호환 변경(때로는 "브레이킹 체인지(breaking change)"라고 부르기도 해요)이에요. 어떤 경우에 "더는 동작하지 않는다"가 정확히 무엇을 의미하는지에 대해 논쟁이 있을 수 있지만, 확실히 다음을 포함해요.
- 컴파일되고 정상적으로 실행되던 코드가 이제 (컴파일 또는 링크 시간에) 오류로 거부되는 경우. 여기에는 언어 구조를 제거하고 새로운 제약을 추가하는 것이 포함돼요.
- 정상적으로 실행되던 코드가 이제 예외를 던지는 경우.
"회색 영역"에 속하는 덜 명확한 경우에는 코너 케이스를 다르게 처리하는 것, 이전과 다른 타입의 예외를 던지는 것, 리플렉션을 통해서만 관찰 가능한 동작을 변경하는 것, 문서화되지 않았거나 정의되지 않은 동작을 수정하는 것, 바이너리 아티팩트의 이름을 바꾸는 것 등이 있어요. 때로는 이런 변경이 결정적이어서 마이그레이션 경험에 큰 영향을 주기도 하고, 때로는 사소할 수도 있어요.
확실히 비호환 변경이 아닌 것의 몇 가지 예는 다음과 같아요.
- 새 경고를 추가하는 것.
- 새 언어 구조를 활성화하거나 기존 구조의 제한을 완화하는 것.
- private/internal API와 그 밖의 구현 세부 사항을 변경하는 것.
언어를 현대적으로 유지하기와 편안한 업데이트 원칙은 비호환 변경이 때로는 필요하지만 신중하게 도입되어야 한다는 점을 시사해요. 우리의 목표는 예정된 변경을 사용자에게 미리 알려서 코드를 편안하게 마이그레이션할 수 있게 하는 것이에요.
이상적으로는 모든 비호환 변경은 문제가 되는 코드에서 보고되는 컴파일 타임 경고(보통 deprecation 경고라고 불러요)를 통해 공지되고, 자동 마이그레이션 지원을 수반해야 해요. 따라서 이상적인 마이그레이션 작업 흐름은 다음과 같아요.
- 변경이 공지된 버전 A로 업데이트한다.
- 다가올 변경에 대한 경고를 확인한다.
- 도구의 도움을 받아 코드를 마이그레이션한다.
- 변경이 일어나는 버전 B로 업데이트한다.
- 전혀 문제가 발생하지 않는다.
실제로는 일부 변경을 컴파일 타임에 정확하게 감지할 수 없어서 경고를 보고할 수 없지만, 적어도 버전 B에 변경이 온다는 것을 버전 A의 릴리스 노트를 통해 사용자에게 알릴 수는 있어요.
컴파일러 버그 처리
컴파일러는 복잡한 소프트웨어이고, 개발자들의 최선의 노력에도 불구하고 버그가 있기 마련이에요. 컴파일러 자체가 실패하게 하거나 잘못된 오류를 보고하거나 분명히 실패하는 코드를 생성하는 버그는, 짜증나고 종종 부끄럽기도 하지만 고치기는 쉬워요. 그 수정이 비호환 변경을 구성하지 않기 때문이에요. 다른 버그는 컴파일러가 실패하지 않는 잘못된 코드를 생성하게 할 수도 있어요. 예를 들어 소스의 일부 오류를 놓치거나 단순히 잘못된 명령어를 생성하는 경우죠. 그러한 버그를 수정하는 것은 기술적으로 비호환 변경이지만(일부 코드가 예전에는 컴파일됐는데 이제는 그렇지 않은 경우), 우리는 나쁜 코드 패턴이 사용자 코드 전반에 퍼지는 것을 막기 위해 가능한 한 빨리 수정하는 쪽으로 기울어요. 우리 의견으로는 이것이 편안한 업데이트 원칙을 지지하는데, 더 적은 사용자가 그 문제를 마주할 기회를 갖게 되기 때문이에요. 물론 이것은 릴리스된 버전에 나타난 직후 발견되는 버그에만 적용돼요.
의사 결정
Kotlin의 원래 창시자인 JetBrains는 커뮤니티의 도움과 Kotlin Foundation과의 협력을 통해 그 진보를 이끌고 있어요.
Kotlin 프로그래밍 언어에 대한 모든 변경은 Lead Language Designer(현재 Michail Zarečenskij)가 감독해요. 리드 디자이너는 언어 진화와 관련된 모든 문제에서 최종 결정권을 가져요. 또한 완전히 안정적인 구성 요소에 대한 비호환 변경은 Kotlin Foundation 아래 지정된 Language Committee(현재 Jeffrey van Gogh, Werner Dietl, Michail Zarečenskij로 구성)의 승인을 받아야 해요.
Language Committee는 어떤 비호환 변경이 이루어질지, 사용자 업데이트를 최대한 매끄럽게 만들기 위해 어떤 정확한 조치를 취해야 할지에 대한 최종 결정을 내려요. 그 과정에서 Language committee 가이드라인 세트에 의존해요.
언어 기능 전달
Kotlin 릴리스 프로세스에 설명된 대로, 언어 기능은 언어 릴리스(2.x.0) 또는 그 이후의 툴링 릴리스(2.x.20)에 제공돼요.
우리는 언어와 툴링 릴리스를 서로 호환되게 유지하려고 노력해서, 컴파일러에 대한 변경은 대부분 최적화와 경고 추가/제거예요. 사전 안정 기능은 언제든 추가, 제거, 변경될 수 있어요.
언어 릴리스는 종종 새 기능을 추가하고, 사전 안정 기능을 안정으로 승격시키며, 이전에 deprecated 된 것을 제거하거나 변경할 수 있어요.
EAP 빌드
언어 및 툴링 릴리스의 안정 버전을 출시하기 전에, 우리는 더 빠르게 반복하고 커뮤니티의 피드백을 모을 수 있게 해주는 EAP("Early Access Preview"의 줄임말)라고 불리는 여러 미리보기 빌드를 게시해요. 언어 릴리스의 EAP는 보통 나중에 안정 컴파일러가 거부할 바이너리를 생성해서, 바이너리 형식의 가능한 버그가 미리보기 기간보다 오래 살아남지 않도록 해요. RC2나 RC3 같은 최종 릴리스 후보는 보통 이 제한을 갖지 않아요. 자세한 내용은 Kotlin Early Access Preview 참여하기를 참고하세요.
사전 안정 기능
위에서 설명한 피드백 루프 원칙에 따라, 우리는 설계를 공개적으로 반복하고, 일부 기능이 사전 안정 상태 중 하나를 가지며 변경될 예정인 언어 버전을 릴리스해요. 그러한 기능은 언제든지, 경고 없이 추가, 변경, 제거될 수 있어요. 우리는 사전 안정 기능이 알아차리지 못한 사용자에 의해 실수로 사용될 수 없도록 최선을 다해 보장해요. 그러한 기능은 보통 코드나 프로젝트 구성에서 어떤 종류의 명시적인 선택(opt-in)을 요구해요.
Kotlin 언어 기능은 다음 상태 중 하나를 가질 수 있어요.
- 탐색 및 설계(Exploration and design). 언어에 새 기능을 도입하는 것을 고려하고 있어요. 여기에는 기존 기능과 어떻게 통합될지 논의하고, 사용 사례를 모으고, 잠재적 영향을 평가하는 것이 포함돼요. 이 기능이 해결할 문제와 다루는 사용 사례에 대한 사용자 피드백이 필요해요. 가능할 때마다 우리는 이러한 사용 사례와 문제가 얼마나 자주 발생하는지도 추정하려고 해요. 일반적으로 아이디어는 YouTrack 이슈로 문서화되고, 거기서 논의가 계속돼요.
- KEEP 논의. 우리는 이 기능이 언어에 추가되어야 한다고 상당히 확신하고 있어요. 우리는 Kotlin Evolution and Enhancement Process(KEEP)라는 문서에서 동기, 사용 사례, 설계, 기타 중요한 세부 사항을 제공하는 것을 목표로 해요. 사용자 피드백은 KEEP에 제공된 모든 정보를 논의하는 데 집중하기를 기대해요.
- 미리보기(In preview). 기능 프로토타입이 준비되었고, 기능별 컴파일러 옵션을 사용해 활성화할 수 있어요. 우리는 코드베이스에 얼마나 쉽게 통합되는지, 기존 코드와 어떻게 상호작용하는지, IDE 지원 문제나 제안 등 기능에 대한 경험에 대한 피드백을 구해요. 기능의 설계는 피드백에 따라 크게 변경되거나 완전히 철회될 수 있어요. 기능이 미리보기 상태일 때는 Experimental 또는 Beta 안정성 수준 중 하나를 가져요.
- 안정(Stable). 언어 기능이 이제 Kotlin 언어에서 일급 시민이 되었어요. 우리는 그 역호환성과 툴링 지원 제공을 보장해요.
- 철회됨(Revoked). 우리는 그 제안을 철회했고 Kotlin 언어에서 그 기능을 구현하지 않을 거예요. Kotlin에 잘 맞지 않는 미리보기 상태의 기능은 철회할 수 있어요.
다양한 구성 요소의 상태
Kotlin/JVM, JS, Native 컴파일러와 다양한 라이브러리 같은 Kotlin의 다양한 구성 요소의 안정성 상태에 대해 더 알아보세요.
라이브러리
언어는 생태계 없이는 아무것도 아니므로, 우리는 매끄러운 라이브러리 진화를 가능하게 하는 데 특별한 주의를 기울여요.
이상적으로는 라이브러리의 새 버전을 이전 버전의 "드롭인 교체(drop-in replacement)"로 사용할 수 있어야 해요. 이는 애플리케이션을 다시 컴파일하지 않더라도(동적 링크에서 가능해요) 바이너리 의존성을 업그레이드하는 것이 아무것도 망가뜨리지 않아야 한다는 뜻이에요.
한편으로 이를 달성하려면 컴파일러가 별도 컴파일(separate compilation)의 제약 하에서 일정한 Application Binary Interface(ABI) 안정성 보장을 제공해야 해요. 그래서 언어의 모든 변경은 바이너리 호환성 관점에서 검토돼요.
다른 한편으로는 어떤 변경이 안전한지에 대해 라이브러리 작성자가 신중한지에 많은 것이 달려 있어요. 따라서 라이브러리 작성자가 소스 변경이 호환성에 어떤 영향을 미치는지 이해하고, 라이브러리의 API와 ABI를 모두 안정적으로 유지하기 위한 특정 모범 사례를 따르는 것이 중요해요. 다음은 라이브러리 진화 관점에서 언어 변경을 고려할 때 우리가 가정하는 몇 가지예요.
- 라이브러리 코드는 공개/보호 함수와 프로퍼티의 반환 타입을 항상 명시적으로 지정해야 해요. 따라서 공개 API에 대해 타입 추론에 의존해서는 안 돼요. 타입 추론의 미묘한 변화는 반환 타입이 의도치 않게 바뀌어 바이너리 호환성 문제로 이어질 수 있어요.
- 같은 라이브러리가 제공하는 오버로드된 함수와 프로퍼티는 본질적으로 같은 일을 해야 해요. 타입 추론의 변화는 호출 지점에서 더 정확한 정적 타입이 알려지게 해서 오버로드 해석에 변화를 일으킬 수 있어요.
- 라이브러리 작성자는
@Deprecated와@RequiresOptIn애너테이션을 사용해 API 표면의 진화를 제어할 수 있어요.@Deprecated(level=HIDDEN)은 API에서 제거된 선언에 대해서도 바이너리 호환성을 보존하는 데 사용할 수 있다는 점에 주의하세요.
또한 관례에 따라 "internal"이라는 이름의 패키지는 공개 API로 간주되지 않아요. "experimental"이라는 이름의 패키지에 있는 모든 API는 사전 안정으로 간주되며 언제든 변경될 수 있어요.
우리는 위에서 밝힌 원칙에 따라 안정적 플랫폼을 위한 Kotlin 표준 라이브러리(kotlin-stdlib)를 진화시켜요. 그 API에 대한 계약 변경은 언어 자체의 변경과 동일한 절차를 거쳐요.
컴파일러 옵션
컴파일러가 받아들이는 명령줄 옵션도 일종의 공개 API이며, 동일한 고려 사항이 적용돼요. 지원되는 옵션("-X" 또는 "-XX" 접두사가 없는 것들)은 언어 릴리스에서만 추가될 수 있고, 제거하기 전에 제대로 deprecated 되어야 해요. "-X" 및 "-XX" 옵션은 실험적이며 언제든 추가되고 제거될 수 있어요.
호환성 도구
레거시 기능이 제거되고 버그가 수정됨에 따라 소스 언어도 변경되어서, 제대로 마이그레이션되지 않은 옛 코드는 더는 컴파일되지 않을 수 있어요. 일반적인 deprecation 주기는 마이그레이션을 위한 편안한 기간을 허용하고, 심지어 그것이 끝나고 변경이 안정 버전에 포함된 후에도, 마이그레이션되지 않은 코드를 컴파일하는 방법이 여전히 있어요.
호환성 옵션
우리는 새 Kotlin 버전이 이전 버전의 동작을 에뮬레이션하게 해주는 호환성 옵션을 제공해요.
-language-version X.Y– Kotlin 언어 버전 X.Y에 대한 호환 모드예요. 컴파일러는 코드가 이후 버전에서 도입된 언어 기능을 사용할 때 오류를 보고해요.-api-version X.Y– Kotlin API 버전 X.Y에 대한 호환 모드예요. 컴파일러는 이후 버전에서 도입된 Kotlin 표준 라이브러리 API(컴파일러가 생성한 코드가 참조하는 API 포함)를 사용하는 선언을 무시해요.
마이그레이션할 시간을 더 주기 위해, JVM에서는 최신 안정 버전 외에도 이전 언어 및 API 버전을 최소 세 개 지원해요. 이렇게 하면 라이브러리 작성자가 이전 컴파일러 버전을 사용하는 소비자와 호환을 유지하면서 더 새로운 컴파일러 릴리스를 채택할 수 있어요. 다른 플랫폼에서도 이전 언어 및 API 버전을 구성할 수 있지만, JVM과 달리 소비자는 여전히 최신 컴파일러 버전을 사용해야 해요.
대부분의 프로젝트에서는 두 옵션을 같은 버전으로 설정해요. 더 낮은 API 버전은 주로 Kotlin 표준 라이브러리의 이전 버전과 호환을 유지해야 할 때 유용해요.
적극적으로 유지 관리되는 코드베이스는 전체 deprecation 주기가 끝날 때까지 기다리지 않고 가능한 한 빨리 버그 수정을 받는 것이 유익할 수 있어요. 이러한 프로젝트는 -progressive 옵션을 활성화해서, 툴링 릴리스에서 이러한 변경이 기본값이 되기 전에 채택할 수 있어요.
이 옵션들은 명령줄 또는 Gradle이나 Maven 빌드 도구로 구성할 수 있어요.
바이너리 형식 진화
최악의 경우 손으로 고칠 수 있는 소스와 달리, 바이너리는 마이그레이션하기가 훨씬 어려워서 바이너리의 경우 역호환성이 중요해요. 바이너리에 대한 비호환 변경은 업데이트를 매우 불편하게 만들 수 있으므로, 소스 언어 구문에 대한 것보다 더 큰 주의를 기울여 도입해야 해요.
완전히 안정적인 컴파일러 버전의 경우 기본 바이너리 호환성 프로토콜은 다음과 같아요.
- 모든 바이너리는 역호환됩니다. 즉, 더 새로운 컴파일러가 더 오래된 바이너리를 읽을 수 있어요(예: 1.3은 1.0부터 1.2까지를 이해해요).
- 더 오래된 컴파일러는 새 기능에 의존하는 바이너리를 거부해요(예: 1.0 컴파일러는 코루틴을 사용하는 바이너리를 거부해요).
- 바람직하게는(보장할 수는 없지만) 바이너리 형식이 다음 언어 릴리스와 대부분 순방향 호환되지만, 그 이후 릴리스와는 호환되지 않아요(새 기능을 사용하지 않는 경우, 예: 1.9는 2.0의 대부분 바이너리를 이해하지만 2.1은 아님).
이 프로토콜은 약간 오래된 컴파일러를 사용하고 있더라도 어떤 프로젝트도 의존성 업데이트가 차단될 수 없기 때문에 편안한 업데이트를 위해 설계되었어요.
모든 타깃 플랫폼이 이 수준의 안정성에 도달한 것은 아니지만 Kotlin/JVM은 도달했어요.
Kotlin klib 바이너리
Kotlin klib 바이너리는 Kotlin 1.9.20에서 Stable 수준에 도달했어요. 그러나 명심해야 할 몇 가지 호환성 세부 사항이 있어요.
- klib 바이너리는 Kotlin 1.9.20부터 역호환됩니다. 예를 들어 2.0.x 컴파일러는 1.9.2x 컴파일러가 생성한 바이너리를 읽을 수 있어요.
- 순방향 호환성은 보장되지 않아요. 예를 들어 2.0.x 컴파일러가 2.1.x 컴파일러가 생성한 바이너리를 읽는다는 보장은 없어요.
- Kotlin cinterop klib 바이너리는 여전히 Beta 상태예요. 현재 우리는 cinterop klib 바이너리에 대해 서로 다른 Kotlin 버전 간의 특정 호환성 보장을 줄 수 없어요.