Skip to content

Latest commit

 

History

History
987 lines (718 loc) · 34.7 KB

코틀린 쿡북.md

File metadata and controls

987 lines (718 loc) · 34.7 KB

코틀린 쿡북

2장 코틀린 기초

코틀린에서 널 허용 타입 사용하기

문제 변수가 절대 null 값을 갖짐 못하게 하고싶다.

해법 물음표를 사용하지 않은 변수의 타입을 정의한다. 또한 널 허용 타입은 안전 호출 연사자 ?. or 엘비스 연산자 ?:와 결합하여 사용한다.

설명

data class Person(
    val first: String,
    val middle: String?,
    val last: String
)

val middleLength = person.middle?.length ?: 0

안전 호출자 ?.와 엘비스 연산자 ?:와 결합하여 사용한다.

자바에서 널 허용성 지시자 추가하기

문제 코틀린 코드가 자바 코드와 상호 작용이 필요하고 널 허용성 애노테이션을 강제하고 싶다.

해법 코틀린 코드에서 JSR-305 널 허용 애노테이션을 강제하려면 컴파일 타임 파라미터 Xjsr305=strict을 사용한다

설명

자바 코드와 상호작용을 하기 위해서 JSR-305 호환 애노테이션을 사용 중이며 코틀린은 해당 호환 라이브러를 지원한다. 해당 라이브러리는 개발이 중지된 상태이다.

java.sourceCompatibility = JavaVersion.VERSION_11

tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs = listOf("-Xjsr305=strict")
		jvmTarget = "11"
	}
}

JSR-305에 정의돈 @Notnull 애노테이션은 when 이라는 속성을 갖는다. when 속성의 값이 when.ALWAYS인 경우 해당 애노테이션의 타입은 널 비허용 타입으로 다뤄진다. when.NAVER라면 널 허용 다뤄진다. when.UNKNOWN 이라면 해당 타입은 널 허용성을 알 수 없는 플랫폼 타입으로 간주한다.

자바를 위한 메서드 중복

문제 기본 파라미터를 가진 코틀린 함수가 있는데, 자바에서 각 파라미터의 값을 직접적으로 명시하지 않고 코틀린 함수를 호출하고 싶다.

해법 @JvmOverloads 애노테이션을 해당 함수에 추가한다.

설명

@Test
    internal fun `코틀린에서 중복 함수 변경 호출하기`() {
        // default 메서드에 따라 3개 메서드 구현
        addProduct(name = "name", price = 10, "DESC")
        addProduct(name = "name", price = 10)
        addProduct(name = "name")
    }

    fun addProduct(name: String, price: Int = 0, desc: String? = null) {
        // ..
    }

자바는 메소드 기본 인자를 지원하지 않기 때문에 자바에서 addProduct 함수를 호출하려면 모든 인자를 제공해야한다.

@JvmOverloads 애노테이션을 addProduct 함수에 추가하면 컴파일 후 생성된 클래스는 예제 처럼 모든 함수 중복을 지원 한다.

@Test
void checkOverloads() {
    assertAll("overloads called from java", 
        () -> System.out.println(Overloadskt.addProduct("Name", 5.0, "Desc")),
        () -> System.out.println(Overloadskt.addProduct("Name", 5.0)),
        () -> System.out.println(Overloadskt.addProduct("Name"))
    );
}

코틀린이 생상한 바이코드를 디컴파일 해보면 다음과 같다.

public final class Person {
   
   @JvmOverloads
   public final void addProduct(@NotNull String name, int price, @Nullable String desc) {
      Intrinsics.checkNotNullParameter(name, "name");
      System.out.println("name: " + name);
      System.out.println("price: " + price);
      System.out.println("String: " + StringCompanionObject.INSTANCE);
   }

   // $FF: synthetic method
   public static void addProduct$default(Person var0, String var1, int var2, String var3, int var4, Object var5) {
      if ((var4 & 2) != 0) {
         var2 = 0;
      }

      if ((var4 & 4) != 0) {
         var3 = null;
      }

      var0.addProduct(var1, var2, var3);
   }

   @JvmOverloads
   public final void addProduct(@NotNull String name, int price) {
      Intrinsics.checkNotNullParameter(name, "name");
      addProduct$default(this, name, price, (String)null, 4, (Object)null);
   }

   @JvmOverloads
   public final void addProduct(@NotNull String name) {
      Intrinsics.checkNotNullParameter(name, "name");
      addProduct$default(this, name, 0, (String)null, 6, (Object)null);
   }
}

Person 클래스는 3개의 생성자를 제공한다. @JvmOverloads 애노테이션을 추가하면 명시적으로 constructor 키워드를 사용해야 한다.

