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 평가

소문을 퍼뜨려라: 동료 개발자들의 마음과 생각을 사로잡기

지금쯤이면 Kotlin의 장점을 충분히 인지하고 있는 핵심 팀이 구성되어 있을 것입니다. 이제부터가 중요한 단계입니다. 도입을 어떻게 확장할 수 있을까요?

이 단계의 핵심은 도입에 회의적인 Java 개발자들과 공감대를 형성하는 데 있습니다. 다음과 같은 기술적 요인과 문화적 요인이 중요한 차이를 만들어냅니다.

기술적 요인: 코드

  • 코드 자체의 강점으로 설득

문화적 요인: 개발자 지원 및 연결

  • 쉽고 원활한 온보딩 지원
  • 자기주도 학습 자료 제공
  • 사내 Kotlin 커뮤니티 구축
  • 여유를 가질 것

코드 자체의 강점으로 설득

Kotlin으로 애플리케이션을 (재)작성하며 쌓은 경험을 바탕으로, 그 이점에 대해 누구나 이해할 수 있는 명확한 방식으로 보여줍니다.

  • 설명하기보다 보여줄 것: Java와 Kotlin 스니펫을 비교해 Kotlin의 간결함을 보여줍니다.
  • 전체적인 패러다임에 초점을 두고 확장할 것: Kotlin은 단순히 Java와 다른 것이 아닙니다. 안전성, 가독성 및 유지 관리 편의성을 극대화하기 위한 간결함, 기본 요소로서의 함수, 확장성을 기반으로 설계되었으며, 이를 통해 Java의 근본적인 한계를 해결하면서도 Java 에코시스템과의 완전한 상호 운용성을 유지하고 있습니다.

다음은 몇 가지 구체적인 예시입니다.

1. null 안전성: 수십억 달러 규모의 실수 방지! 

Java

//This is why we have the billion-dollar mistake (not only in Java…)
Booking booking = null; //🤨 This is allowed but causes:
booking.destination; //Runtime error 😱

Optional booking = null; //Optionals aren’t safe from null either: 🤨
booking.map(Destination::destination); //Runtime error 😱

Java는 Optional과 어노테이션(@NotNull 등)처럼 시간이 지나며 일부 null 안전성 기능을 도입해 왔지만 근본적인 문제를 해결하지는 못했습니다. 또한, Valhalla 프로젝트(null 제한 및 null 가능 타입)는 Java에 null 안전성을 도입하기보다는 더 많은 선택지를 제공하는 방향으로 진행되고 있습니다.

Kotlin

//Important to realize that null is very restricted in Kotlin:
val booking:Booking? = null //…null can only be assigned to Nullable types ✅ 
val booking:Booking = null //null assigned to a Non-nullable types yields a Compilation error 😃 

booking.destination //unsafely accessing a nullable type directly causes a Compilation error 😃
booking?.destination //only safe access is possible  ✅

Kotlin의 null 안전성이 뛰어난 이유는 안전할 뿐 아니라 사용성 또한 매우 우수하기 때문입니다. ‘두 마리 토끼를 모두 잡는’ 대표적 예는 다음과 같습니다.

다음과 같은 도메인이 있다고 가정해 보겠습니다.

Kotlin

data class Booking(val destination:Destination? = null)
data class Destination(val hotel:Hotel? = null)
data class Hotel(val name:String, val stars:Int? = null)

Java

public record Booking(Optional destination) {

   public Booking() { this(Optional.empty()); }

   public Booking(Destination destination) {
       this(Optional.ofNullable(destination));
   }
}

public record Destination(Optional hotel) {

   public Destination() { this(Optional.empty()); }

   public Destination(Hotel hotel) {
       this(Optional.ofNullable(hotel));
   }
}

public record Hotel(String name, Optional stars) {

   public Hotel(String name) {

       this(name, Optional.empty());

   }

   public Hotel(String name, Integer stars) {
       this(name, Optional.ofNullable(stars));
   }
}

객체 생성

Java

//Because Optional is a wrapper, the number of nested objects grows, which doesn’t help readability
final Optional booking = Optional.of(new Booking(
      Optional.of(new Destination(Optional.of(
            new Hotel("Sunset Paradise", 5))))));

Kotlin

//Since nullability is part of the type system, no wrapper is needed: The required type or null can be used. 
val booking:Booking? = Booking(Destination(Hotel("Sunset Paradise", 5)))

