Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

조직 전반으로 Kotlin 도입 확장하기

Read this post in other languages:

선임 소프트웨어 엔지니어이자 JetBrains 공인 Kotlin 트레이너로 활동 중인 Urs Peter가 기고한 글입니다. Urs는 더 체계적인 방법으로 Kotlin 역량을 높이고 싶은 독자를 대상으로 Xebia Academy에서 Kotlin 역량 강화 프로그램도 운영하고 있습니다.

이 글은 Java 기반 환경에서 Kotlin을 성공적으로 도입하기 위한 완벽 가이드라는 제목의 시리즈로 게재되는 다섯 번째 글입니다. 이 시리즈는 한 개발자의 호기심에서 시작해 전사적인 변화로 이어지는 과정을 통해 실제 팀 내에서 Kotlin 도입이 어떻게 점진적으로 확대되는지 소개하고 있습니다.

이 시리즈의 전체 글:

  1. Java 개발자를 위한 Kotlin 시작하기
  2. 실제 프로젝트에서 Kotlin 평가하기
  3. 회사에 Kotlin 도입 확장하기
  4. Kotlin 도입에 대한 의사결정권자의 동의 이끌어내기
  5. 조직 전반으로 Kotlin 도입 확장하기

대규모 Kotlin 도입의 성공 요인

Kotlin에 대한 개발자들의 동의와 경영진의 지원을 이끌어내는 것은 의미 있는 성과이지만 여정은 거기서 끝나지 않습니다. 진정한 도전은 Kotlin을 함께 사용하거나 Kotlin으로 전환해야 하는 기존 Java 코드베이스를 마주하는 순간부터 시작됩니다. 이러한 하이브리드 환경을 어떻게 효과적으로 헤쳐 나갈 수 있을까요?

기존 코드베이스를 관리하는 핵심은 조직의 목표와 운영 현실에 부합하는 전략을 수립하는 데 있습니다. 다음은 실제 현장에서 효과가 입증된 접근 방식입니다.

애플리케이션 수명 주기 전략

애플리케이션은 수명 주기 단계에 따라 서로 다른 접근이 필요합니다. 이 접근 방식을 세 가지 범주로 나누어 살펴보겠습니다.

수명 종료 단계의 애플리케이션

전략: 그대로 두기

애플리케이션이 이미 종료 예정이라면, 비즈니스적인 이유에서라도 마이그레이션을 진행할 필요가 없습니다. 이러한 시스템은 Java로 유지하고 중요한 곳에 에너지를 집중하세요. 애플리케이션의 남은 사용 기간을 고려해 보면 마이그레이션에 드는 비용은 전혀 합리적이지 않습니다.

신규 시스템

전략: 기본은 Kotlin

Kotlin 도입이 완료된 조직에서는 그린필드 프로젝트가 자연스럽게 Kotlin으로 시작됩니다. 도입이 진행 중인 경우에는 팀이 Java와 Kotlin 중에서 선택하는 경우가 많습니다. 현명하게 선택하세요 ;-).

활성화된 애플리케이션

전략: 기능 중심의 실용적 마이그레이션.

활성화된 애플리케이션은 특히 신중한 검토가 필요한 대상입니다. 단순히 재작성을 위한 재작성은 프로덕트 오너를 설득하기 어렵습니다. 대신, 마이그레이션 작업을 새로운 기능 개발과 결합하세요. 이 접근 방식은 코드베이스를 현대화하는 동시에 명확한 비즈니스 가치를 제공합니다. 이때 기존 Java 애플리케이션 확장/변환 섹션에서 논의한 다양한 접근 전략을 활용할 수 있습니다.

Java에서 Kotlin으로의 변환 접근 방식

Java를 Kotlin으로 변환할 때는 장단점이 뚜렷한 여러 선택지가 있습니다.

1. 전체 재작성

적합한 경우: 소규모 코드베이스

과제: 대규모 시스템에서는 많은 시간이 소요됨