data class Person @JvmOverloads constructor(
    val first: String,
    val middle: String?,
    val last: String
)

@JvmOverloads에 의해 생성된 생성자의 경우에 자신의 인자의 개수와 같은 인자의 개수를 같은 부모 클래스의 생성자 호출이 일어나지 않는다.

명시적으로 타입 변환하기

문제 코틀린은 자동으로 기본 타입을 더 넓은 타입으로, 예를 들어 Int를 Long으로 승격하지 않는다.

해법 더 작은 타입을 명시적으로 변환하려면 toInt, toLong 등 구체적으로 변환 함수를 사용한다.

설명 더 짧은 타입으로 자동으로 더 긴 타입으로 승격되지 않는다.

int myInt = 3;
long myLong = myInt; //(1)
  • (1) int가 long으로 자동 승격

자바에서는 자료형을 변환 시키기 위해서는 아래와 같은 코드가 필요하다.

Intefer myInteger = 3;
// Long myWrappedLong = myInteger;          // (1)
Long myWrappedLong = myInteger.longValue(); // (2)
myWrappedLong = Long.valueOf(myInteger);    // (3)
  • (1) 컴파일되지 않음
  • (2) long으로 추출한 다음 래퍼 타입으로 감쌈
  • (3) 래퍼 타입을 벗겨 int 타입을 얻고 long으로 승격시킨 다음 다시 랩

래퍼 타입을 직접 다루는 것은 언박싱을 개발자 스스로 해야 한다는 의미이다. 먼저 포장된 값을 추출하는 작업 없이는 간단하게 Integer 인스턴스를 Long에 할당할 수 없다.

val intVar: Int = 3
// val longVar: Long = intVar       // (1)
val longVar: Long = intVar.toLong() // (2)
  • (1) 컴파일도지 않음
  • (2) 명시적 타입 변환

다행이도 코틀린은 타입 변환을 투명하게 수행하는 연산자가 있기 때문에 다음 코드는 명시적 타입 변환이 필요하지 않다.

val longSum = 3L + intVar

더하기(+) 연산자는 자동으로 intVardml 값을 long으로 변환하고 long 리터널에 그 값을 더한다.

다른 기수로 출력하기

문제 십진법이 아닌 다른 기수를 사용하는 숫자를 출력하고 싶다.

해법 올바른 기수를 위해 확장 함수 toString(radix: int)를 사용하자.

설명 자바에서 숫자를 이진법으로 출력하고 싶다면 Integer.toBinaryString 메소드나 정적 Integer.toString(int, int) 메서드를 사용할 것이다. 첫 번째 인자는 변환할 값이고 두 번째 인자는 원하는 기수이다.

하지만 코틀린에서 자바의 정적 메서드를 사용해서 Byte와 Short, Int, Long에 확장 함수 toSring(radix: Int)을 만들어 놓았다.

42.toString(2) == "101010"

이진 표현에서 비트의 위치는 오른쪽에서 왼쪽로 1, 2, 4, 8, 16 등에 해당한다. 42는 2+8+32 이며 그 이유는 해당 숫자의 비트 위치는 1이고 나머지는 0이기 때문이다.

숫자를 거듭제곱하기

문제 숫자를 거듭제곱하고 싶지만 코틀린에는 미리 정의된 거듭제곱 연산자가 없다

해법 Int와 Long에 정의돼 있는 코틀린 확장 함수 pow에 위임하는 중위 함수를 정의한다.

설명 코틀린에는 자바처럼 내장 거듭제곱 연산자가 없다.

@Test
fun `정수를 지수로 만들기`() {
    val toInt = 2.toDouble().pow(8).toInt()
    println(toInt)
}

비트 시프트 연산자 사용하기

문제 비트 시프트 연산자를 사용하고 싶다.

해법 코틀린에는 비트 시프트를 위한 shr, shl, ushr 같은 비트 중위 연산자가 있다.

설명 비트 연산은 리스트 접근 제어 및 통신 프로토컬, 압축, 암호화 알고리즘, 컴퓨터 그래픽을 비롯한 수많은 애플리케이션에서 사용된다.

함수 설명
shl 부호 있는 외쪽 시프트
shr 부호 있는 오른쪽 시프트
ushr 부호 없는 오른쪽 시프트
@Test
    fun `2를 곱하거나 나누기`() {
    // shl
    then(2).isEqualTo(1 shl 1)
    then(4).isEqualTo(1 shl 2)
    then(8).isEqualTo(1 shl 3)
    then(16).isEqualTo(1 shl 4)
    then(32).isEqualTo(1 shl 5)
    then(64).isEqualTo(1 shl 6)
    then(128).isEqualTo(1 shl 7)

    // shr
    then(117).isEqualTo(235 shr 1)
    then(58).isEqualTo(235 shr 2)
    then(29).isEqualTo(235 shr 3)
    then(14).isEqualTo(235 shr 4)
    then(7).isEqualTo(235 shr 5)
    then(3).isEqualTo(235 shr 6)
}