중첩 객체 탐색

Java

//traversing a graph of Optionals requires extensive unwrapping
final var stars = "*".repeat(booking
                          .flatMap(Booking::getDestination)
                          .flatMap(Destination::getHotel)
                          .map(Hotel::getStars).orElse(0)); //-> "*****"

Kotlin

//Easily traverse a graph of nullable types with: ‘?’, use  ?: for the ‘else’ case.
val stars = "*".repeat(booking?.destination?.hotel?.stars ?: 0) //-> "*****"

중첩 객체 래핑 해제

Java

//extensive unwrapping is also needed for printing a leaf 
booking.getDestination()
       .flatMap(Destination::getHotel)
       .map(Hotel::getName)
       .map(String::toUpperCase)
         .ifPresent(System.out::println);

Kotlin

//In Kotlin we have two elegant options:
//1. we can again traverse the graph with ‘?’
booking?.destination?.hotel.?name?.uppercase()?.also(::println)

//2. We can make use of Kotlin’s smart-cast feature
if(booking?.destination?.hotel != null) {
   //The compiler has checked that all the elements in the object graph are not null, so we can access the elements as if they were non-nullable types
   println(booking.destination.hotel.uppercase())
}

Java가 null 안전성을 제대로 지원하지 않는다는 점은 개발자들에게 큰 부담으로 작용합니다. 그 결과 방어적인 코드와 다양한 null 값 가능 구문(혹은 아예 없는 경우)을 작성하게 되어 코드가 점점 장황해집니다. 또한 NullPointerException은 애플리케이션 충돌 원인에서 약 3분의 1을 차지합니다(JetBrains 블로그). Kotlin의 컴파일타임 null 검사는 이러한 런타임 오류를 완전히 방지합니다. 현재까지도 null 안전성은 Kotlin 마이그레이션을 이끄는 가장 중요한 요인이기도 합니다.

2. 컬렉션은 적이 아닌 아군

Spring의 창시자인 Rod Johnson은 최근 인터뷰(Creator of Spring: No desire to write Java)에서 Kotlin을 사용해 보기로 한 이유가 null 가능 타입 때문이 아니라 지나치게 복잡한 Java Streams API 때문이었다고 했습니다.

다음 예시는 Java Streams API가 왜 그렇게 복잡한지, 그리고 Kotlin이 어떻게 그 모든 문제를 해결하는지를 잘 보여주고 있습니다.

Java

public record Product(String name, int... ratings){}
List products = List.of(
  new Product("gadget", 9, 8, 7), 
  new Product("goody", 10, 9)
);

Map maxRatingsPerProduct =

   //🤨 1. Stream introduces indirection
   products.stream()
     //🤨 1. Always to and from Stream conversion
      .collect(
        //🤨 2. Lacks extension methods, so wrappers are required
        Collectors.groupingBy(
           Product::name,
            //🤨 2. Again…
           Collectors.mapping( groupedProducts ->
                //🤨 3. (too) low-level types, arrays, and primitives cause extra complexity
                //🤨 4. No API on Array, always wrap in stream 
               Arrays.stream(groupedProducts.ratings())
                   .max()
                      //🤨 5. Extra verbosity due to Optional
                      .orElse(0.0),
               //🤨 6. No named arguments: what does this do?
                Collectors.reducing(0, Integer::max)
              )
            ));

Kotlin

//😃 rich and uniform Collection API - even on Java collections - due to extension methods
val maxRatingsPerProduct = products.
      .groupBy { it.name }
      .mapValues { (_, groupedProducts) -> //😃 destructuring for semantic precision
            //😃 built-in nullability support, and the same API for 
            //arrays like other Collections
            groupedProducts.flatMap { it.ratings }
                  .maxOrNull() ?: 0 
            }
      }

Kotlin의 일관된 컬렉션 프레임워크 덕분에 서로 다른 컬렉션 타입 간 변환도 매우 간단합니다.
Java

int[] numbers = {1, 3, 3, 5, 2};

Set unique = Arrays.stream(numbers).boxed().collect(Collectors.toSet());

Map evenOrOdd = unique.stream()
       .collect(Collectors.toMap(
               n -> n,
               n -> n % 2 == 0));

Kotlin

val numbers = arrayOf(1, 3, 3, 5, 2)

