논리 vs 언어: 터키 알파벳 버그가 코틀린 컴파일러 안에서 수년간 숨바꼭질을 벌인 이야기
15분 읽기
2025년 10월 10일
--
Enter를 누르거나 클릭하면 이미지를 원본 크기로 볼 수 있습니다
사진: Markus Spiske / Unsplash
2016년 3월, 터키의 소프트웨어 엔지니어 메흐메트 누리 외즈튀르크(Mehmet Nuri Öztürk)가 코틀린 토론 포럼에 짧은 글을 올렸을 때, 그는 자신이 위험한 표준 라이브러리 버그를 보고하고 있다는 사실을 전혀 몰랐다. 그 버그는 찾아내고 고치는 데 5년이 걸렸다. 그가 알고 있던 건 오직 하나, 빌드가 되지 않는다는 것뿐이었다.
코틀린 1.0은 불과 한 달 전에 세상에 공개되었고, 자바와 안드로이드 개발이라는 두 세계에 꼭 필요했던 신선한 바람을 불어넣겠다고 약속했다. 하지만 메흐메트 누리에게 이 새로운 프로그래밍 언어는 짜증나는 막다른 길이었다. 그의 코드는 도무지 빌드가 되지 않았고, 컴파일러 출력은 문제를 파악할 만한 단서를 전혀 주지 않았다.
그는 이해하기 어려운 오류를 포럼 글에 그대로 붙여넣었다.
Compilation completed with 2 errors and 0 warnings in 10s 126ms
Error:Kotlin: Unknown compiler message tag: INFO
Error:Kotlin: Unknown compiler message tag: LOGGING
코틀린 팀은 빠르게 답했지만, 그들도 단서가 많지 않았다. “이 오류를 한 번만 보셨나요, 아니면 프로젝트를 컴파일할 때마다 보시나요?”
메흐메트 누리는 매번 재현된다고 답했다. 빌드 간에도 일관됐고, 서로 다른 컴퓨터와 운영체제에서도 같았다.
돌파구가 나오기까지는 무려 5개월이 걸렸다. 터키에서 일하던 또 다른 프로그래머, 무함메드 데미르바시(Muhammed Demirbaş)도 같은 수수께끼 같은 빌드 실패 메시지를 겪고 있었고, 스스로 조사에 착수했던 것이다.
“오류의 원인이 제 로케일(locale)이나 언어 설정일 수 있다고 의심합니다.” 무함메드는 메흐메트 누리의 글에 댓글로 이렇게 썼다. 그는 문제일 것 같은 정확한 코드 줄까지 지목했다. “분명 CompilerOutputParser.CATEGORIES 맵에서 터키어 I의 대소문자 문제(uppercase–lowercase Turkish I) 같습니다: I -> ı, İ -> i.”
이는 메흐메트 누리의 문제가 특정 프로젝트에만 국한된 것이 아니라, 컴파일러 자체에서 더 심각한 일이 벌어지고 있다는 증상임을 보여주는 증거였다. 무함메드의 새로운 정보에 고마움을 느낀 코틀린 팀은 YouTrack 버그 트래커에 포럼 글 링크를 포함한 이슈를 등록했다.
“터키 로케일에서 로케일-민감 대문자화(uppercasing) 때문에 컴파일이 실패함.” (KT-13631)
무함메드 데미르바시는 컴파일러 버그에 대한 조사와 평가에서 놀라울 정도로 정확했다. 코틀린은 오픈 소스이기 때문에, 그는 “Unknown compiler message tag” 문자열이 나타나는 정확한 코드 줄을 컴파일러 코드에서 찾아낼 수 있었다.
kotlinval qNameLowerCase = qName.toLowerCase() var category: CompilerMessageSeverity? = CATEGORIES[qNameLowerCase] if (category == null) { messageCollector.report(ERROR, "Unknown compiler message tag: $qName") category = INFO }
이 코드는 무엇을 하며, 왜 가끔 잘못될까?
이 코드는 CompilerOutputParser라는 클래스의 일부로, 코틀린 컴파일러의 메시지가 담긴 XML 파일을 읽는 역할을 한다. 이런 파일은 대략 다음처럼 생겼다.
xml<MESSAGES> <INFO path="src/main/kotlin/Example.kt" line="1" column="1"> This is a message from the compiler about a line of code. </INFO> </MESSAGES>
당시 이 파일의 태그는 전부 대문자였다: <INFO/>, <ERROR/> 등등(출처: GitHub). 마치 할아버지가 쓰던 HTML 1.0 웹페이지처럼.
우리가 본 코틀린 코드에서 qName은 이 파일에서 파싱하는 XML 태그 이름이다. <INFO/> 태그를 보고 있다면 qName은 “INFO”다.
메시지의 의미를 알아내기 위해 CompilerOutputParser는 다음으로 CATEGORIES 맵에서 그 문자열을 조회하여 해당하는 CompilerMessageSeverity enum 항목을 찾는다. 그런데 잠깐: CATEGORIES 맵의 키는 소문자다! (출처: GitHub)
kotlinval categories = mapOf( "error" to CompilerMessageSeverity.ERROR, "info" to CompilerMessageSeverity.INFO, … )
“INFO”를 찾는 대신 “info”를 찾아야 한다. 그래서 맵에서 조회하기 전에 qName.toLowerCase()를 호출하는 것이다. 관련 줄만 다시 보자.
kotlinval qNameLowerCase = qName.toLowerCase() var category: CompilerMessageSeverity? = CATEGORIES[qNameLowerCase]
바로 여기서 버그가 스며든다.
"INFO".toLowerCase()는 우리가 원하던 대로 "info"가 된다."INFO".toLowerCase()는 "ınfo"가 된다.차이가 보이는가? 터키어 버전에서는 소문자 ‘ı’에 점이 없다.
사람 눈에는 그 작은 차이가 잘 안 보일 수 있지만, 컴퓨터에게 이 둘은 완전히 다른 문자열이다. 점 없는 "ınfo"는 CATEGORIES 맵의 키 중 하나가 아니므로, 코드는 <INFO/> 태그에 대한 올바른 CompilerMessageSeverity를 찾지 못하고, “INFO”를 완전히 알 수 없는 메시지 범주라고 불평하게 된다.
그렇다면 왜 터키어 컴퓨터에서 toLowerCase()를 호출하면 이런 이상한 결과가 나올까?
무함메드는 이미 답의 일부를 포럼 댓글에서 제공했다. 튀르크 계열 언어에는 ‘i’가 두 가지가 있다.
게다가 점 있음/없음 구분은 대문자에서도 유지된다.
이 점 없는 대문자 ‘I’는 영어에서 쓰는 것과 같은 문자다. 결과적으로 단일 유니코드 문자 I(U+0049)는 소문자 형태가 두 가지가 된다. 영어에서는 점 있는 i(U+0069), 터키어에서는 점 없는 ı(U+0131).
코틀린의 toLowerCase() 함수에는 이것이 문제가 된다. toLowerCase()가 I 문자를 보면 어떤 소문자 형태를 써야 할까? 터키어 단어 _IRMAK_의 소문자는 점 없는 _ırmak_이 맞다. 하지만 영어 단어 _INFO_의 소문자는 점 있는 _info_여야 한다.
텍스트를 소문자로 바꾸라고 컴퓨터에 요청할 때는, 기술적으로는 어떤 알파벳 규칙(영어, 터키어, 혹은 다른 무엇)을 사용할지도 지정해야 한다. 하지만 그건 번거롭다. 그래서 지정하지 않으면 많은 시스템이—그리고 당시의 코틀린 toLowerCase()도—컴퓨터 설정 시 선택한 언어 설정을 그대로 사용한다. 그래서 터키어 머신에서 "INFO".toLowerCase()가 "ınfo"가 되는 것이고, 터키에서 설치된 IntelliJ가 코틀린 컴파일러의 <INFO/> 메시지를 기대하던 소문자 "info"에 매칭하지 못했던 것이다.
하지만 2016년 당시, 이 모든 것은 그저 처리되지 않은 버그 티켓에 불과했다. 무함메드 데미르바시는 조사 시작점이 될 정확한 위치를 찾아냈지만, 그의 발견과 연결된 YouTrack 이슈는 코틀린 백로그에 있는 수백 개 티켓 중 하나였다. 영향받는 사람이 매우 적게 보고되었기 때문에, 더 철저한 조사는 우선순위가 되지 못했다.
그런데 2년 뒤 코루틴이 출시되면서 모든 것이 바뀌었다. 이 소박한 작은 버그가 코틀린 컴파일러 기반 더 깊숙한 곳으로 파고들었기 때문이다.
Enter를 누르거나 클릭하면 이미지를 원본 크기로 볼 수 있습니다
사진: Igor Omilaev / Unsplash
2018년 10월, 코틀린 1.3이 출시되었다. 그리고 그와 함께 새로운 코루틴 라이브러리의 첫 안정(stable) 버전도 나왔다. 비동기 프로그래밍에 대한 혁신적 접근으로, 안드로이드 앱 개발 경험을 바꿔놓을 것이라 약속했다. 코루틴은 1년 넘게 프리릴리스 테스트를 거쳤고, 이제 프로덕션에서 쓸 준비가 되었다고 판단되자, 온갖 종류의 코틀린 프로그래머들이 열정적으로 받아들일 준비를 하고 있었다.
새 도구를 쓰려면 개발자는 코루틴 라이브러리를 프리릴리스 0.30.x에서 안정 1.0으로 올려야 했고, 동시에 코틀린 언어와 표준 라이브러리도 1.3으로 업그레이드해야 했다.
코틀린이나 자바 프로젝트에서 의존성 업그레이드를 해본 적이 있는가? 그렇다면 여러 라이브러리 간 호환성을 유지하는 일이 얼마나 섬세한 저글링인지 알 것이다. 새 의존성에서 제거되거나 변경된 함수를 코드가 참조하면 컴파일 오류가 난다. 하지만 새로 깨진 참조가 내 코드가 아니라 다른 라이브러리 코드에서 나온다면, 프로그램을 실행하기 전까지는 알 수 없다. 그 라이브러리 코드는 작성자가 이미 컴파일해둔 것이고, 내 빌드 과정에서 다시 컴파일되거나 검사되지 않기 때문이다.
라이브러리 중 하나가 새 클래스패스에서 제공되는 것과 정확히 일치하지 않는 함수를 호출하려 하면 NoSuchMethodError가 발생한다. 그래서 의존성을 업그레이드할 때—특히 여러 개를 한꺼번에 올릴 때—가끔 NoSuchMethodError를 보는 것은 각 라이브러리의 호환 버전 조합을 찾아낼 때까지는 거의 일상적인 일이다.
그래서 터키에 있는 안드로이드 개발자 케말 아틀르(Kemal Atlı)가 반짝이는 새 코루틴 라이브러리를 쓰려 앱을 업그레이드하다가 NoSuchMethodError를 만났을 때, 그건 그저 또 하나의 의존성 버전 불일치처럼 보였다. 하지만 케말은 이 문제를 해결하지 못했다. 코루틴 라이브러리 자체의 버그일 수도 있다고 생각한 그는 GitHub 이슈를 열고, 크래시 난 앱의 스택 트레이스를 붙여넣었다.
java.lang.NoSuchMethodError:
No static method boxİnt(I)Ljava/lang/Integer;
in class Lkotlin/coroutines/jvm/internal/Boxing;
이 예외에는 이미 결정적 단서가 들어 있었다. boxİnt()의 대문자 ‘İ’ 위에 찍힌 작은 점 하나. 하지만 그걸 찾고 있지 않다면 누가 알아차리겠는가? 당장은 아무도 보지 못했다.
코루틴 라이브러리 유지보수자들은 즉시 버전 충돌을 의심하며 “IDE를 재시작하고 clean build를 하면 해결되나요?”라고 답했다. 케말은 두 대의 컴퓨터 중 한 대에서만 문제가 발생한다고 말했는데, 이는 업그레이드 후 빌드 캐시에 오래된 비호환 의존성 버전이 남아있을 수 있음을 시사했다.
일주일 뒤, 또 다른 터키 개발자가 같은 예외로 앱이 크래시 난다는 버그 리포트를 올렸다. 이제 코루틴 라이브러리 유지보수자들은 이 문제가 의존성 버전 불일치 때문에만 일어난다고 확신했다. 다른 상황이었다면 그 결론은 완전히 타당했을지도 모른다. 그들은 자신들 환경에서는 재현에 실패했는데, 이는 오히려 문제가 두 리포터가 프로젝트를 구성하고 빌드한 방식에 특화된 것이라는 증거처럼 보였다.
한 달이 지나서야 누군가 결정적 증거를 발견했다.
“이건 로케일 문제일 수밖에 없어요.” 2018년 12월 말, 또 다른 터키 소프트웨어 엔지니어 에렐 외즈자크럴르(Erel Özçakırlar)가 케말의 이슈에 댓글을 달며 썼다. 에렐은 모두가 놓쳤던 것을 지적했다. 실제 함수 이름은 boxInt()여야 하는데, 스택 트레이스에는 boxİnt()로 나온다는 것. 단순히 기존 함수의 오래된 버전을 호출한 것이 아니라, 코틀린이 터키 알파벳을 써서 애초에 존재한 적 없는 함수명을 “발명”한 듯했다. 게다가 에렐은 시스템 로케일이 영어인 컴퓨터에서 코드를 실행하면 문제가 사라진다는 것도 확인했다.
“시스템 언어 설정에 관한 에렐의 지적이 맞을지도 모르겠네요.” 케말은 더 조사해보겠다고 답했다. 하지만 코틀린 컴파일러 내부가 갑자기 터키 문자를 써서 상상 속 함수를 만들어내는 이유를 파헤치려면—도대체 어디서부터 시작해야 할까? 케말이 원래 버그 리포트 이상으로 제공할 수 있는 것은 많지 않았고, 이슈는 “추가 설명 대기” 라벨을 유지한 채 2019년 초에 (두 번째 유사 이슈와 함께) 닫혔다.
다시 버그는 숨었다. 하지만 이번엔 코틀린 컴파일러 내부 깊숙이 발톱을 박고 있었다. 오타가 난 boxİnt() 함수는 케말의 코드나 그가 쓰는 라이브러리에서 호출된 것이 아니었다. 그 실수는 컴파일러 자체가 그의 앱에 주입하고 있었다.
이를 이해하려면 코루틴이 어떻게 동작하는지 조금 이야기해야 한다.
코틀린 코루틴의 마법 대부분은 전용 라이브러리인 kotlinx.coroutines에서 일어난다. 하지만 코틀린 언어와 컴파일러에 더 밀접하게 통합된 핵심 구성 요소가 하나 있는데, 바로 suspend 키워드다.
함수에 suspend 키워드를 붙이면 코틀린 컴파일러는 함수가 비동기적으로 동작하도록 함수 시그니처를 다시 작성한다. 예를 들어 매개변수 두 개를 가진 suspending 함수를 작성하면, 코틀린 컴파일러가 생성하는 출력에는 실제로 매개변수가 세 개가 된다. 보이지 않는 세 번째 매개변수는 Continuation이며, 함수의 상태를 저장하는 동시에 비동기 결과를 받는 콜백 역할도 한다.
Continuation은 suspending 함수 안의 코드에 따라 다양한 값을 저장하고 전달한다. 그리고 바로 여기서 수수께끼의 boxInt() 함수가 등장한다.
코틀린에서 Int를 만들면, 그 값은 내부적으로 두 가지 다른 자바 타입 중 하나로 저장될 수 있다는 것을 알고 있을지도 모른다. 기본형 int 또는 Integer 객체. 코틀린은 값이 어떻게 사용되는지에 따라 자동으로 선택한다.
코루틴에서 Int를 사용하면, 코틀린은 때때로 이를 기본형 int 저장 방식에서 Integer 객체로 바꿔야 한다. 값이 제네릭 코루틴 continuation 메커니즘을 통과할 수 있도록 하기 위해서다. 이 변환을 박싱(boxing) 이라고 한다. 자바는 이 변환을 자동으로 해주지만, 코루틴 안정 릴리스를 위해 코틀린 팀은 이 변환이 가능한 한 효율적이도록 만들고 싶었다.
JVM이 suspending 함수 실행을 최적화하는 데 도움이 되도록, 코틀린 1.3은 컴파일러가 생성한 코루틴 코드에서 사용할 새 함수 집합을 추가했다: boxBoolean(), boxByte(), boxShort(), boxInt() 등(출처: GitHub). suspend 키워드는 핵심 언어의 일부이므로, 이 함수들은 모든 코틀린 프로그램에서 사용 가능해야 한다. 그래서 코루틴 라이브러리가 아니라 표준 라이브러리에 들어 있다. 다만 internal로 표시되어 있어 직접 호출할 수는 없다.
함수 자체는 문제가 아니다. 철자도 맞고 구현도 너무 단순해서 틀릴 곳이 없다. 버그는 컴파일러가 이 함수들을 호출하는 코드를 생성할 때 발생한다.
값을 올바르게 박싱하려면, 코틀린은 자바 기본형 타입을 해당 박싱 함수에 매핑해야 한다. boolean 값은 boxBoolean()에, byte 값은 boxByte()에, 등등.
여기엔 뻔한 패턴이 있다. 기본형 타입 이름의 첫 글자를 대문자로 만들고, 앞에 “box”를 붙이면 된다. 그리고 코틀린 1.3은 정확히 그렇게 했다. 표준 라이브러리의 capitalize() 함수를 이용해서 말이다(출처: GitHub).
kotlinmap[name] = "box${primitiveType.javaKeywordName.capitalize()}"
capitalize()는 문자열의 첫 글자만 바꾼다. 그래서 "boolean".capitalize()는 "Boolean"이 되고, "int".capitalize()는 "Int"가 된다.
터키에선 그렇지 않다.
또다시 capitalize() 동작은 컴퓨터의 언어 설정에 따라 달라질 수 있다. 또 그 ‘i’ 문제다.
i의 대문자가 İ이고,i의 대문자가 I다.터키어 환경에서 "int".capitalize()의 올바른 결과는 "İnt"다. capitalize()는 “int”가 터키어 텍스트가 아니라 영어 기반의 특별한 프로그래밍 키워드라는 사실을 알 길이 없다. 따라서 터키어 언어 설정 머신에서 돌아가는 코틀린 1.3 컴파일러가 suspending 함수 내부에서 자바 기본형 int를 박싱해야 하면, 존재하지 않는 표준 라이브러리 함수 boxİnt() 호출을 생성해버린다.
이런.
2019년 9월, 또 다른 터키 프로그래머 파티흐 도안(Fatih Doğan)이 마침내 모든 조각을 맞춰, 결국 수정으로 이어질 이슈 리포트를 올렸다. 파티흐는 boxİnt()의 ‘İ’ 위에 찍힌 점이 잘못되었음을 분명히 지적했고, 결정적으로 이 문제를 안정적으로 재현할 수 있는 GitHub 저장소를 만들어 재현 방법을 제공했다.
이 새롭고 상세한 리포트가 올라온 지 하루 만에 코틀린 팀은 문제를 일으키는 코드 줄을 찾아냈다. 그리고 일주일도 되지 않아 수정이 준비됐다(출처: GitHub).
kotlinmap[name] = "box${primitiveType.javaKeywordName.capitalize(Locale.US)}"
특정 Locale을 capitalize()에 넘기면, 어떤 머신에서 실행하든 항상 같은 언어 규칙을 사용한다. 아주 쉬운 변경이다. 코틀린 표준 라이브러리의 모든 대소문자 변환 함수처럼 capitalize()는 이미 선택적 Locale 인자를 받도록 되어 있었다. Locale을 지정하지 않았을 때만 시스템 기본 Locale로 폴백했을 뿐이다.
이 수정은 2019년 11월 Kotlin 1.3.6에 포함되어 릴리스되었고, 마침내 터키 개발자들이 suspending 함수를 안정적으로 사용할 수 있게 되었다.
하지만 이야기는 여기서 끝나지 않는다—전혀. 코루틴은 다시 동작하게 되었지만, 이 사건은 로케일-민감 대소문자 변환에 얼마나 쉽게 치명적으로 걸려들 수 있는지를 보여줬다. 그리고 이야기 초반의 그 빌드 오류는 여전히 고쳐지지 않은 상태였다…
문제의 진짜 심각성을 보여주는 버그가 하나 더 필요했다.
2020년 9월, 무히틴 카플란(Muhittin Kaplan)은 막 코틀린을 배우기 시작했다. 그는 배열 이해도를 확인하기 위해 간단한 프로그램을 썼다.
kotlinfun main() { println("Hello, world!!!") val nums = intArrayOf(1, 2, 3, 4, 5) println(nums[2]) }
하지만 실행하자 난해한 오류가 나타났다.
java.lang.NoSuchMethodError:
'int[] kotlin.jvm.internal.Intrinsics$Kotlin.intArrayOf(int[])'
intArrayOf()는 코틀린 표준 라이브러리에서 가장 기본적인 도구 중 하나이며, 1.0 이전부터 모든 코틀린 버전에 존재해왔다. 설령 존재하지 않거나 잘못 호출했다 해도 오류는 런타임이 아니라 컴파일 타임에 나야 한다.
무히틴은 뭔가 수상하다는 걸 알고 있었고, 자신이 보는 현상을 설명하는 YouTrack 이슈를 등록했다.
“Türkiye에서 인사드립니다(Hi from Türkiye).” 그는 이렇게 시작했다.
Enter를 누르거나 클릭하면 이미지를 원본 크기로 볼 수 있습니다
사진: Markus Winkler / Unsplash
이번에는 코틀린 팀도 무엇을 봐야 하는지 알고 있었다. 오래 걸리지 않아 컴파일러에서 문제를 일으키는 코드 줄을 추적해냈다(출처: GitHub).
javaStringsKt.decapitalize(type.getArrayTypeName().asString()) + "Of"
자바로 작성돼 있지만, 이 코드는 코틀린 표준 라이브러리의 decapitalize() 함수를 호출한다. 그리고 또다시 고정된 Locale을 쓰지 않고 시스템 기본 언어 설정에 의존하고 있었다.
이 코드는 intrinsics 를 구성하는 절차의 일부다. intrinsics는 표준 라이브러리에 실제 구현이 있는 것처럼 보이지만, 사실 컴파일러가 직접 해당 자바 명령이나 심지어 JVM 바이트코드로 치환해버리는 함수들이다. intArrayOf(1, 2, 3)를 쓰면 코틀린은 실제로 intArrayOf() 함수를 호출하지 않는다. 대신 intArrayOf()가 intrinsic으로 등록되어 있음을 인식하고, 배열을 생성하고 채우는 바이트코드를 곧바로 출력한다.
앞서 본 boxInt()처럼 intArrayOf()도 더 넓은 함수 패밀리의 일부다. 기본형 타입마다 배열 생성 함수가 하나씩 있다. type.getArrayTypeName() 호출은 각 배열에 대한 코틀린 클래스 이름—IntArray, BooleanArray 등—을 반환한다. 해당하는 함수—intArrayOf(), booleanArrayOf() 등—는 소문자로 시작해야 하므로 decapitalize()를 호출해야 한다.
그리고 여기 버그가 있다.
터키어 언어 설정 머신에서 "IntArray".decapitalize()(자바에서는 StringsKt.decapitalize("IntArray"))는, 너무 익숙한 점 없는 소문자 ‘ı’를 포함한 "ıntArray"를 반환한다. 여기에 "Of" 접미사를 붙이면, 우리는 intArrayOf()가 아니라 ıntArrayOf()라는 함수에 대한 intrinsic 바이트코드 구현을 등록해버린 것이다. 표준 라이브러리가 광고하는 intArrayOf()와는 다른 함수다!
이 이슈를 고칠 때 코틀린 팀은 어떤 가능성도 남기지 않았다. 컴파일러 코드베이스 전체에서 capitalize(), decapitalize(), toLowerCase(), toUpperCase() 같은 대소문자 변환 연산을 샅샅이 찾아, 로케일 불변(locale-invariant) 대안으로 바꿨다. 53개 파일에 걸쳐 173줄이 바뀌었고—그중에는 코틀린 1.0 시절 메흐메트 누리 외즈튀르크의 빌드를 실패하게 만들었던 컴파일러 출력 XML 파서도 포함되어 있었다(출처: GitHub).
이 대대적인 수정은 2021년 5월 Kotlin 1.5의 더 일반적인 컴파일러 업그레이드 프로젝트 일부로 릴리스되었다. 5년 동안 백로그에 있었던 KT-13631은 마침내 닫혔다.
세 가지 서로 다른 버그—컴파일러 출력, 코루틴, 배열—가 세 가지 서로 다른 함수—toLowerCase(), capitalize(), decapitalize()—때문에 발생했다. 더 근본적인 해결책이 없다면 코틀린의 대소문자 변환 함정은 다음 희생자를 기다리고 있었을 뿐이다.
코틀린 1.5가 출시되기 전부터 코틀린 팀은 로케일-민감 대소문자 변환이 다시는 어떤 코틀린 프로그램도 크래시 내지 못하게 하는 프로젝트를 진행 중이었다.
2020년 10월, 그들은 KEEP-223 “기본값으로 로케일 비의존 대소문자 변환(Locale-agnostic case conversions by default)”을 공개했다. 시스템 언어 설정을 무시하고 고정 로케일을 기본값으로 사용하는 새 함수 집합으로 코틀린의 대소문자 변환 함수를 교체하자는 제안이다. 새 uppercase()와 lowercase() 함수는 Kotlin 1.5 표준 라이브러리에 추가되었고, Kotlin 2.1부터는 기존 toLowerCase()와 toUpperCase()를 사용하면 오류가 발생한다.
그렇다면 capitalize()는?
KEEP-223이 논의되는 동안, capitalize()에는 로케일 민감성 말고도 문제가 더 있다는 점이 점점 분명해졌다. 함수 이름 자체가 놀라울 만큼 모호하다. 거의 모든 영어 사전에서 capitalize 를 찾아보면 두 가지 정의가 경쟁하고 있음을 볼 수 있다. Collins의 예시 하나만 보자.
6. to print or write (a word or words) in capital letters
7. to begin (a word) with a capital letter
코틀린의 capitalize() 함수는 언제나 문자열의 첫 글자만 바꾸도록 설계되어 있었고, 이는 혼란을 정리할 완벽한 기회였다. capitalize()가 모호한 이름이라면, 그 대체 함수는 더 명확하게 무엇이라 불려야 할까? 함수의 동작을 더 분명히 설명하는 이름을 떠올릴 수 있는가?
결국 코틀린 팀은 아예 대체 함수를 제공하지 않기로 했다. 함수가 존재하지 않으면 혼란이나 버그도 일으킬 수 없기 때문이다. 현대 코틀린에서는 문자열의 첫 글자를 바꾸고 싶을 때 replaceFirstChar { … }에 커스텀 람다를 제공한다.
Kotlin 2.1은 2024년 11월 출시되었고, 우리의 이야기는 만족스러운 결말을 맞이했다.
여기서 무엇을 배울 수 있을까? 가장 큰 교훈은, 언어의 표준 라이브러리에 얼마나 큰 책임이 얹혀 있는지라는 점이라고 생각한다. 표준 라이브러리는 단지 기본 알고리즘과 자료구조의 스타터 팩이라고 생각하기 쉽다. 하지만 조금만 더 깊이 들어가 보면, 가장 단순한 문자열 연산조차 인간 문화의 복잡성과 창의성을 상세히 디지털로 모델링한 것에 의존한다는 사실을 발견하게 된다.
그 디지털 모델의 상당 부분은 참고로 Unicode Common Locale Data Repository (CLDR)에서 제공된다. CLDR은 언어 규칙, 날짜/시간 형식, 측정 단위, 통화 등 훨씬 더 많은 것을 문서화한다. 유니코드는 이모지만 위한 게 아니다!
이제 딱 하나 아직도 나를 괴롭히는 질문이 있다. 메흐메트 누리 외즈튀르크는 결국 그 앱을 빌드하는 데 성공했을까?
읽어주셔서 감사합니다!
저는 책도 씁니다. 코틀린의 더 많은 기묘함과 컴파일러의 괴상한 동작이 궁금하다면 제 퍼즐북 Kotlin Brain Teasers도 확인해보세요.
그리고 suspending 함수와 코루틴에 대해 배우는 것을 즐겼다면 Kotlin Coroutine Confidence도 마음에 들 수 있습니다.