비트 불리언 연산자 사용하기

문제 비트 값에 마스크를 적용하고 싶다.

해법 비트 불리언 연산을 위해 코틀린이 제공하는 and, or, xor, inv 비트 연산자를 사용한다.

설명 Int와 Long에 정의되어 있는 시프트 연산자뿐만 아니라 코틀린은 마스킹 연산자 and, or, xor, inv도 정의되어 있다.

inv 함수는 숫자의 모든 비트를 뒤집는다,. 간단한 예를 들면 숫자 4의 바이너리 값은 0b00000100이다. 주어진 모든 비트를 뒤집으면 0b11111011인데, 이 값은 십진수로 251이다. 하지만 숫자 4에서 inv 함수를 호출하면

@Test
fun `숫자 4비트 반전`() {
    // 4 == 0b0000_0100
    // 주어진 비트 보수
    then(-5).isEqualTo(4.inv())
}
@Test
fun `and or xor 간단한 예`() {
    val n1 = 0b000_1100 // 십진수 12
    val n2 = 0b001_1001 // 십진수 25

    val n1_and_n2 = n1 and n2
    val n1_or_n2 = n1 or n2
    val n1_xor_n2 = n1 xor n2

    then(n1_and_n2).isEqualTo(0b000_1000) // 8
    then(n1_or_n2).isEqualTo(0b001_1101) // 29
    then(n1_xor_n2).isEqualTo(0b001_0101) // 21
}

레시피 to Pair 인스턴스 생성하기

문제 Pair 클래스의 인스턴스를 생성하고 싶다.

해법 직접 Pair 클래스의 인스턴스를 생성하기보다 중위 to 함수를 사용한다.

설명

mapOf와 같은 맵 생성을 위한 최상위 함수를 몇 가지 제공한다.

fun <K, V> map(varag pairs: Pair<K, V>): Map<K, V>

Pair는 first, second라는 이름의 두 개의 원소를 갖는 데이터 클래스다. Pair 클래스의 시그니처는 다음과 같다.

data class Pair<out A, out B> : Serializable

2개의 인자를 받는 생성자를 사용해서 Pair 클래스를 생성할 수 있지만 to 함수를 사용하는 것이 더 일반적이다. to 함수는 다음과 같이 정의되어 있다.

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
@Test
fun `mapOf 인자인 pair를 생성하기 위해 to 함수 사용하기`() {
    val map = mapOf(
        "a" to 1,
        "b" to 2,
        "c" to 2
    )
    println(map) // {a=1, b=2, c=2}
}

@Test
fun `map of`() {
    val pair1: Pair<String, Int> = Pair("z", 1)
    val pair2: Pair<String, Int> = "a" to 1
}

3장 코틀린 객체 지향 프로그래밍

cost와 val의 차이 이해가기

문제 런타임 보다는 컴파일 타임에 변수가 상수임을 나타내야 한다.

해법 컴파일 타임 상수에 const 변경자를 사용한다. val 키워드는 변수에 한 번 할당되면 변경이 불가능함을 나타내지만 이러한 할당은 실행 시간에 일어난다.

설명 코틀린 키워드는 val은 값이 변경 불가능한 변수임을 나타낸다. 자바에서는 final 키워드가 같은 목적으로 사용된다. 그렇다면 코틀린에서도 const 변경자도 지원하는 이유는 무엇일까?

컴파일 타임 상수는 반드시 객체나 동반 객체 companion object 선언의 최상위 속성 또는 멤버여야 한다. 컴파일 타임 상수는 문자열 또는 기본 타입의 래퍼 클래스 이며, 사용자 정의 획득자를 가질 수 없다. 컴파일 타임 상수는 컴파일 시점에 값을 사용할 수 있도록 main 하수를 포함한 모든 함수의 바깥쪽에서 할당 돼야 한다.

class Task(
    val name: String,
    _priority: Int = DEFAULT_PRIORITY
) {

    companion object {
        const val MIN_PRIORITY = 1      // (1)
        const val MAX_PRIORITY = 1      // (1)
        const val DEFAULT_PRIORITY = 3  // (1)
    }

    var priority = validPriority(_priority) // (2)
        set(value) {
            field = validPriority(value)
        }

    private fun validPriority(p: Int) = // (3) 
        p.coerceIn(MIN_PRIORITY, MAX_PRIORITY)
}
  • (1): 컴파일 타임 상수
  • (2): 사용자 정의 설정자를 사용하는 속성
  • (3): private 검증 함수