val unique: Set = numbers.toSet() //😃 simply call to to do the conversion

val evenOrOdd: Map = unique.associateWith { it % 2 == 0 }

결과적으로 다음과 같은 효과가 있습니다.

  • 풍부하고 직관적인 컬렉션 API: 파이프라인이 중첩된 컬렉터 호출 내부에서가 아닌 영어처럼 왼쪽에서 오른쪽으로 자연스럽게 읽습니다.
  • 상용구와 복잡성 감소: Collectors.groupingBy, Stream, Optional, Arrays.stream이 필요 없습니다.
  • 하나의 멘탈 모델: List, Set, Array 또는 원시 배열로 시작하더라도 동일한 컬렉션 연산자에 도달할 수 있습니다.
  • 부가 작업이 필요 없는 성능: 컴파일러가 꼭 필요한 경우에만 박싱과 언박싱을 처리하므로, 개발자는 평소처럼 코드를 작성하면 됩니다.
  • null 안전성 통합: 컬렉션 API는 orNull(...)로 끝나는 다양한 헬퍼를 통해 null 값 가능성을 완전하게 지원합니다.
  • 관용적 래퍼를 통한 매끄러운 Java 상호운용성: 확장 함수를 통해 Java 컬렉션을 기반으로 한 기능이 풍부한 컬렉션을 그대로 활용할 수 있어 두 세계의 장점을 모두 누릴 수 있습니다.

간단히 말해, Kotlin은 필터링, 매핑, 그룹화 같은 일상적인 컬렉션 작업을 구성 가능한 필수 함수로 격상하여 사용자가 어떻게 처리할지가 아니라 무엇을 원하는지를 자연스럽게 표현할 수 있도록 지원합니다.

3. Checked Exception 제거와 Kotlin 코드 안전성 확보 전략

Java는 여전히 Checked Exception을 지원하는 몇 안 되는 언어 중 하나입니다. 이는 처음에는 안전성 기능을 위해 도입되었지만 그 목적을 달성하지는 못했습니다. 장황함, 아무 효과가 없거나 RuntimeException으로 다시 던지는 무의미한 catch 블록, 람다 표현식과의 통합 문제 등은 코드 안전성 확보보다 오히려 개발 흐름을 저해하는 원인이 됩니다.

Kotlin은 C#, Python, Scala, Rust, Go 등 대부분의 다른 프로그래밍 언어와 마찬가지로, 복구 불가능한 상황에서만 예외를 사용하는 검증된 패러다임을 따릅니다.

다음은 Checked Exception이 코드 안전성을 높이지 못하고 오히려 문제를 일으키는 상황을 보여주는 예시입니다.

Java

public String downloadAndGetLargestFile(List urls) {
   List contents = urls.stream().map(urlStr -> {
               Optional optional;
               try {
                   optional = Optional.of(new URI(urlStr).toURL());
                   //🤨 Within lambdas checked exceptions are not supported and must always be caught...
               } catch (URISyntaxException | MalformedURLException e) {
                   optional = Optional.empty();
               }
               return optional;
           }).filter(Optional::isPresent)       //Quite a mouthful to get rid of the Optional...
           .map(Optional::get)
           .map(url -> {
               try (InputStream is = url.openStream()) {
                   return new String(is.readAllBytes(), StandardCharsets.UTF_8);
               } catch (IOException e) {
                   //🤨… or re-thrown, which is annoying, I don’t really care about IOE
                   throw new IllegalArgumentException(e);
               }
           }).toList();
   //🤨 An empty List results in a NoSuchElementException, so why is it not checked? The chance that the List is empty is as high as the other two cases above...
   return Collections.max(contents);
}

Kotlin

//😃 safe return type
fun downloadAndGetLargestFile(urls: List): String? =
   urls.mapNotNull {    //😃 convenient utility methods to rid of null
       //😃 try catch is possible, yet runCatching is an elegant way to convert an exception to null
       runCatching { URI(it).toURL() }.getOrNull()
   }.maxOfOrNull{  //😃 safe way to retrieve the max value
       it.openStream().use{ it.reader().readText() }  //😃 convenient extension methods to make java.io streams fluent
   }

4. 기본 요소로서의 함수

Kotlin은 함수를 기본 요소로 취급합니다. Java 개발자 입장에서는 이것의 의미와 중요성에 대해 의문이 생길 수 있습니다.

