잘 알려지지 않은 Kotlin에서 빠른 컴파일의 비밀

Jessie Cho

많은 코드를 빠르게 컴파일하는 것은 어려운 문제입니다. 특히, 컴파일러가 제네릭을 사용한 오버로드 확인 및 유형 추론과 같은 복잡한 분석을 수행해야 하는 경우에는 더욱 그렇습니다. 이 게시물에서는 실행-테스트-디버그를 반복하는 일상적인 상황에서 많이 발생하는 비교적 사소한 변경을 훨씬 빠르게 컴파일할 수 있도록 해주는 Kotlin의 엄청난, 그러나 대부분 드러나지 않은 부분에 대해 이야기 하려고 합니다.

또한, Kotlin의 빠른 컴파일 프로젝트를 담당하는 JetBrains 팀에 합류할 선임급 개발자를 찾고 있으니, 관심이 있으신 분들은 게시물 하단을 참조하세요.

XKCD 만화 #303로 이야기를 시작해 보겠습니다.

XKCD 만화 303: 컴파일

이 게시물은 모든 개발자의 삶에서 매우 중요한 측면을 다룹니다. 바로, 코드를 변경한 후 테스트를 실행하는 데(또는 프로그램의 첫 줄에 도달하는 데) 시간이 얼마나 오래 걸리는가의 문제입니다. 이것을 종종 테스트 시간이라고 합니다.

이 문제가 왜 중요할까요?

  • 테스트까지 시간이 너무 짧으면 커피를 마시거나 잠시 숨을 돌릴 시간조차 나지 않을 겁니다.
  • 테스트까지 시간이 너무 길면, 소셜 미디어를 탐색하기 시작하거나 다른 것을 하느라 주의력이 떨어져 변경한 내용이 무엇이었는지 잘 기억나지 않을 수 있습니다.

두 상황 모두 장단점이 있지만, 컴파일러가 지시할 때가 아니라 의식적으로 휴식을 취하는 것이 가장 좋다고 생각합니다. 컴파일러는 똑똑한 소프트웨어이지만 인간의 건강한 작업 일정을 생각할 정도는 아니니까요.

개발자는 생산적이라고 느낄 때 더 행복해지는 경향이 있습니다. 컴파일로 작업이 중단되면 업무 흐름이 깨지고 순조롭던 진행이 막혀 생산성을 잃게 됩니다. 이런 상황을 즐길 사람은 없겠죠.

컴파일에 그토록 시간이 오래 걸리는 이유는 무엇일까요?

컴파일에 장시간이 걸리는 이유에는 크게 세 가지가 있습니다.

  1. 코드 베이스 크기: 1 MLOC를 컴파일하는 데 일반적으로 1 KLOC 이상 걸립니다.
  2. 도구 체인이 얼마나 최적화되었는지, 여기에는 컴파일러 자체 외에 사용 중인 빌드 도구가 포함됩니다.
  3. 컴파일러의 지능 수준: 사용자를 성가시게 하거나 끊임 없이 힌트와 상용구 코드를 요구하지 않고 알아서 일을 처리할 수 있는지 여부

처음 두 가지 요소는 명백하니, 세 번째 요소인 컴파일러의 똑똑함에 대해 이야기해 보겠습니다. 일반적으로 복잡한 상호 보완적인 관계가 존재하지만, 저희는 Kotlin에서 깨끗하고 읽기 쉬우며 유형 안정적인 코드 쪽을 선택했습니다. 이것은 컴파일러가 상당히 똑똑해야 한다는 것을 의미하는데, 그 이유는 다음과 같습니다.

Kotlin의 현 위치

Kotlin은 프로젝트가 오래 지속되고 규모가 점차 커지며 많은 사람들이 참여하는 산업 환경에서 사용하도록 설계되었습니다. 그래서 우리는 버그를 조기에 발견하고 정확한 도구(코드 완성, 리팩토링 및 IDE에서 사용 위치 검색, 정확한 코드 탐색 등)를 얻기 위해 정적인 유형 안전성을 구현하려고 노력합니다. 그리고, 불필요한 소음이나 양식없이 깨끗하고 읽기 쉬운 코드를 만들고자 노력합니다. 무엇보다도 코드 전체가 유형으로 가득 찬 상황을 피하려는 것입니다. 이것이 바로 람다 및 확장 기능 형식을 지원하는 스마트 유형 추론과 오버로드 해결 알고리즘을 마련한 이유입니다. 스마트 형 변환(흐름 기반 형식 지정) 등도 이러한 맥락에서 나온 것입니다. Kotlin 컴파일러는 코드를 깔끔하고 형식적으로 안전하게 유지하기 위해 자체적으로 많은 것을 파악합니다.