코틀린에서 val는 키워드지만 const는 private, inline 등과 같은 변경자이다. 그런 이유로 cost가 val 키워드를 대체하는 것이 아니라 반드시 같이 씌어야 한다.

사용자 정의 획득자와 설정자 생성하기

문제 값을 할당하거나 리턴하는 방법을 사용자 정의하고 싶다.

해법 코틀린 클래스의 속성에 get, set 함수를 추가한다.

설명 코틀린 클래스에서 필드는 직접 선언할 수 없다.

class Task1(val name: String) {
    var priority = 3
}

이 방식으로 priority를 선언할 때의 단점은 apply 블록을 사용해서 priority에 값을 할당이 가능하지만 인스턴스화할 때 priority에 값을 할당 할 수 없다.

Task1(name = "name").apply { priority = 4 }
var <propertyName>[: <PropertyType>] = [property_initializer]
  [<getter>]
  [<setter>]

속성 초기화 블록, 획득자, 설정자는 선택사항이다. 속성 타입이 초기 값 또는 획득자의 리턴 타입에서 추론 가능하다면 속성 타입 또한 선택사항이다. 하지만 생성자에서 선언한 속성자에서는 타입 선언이 필수다.

val isLowPriority
    get() = priority < 3

var priority = 3
    set(value) {
        field = value.coerceIn(1..5)
    }

isLowPriority의 타입은 get 함수의 리턴 타입으로부터 추론되며 이 경우에는 불리언 타입이다. 사용자 정의 설정자는 속성에 값을 할당할 때 마다 사용된다.

데이터 클래스 정의하기

문제 equals, hashCode, toString 등이 완벽하게 갖춰진 엔ㄴ티티를 나타내는 클래스를 생성하고 싶다.

해법 클래스를 정의할 때 data 키워드를 사용한다.

설명

지원 속성 기법

문제 클래스의 속성을 클라이언트에게 노출하고 싶지만 해당 속성을 초기화하거나 읽는 방법을 제어해야 한다.

해법 같은 타입의 속성을 하나 더 정의하고 사용자 정의 획득자와 설정자를 이용해 원하는 속성에 접근한다.

설명

class Customer(
    val name: String
) {

    private var _message: List<String>? = null // (1)

    val message: List<String> // (2)
        get() {  // (3)
            if (_message == null) {
                _message = loadMessage()
            }
            return _message!!
        }

    private fun loadMessage(): List<String> =
        mutableListOf(
            "Initial concat",
            "Convinced them to use Kotlin",
            "Sold training class. Sweet."
        )
            .also {
                println("Loaded messages")
            }
}
  • (1) 널 허용 private 속성의 초기화
  • (2) 불러올 속성
  • (3) private 함수
@Test
fun `load message`() {
    val customer = Customer("Fred").apply { message }
    then(customer.message).hasSize(3)
}

_messages는 private 이기 때문에 생성자 속성을 사용해 message를 불러올 수 없다. apply함수를 사용하여 messages를 불러오고 로딩 완료 정보를 getter 메서드를 호출한다. 두 번쨰로 messages 속성에 접근할 때 messages는 로딩됐기 때문에 로딩 완료 정보를 출력하지 않는다.

코틀린 내장 lazy 대리자 함수를 사용하면 더 쉬운 방법으로 코드를 구현할 수 있다.

class Customer(
    val name: String
) {

    val message: List<String> by lazy { loadMessage() }
    
    private fun loadMessage(): List<String> =
        mutableListOf(
            "Initial concat",
            "Convinced them to use Kotlin",
            "Sold training class. Sweet."
        )
            .also {
                println("Loaded messages")
            }
}

연산자 중복

문제 라이브러리에 정의된 클래스와 더불어 +와 *같은 연산자를 사용할 수 있는 클라이언트를 만들 고 싶다

해법 코틀린 연산자 중복 메커니짐을 사용해서 +, * 등의 연산자의 연관된 함수를 구현한다.

설명

나중 초기화를 위해 lateinit 사용하기

문제 생성자에 속성 초기화를 위한 정보가 충분하지 않으면 해당 속성을 널비허용 속성으로 만들고 싶다.

해법 속성에 lateinit 변경자를 사용한다.

설명 모든 객체가 생성될 때까지 의존성 주입이 일어나지 않는 의존성 주입 프레임워크에서 발생하거나 유닛 테스트의 설정 메소드안에서 발생한다. 이러한 경우 lateinit를 사용한다.