핵심 차이는 Java의 제한된 접근 방식에 있습니다. Java의 함수 기능은 주로 람다를 통한 호출 부분에 집중되어 있으며, 선언부는 여전히 장황하고 직관적이지 않은 함수 인터페이스에 묶여 있습니다. Kotlin에서는 함수를 정의하고 전달하고 반환하고 조합하는 작업을 상용구 없이 수행할 수 있어, 함수형 프로그래밍을 훨씬 더 자연스럽게 풍부한 표현으로 작성할 수 있습니다.

Java

public void doWithImage(
      URL url, 
      //🤨 Function interfaces introduce an indirection: because we don’t see the signature of a Function we don’t know what a  BiConsumer does, unless we look it up
      BiConsumer f) throws IOException {
        f.accept(url.getFile(), ImageIO.read(url));
}

//🤨 Same here
public void debug(Supplier f) {
   if(isDebugEnabled()) {
      logger.debug("Debug: " + f.get());
   }
}

//🤨 calling no-argument lambdas is verbose
debug(() -> "expensive concat".repeat(1000));

Kotlin

fun doWithImage(
   url: URL,
  //😃 Kotlin has a syntax for declaring functions: from the signature, we see what goes in and what goes out 
   f:(String, BufferedImage) -> Unit) = 
      f(url.file, ImageIO.read(url))

  //😃 same here: nothing goes in, a String goes out
fun debug(msg: () -> String) {
   if(isDebugEnabled) {
            logger.debug(msg())
   }
}

//😃 convenient syntax to pass a lambda: {}
debug{"expensive concat".repeat(1000)}

Kotlin은 함수 선언을 위한 명확하고 간결한 구문을 제공합니다. 시그니처만 보고도 무엇이 들어가고 무엇이 출력되는지 즉시 파악할 수 있으므로 외부의 함수 인터페이스로 이동할 필요가 없습니다.

Java는 함수형 프로그래밍 기능을 java.util.function.*에 속한 대규모 인터페이스 집합을 통해 그대로 노출하는데, 이 인터페이스들은 장황하고 복잡한 시그니처를 가져 함수형 프로그래밍을 사용하는 데 불필요한 부담을 줍니다. 반면 Kotlin은 함수를 기본 요소로 취급하므로 이러한 인터페이스는 개발자에게 숨겨진 상태로 Java 방식과 원활히 상호 운용됩니다.

그 결과, Kotlin에서 함수를 사용하는 과정은 훨씬 더 직관적이고 단순해져 이 강력한 프로그래밍 개념을 코드에 적용하기가 훨씬 쉬워집니다.

5. 코루틴으로 구현하는 부담 없는 동시성 처리

높은 처리량, 단일 요청 내 병렬 처리 또는 스트리밍이 필요하다면 Java에서 사용할 수 있는 유일한 선택지는 Spring WebFlux, Vert.X, Quarkus 등의 프레임워크에서 제공되는 Reactor나 RxJava 같은 반응형 라이브러리입니다.

이러한 라이브러리의 문제는 지나치게 복잡해 사용자에게 사실상 함수형 프로그래밍을 강요한다는 점입니다. 따라서 학습 곡선이 매우 가파르며, 애플리케이션이 부하를 받을 때 치명적인 결과로 이어질 수 있는 오류가 쉽게 발생합니다. 반응형 프로그래밍이 주류가 되지 못한 주된 원인도 이 때문일 것입니다.

참고: 가상 스레드는 반응형 라이브러리를 대체하는 기술이 아니며, 일부 겹치는 부분이 있긴 하지만 완전한 대안은 아닙니다. 가상 스레드는 논블로킹 I/O를 제공하지만, 병렬 처리나 반응형 스트림과 같은 기능은 제공하지 않습니다. 주요 프레임워크에서 이를 지원하면 구조화된 동시성과 범위 지정된 값 역시 병렬 처리가 가능해질 것입니다. 반응형 스트림의 경우, 항상 반응형 라이브러리를 사용해야 합니다.

Spring Boot를 사용하는 Java 개발자가 단일 요청 내에서 병렬 호출을 수행한다고 가정해 보겠습니다. 그러면 다음과 같은 코드가 나옵니다.