똑똑하면서 빠르기까지 할 수 있을까요?

스마트 컴파일러를 빠르게 실행시키려면 도구 체인의 모든 부분을 최적화해야 하며, 이는 저희가 지속적으로 노력하고 있는 부분입니다. 무엇보다도 현재 버전보다 훨씬 빠르게 실행될 차세대 Kotlin 컴파일러를 개발하는 데 주력하고 있습니다. 하지만 여기서 이 내용을 다루려는 것은 아닙니다.

컴파일러가 아무리 빨라도 대형 프로젝트에서는 부족함이 있습니다. 디버깅하는 동안 약간의 변경이 있을 때마다 전체 코드 베이스를 다시 컴파일하는 것은 엄청난 낭비입니다. 그래서 이전 컴파일에서 가능한 한 많은 부분을 재사용하고 꼭 필요한 부분만 컴파일할 수 있게 하려고 합니다.

다시 컴파일해야 하는 코드의 양을 줄이는 일반적인 접근 방식에는 두 가지가 있습니다.

  • 컴파일 회피 — 영향을 받은 모듈만 다시 컴파일
  • 증분 컴파일 — 영향을 받은 파일만 다시 컴파일

(개별 함수 또는 클래스의 변경 사항을 추적하여 파일보다 훨씬 적은 부분만 다시 컴파일하는 더욱 세밀한 접근 방식을 생각할 수도 있지만, 이러한 접근 방식을 산업용 언어에서 실제 구현하는 실용적 방법을 찾기 어려울 뿐만 아니라 필요해 보이지도 않습니다.)

이제 컴파일 회피와 증분 컴파일에 대해 자세히 살펴보겠습니다.

컴파일 회피

컴파일 회피의 핵심 개념:

  • “dirty”(=변경된) 파일을 찾습니다.
  • 이러한 파일이 속한 모듈을 다시 컴파일합니다(이전의 다른 모듈 컴파일 결과를 2진 종속요소로 사용).
  • 변경 사항의 영향을 받을 수 있는 다른 모듈을 확인합니다.
    • 이러한 모듈도 다시 컴파일하고 해당 ABI도 확인합니다.
    • 영향을 받는 모든 모듈이 다시 컴파일될 때까지 반복합니다.

ABI를 비교하는 방법을 알고 있다면 알고리즘이 다소 간단해집니다. 그렇지 않으면, 변경 사항의 영향을 받은 모듈을 다시 컴파일합니다. 물론, 아무도 사용하지 않는 모듈의 변경 사항은 모든 사람이 사용하는 ‘util’ 모듈의 변경 사항보다 빠르게 컴파일됩니다(ABI에 영향을 미치는 경우).

ABI 변경 추적

ABI는 Application Binary Interface(응용 프로그램 2진 인터페이스)의 약자이며 API의 일종이지만 2진과 관련됩니다. 기본적으로, ABI는 종속 모듈이 신경 쓰는 유일한 2진 부분입니다(Kotlin에는 별도 컴파일이 있기 때문이지만, 여기서는 다루지 않겠음).

대략적으로 말해서 Kotlin 2진(JVM 클래스 파일 또는 KLib)에는 선언과 본문이 포함됩니다. 다른 모듈은 선언을 참조할 수 있지만 모든 선언을 참조할 수는 없습니다. 그래서 예를 들어, private 클래스와 멤버는 ABI의 일부가 아닙니다. 본문은 ABI의 일부일 수 있을까요? 이 본문이 호출 사이트에서 인라인인 경우에는 그렇습니다. Kotlin에는 인라인 함수와 컴파일 시간 상수(상수 값)가 있습니다. 인라인 함수의 본문 또는 const val의 값이 변경되면, 종속 모듈을 다시 컴파일해야 할 수 있습니다.

따라서 대략적으로 말하면 Kotlin 모듈의 ABI는 선언, 인라인 함수 본문 및 다른 모듈에서 인식할 수 있는 const val 값으로 구성됩니다.