lateinit과 lazy의 차이 lateinit 변경자는 설명한 제약 사항과 함께 var 속성에 사용된다. lazy 대리자는 속성에 처음 접근할때 평가되는 람다를 받는다.

초기화 비용이 높은데 lazy를 사용한다면 초기화는 반드시 실패한다. 또한 lazy는 val 속성에 사용할 수 있는 반면 lateinit은 var 속성에만 적용할 수 있다. 마지막으로 lateinit 속성은 속성에 접근할 수 있는 모든 곳에서 초기화할 수 있기 때문에 객체 바깥쪽에서도 초기화할 수 있다.

equals 재정의를 위해 안전 타입변환, 에퍼런스 동등, 엘비스 사용하기

문제 논리적으로 동등한 인스턴스인지를 확인하도록 클래스의 equals 메서드를 잘 구현하고 싶다

해법 레퍼런스 동등 연산자 ===, 안전 타입 변환 함수 as?, 엘비스 연산자를 다같이 사용한다.

설명 equals 문법에서 equals 구현은 반사성, 대칭성, 추이성, 일관성이 있어야야 하며 널도 적적할게 처리할 수 있어야한다. hashCode 문법에서 equals 함수가 두 객체를 동등하다고 파단하면 두 객체의 hashCode도 같아야 한다. equals 함수가 재정의되면 hashCode도 같이 정의돼야 한다.

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    other as Customer

    if (name != other.name) return false

    return true
}

싱글톤 생성하기

문제 클래스 하나당 인스턴스는 딱 하나만 존재하게 만들고 싶다.

해법 class 대신 object 키워드를 사용한다. 이를 객체 선언 object declaration

설명

object MySingleton {
    val myProperty = 3

    fun myFunction() = "Hello"
}

생성된 바이트코드를 디컴파일하면 다음과 비슷하다.

public final class MySingleton {
   @NotNull
   public static final MySingleton INSTANCE = new MySingleton(); // (1)
   private static final int myProperty = 3; // (3)

   private MySingleton() { // (2)
   }

   public final int getMyProperty() {
      return myProperty;
   }

   @NotNull
   public final String myFunction() {
      return "Hello";
   }
}
  • (1) INSTANCE 속성 생성자
  • (2) private 생성자
  • (3) 싱글톤의 열성적인 인스턴스화

Nothing에 관한 야단 법석

문제 Nothing 클래스를 사용법에 맞게 적절하게 사용하고 싶다

해법 절대 리턴하지 않는 함수에 Nothing을 사용한다

설명

public class Nothing private constructor()

private 생성자는 클래스 밖에서 인스턴스화할 수 없다는 것을 으미하고, 보다시피 클래스 안쪽에서도 인스턴스화하지 않는다. 따라서 Nothing의 인스턴스는 존재하지 않는다.

Nothing 클래스의 사용은 2가지 사황에서 자연스럽게 발생한다.

fu doNothing(): Nothing = throw Exception("Nothing at All")

리턴 타입을 반드시 구체적으로 명시해야하는데 해당 메서드는 결코 리턴하지 않으므로 리턴 타입은 Nothing 이다.

자바에서는 어떤 타입의 예외를 던지든 메소드의 리턴 타입이 변경되지 않는다. 예외 처리는 전적으로 정상적인 프로그램 실행 흐름의 바깥에 위치하지만 예외 처리를 위한 메소드의 리턴 타입을 변경할 필요는 없다. 하지만 코틀린 타입 시스템은 자바와는 필요 조건이 다르다.

두 번째 Nothing 사용이 일어나는 상황은 변수에 널을 할당할 때 구체적인 타입을 명하지 않는 경우다.

val x = null

x는 분명히 널 할당이 가능한 타입이고 컴파일러는 x에 대한 다른 정보가 없기 때문에 추론된 x의 타입은 Nothing?이다. 더 흥미로운 사실은 코틀린에서 Nothing 클래스는 실제로 다른 모든 타입의 하위 타입이라는 것이다.

Nothing 클래스가 모든 타입의 하위 타입이 되어야하는 이유는 다음과 같다.

val x = if (Random.nextBoolean()) "true" else throw Exception("nope")

x의 추론 타입은 Random.nextBoolean 함수가 생성하는 불리언 값이 참인 경우에만 할당되는 문자열에 따라 String, Comparable, CharSequence, Serializable 또는 Any일 수도 있다. 이 코드의 else 절에 Nothing을 리턴하고 Nothing은 모든 타입의 하위 타입이므로 불리언 탑이이 된다.