@PostMapping("/users")
@ResponseBody
@Transactional
public Mono storeUser(@RequestBody User user) {
   Mono avatarMono = avatarService.randomAvatar();
   Mono validEmailMono = emailService.verifyEmail(user.getEmail());
   //🤨 what does ‘zip’ do?
   return Mono.zip(avatarMono, validEmailMono).flatMap(tuple -> 
       if(!tuple.getT2()) //what is getT2()? It’s the validEmail Boolean…
        //🤨 why can I not just throw an exception?
         Mono.error(new InvalidEmailException("Invalid Email"));
       else personDao.save(UserBuilder.from(user)
                                          .withAvatarUrl(tuple.getT1()));
     );  
  }

런타임 관점에서는 이 코드가 완벽하게 동작하더라도, 어쩔 수 없이 추가된 복잡성이 매우 큽니다.

  • 전체 호출 체인에서 Mono/Flux가 지배적이어서 모든 도메인 객체를 감싸야 합니다.
  • 곳곳에 zip, flatMap 등 복잡한 연산자가 매우 많습니다.
  • 예외 던지기와 같은 표준 프로그래밍 구문을 사용할 수 없습니다.
  • 코드의 비즈니스 의도가 크게 훼손됩니다. 코드는 MonosflatMap에 초점을 맞추고 있어 비즈니스 관점에서 실제로 어떤 일이 일어나는지 모호해집니다.

좋은 소식은 Kotlin 코루틴이 이러한 문제를 해결해 줄 강력한 대안이라는 점입니다. 코루틴은 언어 수준의 반응형 구현으로 볼 수 있습니다. 따라서 두 세계의 장점을 다음과 같이 결합합니다.

  • 이전처럼 순차적으로 코드를 작성할 수 있습니다.
  • 코드는 런타임에 비동기적으로/병렬로 실행됩니다.

위의 Java 코드를 코루틴 기반으로 변환하면 다음과 같은 형태가 됩니다.

@GetMapping("/users")
@ResponseBody
@Transactional
suspend fun storeUser(@RequestBody user:User):User = coroutineScope {
   val avatarUrl = async { avatarService.randomAvatar() }
   val validEmail = async { emailService.verifyEmail() }
   if(!validEmail.await()) throw InvalidEmailException("Invalid email")
   personRepo.save(user.copy(avatar = avatarUrl.await()))
}

Kotlin의 suspend 키워드는 구조화된 논블로킹이 명확하고 간결한 방식으로 실행되도록 합니다. async()await()와 함께 사용하면, 깊게 중첩된 콜백이나 Mono 또는 CompletableFuture와 같은 복잡한 구문 없이도 병렬 처리를 쉽게 수행할 수 있습니다.

그 결과 복잡성은 줄어들고 개발자의 만족도와 유지 관리 편의성은 높아지며 성능은 동일하게 유지됩니다.

참고: 모든 주요 Java 기반 웹 프레임워크가 코루틴을 지원하는 것은 아닙니다. Spring과 Micronaut는 탁월한 지원을 제공합니다. Quarkus는 현재 제한적인 코루틴 지원을 제공합니다.

6. 하지만 계속 발전하는 Java!

Java는 레코드, 패턴 일치 같은 기능을 통해 계속 발전하고 있으며 Amber, Valhalla, Loom과 같은 향후 프로젝트도 진행하고 있습니다. 이러한 꾸준한 발전은 JVM을 강화하고 전체 생태계에도 이점을 제공합니다.

하지만 이러한 ‘새로운’ Java 기능 중 대부분은 Kotlin 개발자 입장에서는 이미 오랜 기간 활용해 온 기능이라는 점에 유의해야 합니다. Null 안전성, value 클래스, 최상위 함수, 기본 인수, 간결한 컬렉션, 그리고 기본 요소로서의 함수는 모두 Kotlin의 설계에 자연스럽게 녹아 있으며 더 통합적이고 개발자 친화적인 방식으로 제공됩니다. 이 때문에 Kotlin 코드는 더 깔끔하고 안전하며 훨씬 생산적으로 느껴집니다.

게다가 Kotlin은 Java의 혁신에서도 이점을 누릴 수 있습니다. 가상 스레드나 Loom과 같은 JVM 수준의 전반적인 발전, 그리고 Valhalla의 성능 향상은 Kotlin에도 그대로 적용됩니다.