ABI의 변화를 감지하는 간단한 방법은 다음과 같습니다.

  • 이전 컴파일의 ABI를 일정 형식으로 저장합니다(효율성을 위해 해시를 저장하는 것이 좋을 수 있음).
  • 모듈을 컴파일한 후, 결과를 저장된 ABI와 비교합니다.
    • 똑같다면 여기서 끝입니다.
    • 변경된 경우, 종속 모듈을 다시 컴파일합니다.

컴파일 회피의 장점과 단점

컴파일 회피의 가장 큰 장점은 상대적으로 단순하다는 것입니다.

이 접근법은 모듈이 작을 때 특히 도움이 되는데, 재컴파일 단위가 전체 모듈이기 때문입니다. 모듈이 크면 다시 컴파일하는 데 시간이 오래 걸립니다. 따라서 기본적으로, 컴파일 회피를 위해서는 작은 모듈이 많이 있어야 하지만 개발자로서 우리가 이것을 원하거나 원하지 않을 수도 있습니다. 작은 모듈을 꼭 설계가 나쁜 것으로 생각할 필요는 없지만 기계가 아닌 사람을 위해 코드를 구성하는 것이 기본 자세가 되어야 할 것입니다.

또 한편으로, 다수의 작은 유용한 기능이 담겨진 ‘util’ 모듈과 같은 것들이 많은 프로젝트에서 이용되고 있는 모습을 보게 됩니다. 다른 거의 모든 모듈도 최소한 일시적으로라도 ‘util’에 의존합니다. 이제, 코드 베이스 전체에서 세 번 사용되는 다른 작은 유용한 함수를 추가한다고 가정해 보겠습니다. 이 함수는 모듈 ABI에 추가되므로 모든 종속 모듈이 영향을 받고, 전체 프로젝트가 다시 컴파일되기 때문에 기다림의 시간이 필요합니다.

그뿐 아니라, 작은 모듈(각 모듈은 다른 여러 모듈에 의존함)이 많다는 것은 각 모듈마다 고유한 종속요소(소스 및 2진) 집합이 포함되므로 프로젝트의 구성이 거대해질 수 있음을 의미합니다. Gradle에서 각 모듈을 구성하는 데 일반적으로 약 50-100ms가 걸립니다. 대규모 프로젝트에 1000개 이상의 모듈이 포함되는 것은 드문 일이 아니므로 총 구성 시간은 1분 이상 걸릴 수 있습니다. 그리고 모든 빌드에서, 그리고 프로젝트를 IDE로 가져올 때마다(예: 새 종속요소가 추가될 때) 실행되어야 합니다.

Gradle에는 컴파일 회피의 일부 단점을 완화하는 여러 기능이 있습니다. 예를 들어, 구성을 캐싱할 수 있습니다. 이 부분에서 아직 개선해야 할 여지가 많으며, 이것이 바로 Kotlin에서 증분 컴파일을 사용하는 이유입니다.

증분 컴파일

증분 컴파일은 컴파일 회피보다 더 세분화되어 모듈이 아닌 개별 파일에서 작동합니다. 결과적으로, 이 컴파일 방식은 모듈 크기를 고려하지 않으며, “인기 있는” 모듈의 ABI가 중대하게 변경된 경우가 아니면 전체 프로젝트를 다시 컴파일하지 않습니다. 일반적으로, 이 접근 방식은 사용자를 크게 제한하지 않으며 테스트까지 시간을 단축합니다. 또한 개발자가 검싸움을 할 일이 적어지게 됩니다.

증분 컴파일은 IntelliJ의 내장 빌드 시스템인 JPS에서 지속적으로 지원되고 있습니다. Gradle은 기본적으로 컴파일 회피만 지원합니다. 1.4 버전을 기준으로, Kotlin Gradle 플러그인은 Gradle의 증분 컴파일 구현을 다소 제한하며 여전히 개선의 여지가 많습니다.

이상적으로는 변경된 파일만 확인하고 해당 파일에 종속된 파일을 정확히 찾아낸 다음, 이러한 모든 파일을 다시 컴파일하는 것이 좋습니다. 이는 멋지고 쉬운 것처럼 들리지만 실제로는 이 종속 파일 집합을 정확하게 결정하는 일은 정말 간단하지 않습니다. 우선, 소스 파일 간에 순환 종속요소가 있을 수 있는데, 이것은 대부분의 최신 빌드 시스템 모듈에 허용되지 않습니다. 그리고 개별 파일의 종속성은 명시적으로 선언되지 않습니다. 동일한 패키지 및 체인 호출에 대한 참조로 인해 가져오기가 종속요소를 결정하기에 충분하지 않습니다. A.b.c()의 경우, 기껏해야 A를 가져와야 하지만 B 유형의 변경도 영향을 미칩니다.