for (n in 1..10) {
    val x = when (n % 3) {
        0 -> "$n %3 == 0"
        1 -> "$n %3 == 1"
        2 -> "$n %3 == 2"
        else -> throw Exception("Houston, we have a problem..")
    }
    assertThru(x is string)
}

예외가 발생하는 경우 리턴 타입은 Nothing이고 String은 String이므로 컴파일러는 x의 타입이 String임을 알 수 있다.

TODO 구횬은 NotImplementedError를 던지므로 TODO 함수는 Nothing를 리턴하는 것이 타당하다.

4장 함수형 프로그래밍

함수형 프로그래밍이라는 요어는 불변성을 선호하고, 순수 함수를 사용하는 경우에 동시성을 쉽게 구현할 수 있으며, 반복보다는 변형을 사용하고, 조건문보다는 필터를 사용하는 코딩 스타일을 지칭한다.

알고리즘에서 fold 사용하기

문제 반복 알고리즘을 함수형 방식으로 구현하고 싶다.

해법 fold 함수를 사용해 시퀀스나 컬렉션을 하나의 값으로 츅약 시킨다.

설명 fold는 2개의 인자를 받는다. 첫 번째는 누적자의 초기 값이며 두 번째는 두 개의 인자를 받아 누적자를 위해 새로운 값을 리턴하는 함수이다.

fun sum(vararg nums: Int) = 
    nums.fold(0) { acc, n -> acc + n}

num 리스트의 각각의 값을 순회하며 첫 번째 인자를 누적 값에 순회 중인 값을 n을 더하는 함수이다.

5장 컬렉션

코틀린은 자바처럼 다수의 객체를 담기 위해 타입을 명시한 컬렉션을 사용한다. 하지만 코틀린은 자바와는 다르게 중개자 역할을 하는 스트림ㄷ을 거치지 않고 여러 가지 흥미로운 메소드를 컬렉션 클래스에 직접 추가한다.

배열 다루기

문제 코틀린에서 배열을 생성하고 배열에 데이터를 추가하고 싶다.

해법 arrayOf 함수를 이용해 배열을 만들고 Array 클래스에 들어 있는 속성과 메서드를 이용해 배열에 들어 있는 값을 다룬다.

설명

String[] strings = new Stirng[4]
strings[0] = "an";
strings[1] = "array";
strings[2] = "of";
strings[3] = "strings";

// 더 쉽게
strings  = "an array of strings".split(" ");

자바에서는 new 키워드를 사용해 배열의 크기를 지정하고 인스턴스화 한다.

val strings = arrayOf("this", "is", "an", "array", "of", "strings")

6장 시퀀시

자바의 스트림과 비슷한 코틀린 시퀀스를 살펴보겠다. 컬렉션에서 처리는 즉시 발생한다. 즉 컬렉션의 map이나 filter가 호출될 때 컬렉션의 모든 요서는 즉시 처리된다. 반면에 스퀀스는 지연 처리된다. 데이터를 사용하면 각각의 원소는 자신의 다음 원소가 처리되기 전에 전체 파이프라인을 완료한다.

지연 스퀀스 사용하기

문제 특정 조건을 만족하는 데 필요한 최소량의 데이터만 처리하고 싶다.

해법 코틀린 스퀀스를 쇼트 서킷 함수와 함께 사용한다. 특정 조건에 다다를 때까지 오직 필요한 데이터만 처리하는 방식을 쇼트 서킷이라 부른다. 람다를 받는 중복된 버전의 first 사용을 잊어버린다면 필요한 것보다 더 많은 일을 해야 하는 처리 방식으로 돌아게 된다.

설명

@Test
fun `asSequence`() {
    (100 until 2_000_000).asSequence()
        .map { println("doubling $it"); it * 2 }
        .filter { println("filtering $it"); it % 3 == 0 }
        .first()
}

어떤 방법을 사용하든 시퀀스의 각 요소는 다음 원소로 진행하기 전에 완전한 전체 파이프라인에서 처리되기 때문에 오직 6개의 연산만이 수행된다.

시퀸스 API는 컬렉션에 들어 있는 함수와 똑같은 함수를 가지고 있지만 시퀸스에 대한 연산은 중간 연산과 최종 연산 이라는 범주로 나뉜다. map과 filter같은 중간 욘산같은 새로운 시퀀스를 리턴한다.

7장 영역 함수

객체 컨텍스트 안에서 코드 블록을 실행할 목적으로 만든 다수의 함수가 포함돼 있다. let, run, apply, also에 대해 설명한다.

apply로 객체 생성 후에 초기화 하기

문제 객체를 사용하기 전에 생성자 인자만으로 할 수 없는 초기화 작업이 필요하다.

해법 apply 함수를 사용한다.