요약하면, Java도 발전하고 있지만 Kotlin은 처음부터 개발자에게 필요한 최신 도구를 제공하도록 설계되어 안전하고 현대적이며 미래 지향적인 선택지가 되었습니다.

7. Kotlin이 지닌 진화적 이점

오래된 프로그래밍 언어는 현대적 대안에 비해 피할 수 없는 기존의 부담을 안고 있습니다. 방대한 기존 코드베이스를 지원하면서 언어를 발전시키는 일은 언어 설계자들의 고유한 과제입니다. Kotlin은 다음의 두 가지 이점을 갖고 있습니다.

거인의 어깨 위에 서기: 바퀴를 다시 발명하는 대신, Kotlin 초기 설계 팀은 주요 프로그래밍 언어에서 검증된 패러다임을 모아 하나의 응집력 있는 체계로 통합했습니다. 이 접근 방식은 수많은 프로그래밍 커뮤니티의 진화적 학습을 극대화했습니다.

Java의 한계에서 배우기: Kotlin 설계자들은 Java의 문제점을 관찰하고, 이를 토대로 처음부터 견고한 해결책을 개발할 수 있었습니다.

Kotlin의 발전을 더 깊이 이해하고 싶다면, 초기 설계 팀의 Andrey Breslav가 KotlinDevDay Amsterdam에서 발표한 Shoulders of Giants: Languages Kotlin Learned From(거인의 어깨: Kotlin의 토대가 된 언어들)을 확인해 보세요.

문화적 요소: Kotlin 도입 여정에서 개발자를 지원하고 연결

1. 쉽고 원활한 온보딩 지원

Java와 Kotlin 코드 스니펫을 비교해서 보여주는 목적은 Kotlin이 얼마나 매력적인 언어인지 알리기 위한 것입니다. 하지만 코드만으로는 Java 개발자들의 마음을 얻기에 충분하지 않습니다. 도입을 가속화하고 원활하게 시작할 수 있도록 다음을 제공해 보세요.

  • 샘플 프로젝트: Java와 Kotlin 코드를 나란히 보여주는 바로 실행 가능한 프로젝트로, 팀이 마이그레이션 과정에서 실질적인 참고 자료로 사용할 수 있습니다.
  • 내장된 품질 검사: SonarQube, ktlint, detekt 같은 도구가 사전 구성되어 있어 첫날부터 깔끔하고 일관되며 유지 관리 가능한 코드를 작성할 수 있도록 지원합니다. 이를 통해 일관된 린트 규칙, 테스트 프레임워크, 라이브러리, CI 파이프라인을 적용해 팀 간 마찰을 줄일 수 있습니다.
  • 코칭 및 지원: 경험 많은 Kotlin 엔지니어가 새 팀을 코칭하고, 질문에 답변하며, 초기 개발 단계에서 실전 조언을 제공합니다.
    • 이 부분이 무엇보다 중요합니다. 이미 같은 여정을 경험한 타 팀의 숙련된 개발자가 단 몇 시간만 도와줘도, 불필요한 시행착오와 기술 부채를 크게 줄일 수 있습니다.

약간의 지원과 코칭만으로도 Kotlin에 대한 지속적인 열정을 키우는 가장 강력한 방법이 됩니다.

2. (자기 주도) 학습 자료 제공

특히 Java에서 넘어오는 경우, Kotlin 기본 지식을 스스로 학습할 수 있습니다. 일부 자료를 미리 제공하면 Kotlin을 생산적으로 활용하는 여정을 더 수월하게 만들고 진입 장벽을 낮출 수 있습니다.

참고: 자기주도 학습은 기초를 익히는 데 유용하지만 단점도 있습니다. 단점 중 하나는 학습이 선택 사항이라는 점입니다. 종일 업무에 시달리다 보면 자기 주도 학습을 건너뛰고 싶은 마음이 들 수 있습니다. 게다가 올바르게 적용된 관용적 Kotlin의 미묘한 뉘앙스를 아는 실무자의 귀중한 피드백을 놓치게 될 수 있습니다. 그리고 자기 주도 학습 후에 Java 같은 Kotlin을 쓰게 될 가능성이 큽니다. 이렇게 작성하면 일부 장점은 있을지 몰라도 언어의 잠재력을 충분히 끌어내지 못합니다.

