자바 개발자가 코틀린으로 넘어갈 때 꼭 알아야 할 것들

최근 몇 년간 안드로이드 앱 개발은 물론, 서버 사이드 개발에서도 코틀린(Kotlin)의 채택률이 폭발적으로 증가하는 것을 지켜보며 저 또한 자연스럽게 코틀린의 세계에 발을 들였습니다. 처음에는 '자바와 100% 호환되니 배우기 쉽겠지'라는 가벼운 마음으로 시작했지만, 코틀린을 깊이 파고들수록 단순히 자바의 대체재가 아닌, 개발자의 생산성과 코드의 안정성을 극대화하는 완전히 새로운 패러다임을 제시하는 언어라는 것을 깨닫게 되었습니다. 이 글은 과거의 저처럼 자바에 익숙하지만 코틀린으로의 전환을 고민하거나 이제 막 시작한 개발자분들을 위한 실용적인 가이드입니다. 자바의 장황함과 boilerplate 코드에 지쳤다면, 코틀린이 제공하는 간결함과 강력함의 신세계에 함께 빠져보시죠.

1. 보일러플레이트 코드의 종말: 데이터 클래스 (Data Class)

자바 개발자라면 누구나 DTO(Data Transfer Object)나 VO(Value Object)를 만들 때 겪는 고통을 아실 겁니다. 필드를 선언하고, 생성자를 만들고, 수많은 getter와 setter, 그리고 `equals()`, `hashCode()`, `toString()` 메서드를 오버라이드하는 끝없는 반복 작업 말이죠. 물론 Lombok 같은 라이브러리가 이 고통을 줄여주지만, 어노테이션 프로세서에 의존해야 하고 IDE 플러그인 설치가 강제되는 등 번거로움이 따릅니다. 코틀린은 이 모든 것을 언어 자체에서 `data class`라는 키워드 하나로 해결합니다.


// 코틀린의 data class 선언
data class Customer(val name: String, val email: String)

이 한 줄의 코드가 자바에서는 얼마나 길어질까요? Lombok을 사용하지 않는 순수 자바 코드와 비교해보겠습니다.

Java (POJO) Kotlin (Data Class)

public class Customer {
    private final String name;
    private final String email;

    public Customer(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Customer customer = (Customer) o;
        return java.util.Objects.equals(name, customer.name) &&
               java.util.Objects.equals(email, customer.email);
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, email);
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

data class Customer(
    val name: String, 
    val email: String
)

// 아래 기능들이 자동으로 생성됨:
// - 주 생성자에 선언된 모든 프로퍼티에 대한 getter
//   (var로 선언 시 setter도 포함)
// - equals()
// - hashCode()
// - toString()
// - copy()
// - componentN() 함수들
자동 생성의 마법: 코틀린 컴파일러는 `data` 키워드를 보고 주 생성자(primary constructor)에 선언된 프로퍼티들을 기반으로 다음 메서드들을 자동으로 생성해줍니다.
  • equals(): 객체의 내용(프로퍼티 값)이 같은지 비교합니다.
  • hashCode(): equals()와 일관성을 유지하는 해시 코드를 생성합니다. 컬렉션(Map, Set)에서 키로 사용할 때 필수적입니다.
  • toString(): "Customer(name=John Doe, email=john.doe@example.com)"와 같이 읽기 좋은 형식의 문자열을 반환합니다.
  • copy(): 객체의 불변성을 유지하면서 일부 프로퍼티만 변경된 새로운 객체를 생성할 때 매우 유용합니다. 예를 들어 customer.copy(email = "new.email@example.com")와 같이 사용할 수 있습니다.
  • componentN(): 구조 분해 선언(destructuring declaration)을 가능하게 합니다. val (name, email) = customer와 같이 객체의 프로퍼티를 개별 변수로 바로 할당할 수 있습니다.

자바에서는 수십 줄에 달하는 코드가 코틀린에서는 단 한 줄로 끝납니다. 이는 단순히 코드 라인 수가 줄어드는 것을 넘어, 데이터 모델의 본질에만 집중하게 하여 코드의 가독성과 유지보수성을 혁신적으로 향상시킵니다. `코틀린 데이터 클래스`는 자바 개발자가 코틀린으로 넘어왔을 때 가장 먼저 체감하는 강력한 기능 중 하나입니다.

2. 메서드 오버로딩의 대안: 기본값과 명명된 인자

자바에서 선택적 파라미터를 갖는 메서드를 만들려면 어떻게 해야 할까요? 대부분 여러 개의 오버로딩된 메서드를 만들거나, 빌더 패턴(Builder Pattern)을 사용합니다. 하지만 두 방법 모두 상당한 양의 코드를 추가로 작성해야 하는 단점이 있습니다. 코틀린은 함수(메서드) 선언 시 파라미터에 기본값을 지정하는 기능을 제공하여 이 문제를 우아하게 해결합니다.


// 코틀린 함수에 파라미터 기본값 설정
fun sendEmail(
    to: String,
    from: String = "noreply@example.com", // 기본값
    subject: String = "No Subject",         // 기본값
    body: String
) {
    println("Sending email to $to from $from with subject '$subject'")
    println("Body: $body")
}

이렇게 정의된 함수는 다양한 방식으로 호출할 수 있습니다.


// 1. 모든 인자 전달
sendEmail(
    to = "user@example.com", 
    from = "admin@example.com", 
    subject = "Important Update", 
    body = "Please read this."
)

// 2. 기본값이 있는 'from'과 'subject' 생략
sendEmail(to = "user@example.com", body = "Hello there!")
// 출력: Sending email to user@example.com from noreply@example.com with subject 'No Subject'

// 3. 순서와 상관없이 명명된 인자(named arguments) 사용
sendEmail(body = "Final reminder.", subject = "Reminder", to = "user@example.com")
// 출력: Sending email to user@example.com from noreply@example.com with subject 'Reminder'

여기서 주목할 점은 명명된 인자(Named Arguments)입니다. `to = "..."`, `body = "..."` 와 같이 파라미터 이름을 명시하여 값을 전달할 수 있습니다. 이 기능은 두 가지 엄청난 장점을 제공합니다.