이러한 모든 복잡한 문제로 인해 증분 컴파일은 여러 라운드를 진행하여 영향을 받는 파일 집합을 근사화하려고 합니다. 이 과정을 개괄적으로 요약하면 다음과 같습니다.

  • “dirty”(=변경된) 파일을 찾습니다.
  • 다시 컴파일합니다(다른 소스 파일을 컴파일하는 대신 이전 컴파일의 결과를 2진 종속요소로 사용)
  • 이러한 파일에 해당하는 ABI가 변경되었는지 확인합니다.
    • 변경되지 않았으면 여기서 끝납니다!
    • 변경되었으면 변경 사항의 영향을 받는 파일을 찾아 더티 파일 세트에 추가하고 다시 컴파일합니다.
    • ABI가 안정화될 때까지 반복합니다(이를 “고정점”이라고 함).

ABI를 비교하는 방법을 이미 알고 있으므로, 기본적으로 두 가지 까다로운 부분만 남습니다.

  1. 이전 컴파일의 결과를 사용하여 임의의 소스 하위 집합을 컴파일합니다.
  2. 주어진 ABI 변경 사항의 영향을 받는 파일을 찾습니다.

두가지 모두 적어도 Kotlin 증분 컴파일러의 기능 중 일부입니다. 하나씩 살펴보겠습니다.

더티 파일 컴파일

컴파일러는 이전 컴파일 결과의 하위 집합을 사용하여 더티가 아닌 파일의 컴파일을 건너뛰고, 파일에 정의된 기호만 로드하여 더티 파일에 대한 2진을 생성하는 방법을 알고 있습니다. 이것은 증분 기능을 위해서가 아니라면 컴파일러가 꼭 수행해야 하는 작업은 아닙니다. 소스 파일별 작은 2진 대신 모듈에서 하나의 큰 2진을 생성하는 것은 JVM 세계 밖에서는 그렇게 일반적이지 않습니다. 그리고 이것은 Kotlin 언어의 기능이 아니라 증분 컴파일러의 세부적인 구현 내용입니다.

더티 파일의 ABI를 이전 결과와 비교할 때 운이 좋아 더 이상 재컴파일을 반복할 필요가 없는 경우가 있을 수 있습니다. 더티 파일의 재컴파일만 필요한(ABI가 변경되지 않기 때문에) 몇 가지 변경의 예를 살펴보겠습니다.

  • 주석, 문자열 리터럴(const val 제외) 등
    • 예: 디버그 출력에서 변경
  • 인라인이 아니고 반환 유형 추론에 영향을 주지 않는 함수 본문에 국한된 변경 사항
    • 예: 디버그 출력 추가/제거 또는 함수의 내부 논리 변경
  • 비공개 선언에 국한된 변경(클래스 또는 파일에 대해 비공개일 수 있음)
    • 예: 비공개 함수에 도입 또는 이름 변경
  • 선언의 순서 변경

알 수 있는 바와 같이 이러한 경우는 코드를 디버깅하고 반복적으로 개선할 때 매우 일반적입니다.

더티 파일 세트 확대

운이 좋지 않고 일부 선언이 변경된 경우, 코드가 한 줄도 변경되지 않았더라도 더티 파일에 의존하는 일부 파일이 재컴파일 시 다른 결과를 생성할 수 있습니다.

간단한 방법은 이 시점에서 포기하고 전체 모듈을 다시 컴파일하는 것입니다. 그러면 컴파일 회피와 관련된 모든 문제들이 전면에 대두됩니다. 큰 모듈은 선언을 수정하는 즉시 문제가 되고, 위에서 설명한 것처럼 수많은 작은 모듈이 성능 비용을 초래합니다. 따라서 더 세분화해야 합니다. 즉, 영향을 받는 파일을 찾아서 이 파일을 다시 컴파일해야 합니다.

따라서 실제로 변경된 ABI 부분에 의존하는 파일을 찾아야 합니다. 예를 들어, 사용자가 foo의 이름을 bar로 변경했다면 foobar 이름에 관련이 있는 파일만 다시 컴파일하고, 이 ABI의 일부 다른 부분을 참조하더라도 다른 파일은 그대로 두어야 합니다. 증분 컴파일러는 어떤 파일이 이전 컴파일의 어떤 선언에 의존하는지 기억하므로, 이 데이터를 모듈 종속요소 그래프처럼 사용할 수 있습니다. 이것도 증분이 아니었다면 컴파일러가 일반적으로 수행하는 작업이 아닙니다.

