Kotlin으로 변환

이 Codelab에서는 자바에서 Kotlin으로 코드를 변환하는 방법을 배웁니다. Kotlin 언어 규칙의 특성과 함께, 작성하는 코드가 그 규칙을 따르는지 확인하는 방법도 알아봅니다.

이 Codelab은 자바를 사용하는 개발자 중 프로젝트를 Kotlin으로 마이그레이션하는 것을 고려 중인 개발자에게 적합합니다. IDE를 사용하여 Kotlin으로 변환할 두 개의 자바 클래스로 시작하겠습니다. 그런 다음 변환된 코드를 살펴보고 코드를 보다 자연스럽게만들어 개선하는 방법과 흔히 발생하는 위험을 피하는 방법을 살펴보겠습니다.

학습할 내용

자바를 Kotlin으로 변환하는 방법을 알아봅니다. 그 과정에서 다음과 같은 Kotlin 언어 기능과 개념을 배우게 됩니다.

  • null 허용 여부 처리
  • 싱글톤 구현
  • data 클래스
  • 문자열 처리
  • Elvis 연산자
  • 해체
  • 속성 및 지원 속성
  • 기본 인수 및 이름이 지정된 매개변수
  • 컬렉션 사용
  • 확장 함수
  • 최상위 함수 및 매개변수
  • let, apply, with, run 키워드

전제 조건

이미 자바를 잘 알고 있어야 합니다.

필요한 항목

새 프로젝트 생성

IntelliJ IDEA를 사용한다면 Kotlin/JVM으로 새 자바 프로젝트를 만듭니다.

Android 스튜디오를 사용한다면 활동을 추가하지 않고 새 프로젝트를 만듭니다. 최소 SDK의 값은 무엇이든 상관없으며 결과에 아무런 영향을 주지 않습니다.

코드

User 모델 객체와 Repository 싱글톤 클래스를 만듭니다. 이 클래스는 User 객체와 호환되고 사용자 목록 및 형식이 지정된 사용자 이름 목록을 노출합니다.

app/java/<yourpackagename> 아래에 User.java라는 새 파일을 만들고 다음 코드에 붙여넣습니다.

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

@Nullable이 정의되지 않았다고 IDE에 표시됩니다. 따라서 Android 스튜디오를 사용한다면 androidx.annotation.Nullable을, IntelliJ를 사용하면 org.jetbrains.annotations.Nullable을 가져옵니다.

Repository.java라는 새 파일을 만들고 다음 코드에 붙여넣습니다.

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

IDE에서는 자바 코드가 원활하게 Kotlin 코드로 자동 변환되지만 약간의 도움이 필요할 때도 있습니다. IDE에서 변환 시 초기 전달을 진행하겠습니다. 그런 다음 결과 코드를 살펴보고 코드가 그렇게 변환된 과정과 이유를 파악해 보겠습니다.

User.java 파일로 이동한 다음 파일을 Kotlin으로 변환합니다(메뉴 바 -> Code -> Convert Java File to Kotlin File).

변환 후 IDE에서 수정할지 묻는 메시지가 표시되면 Yes를 누릅니다.

25e57db5b8e76557.png

다음과 같은 Kotlin 코드가 표시됩니다.

class User(var firstName: String?, var lastName: String?)

User.java의 이름이 User.kt로 변경되었습니다. Kotlin 파일의 확장자는 .kt입니다.

자바의 User 클래스에는 두 가지 속성, 즉 firstNamelastName이 있습니다. 각 속성에는 값을 변경할 수 있는 getter 메서드와 setter 메서드가 있습니다. 변경 가능한 변수에 관한 Kotlin의 키워드는 var이므로 변환기는 이러한 각 속성에 var를 사용합니다. getter만 있는 자바 속성은 변경할 수 없으며 val 변수로 선언됩니다. val은 자바의 final 키워드와 유사합니다.

Kotlin과 자바의 주요 차이점 중 하나는 Kotlin은 변수가 null 값을 허용할 수 있는지 명시적으로 지정한다는 점입니다. Kotlin은 이를 위해 유형 선언에 ?를 추가합니다.

우리가 firstNamelastName을 null 허용으로 표시했으므로 자동 변환기에서 그러한 속성을 String?이 있는 null 허용으로 자동으로 표시했습니다. 개발자가 org.jetbrains.annotations.NotNull 또는 androidx.annotation.NonNull을 사용해 자바 요소를 null이 아닌 것으로 주석 처리하면 변환기는 이를 인식하고 Kotlin에서도 필드를 null이 아닌 상태로 만듭니다.