  1. 가독성 향상: 함수 호출 코드만 봐도 각 값이 어떤 파라미터에 전달되는지 명확하게 알 수 있습니다. 특히 `true`, `false` 같은 불리언 값이나 숫자 값이 여러 개일 때 혼동을 방지해줍니다.
  2. 유연성: 파라미터 순서를 지키지 않아도 됩니다. 기본값이 없는 파라미터와 섞어 쓸 때도, 필요한 값만 이름으로 지정하여 전달하면 되므로 매우 편리합니다.

자바의 빌더 패턴과 비교해보면 코틀린의 방식이 얼마나 간결한지 명확히 드러납니다. `자바 코틀린 비교`를 통해 이 차이를 확인해 보세요.

Java (Builder Pattern) Kotlin (Default & Named Arguments)

// 1. 빌더 패턴을 위한 클래스 작성
public class Email {
    private final String to;
    private final String from;
    private final String subject;
    private final String body;

    // ... 생성자, getter ...

    public static class Builder {
        private final String to;
        private final String body;
        private String from = "noreply@example.com";
        private String subject = "No Subject";

        public Builder(String to, String body) {
            this.to = to;
            this.body = body;
        }
        public Builder from(String from) {
            this.from = from;
            return this;
        }
        public Builder subject(String subject) {
            this.subject = subject;
            return this;
        }
        public Email build() {
            return new Email(this);
        }
    }
}
// 2. 빌더를 이용한 객체 생성 및 사용
Email email = new Email.Builder("user@example.com", "Hello")
                        .subject("Reminder")
                        .build();

// 1. 함수 선언
fun sendEmail(
    to: String,
    from: String = "noreply@example.com",
    subject: String = "No Subject",
    body: String
) { /* ... */ }

// 2. 함수 호출
sendEmail(
    to = "user@example.com",
    body = "Hello",
    subject = "Reminder"
)

결과적으로, 코틀린은 불필요한 보일러플레이트 코드 없이도 빌더 패턴의 장점(가독성, 유연성)을 모두 누릴 수 있게 해줍니다.

3. 컬렉션 다루기: 지루한 반복문 대신 함수형 API

자바 8에서 Stream API가 도입되면서 컬렉션 처리가 많이 개선되었지만, 코틀린은 이를 더욱 직관적이고 간결하게 만들었습니다. 복잡한 `for` 루프나 Stream 파이프라인 대신, 컬렉션 자체에 내장된 풍부한 고차 함수(higher-order functions)를 사용할 수 있습니다.

가장 기본적인 예로 리스트에서 특정 조건에 맞는 원소만 필터링하는 경우를 살펴보겠습니다.


val numbers = listOf(1, -2, 3, -4, 5, -6)

// 0보다 큰 숫자만 필터링
val positives = numbers.filter { x -> x > 0 }
println(positives) // 출력: [1, 3, 5]

// 람다의 인자가 하나일 경우 'it'으로 대체 가능
val positivesWithIt = numbers.filter { it > 0 }
println(positivesWithIt) // 출력: [1, 3, 5]

filter 함수는 각 원소를 인자로 받아 `Boolean`을 반환하는 람다(lambda) 식을 인자로 받습니다. 이 람다 식이 `true`를 반환하는 원소들만 모아 새로운 리스트를 만들어 반환합니다. 자바 Stream API와 비교하면, 중간 연산자와 종단 연산자를 구분할 필요 없이 훨씬 직관적입니다.

코틀린 컬렉션 API의 핵심 꿀팁:
  • map: 각 원소를 변환하여 새로운 리스트를 만듭니다. numbers.map { it * it } -> `[1, 4, 9, 16, 25, 36]`
  • forEach: 각 원소를 순회하며 특정 작업을 수행합니다. 반환 값은 없습니다. numbers.forEach { println(it) }
  • find (또는 firstOrNull): 조건에 맞는 첫 번째 원소를 찾습니다. 없으면 `null`을 반환합니다. numbers.find { it < 0 } -> `-2`
  • any: 조건에 맞는 원소가 하나라도 있으면 `true`를 반환합니다. numbers.any { it > 10 } -> `false`
  • all: 모든 원소가 조건을 만족하면 `true`를 반환합니다. numbers.all { it != 0 } -> `true`
  • groupBy: 특정 조건에 따라 원소들을 그룹화하여 Map을 만듭니다. numbers.groupBy { if (it > 0) "POSITIVE" else "NEGATIVE" }

이러한 함수들을 연쇄적으로 호출(chaining)하여 복잡한 데이터 처리 로직을 선언적으로, 그리고 매우 간결하게 표현할 수 있습니다. 예를 들어, 음수를 제곱하여 양수로 만든 뒤 10보다 큰 수만 골라내는 로직은 다음과 같이 작성할 수 있습니다.


val result = numbers
                .filter { it < 0 }      // [-2, -4, -6]
                .map { it * it }        // [4, 16, 36]
                .filter { it > 10 }     // [16, 36]

println(result) // 출력: [16, 36]

이러한 `코틀린 관용구`는 데이터 처리 로직을 명확하게 드러내어 코드를 읽고 이해하기 쉽게 만들어줍니다.

4. Null과의 전쟁 종식: 코틀린의 Null 안전성(Null Safety)

자바 개발자를 가장 괴롭히는 예외 중 하나는 단연 `NullPointerException`(NPE)일 것입니다. "10억 달러의 실수"라고 불리는 `null` 참조는 수많은 버그의 원인이 되어왔습니다. 코틀린은 타입 시스템 자체에 Null 안전성을 통합하여 이 문제를 컴파일 시점에 원천적으로 방지하려고 노력합니다.

코틀린의 모든 타입은 기본적으로 `null`을 허용하지 않습니다(Non-nullable). `null`을 담고 싶다면 타입 뒤에 물음표(`?`)를 붙여 `Nullable` 타입임을 명시적으로 선언해야 합니다.


var nonNullableString: String = "Hello"
// nonNullableString = null // 컴파일 에러!

var nullableString: String? = "World"
nullableString = null // OK

컴파일러는 `Nullable` 타입의 변수를 다룰 때 엄격한 규칙을 적용합니다. `null`일 가능성이 있는 변수의 프로퍼티나 메서드에 직접 접근하려고 하면 컴파일 에러를 발생시킵니다.


// println(nullableString.length) // 컴파일 에러! nullableString은 null일 수 있습니다.

이러한 `Nullable` 변수를 안전하게 사용하기 위해 코틀린은 여러 강력한 도구를 제공합니다. 이것이 바로 `코틀린 null 처리`의 핵심입니다.

4.1. 안전한 호출 (Safe Call Operator): `?.`

객체가 `null`이 아닐 경우에만 메서드나 프로퍼티에 접근하고, `null`일 경우에는 `null`을 반환합니다. 끝없이 이어지던 `if (obj != null)` 체크를 대체합니다.


val length = nullableString?.length
println(length) // nullableString이 "World"면 5, null이면 null을 출력

안전한 호출은 연쇄적으로 사용할 수 있습니다. user?.address?.city 와 같은 코드에서 `user`나 `address` 중 하나라도 `null`이면 전체 표현식은 `null`이 되어 NPE를 방지합니다.

4.2. 엘비스 연산자 (Elvis Operator): `?:`

왼쪽의 표현식이 `null`이 아니면 그 값을 사용하고, `null`이면 오른쪽의 값을 사용합니다. `null`일 경우 사용할 기본값을 지정할 때 매우 유용합니다.


// nullableString이 null이면 "Unknown"을 사용
val name = nullableString ?: "Unknown" 
println(name)

// 파일 목록이 null이면 빈 리스트를 반환
val files = File("Test").listFiles()
val fileCount = files?.size ?: 0 // files가 null이면 0을 사용
println("파일 개수: $fileCount")

엘비스 연산자는 예외를 던지는 것과 같이 활용할 수도 있습니다.


fun getUserEmail(userId: String): String {
    val user = findUserById(userId)
    return user?.email ?: throw IllegalArgumentException("User not found: $userId")
}

이 코드는 `user`가 `null`일 경우 예외를 발생시켜, `null`이 절대로 반환되지 않음을 보장합니다.

4.3. 안전한 실행을 위한 `let` 함수

특정 객체가 `null`이 아닐 경우에만 특정 코드 블록을 실행하고 싶을 때 `?.let { ... }` 패턴을 사용합니다. 코드 블록 안에서는 해당 객체를 `it`이라는 이름의 Non-nullable 타입으로 사용할 수 있습니다.


val user: User? = findUserById("someId")

user?.let {
    // 이 블록은 user가 null이 아닐 때만 실행됩니다.
    // 여기서 'it'은 User 타입이며 null이 아님이 보장됩니다.
    println("사용자 이름: ${it.name}")
    sendWelcomeEmail(it.email)
}

이 패턴은 `if (user != null)` 체크와 변수 재할당을 한 번에 처리해주어 코드를 매우 깔끔하게 만듭니다.

절대 사용하지 말아야 할 것: Not-null Assertion (`!!`)

코틀린은 `variable!!` 와 같이 `!!` 연산자를 제공하여 `Nullable` 타입을 강제로 `Non-nullable` 타입으로 변환할 수 있습니다. 하지만 이는 컴파일러에게 "이 변수는 절대 `null`이 아니니 내가 책임질게"라고 말하는 것과 같습니다. 만약 해당 변수가 `null`이라면, 자바와 똑같이 `NullPointerException`이 발생합니다. `!!`는 NPE를 컴파일러의 도움 없이 개발자 스스로 책임지겠다는 뜻이므로, 정말 `null`이 될 수 없다고 100% 확신하는 예외적인 상황(예: 자바 라이브러리 연동)이 아니라면 절대 사용하지 않는 것이 좋습니다.

5. 유연성의 극치: 확장 함수 (Extension Functions)

상속이나 디자인 패턴을 사용하지 않고도 기존 클래스에 새로운 함수를 추가할 수 있다면 어떨까요? 코틀린의 `코틀린 확장 함수`는 바로 이 마법 같은 일을 가능하게 합니다. `String`, `Int`와 같은 코어 클래스나 외부 라이브러리의 클래스에 내가 원하는 유틸리티 함수를 마치 원래 있던 멤버 함수처럼 추가할 수 있습니다.

예를 들어, 문자열이 유효한 이메일 형식인지 확인하는 함수를 `String` 클래스에 직접 추가해 보겠습니다.


// String 클래스에 isEmail() 확장 함수 추가
fun String.isEmail(): Boolean {
    val emailRegex = "^[A-Za-z](.*)([@]{1})(.{1,})(\\.)(.{1,})".toRegex()
    return this.matches(emailRegex)
}

fun main() {
    val email = "test@example.com"
    val notEmail = "hello world"

    // 마치 String의 내장 함수처럼 사용 가능
    println("'$email' is a valid email: ${email.isEmail()}")      // true
    println("'$notEmail' is a valid email: ${notEmail.isEmail()}")  // false
}

`fun String.isEmail()` 에서 `String.` 부분이 바로 이 함수가 `String` 클래스를 확장한다는 의미입니다. 함수 내부에서는 `this` 키워드를 통해 수신 객체(receiver object), 즉 함수를 호출한 `String` 인스턴스에 접근할 수 있습니다.

확장 함수의 동작 원리

확장 함수가 실제로 클래스의 코드를 변경하는 것은 아닙니다. 컴파일 타임에 정적 메서드 호출로 변환됩니다. 즉, email.isEmail() 코드는 컴파일 후 ExtensionFunctionsKt.isEmail(email) 와 같은 정적 메서드 호출로 바뀌게 됩니다. 따라서, 확장 함수는 캡슐화를 깨지 않으며, 클래스의 `private`이나 `protected` 멤버에는 접근할 수 없습니다.

확장 함수는 특정 도메인에 특화된 유틸리티 함수를 만들 때 특히 유용합니다. 예를 들어, 안드로이드 개발에서는 `dp` 단위를 `px`로 변환하는 확장 함수를 만들어 UI 코드를 직관적으로 작성할 수 있습니다.


// Int를 dp 단위로 변환하는 확장 함수 (Android Context가 필요)
fun Int.toDp(context: Context): Int {
    val density = context.resources.displayMetrics.density
    return (this * density).toInt()
}

자바에서는 이런 기능을 구현하려면 `StringUtils.isEmail(email)` 이나 `UiUtils.toDp(16, context)` 와 같은 정적 유틸리티 클래스를 만들어야 했습니다. 코틀린의 확장 함수는 코드를 더욱 객체 지향적이고 가독성 높게 만들어 줍니다.

6. 더 강력해진 제어문: 표현식으로서의 if, when, try-catch

자바에서 `if`, `switch`, `try-catch`는 값을 반환할 수 없는 '구문(Statement)'입니다. 따라서 이들의 결과에 따라 변수에 값을 할당하려면, 변수를 먼저 선언하고 각 분기 안에서 값을 할당해야 합니다. 코틀린에서는 이들 대부분이 값을 반환하는 '표현식(Expression)'으로 사용될 수 있어 코드가 훨씬 간결해집니다.

6.1. `if` 표현식

자바의 삼항 연산자(`condition ? value_if_true : value_if_false`)의 더 강력한 버전이라고 생각할 수 있습니다.


val a = 10
val b = 20

// 'if'문을 표현식으로 사용하여 바로 변수에 할당
val max = if (a > b) {
    println("a is larger")
    a // 블록의 마지막 표현식이 반환값
} else {
    println("b is larger")
    b // 블록의 마지막 표현식이 반환값
}

println("Max value is $max")

6.2. `when` 표현식: 자바 `switch`의 완벽한 진화

`when`은 자바의 `switch` 문을 훨씬 더 유연하고 강력하게 만든 기능입니다. `break`를 쓸 필요가 없으며, 다양한 종류의 조건을 검사할 수 있고, 표현식으로 사용될 때 컴파일러가 `else` 분기를 강제하여 잠재적인 버그를 막아줍니다.


fun getGrade(score: Int): String {
    return when (score) {
        100 -> "Perfect!"
        in 90..99 -> "A" // 범위(range) 검사
        in 80..89 -> "B"
        !in 0..100 -> "Invalid Score" // 범위 밖 검사
        else -> "C"
    }
}

println(getGrade(95)) // 출력: A

`when`은 인자 없이 `if-else if` 체인을 대체하는 용도로도 사용할 수 있습니다.


val x: Any = "Hello"

when (x) {
    is String -> println("x is a String of length ${x.length}") // 스마트 캐스팅
    is Int -> println("x is an Int")
    else -> println("x is something else")
}

여기서 `is String` 검사가 통과하면, 해당 블록 안에서는 `x`가 `String` 타입으로 스마트 캐스팅(Smart Cast)되어 별도의 형변환 없이 `length` 프로퍼티에 접근할 수 있습니다. `자바 코틀린 문법`의 가장 큰 차이점 중 하나입니다.

6.3. `try-catch` 표현식

예외 처리 구문조차 값을 반환할 수 있습니다. `try` 블록의 마지막 표현식이 성공 시의 반환값이 되고, `catch` 블록의 마지막 표현식은 예외 발생 시의 반환값이 됩니다.


val input = "123"
val number: Int = try {
    Integer.parseInt(input)
} catch (e: NumberFormatException) {
    0 // 파싱 실패 시 기본값 0을 반환
}

println(number) // 출력: 123

이러한 '표현식 기반' 제어문은 불필요한 임시 변수 선언을 줄이고, 로직을 한 곳에 모아주어 코드의 응집도를 높여줍니다.

7. 그 외 유용한 코틀린 관용구들

지금까지 소개한 내용 외에도 코틀린에는 개발을 즐겁게 만드는 수많은 `코틀린 꿀팁`과 관용구들이 있습니다.