좋은 코칭을 받지 못한다면, 전통적인 교육 과정이 매우 도움이 될 수 있습니다. 이런 과정에 참여하는 것이 좋습니다. 같은 수준의 동료들과 교류하고, 경험 많은 전문가에게 질문에 대한 답을 들으며, 초기 전환 과정에서 비관용적인 Kotlin을 덜 사용하면서 훨씬 빠르게 실력을 향상할 수 있기 때문입니다.

3. 사내 Kotlin 커뮤니티 구축

회사 전반에서 Kotlin 전문성을 높이는 가장 빠른 방법 중 하나는 사내 커뮤니티를 만들어 이를 육성하는 것입니다.

  • 사내 Kotlin 커뮤니티 시작
    • 먼저 Kotlin 커뮤니티에 기여할 의지가 있는 3~6명 규모의 핵심 개발자 팀을 찾으세요. 또한, 이들이 업무에 대해 관리자로부터 시간과 크레딧을 받을 수 있도록 보장하세요.
    • 팀이 구성되면 유명한 Kotlin 커뮤니티 연사를 초청해 회사 전체 규모로 킥오프를 진행하세요. 이렇게 하면 Kotlin 기술 도입의 동기 부여를 강화하고 추진력을 확보할 수 있습니다.
    • 정기적인 모임(월별 또는 격주)을 잡아 추진력이 지속되도록 하세요.
    • 질문, 코드 스니펫, 이벤트 노트가 공유되는 공유 채팅 채널/위키를 만드세요.
  • (외부) 연사 초청
    • 실제 프로덕션에서 Kotlin을 적용해 본 엔지니어를 초대해 솔직한 경험담을 공유해 보세요.
    • 심화 기술 발표(코루틴, KMP, 함수형 프로그래밍 등)와 고차원 사례 연구(마이그레이션 전략, 도구 활용 팁 등)를 번갈아 진행해 보세요.
  • 다른 사내 프로젝트에서 얻은 교훈 공유
    • 프로젝트 리드에게 Kotlin 도입 과정에서 효과가 있었던 것과 없었던 것, 측정 가능한 성과 등을 발표하도록 요청해 보세요.
    • 이러한 인사이트는 새 팀이 참고할 수 있는 ‘Kotlin 플레이북’ 형태로 정리할 수 있습니다.
  • 자사 개발자를 위한 무대 마련
    • 5~10분 안에 멋진 팁, 라이브러리 또는 극복했던 실패를 시연할 수 있는 라이트닝 토크 세션을 운영하세요.
    • 기여자들을 공개적으로 칭찬하세요. 전사적 회의나 사내 뉴스레터를 통해 칭찬하면 참여도와 지식 공유를 증진시킵니다.
  • 빠른 피드백 루프 유지
    • 각 세션이 끝난 후, 명확성과 유용성에 대한 빠른 설문을 수집하고, 이에 따라 향후 의제를 조정하세요.
    • 조직 업무를 순환시켜 커뮤니티가 단 한 명의 핵심 인물에 의존하지 않고 장기적으로 복원력을 유지하도록 하세요.

참고: 위 제안 중 상당수는 단순해 보일 수 있지만 커뮤니티를 활기차고 생동감 있게 유지하는 데 필요한 노력을 과소평가해서는 안 됩니다.

4. 여유를 가질 것

문화적 변화는 시간이 걸립니다. 개인적으로 효과를 본 도구에 대한 과도한 열정은 자칫 무리한 추진으로 이어져 오히려 역효과를 불러올 수 있습니다. 효과적인 접근 방식은 설명하기보다 보여주는 것을 우선 지침으로 삼아, 위에서 논의된 모든 활동을 통해 도입 과정을 촉진하는 것입니다.

시리즈의 다음 게시물

개발자를 설득하는 데서 의사결정자를 설득하는 것으로 초점을 전환합니다. 다음 글에서는 실제 데이터와 측정 가능한 결과를 기반으로 Kotlin 도입을 위한 강력한 비즈니스 사례를 구축하는 방법을 다룹니다. 개발자의 성공을 경영진을 설득하는 논리로 전환하는 방법, 생산성 향상을 비용 절감으로 연결하는 방법, 그리고 Kotlin이 단순한 기술 업그레이드를 넘어 팀과 회사 모두에게 전략적인 방향임을 증명하는 방법을 배우게 됩니다.

Urs Peter

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

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

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

image description

Discover more