기본 변환 작업은 이미 끝났습니다. 하지만 이를 좀 더 자연스럽게 작성할 수 있습니다. 그 방법을 살펴보겠습니다.

data 클래스

User 클래스는 데이터만 갖습니다. Kotlin은 이 역할과 함께 클래스의 키워드, 즉 data를 갖습니다. 이 클래스를 data 클래스로 표시하면 컴파일러가 자동으로 getter와 setter를 생성합니다. 또한 equals(), hashCode(), toString() 함수도 파생합니다.

User 클래스에 data 키워드를 추가해 보겠습니다.

data class User(var firstName: String, var lastName: String)

자바와 마찬가지로 Kotlin은 한 개의 기본 생성자와 한 개 이상의 보조 생성자를 가질 수 있습니다. 위 예시에 있는 생성자는 User 클래스의 기본 생성자입니다. 생성자가 여러 개 있는 자바 클래스를 변환할 경우 변환기는 Kotlin에서도 생성자를 여러 개 자동으로 만듭니다. 그러한 생성자는 constructor 키워드로 정의됩니다.

이 클래스의 인스턴스를 생성하려면 다음과 같이 하면 됩니다.

val user1 = User("Jane", "Doe")

동등성

Kotlin에는 두 가지 유형의 동등성이 있습니다.

  • 구조 동등성은 == 연산자를 사용하고 equals()를 호출하여 두 인스턴스가 동일한지 확인합니다.
  • 참조 동등성은 === 연산자를 사용하고 두 참조가 동일한 객체를 가리키는지 확인합니다.

data 클래스의 기본 생성자에 정의된 속성이 구조 동등성 검사에 사용됩니다.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

Kotlin에서는 함수 호출 시 인수에 기본값을 할당할 수 있습니다. 기본값은 인수가 생략된 경우에 사용됩니다. Kotlin에서는 생성자도 함수이므로 기본 인수를 사용하여 lastName의 기본값이 null임을 지정할 수 있습니다. 이를 위해서는 nulllastName에 할당하면 됩니다.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

Kotlin에서는 함수 호출 시 인수에 라벨을 지정할 수 있습니다.

val john = User(firstName = "John", lastName = "Doe")

다른 사용 사례처럼 firstName의 기본값은 null이고 lastName의 기본값은 그렇지 않다고 가정해 보겠습니다. 이 경우에는 기본 매개변수가 기본값이 없는 매개변수보다 앞에 오기 때문에 이름이 지정된 인수를 사용하여 함수를 호출해야 합니다.

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

기본값은 Kotlin 코드에 중요하며 자주 사용되는 개념입니다. 이 Codelab에서는 항상 User 객체 선언에 성과 이름을 지정하고자 하므로 기본값이 필요하지 않습니다.

Codelab을 계속하기 전에 User 클래스가 data 클래스인지 확인합니다. 이제 Repository 클래스를 Kotlin으로 변환해 보겠습니다. 자동 변환 결과는 다음과 같이 표시됩니다.

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