코드베이스를 처음부터 다시 작성하면 가장 깔끔하고 관용적인 Kotlin 코드를 얻을 수 있습니다. 이 접근 방식은 마이크로서비스와 같은 소규모 코드베이스에 적합합니다. 대규모 코드베이스의 경우, 일반적으로 비용 부담이 지나치게 커질 수 있습니다.

2. IDE 자동 변환 후 수동 다듬기

적합한 경우: 리팩터링에 별도 시간을 투입할 수 있는 중간 규모 코드베이스

과제: 수동 다듬기는 필수

IntelliJ IDEA의 Convert Java to Kotlin(Java를 Kotlin으로 변환 기능은 관용적이지 않은 구문의 기계적 변환을 제공합니다. 다음 예를 생각해보겠습니다.

1차 시도:

Java

record Developer(
       String name,
       List languages
) {}

원시 자동 변환 결과:

Kotlin

@JvmRecord
data class Developer(
   val name: String?,
   val languages: MutableList?
)

이 변환에는 여러 문제가 있습니다.

  • 모든 것이 null 가능으로 변환됩니다(과도한 방어적 처리).
  • Java 컬렉션은 Kotlin의 기본 읽기 전용 List 대신 Kotlin의 MutableList가 됩니다.

이때 jspecify 어노테이션을 활용하면 변환을 개선할 수 있습니다.

모든 Java 타입을 Kotlin의 null 가능 타입으로 변환해야 하는 경우, @NonNull/@Nullable 어노테이션으로 해결할 수 있습니다. 이 어노테이션을 사용하는 방법은 다양합니다. 그 중 최신 방법은 jspecify로, 최근 Spring에서도 공식 지원되고 있습니다.

   org.jspecify
   jspecify
   1.0.0


implementation("org.jspecify:jspecify:1.0.0")

jspecify를 사용하여, Java 코드에 @Nullable 및 @NonNull 어노테이션을 달 수 있습니다.

2차 시도:

Java

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;

record Developer(
       @NonNull String name, 
       @NonNull List languages,
       @Nullable String email
) {}

이제 자동 변환 결과가 훨씬 개선되었습니다.

Kotlin

@JvmRecord
data class Developer(
   //😃 non-null as requested
   val name: String, 
   //😃 both, collection and type are non-null
  val languages: MutableList,
   //😃 nullable as requested
  val email: String?, 
)

자동 변환 방식의 한계:

jspecify 애노테이션을 사용하더라도 복잡한 Java 패턴은 제대로 변환되지 않습니다. 섹션 3에 제시된 자동 변환 코드 예시를 참고하세요. 더 이상 Checked Exception은 없고 더 안전한 Kotlin 코드가 되었습니다.

Kotlin

fun downloadAndGetLargestFile(urls: MutableList): String? {
     //🤨 using Stream, instead of Kotlin Collections
     val contents = urls.stream().map { urlStr: String? ->
     //🤨 still using Optional, rather than Nullable types
     //🤨 var but we want val!
     var optional: Optional
     //🤨 useless try-catch, no need to catch in Kotlin
     try {
         optional = Optional.of(URI(urlStr).toURL())
     } catch (e: URISyntaxException) {
           optional = Optional.empty()
     } catch (e: MalformedURLException) {
           optional = Optional.empty()
     }
     optional
   }.filter { it!!.isPresent() }//🤨 discouraged !! to force conversion to non-null
    .map {it.get() }
    .map { url: URL ->
      //🤨 useless try-catch, no need to catch in Kotlin
      try {
        url.openStream().use { `is` -> 
           String(`is`.readAllBytes(), StandardCharsets.UTF_8)
        }
      } catch (e: IOException) {
         throw IllegalArgumentException(e)
       }
    }.toList()
   //🤨 usage of Java collections…
   return Collections.max(contents)
}

자동 변환으로는 원하는 관용적 결과를 얻을 수 없습니다.

Kotlin

fun downloadAndGetLargestFile(urls: List): String? =
   urls.mapNotNull {
       runCatching { URI(it).toURL() }.getOrNull()
   }.maxOfOrNull{ it.openStream().use{ it.reader().readText() } }

자동 변환은 출발점으로 삼을 수 있지만, 진정으로 관용적인 Kotlin을 구현하려면 상당한 수작업과 관용적 Kotlin에 대한 이해가 필요합니다.

3. AI 지원 변환

적합한 경우: 탄탄한 테스트 인프라를 갖춘 대규모 코드베이스

과제: 수동 검토 필수

AI를 활용하면 기본적인 자동 변환보다 더 관용적 결과를 얻을 수 있지만 그만큼 사전 준비가 관건입니다.

전제 조건:

  1. 포괄적인 테스트 커버리지: LLM은 예측 불가하므로, AI의 할루시네이션을 포착할 수 있는 신뢰할 수 있는 테스트가 필요합니다.
  2. 정교하게 설계된 시스템 프롬프트: 조직의 표준에 부합하는 관용적인 Kotlin 변환을 위해 상세한 지침을 작성해야 합니다. 이 시스템 프롬프트를 출발점으로 활용할 수 있습니다.
  3. 광범위한 코드 검토: AI의 결과물은 논리적, 관용적 정확성을 보장하기 위해 철저한 검토가 필요하며, 이는 대규모 코드베이스에서는 상당한 정신적 부담이 될 수 있습니다.

제안된 시스템 프롬프트를 사용해 변환을 유도하면 결과는 상당히 만족스러울 수 있어도 여전히 완벽하지는 않습니다.

Kotlin

fun downloadAndGetLargestFile(urls: List): String? {
   val contents = urls
      .mapNotNull { 
            urlStr -> runCatching { URI(urlStr).toURL() }.getOrNull() 
      }.mapNotNull { url -> runCatching { 
            url.openStream().use { it.readAllBytes().toString(UTF_8)
         } 
      }.getOrNull() }

   return contents.maxOrNull()
}

4. 대규모 자동 변환

적합한 경우: 체계적인 변환이 필요한 초대형 코드베이스

현재 Java 코드베이스를 대규모로 관용적 Kotlin으로 변환하기 위한 공식 도구는 없습니다. 하지만 Meta와 Uber는 백엔드 애플리케이션에도 그대로 적용할 수 있는 접근 방식을 활용해 Android 코드베이스에서 이 문제를 성공적으로 해결했습니다. 다음 문서와 발표에는 Meta와 Uber가 이 과제에 어떻게 접근했는지에 대한 인사이트가 나와 있습니다.

Meta의 접근 방식:

Meta

Uber의 접근 방식:

Uber

  • 전략: AI를 활용해 변환 규칙을 생성하는 규칙 기반의 결정론적 변환
  • 리소스: KotlinConf 발표

두 회사 모두 수동 변환이나 단순한 자동화에 의존하지 않고, 체계적이고 반복 가능한 프로세스를 구축하여 성공을 거두었습니다. 이들의 규칙 기반 접근 방식은 수백만 줄의 코드 전반에 걸쳐 일관성과 품질을 보장합니다.

주의 사항: Java 코드를 대규모로 Kotlin으로 변환하면 조직 수준의 과제가 발생합니다. 이에 대해 신뢰성을 확보하려면 생성된 코드를 사람이 직접 검토하는 단계가 여전히 필요합니다. 그러나 충분한 계획 없이 진행하면, 자동 변환으로 쏟아지는 수많은 풀 리퀘스트로 인해 엔지니어의 업무가 과중해질 수 있습니다. 따라서 조직에 대한 영향 역시 신중하게 고려해야 합니다.

기억하세요. 대규모 Kotlin 도입의 성공은 단순히 코드를 변환하는 데 있지 않습니다. 팀의 전문성을 구축하고, 코딩 표준을 정립하며, 조직에 장기적인 가치를 제공하는 지속 가능한 프로세스를 만드는 것이 핵심입니다.

어떤 접근 방식을 언제 사용해야 하는지에 대해 간략히 정리해 보겠습니다.

  • 소규모 애플리케이션이거나 어차피 재작성할 예정인가요?
    Kotlin으로 재작성(1)
  • 리팩터링에 투자할 시간이 확보된 중간 규모 코드베이스이거나, Kotlin을 학습 중인 팀인가요?
    IntelliJ IDEA 자동 변환 + 보정(먼저 테스트부터 시작) (2)
  • 반복되는 패턴이 많고 테스트가 잘 갖춰진 중/대규모 코드베이스인가요?
    AI 지원 접근 방식 (3)
  • 여러 서비스 전반에서 조직 차원의 Kotlin 마이그레이션이 필요한가요?
    구체적인 계획을 세워 대규모로 자동 변환(플랫폼 주도) (4)

Kotlin의 잠재력을 최대한 활용

Kotlin은 개발자의 생산성을 빠르게 끌어올립니다. 간결한 구문, 안전성 기능, 풍부한 표준 라이브러리를 통해 많은 개발자가 몇 주 만에 코드를 개선할 수 있으며, 이는 자기 주도 학습이나 실무 경험을 통해서도 충분히 가능합니다. 하지만 지침이 없으면 Kotlin을 Java처럼 사용하는 방식에 빠져 가변 구조에 집착하고, 장황한 패턴을 유지하며, 관용적인 기능을 놓치게 됩니다.

다음 단계로 나아가기

다음 단계로 나아간다는 것은 불변성, 표현식 중심 코드, DSL, 코루틴을 활용한 구조적 동시성(가상 스레드 사용 여부와 무관)을 수용한다는 의미로, 많은 개발자가 바로 이 지점에서 어려움을 겪습니다.

개인적 경험에 따르면 이 단계에서는 자기 주도 학습보다 외부 교육이 훨씬 더 효과적이었습니다. 수년간 Kotlin을 사용해 온 개발자도 관용적인 패턴을 체득하고 언어의 잠재력을 온전히 끌어내기 위해 집중적인 코칭을 받는 경우가 많습니다.

Kotlin 히어로

Kotlin으로 갈 것인가, 말 것인가: 원하는 회사의 미래는?

궁극적으로 Kotlin 도입에 대한 결정은 조직의 엔지니어링 문화를 반영합니다. 다음 중 무엇을 더 중시하시나요?

  • 전통적인 접근 방식(Java): 보수적이고, 형식에 얽매여 있으며, 안정적이다.
  • 진보적인 접근 방식(Kotlin): 실용적이고, 현대적이며, 유연합니다.

두 접근 방식 모두 장점이 있습니다. Java로는 일을 완료할 수 있습니다. Kotlin으로는 일을 더 효과적으로 완료하는 동시에 개발자 만족도를 높이고 버그를 줄일 수 있습니다. 문제는 Kotlin이 더 나은가가 아니라, 조직이 더 나아지기 위해 투자할 준비가 되어 있는가입니다.

첫 Kotlin 테스트에서 전사적 도입에 이르기까지의 여정은 항상 순탄하지는 않지만 올바른 접근 방식을 취한다면 놀라울 정도로 예측성을 높일 수 있습니다. 작게 시작하고, 가치를 입증하며, 커뮤니티를 구축하고, 신중하게 확장하세요.

지금의 개발자와 미래의 개발자 모두가 이에 대해 감사할 것입니다.

Urs Peter

Urs는 20년 이상에 걸쳐 Kotlin과 Scala를 주로 사용하여 복원력 있고 확장 가능하며 임무 수행에 필수적인 시스템을 구축한 풍부한 경험을 가진 소프트웨어 엔지니어이자 솔루션 아키텍트, 콘퍼런스 연사, 트레이너입니다.

컨설턴트로 활동하는 외에도 열정적인 트레이너로서 Kotlin 및 Scala 언어 과정부터 마이크로서비스 및 이벤트 기반 아키텍처에 대한 아키텍처 교육까지 광범위한 주제를 다루는 다양한 교육 과정을 저술했습니다.

천성적으로 사교적인 사람으로, 모임과 콘퍼런스에서 지식을 공유하고 동료들과 영감을 나누는 것을 좋아합니다. Urs는 JetBrains 인증 Kotlin 트레이너입니다.

image description

Discover more