  • 싱글톤(Singleton): 자바에서는 여러 줄의 코드가 필요한 싱글톤 패턴을 `object` 키워드 하나로 간단하게 만들 수 있습니다.
    
    object DatabaseManager {
        fun connect() { /* ... */ }
    }
    // 사용: DatabaseManager.connect()
        
  • 지연 초기화 (Lazy Initialization): `by lazy`를 사용하면 프로퍼티가 처음 접근될 때 단 한 번만 초기화 블록이 실행됩니다. 초기화 비용이 큰 객체에 유용합니다.
    
    val heavyObject: HeavyObject by lazy {
        println("HeavyObject is being initialized...")
        HeavyObject()
    }
    
    // 처음 접근 시에만 "HeavyObject is being initialized..."가 출력됨
    heavyObject.doSomething()
    heavyObject.doSomething()
        
  • 범위(Ranges): `1..100` (100 포함), `1 until 100` (100 미포함), `10 downTo 1`, `0..10 step 2` 등 직관적인 범위 연산자를 제공하여 `for` 루프를 쉽게 작성할 수 있습니다.
    
    for (i in 1..5) { print(i) } // 12345
    println()
    for (i in 5 downTo 1 step 2) { print(i) } // 531
        
  • 스코프 함수 (`apply`, `with`, `run`, `also`): `let` 외에도 특정 객체의 컨텍스트 내에서 코드 블록을 실행하는 다양한 스코프 함수를 제공합니다. 객체 초기화, 연쇄 호출 등에 매우 유용합니다.
    