이상적으로는 모든 파일에 대해 어떤 파일이 여기에 종속되어 있는지, 이 파일에서 ABI의 어떤 부분이 중요하게 관련되는지를 저장해야 합니다. 실제로 모든 종속요소를 그렇게 정확하게 저장하는 것은 비용이 너무 많이 듭니다. 그리고 많은 경우에 전체 시그니처를 저장하는 것이 의미가 없습니다.

다음 예를 생각해보겠습니다.

파일: dirty.kt

파일: clean.kt

사용자가 함수 changeMe의 이름을 foo로 바꿨다고 가정해 보겠습니다. clean.kt가 변경되지는 않지만 bar()의 본문은 재컴파일할 때 변경됩니다. 이제 clean.kt의 foo(Any)가 아니라 dirty.kt의 foo(Int)를 호출하고 반환 유형도 변경됩니다. 즉, dirty.kt와 clean.kt를 모두 다시 컴파일해야 한다는 것을 의미합니다. 증분 컴파일러는 이것을 어떻게 알아낼 수 있을까요?

변경된 파일: dirty.kt를 다시 컴파일하는 것으로 시작합니다. 그러면 ABI의 어떤 내용이 변경되었음을 알 수 있습니다.

  • 함수 changeMe가 더 이상 존재하지 않습니다.
  • Int를 입력 받아서 Int를 반환하는 함수 foo가 있습니다.

이제 clean.kt가 foo 이름에 의존한다는 것을 알 수 있습니다. 즉, clean.kt와 dirty.kt를 모두 다시 컴파일해야 한다는 것을 의미합니다. 왜 그럴까요? 유형을 신뢰할 수 없기 때문입니다.

증분 컴파일은 모든 소스의 전체 재컴파일과 동일한 결과를 생성해야 합니다. dirty.kt에 새로 나타난 foo의 반환 유형을 살펴 보겠습니다. 이 파일은 추론되고, 실제로 파일 간의 순환 종속요소인 clean.kt의 bar 유형에 의존합니다. 따라서 여기에 clean.kt를 추가하면 반환 유형이 변경될 수 있습니다. 이 경우, 컴파일 오류가 발생하지만 clean.kt를 dirty.kt와 함께 다시 컴파일할 때까지 우리는 이 사실을 알 수 없습니다.

최신의 증분 컴파일을 위해 Kotlin에 적용되는 큰 틀의 규칙은 신뢰할 수 있는 것은 이름뿐이라는 것입니다. 각 파일에 대해 다음을 저장하는 이유가 여기에 있습니다.

  • 생성되는 ABI
  • 컴파일하는 동안 조회된 이름(전체 선언이 아님)

이 모든 것을 저장하는 방법에 일부 최적화가 가능합니다. 예를 들어, 일부 이름은 파일 외부에서 검색되지 않습니다(예: 지역 변수의 이름 및 경우에 따라 지역 함수). 색인에서 이러한 이름을 생략할 수 있습니다. 알고리즘을 더 정확하게 만들기 위해 각 이름을 조회할 때 어떤 파일이 참조되었는지 기록합니다. 그리고 색인을 압축하기 위해 해싱을 사용합니다. 여기에 일부 개선의 여지가 남아 있습니다.

아마도 알아챘겠지만 초기 더티 파일 세트를 여러 번 다시 컴파일해야 합니다. 아쉽게도 이 문제를 해결할 방법은 없습니다. 순환 종속요소가 있을 수 있으며 영향을 받는 모든 파일을 한꺼번에 컴파일하는 것만으로 올바른 결과를 얻을 수 있습니다. 최악의 경우, 이중 작용이 일어나 증분 컴파일이 컴파일 회피였을 때보다 더 많은 작업을 수행할 수 있으므로 이를 보호하는 경험적 지식이 있어야 합니다.

모듈 경계를 넘는 증분 컴파일

지금까지 가장 큰 문제는 모듈 경계를 넘을 수 있는 증분 컴파일입니다.

하나의 모듈에 더티 파일이 있고 몇 번의 순환을 거쳐 고정점에 도달한다고 가정해 보겠습니다. 이제 이 모듈의 새 ABI를 얻게 되고 종속 모듈에 대해 뭔가를 해야 합니다.