설명

부수 효과를 위해 also 사용하기

문제 코드 흐름을 방해하지 않고 메시지를 출력하거나 다른 부수 효과를 생성하고 싶다. 해법 also 함수를 사용해 부수 효과를 생성하는 동작을 수행한다.

설명

let 함수와 엘비스 연산자 사용하기

문제 오직 널이 아닌 레퍼런스의 코드 블록을 실행하고 싶지만 레퍼런스가 널이라면 기본 값을 리턴 하고 싶다

해법 엘비스 연산자를 결합한 안전 호출 연산자와 함께 let 영역 함수를 사용하자

설명

fun processString(str: String?) {
    str?.let { it ->
        when {
            it.isEmpty() -> "Empty"
            it.isBlank() -> "Blank"
            else -> it.capitalize()
        }
    } ?: "Null"
}

임시 변수로 let 사용하기

문제 연산 결과를 임시 변수에 할당하지 않고 처리하고 싶다.

해법 연산에 let 호출을 연쇄하고 let에 제공한 람다 또는 레퍼헌스 안에서 그 결과를 처리한다.

설명

8장 코틀린 대리자

lazy, observable, vetoable, notnull 대리자 및 사용자 정의 대리자 등을 사용하는 방법을 배운다. 클래스 대시자를 통해 상속을 합성으로 대체할 수 있고, 속성 대리자를 토ㅇ해 어떤 속성의 획득자와 설정자를 다른 클래스에 있는속성의 획득자와 설정자로 대체할 수 있다.

문제 다른 클래스의 인스턴스가 포함된 클래스를 만들고, 그 클래스에 연산을 위임하고 싶다.

해법 연산을 위임할 메서드 포함된 인터페이스를 만들고, 클래스에 해당 인터페이스를 구현한 다음 by 키워드를 사용해 바깥쪽에 래퍼 클래스를 만든다.

설명 최근 객체 지향 디자인은 강한 결합 없이 기능을 추가할때 상속 보다는 합성을 선호한다. 코틀린에서 by 키워드는 포함된 객체에 있는 모든 public 함수를 이 객체를 담고 있는 컨테이너를 통해 노출할 수 있다.

포함된 객체에는 모든 public 함수들이 이 객체를 담고 있는 컨테이너를 통해 노출하려면, 포함된 객체의 public 메서드의 인터페이스를 생성해야 한다. 따라서 Phone, Camera 클래스는 예제 처럼 인터페이스를 구현 해야한다.

interface Dialable {
    fun dial(number: String): String
}

class Phone : Dialable {
    override fun dial(number: String) =
        "Dialing $number..."
}

interface Snappable {
    fun takePicture(): String
}

class Camera : Snappable {
    override fun takePicture() =
        "Taking picture..."
}

SmartPhone 클래스가 아래 예제 처럼 생성자에서 Phone, Camera를 인스턴스화하고 모든 public 함수를 Phone, Camera 인스턴스에 위임하도록 정의할 수 있다.

class SmartPhone(
    private val phone: Dialable = Phone(),
    private val camera: Snappable = Camera()
) : Dialable by phone, Snappable by camera //(1)

class SmartPhoneTest {

    private val smartPhone: SmartPhone = SmartPhone() //(2)

    @Test
    fun `Dialing delegates to internal phone`() {
        then("Dialing 555-1234...").isEqualTo(smartPhone.dial("555-1234")) //(3)
    }

    @Test
     fun `Taking picture delegates to internal camera`() {
        then("Taking picture...").isEqualTo(smartPhone.takePicture()) //(3)
    }
}
  • (1): 키워드 by를 사용해 위임
  • (2): 인자 없는 SmartPhone을 인스턴스화함
  • (3): 위임함수를 호출

포함된 Phone, Camera 객체는 SmartPhone을 통해 노출되는 것이 아니라 오직 포함된 객체의 public 함수만이 노출된다. Phone, Camera 클래스에는 많은 함수가 있을 수도 있지만 Dialable, Snappable 인터페이스에 선언되어 있고, 이와 일치하는 함수만 사용이 가능하다. 인터페이스를 정의하는 작업은 불필요해 보이지만 정확한 관계를 유지할 수 있게 만든다.

public final class SmartPhone implements Dialable, Snappable {
   @NotNull
   private final Dialable phone;
   @NotNull
   private final Snappable camera;

   public SmartPhone(@NotNull Dialable phone, @NotNull Snappable camera) {
      Intrinsics.checkNotNullParameter(phone, "phone");
      Intrinsics.checkNotNullParameter(camera, "camera");
      super();
      this.phone = phone;
      this.camera = camera;
   }