    // 'apply'는 객체 초기화에 주로 사용됨 (수신 객체 자신을 반환)
    val textView = TextView(context).apply {
        text = "Hello"
        textSize = 16.0f
        setTextColor(Color.BLACK)
    }
        
  • 문자열 템플릿 (String Templates): 자바의 `+` 연산자나 `String.format()`보다 훨씬 간결하게 문자열 안에 변수나 표현식을 삽입할 수 있습니다. "User name is $name", "Total price is ${item.price * quantity}" 와 같이 사용합니다.

결론: 코틀린은 선택이 아닌 필수

오늘 우리는 자바 개발자의 관점에서 코틀린이 제공하는 핵심적인 문법과 관용구들을 살펴보았습니다. 데이터 클래스를 통한 보일러플레이트 제거, Null 안전성을 통한 NPE 방지, 확장 함수를 통한 유연성 확보, 그리고 간결한 컬렉션 API와 표현식 기반 제어문까지. 코틀린은 단순히 코드를 짧게 만드는 것을 넘어, 더 안전하고, 더 읽기 쉽고, 더 즐겁게 프로그래밍할 수 있는 환경을 제공합니다.

만약 당신이 여전히 자바의 세계에 머물러 있다면, 코틀린으로의 전환을 더 이상 망설일 이유가 없습니다. 자바와 100% 상호 운용되므로 기존 자바 프로젝트에 점진적으로 코틀린 코드를 추가하며 시작할 수 있습니다. 작은 유틸리티 클래스나 데이터 클래스부터 코틀린으로 작성해보세요. 머지않아 당신의 코드베이스에서 코틀린의 비중이 점점 커지는 것을 발견하게 될 것입니다. 코틀린은 당신의 개발자 인생에 가장 생산적인 변화를 가져다줄 것입니다.

Post a Comment