물론, 우리는 초기 모듈의 ABI에서 어떤 이름이 영향을 받았는지 알고 있으며 종속된 모듈의 어떤 파일이 이러한 이름을 조회했는지 알고 있습니다. 이제, 본질적으로 동일한 증분 알고리즘을 적용할 수 있지만 더티 파일 세트가 아닌 ABI 변경에서 시작합니다. 한편, 모듈 간에 순환 종속요소가 없다면 종속 파일만 다시 컴파일하는 것으로 충분합니다. 하지만 해당 ABI가 변경된 경우에는 동일한 모듈의 파일을 더 많이 세트에 추가하고 동일한 파일을 다시 컴파일해야 합니다.

Gradle에서 이를 완전히 구현하는 것은 공개적인 도전입니다. Gradle 아키텍처에 약간의 변경이 필요할 수 있지만 과거 경험을 통해 이것이 가능하고 Gradle 팀에서 기꺼이 환영할 일이라는 것을 알고 있습니다.

이 블로그 게시물에서 다루지 않은 사항

여기서 목표한 것은 Kotlin에서 환상적으로 발휘되는 빠른 컴파일을 엿보는 기회를 주는 것이었습니다. 아래의 내용은 포함하지만 이에 국한되지 않는, 이번 블로그에서는 다루지 않은 훨씬 더 많은 내용이 있습니다.

  • 빌드 캐시
  • 구성 캐시
  • 작업 구성 회피
  • 증분 컴파일 색인 및 기타 캐시를 디스크에 효율적으로 저장
  • Kotlin+Java 결합 프로젝트에서 증분 컴파일
  • Java 종속요소룰 두 번 읽지 않도록 메모리에서 javac 데이터 구조 재사용
  • KAPTKSP의 증분 기능
  • 더티 파일을 빠르게 찾기 위한 파일 감시기

요약

이제, 현대적 프로그래밍 언어에서 빠른 컴파일이 대두시키는 문제에 대해 기초적인 개념을 갖게 되었습니다. 일부 언어의 경우, 이 모든 작업을 수행할 필요가 없도록 하기 위해 컴파일러를 의도적으로 덜 똑똑하게 만들었습니다. 좋든 나쁘든 Kotlin은 다른 길을 갔고, Kotlin 컴파일러를 매우 스마트하게 만드는 기능들은 강력한 추상화, 가독성 및 간결한 코드를 동시에 제공하면서 사용자들에게 가장 사랑 받는 기능이 되었습니다.

핵심 형식 검사 및 이름 확인 알고리즘의 구현을 새롭게 고찰함으로써 컴파일 속도를 훨씬 높일 차세대 컴파일러의 개발이 진행되고 있지만, 이 블로그 글에서 설명한 모든 내용은 앞으로 계속해서 유익한 정보로 남을 것입니다. 그 이유 중 하나는 현재 kotlinc보다 훨씬 빠른 컴파일러를 사용하면서도 IntelliJ IDEA의 증분 컴파일 기능을 유익하게 활용하는 Java 프로그래밍 언어에서의 경험 때문입니다. 또 다른 이유는 우리의 목표가 컴파일이 전혀 없이 변경 사항을 즉각적으로 채택하는 인터프리터 언어의 개발 주기에 최대한 가까워지는 것입니다. 따라서, 빠른 컴파일을 위한 Kotlin의 전략은 최적화된 컴파일러 + 최적화된 도구 체인 + 정교한 증분 처리입니다.

JetBrains 팀에 합류하세요!

이런 종류의 문제를 해결하는 데 관심이 있다면, 현재 Kotlin의 빠른 컴파일을 위한 JetBrains 팀에서 인재를 찾고 있으니 문을 두드려 보기 바랍니다. 영어러시아로 작성된 채용 공고를 살펴보세요. 컴파일러 또는 빌드 도구에 대한 사전 경험은 필요하지 않습니다. 모든 JetBrains 사무소에서 채용 중입니다(상트페테르부르크, 뮌헨, 암스테르담, 보스턴, 프라하, 모스크바, 노보시비르스크) 또는 전 세계 어디서든 원격 근무가 가능합니다. 많은 관심 부탁 드립니다!

이 게시물은 Andrey Breslav가 작성한 The Dark Secrets of Fast Compilation for Kotlin을 번역한 글입니다.