   // $FF: synthetic method
   public SmartPhone(Dialable var1, Snappable var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 1) != 0) {
         var1 = (Dialable)(new Phone());
      }

      if ((var3 & 2) != 0) {
         var2 = (Snappable)(new Camera());
      }

      this(var1, var2);
   }

   @NotNull
   public String dial(@NotNull String number) {
      Intrinsics.checkNotNullParameter(number, "number");
      return this.phone.dial(number);
   }

   @NotNull
   public String takePicture() {
      return this.camera.takePicture();
   }

   public SmartPhone() {
      this((Dialable)null, (Snappable)null, 3, (DefaultConstructorMarker)null);
   }
}

해당 코드를 디컴파일해 보면 내부적으로 SmartPhone 클래스는 위임된 속성을 인터페이스 타입으로 정의한다. 이 인터페이스 타입에 사응하는 틀래스 인스턴스는 생성자를 통해 제공된다. 그런 다음 대리자 메서드는 해당 하는 메서드를 필드에서 호출한다.

Lazy 대리자 사용하기

문제 어떤 속성이 필요할 때까지 해당 속성의 초기화를 지연시키고 싶다.

해법 코틀린 표준 라이브러리인 lazy 대리자를 사용하자.

설명 코틀린은 어떤 속성의 획득자와 설정자가 대리자라고 불리는 다른 객체에서 구현되어 있다는 것을 암시하기 위해 by 키워드를 사용한다. 코틀린 표준 라이브러리에서는 다수의 대리자 함수가 있다. 그 중 가장 인기 있는 대리자 함수가 lazy 이다.

val ultimateAnswer: Int by lazy {
    println("Computing the answer")
    42
}

ultimateAnswer의 값을 lazy에 제공된 람다 식이 평가되는 때, 즉 해당 변수에 처음 접근하게 될 때까지 계산하지 않는 것이다. 따라서 Computing the answer는 오직 한 번만 출력한다.

@Test
    fun `test answer`() {
    println(ultimateAnswer)
    println(ultimateAnswer)
}
// Computing the answer
// 42
// 42

첫 ultimateAnswer 호출은 lazy가 받은 람다를 실행하고 그 다음 변수에 저장될 42를 리턴한다. 내부적으로 코틀린은 이 값을 캐시하는 Lazy 타입의 ultimateAnswer$delegate라는 특별한 속성을 생성한다. LazyThreadSafeMode 타입의 인자는 다음과 같은 값으 이넘을 받는다.

  • SYNCHRONIZED: 오직 하나의 스레드만 Lazy 인스턴스를 초기화할 수 있게 락을 사용
  • PUBLICATION: 초기화 함수가 여러 번 호출될 수 있지만 첫 번째 리턴값만 사용됨
  • NONE: 락이 사용되지 않음

어떤 객체를 lazy의 lock의 인자로 제공하면 값을 계산할 때 이 객체가 대리자를 동기화한다. lock의 인자로 아무것도 제공되지 않았다면 대리자는 자신 스스로 동기화한다. lazy 대리자는 복잡한 객체를 인스턴스화할 때 적잡한데 lazy 기본 원리는 모든 경우에 다 동일하다.

값이 널이 될 수 없게 만들기

문제 처음 접근이 일어나기 전에 값이 초기화 되지 않는다면 예외를 던지고 싶다.

해법 notNull 함수를 이용해 값이 설정되지 않았다면 예외를 던지는 대리자를 제공한다.

설명 보통 코틀린 클래스의 속성은 클래스 생성 시에 초기화된다. 속성 초기화를 지연시키는 한 가지 방법은 속성에 처음 접근하기 전에 속성이 사용되면 예외를 던지는 대리자를 제공하는 notNull 함수를 사용하는 것이다. 속성이 사용되기 전에 어디가에서 반드시 초기화돼야 하는 shouldNotBeNull 속성을 선언한다.

var shouldNotBeNull: String by Delegates.notNull<String>()

속성에 값이 제동되기 전에 접근을 시도하면 코티르니 IllegaSateException을 발생 시킨다.

observalve과 vetoable 대리자 사용하기

문제 속성의 변경을 가로채서, 필요에 따라 변경을 거부하고 싶다.

해법 변경 감지에는 observable 함수를 사용하고, 변경의 적용 여부를 결정할 때는 vetoable 함수와 람다를 사용하자.

설명 observable, vetoable 함수의 구현은 개발자가 대리자를 작성할 때 참고할 만한 좋은 패턴이다.

9장 테스트

10장 입력/출력

11장 그 밖의 코틀린 기능

12장 스프링 프레임워크

13 코루틴과 구조적 동시성