자동 변환기에서 진행된 작업을 살펴보겠습니다.

  • users 목록은 null 허용 상태입니다. 객체가 선언 시 인스턴스화되지 않았기 때문입니다.
  • Kotlin에서 getUsers() 같은 함수가 fun 수정자로 선언되었습니다.
  • getFormattedUserNames() 메서드는 이제 formattedUserNames라는 속성이 되었습니다.
  • 사용자 목록(처음에는 getFormattedUserNames(의 일부였음)을 처음부터 끝까지 반복할 때의 구문이 자바 구문과 다릅니다.
  • static 필드가 이제 companion object 블록의 일부가 되었습니다.
  • init 블록이 추가되었습니다.

계속 진행하기 전에 코드를 조금 정리해 보겠습니다. 생성자를 살펴보면 변환기에서 users 목록이 null 허용 객체가 포함된 변경 가능한 목록으로 바뀐 것을 알 수 있습니다. 실제로 목록이 null이 될 수 있지만 null 사용자를 보유할 수 없다고 가정해 보겠습니다. 다음과 같이 해 보겠습니다.

  • users 유형 선언 내 User?에서 ?를 삭제합니다.
  • getUsers()의 반환 유형에서는 List<User>?를 반환하도록 User?에서 ?를 삭제합니다.

Init 블록

Kotlin에서는 기본 생성자에 코드를 포함할 수 없으므로 초기화 코드가 init 블록에 배치됩니다. 기능은 동일합니다.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

init 코드는 대부분 속성 초기화를 처리합니다. 이 작업은 속성 선언에서도 가능합니다. 예를 들어 Repository 클래스의 Kotlin 버전에서 사용자 속성이 선언에서 초기화되었음을 알 수 있습니다.

private var users: MutableList<User>? = null

Kotlin의 static 속성 및 메서드

자바에서는 필드 또는 함수가 클래스 인스턴스가 아닌 클래스에 속한다는 것을 나타내기 위해 필드 또는 함수에 static 키워드를 사용합니다. Repository 클래스에 INSTANCE 정적 필드를 만든 이유도 바로 이 때문입니다. 이에 상응하는 Kotlin이 바로 companion object 블록입니다. 여기에서는 정적 필드와 정적 함수도 선언합니다. 변환기가 동반 객체 블록을 만들고 INSTANCE 필드를 여기로 옮겼습니다.

싱글톤 처리

Repository 클래스의 인스턴스가 하나만 필요하므로 자바에서 싱글톤 패턴을 사용했습니다. Kotlin을 사용하면, class 키워드를 object로 바꿔 컴파일러 수준에서 이 패턴을 적용할 수 있습니다.

프라이빗 생성자를 삭제하고 클래스 정의를 object Repository로 바꿉니다. 동반 객체도 삭제합니다.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

object 클래스를 사용할 때 다음과 같이 객체에서 직접 함수와 속성을 호출합니다.

val formattedUserNames = Repository.formattedUserNames

참고로, 공개 상태 수정자가 없는 속성은 Repository 객체의 formattedUserNames 속성처럼 기본적으로 공개 상태입니다.

Repository 클래스를 Kotlin으로 변환할 때 자동 변환기는 사용자 목록을 null 허용 상태로 만들었습니다. 사용자 목록이 선언될 때 객체에 초기화되지 않았기 때문입니다. 결과적으로 users 객체가 사용되는 모든 경우에 null이 아닌 어설션 연산자 !!를 사용해야 합니다. 변환된 코드 전체에 users!!user!!이 표시됩니다. !! 연산자는 모든 변수를 null이 아닌 유형으로 변환하기 때문에, 개발자는 속성에 액세스하거나 변수에 관해 함수를 호출할 수 있습니다. 그러나 변수 값이 실제로 null인 경우 예외가 발생합니다. !!를 사용하면 런타임에 예외가 발생할 위험이 있습니다.

그 대신, 다음 메서드 중 하나를 사용하여 null 허용 여부를 처리하는 것이 좋습니다.

  • null 검사 실행( if (users != null) {...} )
  • elvis 연산자 ?: 사용(Codelab 후반부에서 다룸)
  • 일부 Kotlin 표준 함수 사용(Codelab 후반부에서 다룸)

여기에서는 사용자 목록이 null 허용 상태가 될 필요가 없습니다. 객체가 init 블록에서 생성된 직후 사용자 목록이 초기화되었기 때문입니다. 따라서 users 객체를 선언할 때 직접 인스턴스화할 수 있습니다.

컬렉션 유형의 인스턴스를 생성할 때 Kotlin은 코드를 보다 읽기 쉽고 유연하게 만드는 몇 가지 도우미 함수를 제공합니다. 여기서는 usersMutableList를 사용합니다.

private var users: MutableList<User>? = null

간단히 mutableListOf() 함수를 사용하고 목록 요소 유형을 제공할 수 있습니다. mutableListOf<User>()User 객체를 포함할 수 있는 빈 목록을 만듭니다. 이제 컴파일러에서 변수의 데이터 유형을 추론할 수 있으므로 users 속성의 명시적 유형 선언을 삭제합니다.

private val users = mutableListOf<User>()

또한 사용자에 사용자 목록에 관한 변경할 수 없는 참조가 포함되므로 varval로 변경했습니다. 참조는 변경이 불가능하지만, 목록 자체는 변경할 수 있습니다(요소는 추가 또는 삭제 가능함).

users 변수가 이미 초기화되었으므로 init 블록에서 이 초기화를 삭제합니다.

users = ArrayList<Any?>()

그러면 init 블록의 모습은 다음과 같습니다.

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

이 같은 변경으로 이제 users 속성이 null이 아닌 상태가 되었으며, 불필요한 !! 연산자 반복을 모두 삭제할 수 있습니다. 참고로, Android 스튜디오에서 컴파일 오류가 계속 발생하지만, Codelab의 다음 몇 단계를 계속 진행해 오류를 해결할 수 있습니다.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

또한 userNames 값의 경우 ArrayList의 유형을 보유 Strings로 지정하면 명시적 유형을 추론할 수 있으므로 선언에서 이를 삭제할 수 있습니다.

val userNames = ArrayList<String>(users.size)

해체

Kotlin에서는 해체 선언이라는 구문을 사용하여 한 객체를 다수의 변수로 해체할 수 있습니다. 변수를 여러 개 만들어 독립적으로 사용할 수 있습니다.

예를 들어 data 클래스가 해체를 지원하므로 for 루프의 User 객체를 (firstName, lastName)으로 해체할 수 있습니다. 그렇게 하면 firstNamelastName 값과 관련한 작업을 직접 할 수 있습니다. 아래와 같이 for 루프를 업데이트합니다. user.firstName의 모든 인스턴스를 firstName으로 바꾸고 user.lastNamelastName으로 바꿉니다.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if 표현식

userNames 목록에 있는 이름은 아직 원하는 형식이 아닙니다. lastNamefirstName 모두 null이 될 수 있으므로, 형식이 지정된 사용자 이름 목록을 만들 때 null 허용 여부를 처리해야 합니다. 두 이름 중 하나가 누락되면 "Unknown"을 표시하고자 합니다. name 변수는 한 번 설정되면 변경되지 않으므로 var 대신 val을 사용할 수 있습니다. 이 변경부터 먼저 합니다.

val name: String

이름 변수를 설정하는 코드를 살펴봅니다. 변수가 같음(=)으로 그리고 코드의 if/else 블록으로 설정되는 모습이 생소할 수 있습니다. Kotlin에서는 if, when, for, while이 표현식이어서 값을 반환하기 때문에 가능합니다. if 문의 마지막 라인은 name에 할당됩니다. 이 블록의 유일한 목적은 name 값을 초기화하는 것입니다.

기본적으로 여기에 나와 있는 이 로직은 lastName이 null이면 namefirstName 또는 "Unknown"으로 설정됩니다.

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Elvis 연산자

elvis 연산자 ?:을 사용하여 이 코드를 보다 자연스럽게 작성할 수 있습니다. elvis 연산자는 좌변이 null이 아니면 좌변의 표현식을 반환하고 좌변이 null이면 우변의 표현식을 반환합니다.

따라서 다음 코드에서 null이 아니면 firstName이 반환됩니다. firstName이 null이면 표현식은 우변의 값인 "Unknown"을 반환합니다.

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

Kotlin에서는 문자열 템플릿을 사용하여 String에 관련된 작업을 쉽게 할 수 있습니다. 문자열 템플릿을 사용하면 변수 앞에 $ 기호를 사용하여 문자열 선언 내에서 변수를 참조할 수 있습니다. 또한 { } 안에 표현식을 넣고 그 앞에 $ 기호를 사용하는 방식으로 문자열 선언 내에 표현식을 둘 수도 있습니다. 예: ${user.firstName}.

현재 코드에서는 문자열 연결을 사용하여 firstNamelastName을 사용자 이름으로 결합합니다.

if (firstName != null) {
    firstName + " " + lastName
}

대신 문자열 연결을 다음으로 바꿉니다.

if (firstName != null) {
    "$firstName $lastName"
}

문자열 템플릿을 사용하여 코드를 단순화할 수 있습니다.

더 자연스럽게 코드를 작성할 방법이 있는 경우 IDE에 경고가 표시됩니다. 코드에 물결선이 표시되고, 그 위로 마우스를 가져가면 코드 리팩터링 추천 방법이 표시됩니다.

현재 할당에 name 선언을 결합할 수 있다는 경고가 표시됩니다. 이를 적용해 보겠습니다. name 변수의 유형을 유추할 수 있으므로 명시적 String 유형 선언을 삭제할 수 있습니다. 이제 formattedUserNames의 모습은 다음과 같습니다.

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

추가로 한 가지 더 수정할 수 있습니다. 성과 이름이 누락되면 UI 로직에 "Unknown"이 표시되므로 현재 null 객체가 지원되지 않습니다. 따라서 formattedUserNames의 데이터 유형에서 List<String?>List<String>으로 바꿉니다.

val formattedUserNames: List<String>

formattedUserNames getter를 좀 더 자세히 살펴보고 이를 더 자연스럽게 만드는 방법을 알아보겠습니다. 이제 코드는 다음 작업을 실행합니다.

  • 새 문자열 목록 생성
  • 사용자 목록을 처음부터 끝까지 반복
  • 사용자의 성과 이름을 토대로 각 사용자에 형식이 지정된 이름 생성
  • 새로 만든 목록 반환
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

Kotlin은 자바 컬렉션 API의 기능을 확장하여 개발의 속도와 안전성을 더 높이는 광범위한 컬렉션 변환 목록을 제공합니다. 그러한 컬렉션 변환 중 하나가 map 함수입니다. 이 함수는 지정된 변환 함수를 원래 목록의 각 요소에 적용한 결과가 포함된 새 목록을 반환합니다. 따라서 수동으로 새 목록을 만들어 사용자 목록을 처음부터 끝까지 반복할 필요 없이 map 함수를 사용하여 for 루프에 있는 로직을 map 본문 내부에 옮길 수 있습니다. 기본적으로 map에 사용된 현재 목록 항목의 이름은 it이지만 가독성을 위해 it을 고유한 변수 이름으로 바꾸어도 됩니다. 여기서는 이름을 user라고 지정하겠습니다.

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

user.lastName이 null이면 Elvis 연산자를 사용해 "Unknown"을 반환합니다. user.lastName의 유형이 String?이고 nameString이 필요하기 때문입니다.

...
else {
    user.lastName ?: "Unknown"
}
...

이를 훨씬 더 간결하게 하기 위해 name 변수를 완전히 삭제할 수 있습니다.

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

자동 변환기가 getFormattedUserNames() 함수를 맞춤 getter가 있는 formattedUserNames라는 속성으로 바꾼 것을 확인했습니다. 내부적으로 Kotlin은 여전히 List를 반환하는 getFormattedUserNames() 메서드를 생성합니다.

자바에서는 getter 및 setter 함수를 통해 클래스 속성을 노출합니다. Kotlin을 사용하면 필드와 함께 표현된 클래스의 속성을 함수와 함께 표현된 기능(클래스가 할 수 있는 동작)과 더 정확하게 구분할 수 있습니다. 여기서 Repository 클래스는 매우 간단하고 어떠한 동작도 하지 않으므로 필드만 포함합니다.

자바 getFormattedUserNames() 함수에서 트리거된 로직이 이제 formattedUserNames Kotlin 속성의 getter를 호출할 때 트리거됩니다.

formattedUserNames 속성에 대응하는 필드가 명시적으로는 없지만 Kotlin은 field라는 자동 지원 필드를 제공합니다. 필요한 경우 맞춤 getter와 setter에서 이 필드에 액세스할 수 있습니다.

하지만 자동 백업 필드에 없는 추가 기능이 필요할 때가 있습니다.

한 가지 예를 살펴보겠습니다.

Repository 클래스에는 변경 가능한 사용자 목록이 있고, 이 목록은 자바 코드에서 생성된 함수 getUsers()에 노출되어 있습니다.

fun getUsers(): List<User>? {
    return users
}

여기서 문제는 users를 반환하면 Repository 클래스의 모든 소비자가 사용자 목록을 수정할 수 있습니다. 이는 좋은 생각이 아닙니다. 지원 속성을 사용하여 이 문제를 해결해 보겠습니다.

먼저 users 이름을 _users로 바꿉니다. 변수 이름을 강조표시하고 Refactor > Rename을 마우스 오른쪽 버튼으로 클릭합니다. 그런 다음 사용자 목록을 반환하는, 변경 불가능한 퍼블릭 속성을 추가합니다. 그 이름을 users라고 하겠습니다.

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

이 시점에서 getUsers() 메서드를 삭제할 수 있습니다.

위와 같이 변경하면 비공개 _users 속성이 공개 users 속성의 지원 속성이 됩니다. Repository 클래스 외부에서는 _users 목록을 수정할 수 없습니다. 클래스의 소비자가 users를 통해서만 목록에 액세스할 수 있기 때문입니다.

전체 코드:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

이제 Repository 클래스는 User 객체의 형식이 지정된 사용자 이름을 계산하는 방법을 인식합니다. 하지만 다른 클래스에서 동일한 형식 지정 로직을 재사용하려면 그 로직을 복사하여 붙여넣거나 User 클래스로 옮겨야 합니다.

Kotlin에는 클래스, 객체 또는 인터페이스 외부에서 함수와 속성을 선언하는 기능이 있습니다. 예를 들어 List의 새 인스턴스를 만드는 데 사용된 mutableListOf() 함수는 이미 Kotlin 표준 라이브러리에서 Collections.kt에 정의되어 있습니다.

자바에서는 유틸리티 기능이 필요할 때마다 대부분 Util 클래스를 생성하고 그 기능을 정적 함수로 선언할 것입니다. Kotlin에서는 클래스 없이 최상위 함수를 선언할 수 있습니다. 하지만 Kotlin은 확장 함수를 생성하는 기능도 제공합니다. 확장 함수는 특정 유형을 확장하지만 그 유형을 벗어나 선언되는 함수입니다.

확장 함수와 확장 속성의 공개 상태는 공개 상태 수정자를 사용해 제한할 수 있습니다. 공개 상태 수정자는 확장이 필요한 클래스에만 확장 함수와 확장 속성이 사용되어 네임스페이스를 복잡하게 만들지 읺도록 합니다.

User 클래스의 경우 형식이 지정된 이름을 계산하는 확장 함수를 추가하거나 형식이 지정된 이름을 확장 속성에 포함할 수 있습니다. 이를 Repository 클래스 외부에서 동일한 파일에 추가할 수 있습니다.

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

그런 다음 확장 함수와 확장 속성을 User 클래스의 일부인 것처럼 사용할 수 있습니다.

형식이 지정된 이름이 User 클래스의 속성이고 Repository 클래스의 기능은 아니므로 확장 속성을 사용해 보겠습니다. 이제 Repository 파일의 모습은 다음과 같습니다.

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

Kotlin 표준 라이브러리는 확장 함수를 사용하여 몇몇 자바 API의 기능을 확장합니다. IterableCollection의 많은 기능이 확장 함수로 구현되었습니다. 예를 들어 이전 단계에서 사용한 map 함수는 Iterable의 확장 함수입니다.

Repository 클래스 코드에서 몇 가지 User 객체를 _users 목록에 추가합니다. 이러한 호출은 Kotlin 범위 함수를 사용하여 더 자연스럽게 진행할 수 있습니다.

이름별로 객체에 액세스하지 않고 특정 객체의 컨텍스트에서만 코드를 실행할 수 있도록 Kotlin은 let, apply, with, run, also의 5가지 범위 함수를 제공합니다. 이러한 함수를 사용하면 코드가 더 읽기 쉽고 간결해집니다. 모든 범위 함수에는 수신자(this)가 있고, 인수(it)도 있을 수 있으며 값을 반환할 수도 있습니다.

다음은 각 함수를 사용할 상황을 기억하는 데 도움이 되는 요약본입니다.

6b9283d411fb6e7b.png

Repository에서 _users 객체를 구성 중이므로 apply 함수를 사용하여 코드를 더 자연스럽게 만들 수 있습니다.

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

이 Codelab에서는 코드를 자바에서 Kotlin으로 변환하는 데 필요한 기본사항을 설명했습니다. 이 변환은 개발 플랫폼과는 관계가 없으며, 코드를 자연스러운 Kotlin으로 작성하는 데 도움이 됩니다.

자연스러운 Kotlin에서는 작성 코드가 짧고 보기가 좋습니다. Kotlin의 모든 기능을 사용하면 아주 다양한 방법으로 코드를 더욱 안전하고 간결하며 읽기 쉽게 만들 수 있습니다. 예를 들어 init 블록을 제거하고 선언에 직접 users를 사용하여 _users 목록을 인스턴스화하는 방법으로 Repository 클래스를 최적화할 수도 있습니다.

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

지금까지 null 허용 여부 처리, 싱글톤, 문자열, 컬렉션부터 확장 함수, 최상위 함수, 속성, 범위 함수까지 다양한 주제를 다루었습니다. 또한 두 개의 자바 클래스를 두 개의 Kotlin 클래스로 바꾸었습니다. 현재 그 모습은 다음과 같습니다.

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

다음은 자바 기능을 Kotlin에 매핑하는 방법의 요약본입니다.

자바

Kotlin

final 객체

val 객체

equals()

==

==

===

데이터를 보유하기만 하는 클래스

data 클래스

생성자 내 초기화

init 블록 내 초기화

static 필드 및 함수

companion object에 선언된 필드 및 함수

싱글톤 클래스

object

Kotlin 정보와 플랫폼에서 Kotlin을 사용하는 방법을 자세히 알아보려면 다음 리소스를 참고하세요.