아래 교재 및 문서를 보고 공부한 내용을 정리한 공간입니다.
-
2011년 JetBrains는 JVM에서 실행되는 Kotlin 프로그래밍 언어의 개발을 발표하였다.
-
그로부터 6년후 구글에서는 Kotlin을 Android 시스템 공식 개발 언어로 발표하였다.
-
Spring Framework 5부터는 Kotlin을 정식으로 지원한다. (링크)
-
-
Kotlin은 JVM 위에서 동작한다. Kotlin code를 compile하면 JVM에서 실행할 수 있는 bytecode로 변환된다.
-
Java는 최초의 JVM 언어이다.
-
Kotlin, Scala와 같이 다른 JVM 언어들은 Java으 단점을 보왼하기 위해 출현하였다.
-
Kotlin은 JVM에만 국한되지 않고, JavaScript, Native binary로도 컴파일될 수 있다.
-
1. Type
val name: String = "yeongjun" // value (read-only)
var nickname: String = "jun" // variable
-
변수 선언은
var
(variable)와val
(value) 키워드를 사용한다. -
Kotlin은 Reference 타입만 제공한다.
-
Java에서 primitive 타입은 generic에 사용할 수 없다.
-
Kotlin Compiler는 더 좋은 성능을 위해 가능한 한 Java byatcode의 primitive 타입을 사용한다.
-
Kotlin은 내부적으로 primitive 타입의 성능을 제공하면서 우리가 reference 타입을 쉽게 사용하도록 해준다.
Kotlin Codevar price: Int = 5
Decompile 된 bytecodeint price = 5;
-
-
Kotlin은 static type system을 사용한다.
-
static type checking를 통해 컴파일하기 전에 타입 오류를 알려준다.
-
-
Kotlin은 type inference(타입 추론)이 있다.
val name = "jun" // (1)
-
초깃값을 지정하는 경우에 변수의 타입을 생략할 수 있다.
-
Note
|
최소 공통 타입
타입 인터페이스와 표현식 평가로 인해 때때로 어떤 타입을 반환될지 확실치 않은 코틀린 표현식이 있을 수 있다.
대부분 언어는 최소 공통 타입(가장 가까운 공통되는 타입?)을 반환하는 것으로 해결한다.
하지만 코틀린은 최소 공통 타입을 검색하지 않고, |
Constant
const val MAX_LENGTH: Int = 5000
-
val
은 read-only지만 constant는 아니다.-
val
변수가 다른 값을 반환하는 특별한 경우가 있다.
-
-
컴파일 시점 상수는 프로그램 실행 전에 생성과 초기화된다.
-
프로그램 실행 전에 컴파일러가 알 수 있어야 하므로 built-in type이어야 한다.
-
-
const
키워드를 사용해 컴파일 시점 상수를 선언할 수 있다.-
이 키워드를 통해 컴파일러에게 이 값이 절대 변경되지 않는다는 것을 알려준다.
-
-
함수 밖에 정의된 변수를 top-level(혹은 file-level) 변수라고 한다.
파일 수준 변수는 항상 선언되ㅏㄹ 때 초깃값이 지정되어야 하며, 그렇지 않으면 컴파일 에러가 발생한다.const val MAX_LENGTH: Int = 5000 // (1) fun main(args: Array<String>) { ... }
-
파일 수준 변수는 프로젝트 어디서든 사용할 수 있다(단, 제한자를 사용하면 범위를 변경할 수 있다).
-
Note
|
다양한 Constant 선언 방법
Kotlin에는 Companion objects
const vals
|
Tip
|
Kotlin Bytecode로 살펴보기
IntelliJ에서 Kotlin 코드에서 Action 검색(⌘⇧A)에 "Show Kotlin bytecode"를 입력하면 bytecode를 볼 수 있다. |
Strings
-
Kotlin에서는 var이나 val 중 어느것으로 정의되든 모든 문자열은 불변이다.
-
==
을 통해 문자열 비교가 가능하다.-
이 연산자가 문자열의 비교에 사용될 때는 문자열의 각 문자를 같은 순서로 하나씩 비교한다.
-
Java에서는 문자열 비교에
equals
메서드를 사용해야 한다.
-
-
===
연산자를 통해 참조 동등referential equality 비교가 가능하다.-
힙 메모리영역에 있는 같은 객체를 참조하는지 검사한다.
-
println("Hello " + name) // (1)
println("Hello $name") // (2)
println("Hello ${if (isUpperCase) "YEONGJUN" else "yeongjun"}") // (3)
-
문자열값에
+
를 사용하는 것을 문자열 결합(string concatenation)이라고 한다. -
$
은 string template을 나타낸다. -
중괄호로 묶으면 내부에 표현식을 사용할 수 있다.
val str = "hello world! hello yeongjun!"
val indexOfFirstSpace = str.indexOf(' ') // (1)
val result = str.substring(0 until indexOfFirstSpace) // (2)
println(result) // "hello"
-
indexOf
는 문자열에서 첫번째로 찾고자하는Char
타입의 문자를 인자로 받는다. -
substring
은IntRange
를 인자로 받는다.
val str = "1,2"
var data = str.split(',') // (1)
val first = data[0] // (2)
val second = data[1]
val (f, s) = str.split(',') // (3)
-
split
는 delimiter로 문자열을 추출한다. -
각 요소는 indexed operator 라고 불리는 대괄호 안에 인덱스를 지정해서 가져올 수 있다.
-
List가 반환되므로 해체 선언destructuring declaration을 활용할 수 있다.
var str = "abcd"
var result = str.replace(Regex("[abcd]")) { // (1)
when (it.value) {
"a" -> "1"
"b" -> "2"
"c" -> "3"
"d" -> "4"
else -> it.value
-
replace
의 두번째 인자로 익명함수를 받는다.
"abc".forEach {
println("$it\n")
}
Unicode
-
Char
타입은 유니코드 문자다. -
이스케이프 시퀀스인
\u
를 통해 유니코드는 나타낼 수 있다.val capitalA: Char = 'A' val unicodeCapitalA: Char = '\u0041'
Note
|
Escape Sequence
컴파일러에게 특별한 의미를 갖은 문자라는 것을 알려주는 데 사용된다.
|
Number
Type | Bit | Max | Min |
---|---|---|---|
|
8 |
127 |
-128 |
|
16 |
32767 |
-32767 |
|
32 |
231 - 1 |
-2147483648 |
|
64 |
263 - 1 |
263 |
|
32 |
3.4028235E8 |
1.4E-45 |
|
64 |
2 |
4.9E-324 |
-
숫자 타입은 크게 정수와 실수로 분류된다.
-
정수는 소숫점 없는 수:
Int
-
소수는 소숫점이 있는 수:
Float
,Double
-
소숫점 값을 구하려면 Kotlin이 부동 소수점 연산을 수행하도록 해야 한다.
val result = intValue / 100.0 // 100.0이 들어가므로 부동 소수점 연상 수행
-
부동 소숫점floating point은 위치가 달라질 수있는 소수점을 의이하며 실수의 근사치이다.
-
정밀도가 훨씬 더 높은 값의 처리가 필요할 때는
BigDecimal
타입을 사용할 수 있다.
-
-
String
을 숫자 타입으로 변환하는 함수들-
toFloat
-
toDouble
-
toDoubleOrNull
: 숫자로 변환할 수 없을 때 null 반환val gold: Int = "5.91".toIntOrNull() ?: 0
-
toIntOrNull
-
toLong
-
toBigDecimal
-
-
format 함수를 호출하여 형식을 지정할 수 있다.
println("amount: ${"%.2f".format(balance)}")
-
Kotlin은 비트 연산bitwise operation을 하는 함수들을 제공한다.
BigDecimal
-
Kotlin에서는 연산자 오버로딩이 가능하므로
BigDecimal
사용이 편리해졌다.val a = BigDecimal(10) val b = BigDecimal(3) val c = 3.0.toBigDecimal() TODO
Nothing
-
실행될 수 없는 표현식을 나타낸다. 기본적으로 예외를 던진다.
Annotation
annotation class ClientIp
@Target(AnnotationTarget.CLASS) // (1)
@Retention(AnnotationRetention.RUNTIME) // (2)
annotation class ClientIp
-
어노케이션을 선언할 수 있는 대상
-
어노테이션의 라이프라이클. 언제까지 살아 남을지
-
SOURCE: 소스(.java) 코드까지 남음
-
CLASS: 클래스(.class) 파일까지 남음 = 바이트 코드
-
RUNTIME: 런타임까지 남음
-
2. Conditionals
if/else
fun main(args: Array<String>) {
val name = "yeongjun"
val point = 10000
if (point == 0) { // (1)
println("The point is empty")
} else if (point < 0) {
println("error")
} else {
println("point: " + point)
}
}
-
==
는 Kotlin의 비교 연산자(comparison operator) 중 하나다.
Note
|
Kotlin의 새로운 비교 연산자
|
Conditional expression
-
조건 표현식(conditional expression)은 조건문과 비슷하지만, if/else를 값으로 지정한다.
val message = if (point == 0) { "The point is empty" } else if (point < 0) { "error" } else { "point: " + point } println(message)
-
표현식이 하나만 있을 경우에는 중괄호를 생략할 수 있다.
val code = if (isEmpty) "EMPTY" else "NOT_EMPTY"
TipTernary Conditional OperatorKotlin에서 삼항 연산자는 아래와 같이 표현할 수 있다.
if (a) b else c
References -
..
키워드를 사용하면 범위(range)를 나타낼 수 있다.val healthStatus = if (healthPoints == 100) { "BEST" } else if (healthPoints in 90..99) { // (1) "GOOD" } else if (healthPoints in 75..89) { "NOT_BAD" } else { "BAD" }
-
어떠한 값이 범위에 포함되는지 검사할 때는
in
키워드를 사용한다.
-
-
Kotlin은 범위에 관련된 다양한 함수를 지원한다.
1 in 1..3 // res0: kotlin.Boolean = true (1..3).toList() // res1: kotlin.collections.List<kotlin.Int> = [1, 2, 3] 1 in 3 downTo 1 // res2: kotlin.Boolean = true (3 downTo 1).toList(); // res3: kotlin.collections.List<kotlin.Int> = [3, 2, 1] 1 in 1 until 3 // res4: kotlin.Boolean = true 3 in 1 until 3 // res5: kotlin.Boolean = false 2 in 1..3 // res6: kotlin.Boolean = true 2 !in 1..3 // res7: kotlin.Boolean = false 'x' in 'a'..'z' // res8: kotlin.Boolean = true
when
val healthStatus = when (healthPoints) {
100 -> "BEST"
in 90..99 -> "GOOD"
75..89 -> "NOT_BAD"
else -> "BAD"
}
-
Java에서의 Swtich
3. Function
private fun getHealthStatus(healthPoint: Int): String {
val healthStatus = if (healthPoints == 100) { // (1)
"BEST"
} else if (healthPoints in 90..99) {
"GOOD"
} else if (healthPoints in 75..89) {
"NOT_BAD"
} else {
"BAD"
}
return healthStatus;
}
-
healthStatus 변수를 local variable이라고 한다.
-
Kotlin에서는 기본적으로 함수의 가시성 제한자(visibility modifier)가 public이다.
-
parameter는 함수 몸체(body)에서 변경할 수 없으므로
val
이다. -
지역 변수(local variable)은 함수의 scope에만 존재한다.
-
지역 변수는 정의된 함수 범위안에서 사용되기 전에 초기화하면 된다.
-
-
함수의 헤더(header) 부분에 default argument를 사용할 수 있다.
fun main(args: Array<String>) { getPoint(50) getPoint() // function overloading } private fun getPoint(defaultPoint: Int = 100) { return defaultPoint; }
-
Kotlin은 함수 오버로딩(function overloading)을 지원한다.
-
Kotlin은 하나의 표현식만 갖는 함수는 대입 연산자(
=
)를 통해 단일 표현식single-expression 함수로 표현할 수 있다.private fun getPoint(defaultPoint: Int = 100): Int = defaultPoint
-
Kotlin에서 반환값이 없는 함수는 Unit 함수라고 한다(반환 타입이 Unit이라는 뜻이다).
private fun printPoint(defaultPoint: Int = 100): Int = println("point: $defaultPoint")
-
Kotlin에서는 함수에서 return 키워드를 사용하지 않으면 그 함수의 반환 타입은 Unit이다.
-
Unit은 아무것도 반환하지 않는 함수의 반환타입을 나타낸다.
-
제네릭 함수는 반드시 반환타입을 나타내야 하는데, Kotlin은 이 문제를 Unit 타입을 통해 해결하였다.
-
-
Kotlin은 지명 함수 인자(named function argument)를 지원한다.
printlnPlayerStatus( healthStatus = status, color = "GREEN". name = "yeongjun", isAdult = true)
-
Kotlin은 함수 타입도 반환 타입에 사용될 수 있다. 즉, 함수를 반환하는 함수를 정의할 수 있다.
-
다른 함수를 인자로 받거나 반환하는 함수를 고차 함수(higher-order function)라고도 한다.
fun main(args: Array<String>) { runSimulation() // output: // >> year에 1 추가됨 // Hello yeongjun! (year: 2020) // >> year에 2 추가됨 // Hello 0jun! (year: 2021) } fun runSimulation() { val getMessage = configureGettingMessage() println(getMessage("yeongjun")) println(getMessage("0jun")) } fun configureGettingMessage(): (String) -> String { val hello = "Hello" // (1) var addYear = 0 // (2) return { name: String -> val currentYear = 2019 addYear += 1 // (3) println(">> year에 $addYear 추가됨") "$hello $name! (year: ${currentYear + addYear})" } }
-
외부 함수에
val
로 선언된 변수를 그것을 사용하는 람다식 코드에서 그 값이 바로 저장된다. -
외부 함수에
var
로 선언된 변수는 그 값이 별도의 객체로 저장되며, 그 객체의 참조값이 람다식 코드에 저장되어 값을 변경할 때 사용된다. -
Kotlin에서 익명 함수가 자신의 범위 밖에 정의된 변수를 변경하고 참조할 수 있다.
-
-
-
Kotlin은
vararg
키워드로 가변인자variable arguments를 지원한다.fun toArray(vararg ids: String) = toArray2(ids) fun toArray2(ids: Array<out String>) = ids // (1)
-
out
키워드는 오른쪽 타입을 포함해서 서브 타입도 타입 인자가 될 수 있다는 것을 뜻한다.
(제네릭 타입의 슈퍼-서브 타입 관계를 나타낸다).fun multipleVarargs(vararg names: String, vararg sizes: Int) { // (1) // 컴파일 에러 }
-
다중 vararg 파라미터는 가질 수 없으며, 다른 타입이라도 불가능하다.
-
Note
|
Unit 타입 vs Nothing 타입
Nothing 타입도 Unit 타입처럼 값을 반환하지 않는 함수를 나타나는대 사용한다. 하지만 함수의 실행이 끝나더라도 호출 코드로 제어가 복귀되지 않는다. /** * Always throws [NotImplementedError] stating that operation is not implemented. */ @Kotlin.internal.InlineOnly public inline fun TODO(): Nothing = throw NotImplementedError()
개발자는
코드를 개발할 때 Nothing 타입을 사용하면 또 다른 장점이 있다. 제어가 복귀되지 않기 때문에 이 함수의 다음 코드는 절대 실핼될 수 없다는 것을 컴파일러는 알고 있다. 그러므로 컴파일러는 절대 실행될 수 없는(unreachable) 코드임을 나타내는 경고를 알려준다. |
backtick
fun main(args: Array<String>) {
`**~prolly not a good idea!~**`()
}
fun `**~prolly not a good idea!~**`() {
...
}
-
Kotlin에는 함수명이 백틱(backtick) 기호(
`
)로 감싸인 함수를 정의할 수 있다. -
Java와 Kotlin 의 예약어(reserved keyword)는 다르므로, Java와의 상호운용 시에 생길 수 있는 함수 이름 충돌을 피하기 위함이다.
-
코드를 테스트하는 파일에서 사용되는 함수 이름을 더 알기 쉽게 나타내기 위함이다.
-
JUnit5에서는
@DisplayName
어노테이션 지원하는데 Kotlin에서는 백틱을 이용하면 된다.
-
Anonymous function
-
중괄호를 통해 익명함수를 사용할 수 있다.
val numLetters = "Mississippi".count({ letter -> letter == 's' })
-
익명함수의 닫는 중괄호 다음에 빈 괄호(
()
)를 사용하여 함수를 호출할 수 있다.println({ val year = 2020 "Hello $year" }())
-
익명함수도 타입을 가지며 이를 함수 타입이라고 한다.
-
익명함수는 변수명 다음에 콜론(
:
)과 함수 타입 정의를 통해 선언할 수 있다. -
함수 타입은 콜론 다음에는 매개변수와 화살표 뒤에 반환 타입을 지정할 수 있다.
val greetingFunction: () -> String = { // (1) val year = 2020 "Hello $year" // (2) } println(greetingFunction())
-
익명 함수 및 함수 타입 정의
-
return 키워드가 없지만 익명함수는 암시적으로 또는 자동으로 함수 정의의 마지막 코드를 결과로 반환한다.
-
-
-
익명함수도 함수처럼 인자를 받을 수 있으며, 함수명은 함수 내부에 지정한다.
val greetingFunction: (Int) -> String = { year -> "Hello $year" }
-
하나의 인자만 받는 익명 함수에는 매개변수 이름을 지정하는 대신 편리하게 it 키워드를 사용할 수 있다.
val greetingFunction: (Int) -> String = { "Hello $it" }
// as-is val numLetters = "Mississippi".count({ letter -> letter == 's' }) // to-be val numLetters = "Mississippi".count({ it == 's' })
-
익명함수에도 타입 추론(type inference)이 적용된다.
val greetingFunction = { val year = 2020 "Hello $year" } val greetingFunction = { year: Int -> // (1) "Hello $year" }
-
타입 추론을 통해 함수 타입인
: (String) → String
을 생략할 수 있다.
-
Lambda
Important
|
Lambda 관련 용어
|
Note
|
Lambda?
|
-
함수에서 마지막 매개변수로 함수 타입을 받을 때는 람다 인자를 둘러싼 괄호를 생략할 수 있다.
// as-is "Mississippi".count({ it == 's' }) // to-be "Mississippi".count { it == 's' }
as-isfun runSimulation(name: String, getMessage: (String, Int) -> String) { val year = (2019..2020).shuffled().last() println(getMessage(name, year)) } fun main(args: Array<String>) { val func = { name: String, year: Int -> println("Hello $year, $name") } runSimulation("yeongjun", func) }
to-befun runSimulation(name: String, getMessage: (String, Int) -> String) { val year = (2019..2020).shuffled().last() println(getMessage(name, year)) } fun main(args: Array<String>) { runSimulation("yeongjun") { name: String, year: Int -> println("Hello $year, $name") } }
-
이러한 단축 문법으로 코드를 더 깔끔하게 작성할 수 있고, 함수 호출의 핵심부분을 더 빨리 파악할 수 있다.
-
(내 생각)
runSimulation
이 일인수(single-argument) 함수가 된 것 같다. 커링된 결과라고 볼 수 있을까?
-
-
이 기능은 코틀린으로 도메인 특화 언어DSL, Domain Specific Language를 생성할 수 있는 가능성을 열어준다.
Inline functions
고차 함수는 매우 유용하고 멋지지만 주의할 점이 있다. 성능 패널티다. 코틀린의 컴파일러는 컴파일 시간에 바로가기 구문을 완벽한 함수 오브젝트로 변환할 수 있다. 다시 말해, 컴파일 시간에 람다가 할당된 오브젝트로 변환되고, 개발자는 그것의 invoke 연산자를 호출한다는 것이다. 이러한 작업은 얼마나 작은지는 관계 없이 CPU 파워와 메모리를 소모한다.
// 실제 코드
val capitalize = { str: String -> str.capitalize() }
// 컴파일된 코드
val capitalize = object : Function1<String, String> {
override fun invoke(p1: String): String{
return p1.capitalize()
}
}
Kotlin에서 람다를 사용할 때 람다 함수를 최적화하는 방법으로 인라인 함수(inline fun
)가 있다.
람다를 정의하면 JVM에서 객체로 생성된다. 또한, JVM은 람다를 사용하는 모든 변수의 메모리 할당을 수행하므로 메모리가 많이 사용된다. 결국 람다는 성능에 영향을 줄 수 있는 메모리 부담을 초래할 수 있다.
Kotlin은 다른 함수의 인자로 람다를 사용할 때 부담을 없앨 수 있는 인라인(inline)이라는 최적화 방법을 제공한다. 인라인을 사용하면 람다의 객체 사용과 변수의 메모리 할당을 JVM이 하지 않아도 된다.
inline fun runSimulation(name: String, getMessage: (String, Int) -> String) {
val year = (2019..2020).shuffled().last()
println(getMessage(name, year)
}
위처럼 inline
키워드를 추가하면 runSimulation
함수가 호출될 때 람다가 객체로 전달되지 않는다.
왜냐하면 Kotlin Compiler가 bytecode를 생성할 때 람다 코드가 포함된 runSimulation
함수 몸체 전체 코드를
복사한 후 이 함수를 호출하는 코드에 붙여넣기 하기 때문이다.
inline
키워드가 없을 경우 디컴파일된 코드...
public static final void main(@NotNull String[] args) {
LocalTestKt.runSimulation("yeongjun", (Function2)null.INSTANCE);
}
...
inline
키워드가 있을 경우 디컴파일된 코드public static final void main(@NotNull String[] args) {
String name$iv = "yeongjun";
int $i$f$runSimulation = false;
short var3 = 2019;
int year$iv = ((Number)CollectionsKt.last(CollectionsKt.shuffled((Iterable)(new IntRange(var3, 2020))))).intValue();
int var7 = false;
String var8 = "Hello " + year$iv + ", Hello " + name$iv;
boolean var6 = false;
System.out.println(var8);
}
함수 자체가 호출 코드안에 들어간다. 이러한 점이 컴파일된 바이트 코드의 양은 많아질 수 있겠지만, 함수 호출을 하거나 추가 객체 생성하는 런타임 오버헤드를 줄일 수 있게 된다. (호출되는 곳에 코드 블럭이 인라인 된다고 해서 inline fun 인건가..?)
그러나 이렇게 할 수 없는 경우가 더러 있다. 예를 들어 람다를 인자로 받는 재귀 함수(recursive function)의 경우다. 재귀 함수는 자신의 몸체 코드를 여러 번 반복 호출하여 실행하므로 이것을 인라인 처리하면 같은 코드가 무수히 많이 복사 및 붙여넣기 된다. 따라서 Kotlin Compiler는 재귀 함수를 단순히 인라인 처리하지 않고 효율성이 좋은 루트 형태로 변경한다.
또한, inline 함수는 내부적으로 코드를 복사하기 때문에 인자로 받은 함수를 다른 함수로 전달되거나 참조될 수 없다.
해결 방법으로 함수로 받은 특정 인자만 인라인하지 않고자 할 경우 noinline
키워드를 사용한다.
- inline fun newMethod(a: Int, func: () -> Unit, func2: () -> Unit) {
+ inline fun newMethod(a: Int, func: () -> Unit, noinline func2: () -> Unit) {
func()
someMethod(10, func2)
}
fun someMethod(a: Int, func: () -> Unit):Int {
func()
return 2*a
}
fun main(args: Array<String>) {
newMethod(2, {println("Just some dummy function")},
{println("can't pass function in inline functions")})
}
Note
|
TODO: scala랑 동일한 기능인지 찾아볼 것 |
Note
|
recursive function
코틀린에서 재귀 함수는 스택을 유지하지만 |
noinline
crossinline
Function reference
-
함수 참조(function reference)는 이름이 있는 함수가 인자로 전달될 수 있게 한다.
-
람다 표현식을 사용할 수 있는 곳이라면 어디든 함수 참조를 사용할 수 있다.
fun main(args: Array<String>) {
runSimulation("yeongjun", ::printYear) { name, year -> // (1)
"Hello $year, Hello $name"
}
}
fun printYear(year: Int) {
println("Hello $year")
}
fun runSimulation(
name: String,
yearPrinter: (Int) -> Unit,
getMessage: (String, Int) -> String
) {
val year = (2019..2020).shuffled().last()
yearPrinter(year)
println(getMessage(name, year))
}
-
함수 참조를 얻을 때는 참조하고자 하는 함수 이름 앞에
::
연산자를 사용한다.-
Java에는 method reference가 있다.
-
Closure
-
Kotlin의 Lambda는 클로저(closure)다.
-
클로저는 'close over’가 합쳐진 용어이다.
-
다른 함수에 포함된 함수에서 자신을 포함하는 함수의 매개변수와 변수를 사용할 수 있는 것을 말한다.
-
Scope Functions
-
Kotlin 라이브러리에 있는 표준 함수는 보편적으로 사용할 수 있는 유틸리티 함수이며, 람다를 인자로 받아 동작한다.
-
(
Standard.kt
에 있어서 표준 함수라고 말한 것 같다) -
표준 함수는 내부적으로 확장 함수extension function이며, 확장 함수를 실행하는 주체를 수신자 또는 수신자 객체라고 한다.
-
-
원칙적으로 중첩은 하지 않는 것이 좋다.
-
스코프 함수가 중첩되면 코드의 가독성이 떨어지고 파악하기 어려워 진다.
-
수신자 객체의 람다에 수신 객체가 암시적으로 전달되는
apply
,run
,with
는 중첩하지 말아라.-
위 함수들은 수신 객체를
this
또는 생략하여 사용하며, 수신 객체 이름을 다르게 지정할 수 없으므로 혼동하기 쉬워진다.
-
-
also
,let
을 중첩해야한다면 암시적 수신 객체를 가르키는it
을 사용하지 말고, 명시적인 이름을 사용하라.
-
-
중첩이 아닌 호출 체인에 결합하면 코드의 가독성이 향상된다.
func | param | lambda params* | return |
---|---|---|---|
|
lambda |
context object |
lambda result |
|
lambda |
- |
context object |
|
lambda |
- |
lambda result |
|
context object, lambda |
- |
lambda result |
|
lambda |
context object |
context object |
|
lambda |
context object |
|
|
lambda |
context object |
|
-
param이 lambda일 때 lambda에 전달되는 파라미터를 의미한다.
Note
|
사용하면 괜찮은 예
|
it vs this
-
스코프가 다름
-
it
을 파라미터로 받을 경우 파라미터를 통한 외부 참조 -
this
는 자기자신 내부 참조
let
// as-is
val firstElement = listOf(1,2,3).first()
val firstItemSquared = firstElement * firstElement
// to-be
val firstItemSquared = listOf(1,2,3).first().let { it * it }
-
이 함수는 인자로 전달된 람다를 실행한 후 결과를 반환해 준다.
-
연산하기 위한 값을 따로 변수로 지정할 일이 없어진다.
-
연산에 필요한 부분(scope)에서만 사용하고 버린다.
-
-
null 복합 연산자와 같이 사용하면 NPE 예외 처리 및 기본값을 지정할 수 있다.
fun appendPrefix(str: String?): String { val prefix = "[0jun]" return str?.let { "$prefix $str" } ?: "$prefix empty string" }
apply
// as-is
val file = File("example.txt")
file.setReadable(true)
file.setWritable(true)
file.setExecutable(false)
// to-do
val file = File("example.txt").apply {
setReadable(true)
setWritable(true)
setExecutable(false)
}
-
이 함수는 구성 함수라고 생각할 수 있다.
-
일반적으로 초기화와 인스턴스에 사용된다.
-
람다 내부의 모든 함수 호출이 수신자에 관련되어 호출되므로 때로는 이것을 연관 범위relative scoping 또는 수신자에 대한 암시적 호출implicitly called이라고도 한다.
-
수신자 객체의 람다 내부에서 객체의 함수를 사용하지 않고 자신(수신 객체)을 반환하려는 경우에 사용
-
객체 초기화는 수신 객체의 프로퍼티만을 사용하는 대표적인 경우
-
Note
|
Java에서 apply는?
Java에서 Kotlin의 코드와 비슷한 문법으로 초기화하려면 double brace initialization 을 활용할 수 있다. double brace initialization
코드만 보기엔 비슷해보이지만 동작은 전혀 다르다. Kotlin의 |
Note
|
apply vs. let
|
run
fun isZero(num: String) = name.toInt == 0
"0".run(::isZero)
-
run은 수신자 객체를 반환하지 않는다.
-
run은 람다의 결과(true/false)를 반환한다.
-
함수 호출이 여러 개 있을 때는 run을 사용하면 편리하다.
fun nameIsLong(name: String) = name.length >= 20 fun playerCreateMessage(nameTooLong: Boolean): String { return if (nameTooLong) { "name is too long" } else { "name is not long" } } // as-is println( playerCreateMessage( nameIsLong("Polarcubis, supreme master of ......") // (1) ) ) // to-be "Polarcubis, supreme master of ......" .run(::nameIsLong) .run(::playerCreateMessage) .run(::println)
-
중첩된 함수는 이해하기가 어렵다. 실행순서도 아래에서 위로, 안쪽에서 바깥쪽으로 실행된다.
-
with
-
수신자 객체가 non-nullable 하고, 결과가 필요하지 않는 경우에 사용
val payment = getPayment() with(payment) { println(amount) println(currency) }
-
with은 run과 동일하게 동작하지만 호출 방식이 다르다.
-
수신자 객체를 첫 번쨰 매개변수의 인자로 받는다.
val numTooLong = with("abcdefh") { length >= 0 }
-
이런 방식은 나머지 다른 표준 함수들과 일관성이 없으므로 with 대신 run을 사용할 것을 권한다.
also
-
수신자의 람다에서 수신자 객체를 전혀 사용하지 않거나 수신자 객체의 속성을 변경하지 않고 사용하는 경우에 사용
-
let과 비슷하지만, also는 람다 결과를 반환하지 않고 수신자 객체를 반환한다.
-
서로 다른 처리를 also를 사용해서 연쇄 호출할 수 있다.
File("file.txt") .also { print(it.name) } .also { fileContents = it.readLines() }
-
객체의 사이드이펙트를 확인할 때 혹은 수신 객체의 프로퍼티에 데이터를 할당하기 전 해당 데이터의 유효성을 검사할 때 유효
class Payment(amount: Long) { val amount = amount.also { require(it >= 0) print(it) } }
Note
|
부수 효과
컴퓨터 프로그램에서 자신의 범위 외부에 있는 오브젝트/데이터를 함수가 수정할 때 이것을 부수 효과side effect라고 한다. |
takeIf
-
lambda의 반환값이 true면 수신자 객체가, false면 null이 반환된다.
// as-is val file = File("file.txt") val content = if ( file.canRead() && file.canWrite() ) { file.readText() } else { null } // to-be val content = File("file.txt") .takeIf { it.canRead() && it.canWrite() } ?.readText()
takeUnless
-
takeIf와 비슷하지만 lambda의 결과가 false일때 수신자 객체를 반환한다.
-
복작한 조건을 검사할 때는 takeUnless를 제한적으로 사용할 것을 권한다. 코드를 이해하는 데 더 많은 시간이 걸리기 때문이다.
Invoke
-
invoke
함수는 연산자다.invoke
연산자는 이름 없이 호출될 수 있다.enum class UserType { STUDENT, WORKER } internal class Tests { @Test fun test() { val user = User("jun") user(UserType.STUDENT) Users("학생입니다.") Users(UserType.WORKER) User(1) } } class User(val name: String) { operator fun invoke(type: UserType) = when (type) { UserType.STUDENT -> "학생" UserType.WORKER -> "회사원" } companion object { operator fun invoke(value: Int) = User(value.toString()) } } object Users { operator fun invoke(name: String) = User(name) operator fun invoke(type: UserType) = User( when (type) { UserType.STUDENT -> "학생" UserType.WORKER -> "회사원" } ) }
Warning이러한 방식으로 invoke를 사용하는 것이 맞는지 확인이 필요하다. 문제가 없다면 invoke를 활용하여 생성자를 추가하지 않고도 생성자와 같이 사용할 수 있다.
-
이것이 괄호로 람다 함수를 직접 호출할 수 있는 이유다.
Type-safe builders
-
타입 안전 빌더는 데이터를 선언적 방식으로 정의할 수 있게 해준다.
-
https://kotlinlang.org/docs/type-safe-builders.html#how-it-works
-
Scope control: @DslMarker
4. Null-Safety
public fun readLine(): String?
-
Kotlin은 null 값을 가질 수 있다고 지정하지 않으면 null 값을 가질 수 없다.
-
따라서 null 값으로 생기는 문제를 런타임이 아닌 컴파일 시점에 방지할 수 있다.
-
-
?
키워드를 통해 변수가 nullable 하다는 것을 나타낼 수 있다.val nullable: String? = item // allowed, always works val notNull: String = item // allowed, may fail at runtime
safe call operator
var name = readLine()?.capitalize()
-
컴파일/런타임 에러 없이 항상 실행되도록 해야 할 경우 사용된다.
-
컴파일러가 안전 호출 연산자(
?.
)를 발견하면 null 값을 검사하는 코드를 자동으로 추가해준다. -
null이 아니면
capitalize
함수를 호출하고, null이면 다른 코드를 수행한다.-
이 경우
println(name)
의 결과는 null이 출력된다.
-
-
안전 호출 연산자를 연속적으로 사용할 수 있다.
name?.capitalize()?.plus(", hello")
-
null일 때 샐행되는 코드가 복잡해진다면 if /
!=
연산자를 사용해서 null 검사를 하자.var name = readLine() if (name != null) { name = name.capitalize() } else { // create user with default name // get readLine // set name // ... }
Tip
|
safe call operator with let function
|
non-null assertion operator
var name = readLine()!!.capitalize()
-
non-null 단언 연산자(assertion operator)인
!!
는 null이 될 수 없다는 것을 단언하는 연산자다.-
이 연산자는 double-bang 연산자라고도 한다.
-
-
왼쪽의 피연산자 값이 null이 아니면 정상적으로 코드를 수행하고, null이면 런타임시에 NPE 예외를 발생시킨다.
-
한 함수에서 단언 연산자를 통해 non-null이라는 것을 확인한다면, 이 값을 받아 사용하는 함수들에서는 null checking을 할 필요가 없을 것이다.
-
null 가능 변수에
!!
연산자를 사용하는 것은 위험하다.
null coalescing operator
// var name = if (name == null) "Yeongjun" else name
var name = name ?: "Yeongjun"
var name = readLine()
name?.let {
name = it.capitalize()
} ?: println("name is null")
-
null 복합 연산자(null coalescing operator)인
?:
는 왼쪽 피연산자의 값이 null이면 오른쪽 피연산자를 실행하고, null이 아니면 왼쪽 피연산자의 결과를 반환한다.
Note
|
Elvis operator
null 복합 연산자인 |
Note
|
Platform Types
Java의 타입들을 Kotlin에서는 따로 취급되며 platform types라고 부른다. |
5. Exception
-
Kotlin에서는 모든 예외가 unchecked 예외다.
-
대부분의 checked 에러는 발생하더라도 우리가 특별히 할 것이 없다.
catch (e: IOException) { }
와 같이 작성하는 경우가 많다. -
checked 예외는 문제를 해결하기보다는 오히려 더 많은 문제를 야기하므로, Kotlin을 포함한 현대 언어에서는 unchecked 예외를 지원한다.
(코드 중복, 이해하기 어려운 에러 복구 로직, 예외를 기록없이 무시)
-
-
처리되지 않은 예외를 미처리 예외(unhandled exception)이라고 한다.
-
프로그램 실행이 중단되는 것을 크래시(crash)라고 한다.
-
throw
키워드를 사용하며, 예외를 발생시키는 것을 예외를 던진다(throw)고 한다. -
IllegalStateException
예외는 프로그램이 정상적이 아닌 상태가 되었다는 것을 뜻한다.
fun test(num: Int?) {
try {
num ?: throw CustomException() // (1)
} catch (e: Exception) {
println(e)
}
}
class CustomException() : IllegalStateException("custom exception") // (2)
-
num
이 null일 경우 커스텀 예외를 던진다. -
커스텀 예외를 만들 수 있다.
Precondition function
-
코틀린은 편의를 위해 전제 조건 함수precondition function를 제공한다.
-
코드가 실행되기 전에 충족되어야 하는 전제 조건을 정의하는 함수이다.
Function | Description |
---|---|
|
첫번째 인자가 null이면 |
|
첫번째 인자가 null이면 |
|
첫번째 인자가 false면 |
|
첫번째 인자가 null이면 제공된 메시지와 함께 |
|
인자가 false면 |
Improved exception handling
-
Kotlin 1.3부터 더 나은 예외 핸들링을 위한 API가 추가되었다.
-
Result
:-Catching
API가 반환하는 타입이다.
-
Result
는 함수의 반환 값으로 사용할 수 없으며, 함수 안에서 처리하고 결과를 반환해야 한다.
try-with-resource
File("/home/aasmund/test.txt").inputStream().use {
val bytes = it.readBytes()
println(bytes.size)
}
-
JDK7에는
Closable
을 구현한 객체에 대해서close()
처리해주는 try-with-resource 기능이 있다. -
Kotlin에서는
use()
함수를 활용할 수 있다.
6. Collection
@startuml hide empty field hide empty method interface Iterable interface MutableIterable interface Collection interface MutableCollection interface List interface MutableList interface Set interface MutableSet interface Map interface MutableMap Iterable <-down- MutableIterable MutableIterable <-down- MutableCollection Iterable <-down- Collection Collection <-- List Collection <-- Set Collection <-- Map Collection <-left- MutableCollection List <-- MutableList Set <-- MutableSet Map <-- MutableMap MutableCollection <-- MutableList MutableCollection <-- MutableSet MutableCollection <-- MutableMap @enduml
-
Kotlin의 collection은 Eager evaluation으로 동작한다.
-
Java의 Stream처럼 Lay evaluation으로 동작하고자 한다면 Kotlin의 Sequence를 사용하면 된다.
-
-
Kotlin의 collection에는 mutable 타입과 read-only 타입이 있다.
-
Kotlin 컬렉션은 기본적으로 read-only이다.
-
Kotlin의 표준 라이브러리의 모든 클래스나 함수 등은
import
를 지정하지 않아도 바로 사용할 수 있다.-
다른 패키지에 같은 이름의 클래스나 함수 등을 사용할 때는
as
키워드로 alias를 지정해 충돌을 해결할 수 있다.import com.util.Value import com.utils2.Value as Value2
-
Note
|
Immutable vs ReadOnly
Kotlin에서 Immutable 보단 read-only 라는 용어를 사용했다. 실제로는 변경 가능하기 때문이다. (Kotlin SDK의 javadoc에도 read-only라고 나타나있다) read-only List는 특정 요소를 추가/삭제/변경하는 함수들을 가지고 있진 않지만, 아래 코드의 element는 Mutable List이므로 요소의 값이 변경될 수 있다.
또 다른 예를 보자.
Kotlin은 List의 불변셩을 강요하지 않는다. 따라서 요소를 변경할 수 없게 하는 것은 우리에게 달려 있다는 것을 기억하자. |
List
-
listOf
를 통해 read-only list를 생성할 수 있다.val list: List<String> = listOf("Yeongjun", "0jun", "wicksome") (1) println(list.first()) // get first element println(list[2]) println(list.last()) // get last element
-
List
는 generic type이다.<String>
은 매개변수화 타입parameterized type이며, element의 타입을 지정한다.
-
-
안전한 인덱스 사용을 위한 메서드를 제공한다. (
ArrayIndexOutOfBoundsException
를 피하기 위함)list[3] // throw list.getOrElse(3) { "jun" } // (1) list.getOrNull(3) ?: "jun" // (2)
-
안전한 인덱스 사용을 위해
getOrElse
메서드를 사용할 수 있다. 두 번째 인자인 람다에 반환값을 나타낼 수 있다. -
getOrNull
은 예외를 던지는 대신 null을 반환한다.
-
-
mutableListOf
를 통해 mutable한 list로 생성할 수 있다.val list = listOf(1, 2, 3).toMutableList() val mutableList = mutableListOf(1, 2, 3)
-
변경자 함수mutator function을 통해 element를 변경할 수 있다.
val list = mutableListOf(1, 2, 3) list[0] = 0 list.add(4) list.addAll(listOf(5, 6)) list += listOf(7, 8) list -= listOf(1, 2, 3) println(list) // [0, 4, 5, 6, 7, 8] list.removeIf { it % 2 == 0 } println(list) // [5, 7] list.clear()
-
-
List는 destructure이 가능하다.
val (first, middle, last) = listOf(1, 2, 3) val (first, second, _) = listOf(1, 2, 3) // (1)
-
해체를 원하지 않을 경우
_
를 사용할 수 있다. 즉, first, second 변수명만 사용 가능하다.
-
-
다양한 함수를 제공한다.
val list = listOf(1, 2, 3, 4, 1) list.contains(1) list.containsAll(listOf(1, 2) var numberSet = list.toSet() var list2 = list.distinct() // (1)
-
distinct
메서드를 통해 중복제거가 가능하다. 내부적으로 toSet, toList를 호출한다.
-
-
Kotlin은
Array
라는 참조 타입으로 배열을 지원한다.-
IntArray
타입은 Java의 기본 배열 타입으로 컴파일 된다.val args: IntArray = intArrayOf(1, 2, 3)
-
가급적이면 List와 같은 컬렉션을 사용하자. Kotlin 컬렉션은 mutable, read-only 개념을 제공하면서 다양한 함수를 지원하고, 대부분의 경우에 컬렉션이 더 좋은 선택이다.
-
Iteration
for (i in 1..10) { println(i) } // (1)
listOf(1, 2, 3).forEach { println(it) }
listOf(1, 2, 3).forEachIndexed { index, i -> println("$index $i") } (2)
-
Kotlin은 성능 향상을 위해 컴파일러가 for f루프틑 최적한 후 Java 버전의 for 루프를 사용하도록 바이트코드를 생성한다.
-
forEach
와forEachIndexed
함수는 다른 Iterable 타입에서도 사용할 수 있다.
var count = 0
while (count <= 9) { // (1)
if (isClose()) {
break // (2)
}
addProduct()
count++
}
-
while 루프는 무한루프가 가능하므로 사용할 때 조심해야 한다.
-
break
키워드를 통해 while 루프를 빠져나올 수 있다.
Note
|
chunked() vs windowed()
TODO: Webflux의 |
Set
-
Set의 element는 고유하며(중복 없음), 순서를 갖지 않는다.
-
setOf
를 통해 생성할 수 있다. -
elementAt(Int)
를 사용하면 인덱스 기반으로 사용이 가능하나, List 사용시보다 처리 속도가 느리다.-
사용하고자 하는 자료구조에 맞는 컬렉션을 사용하자.
-
Map
-
Map은 Key와 Value의 쌍(
Pair
)으로 데이터(이것을 entry라고 한다)를 저장한다.-
Map의 Key는 고유하다.
-
-
mapOf
,mutableMapOf
함수를 통해 생성할 수 있다.mapOf( "Eli" to 10.5, // (1) "Mordoc" to 8.0 )
-
to
는 키워드 처럼 보이지만 내부적으로는 컴파일러가"Eli".to(10.5)
와 같은 코드로 변환한다.
to
는Pair
를 반환하는 함수이며,Tuples.kt
에 있다. 이 파일에는Pair
,Triple
클래스를 포함한다.
(to
연산자를 overloading 한것이다.)
-
-
다양한 함수를 제공한다.
val map = mapOf( "ab" to 10, "cd" to 20 ) map["ab"] // 10 map["xx"] // null map.getValue("xx") // throw NoSuchElementException map.getOfElse("xx") { 30 } // 30 map.getOrDefault("xx", 30) // 30 map.getOrPut("xx") { 30 } // 30
7. Class
class Player
class Player()
class Player { }
class Player() { }
val player = Player() // create a instance of Player using primary constructor
-
하나의 파일에 하나 이상의 클래스를 정의할 수 있다.
-
어플리케이션에 규모가 커지는 데 따른 기능 추가나 유지보수 용이성을 고려하려 가급적 하나의 클래스를 하나의 파일에 정의하는 것이 좋다.
-
-
클래스에는 행동behavior과 데이터data를 정의한다.
Player.ktclass Player { val name = "madrigal" // property fun castFireball(numFireballs: Int = 2) { // class function priuntln("한 덩어리의 파이어볼이 나타난다. (x$numFireballs)") } }
-
behavior → class function
-
클래스 내부에 정의된 함수를 클래스 함수class function이라고 한다.
-
-
data → property
-
클래스의 데이터는 속성property라고 한다.
-
-
Visibility
-
가시성 제한자를 통해 정보은닉information hiding, 캡슐화encapsulation가 가능하다.
-
Kotlin은 클래스 함수나 속성에 가시성 제한자visibility modifier를 지정하지 않으면 기본적으로
public
이다.-
public
: 외부에서 사용 가능 (default) -
private
: 함수나 속성이 정의된 클래스 내부에서만 사용 가능 -
protected
: 함수나 속성의 정의된 클래스 내부 또는 서브 클래스에서만 사용 가능 -
internal
: 함수나 속성이 정의된 클래스가 포함된 module에서 사용 가능
-
-
속성의 가시성이
public
이라면 getter/setter도public
이다.
class Player {
var name = "madrigal"
get() = field.capitalize()
private set(value) { // (1)
field = value.trim()
}
}
-
Getter/Setter의 가시성은 기본적으로 동일하지만, 이처럼 Setter의 가시성만 변경할 수도 있다.
-
Java의 기존 가시성인 package-private은 Kotlin에는 없다.
-
Kotlin은 패키지를 namespace를 관리하기 위한 용도로만 사용한다.
-
-
Kotlin은 package-private의 대안으로
internal
이라는 새로운 가시성 변경자를 도입했다.-
모듈 내부에서만 볼 수 있음을 뜻한다.
-
-
모듈module은 한번에 한꺼번에 컴파일되는 코틀린 파을들을 의미한다.
-
인텔리J, 이클립스, maven, gradle 등의 프로젝트가 모듈이 될 수 있다.
-
-
Kotlin은 최상위 선언에 대해 private 가시성을 허용한다.
-
최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함된다.
-
비공개 가시성인 최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용할 수 있다.
-
하위 시스템의 자세한 구현 사항을 외부에 감추고 싶을 때 유용한 방법이다.
-
-
Kotlin과 Java의
protected
는 다르다는 사실에 유의하라.-
Kotlin의
protected
맴버는 오직 그 클래스나 그 클래스를 상속한 클래스 안에서만 보인다. -
Kotlin에서 클래스를 확장한 함수는 그 클래스의
private
이나protected
맴버에 접근할 수 없다.
-
Note
|
Kotlin의 |
Property
-
클래스 속성은 클래스의 데이터, 즉 상태나 특성을 나타낸다.
-
Kotlin은 Getter를 통해 값을 가져오고, Setter를 통해 값이 설정한다.
-
Kotlin에서 클래스의 필드field는 속성property의 데이터가 저장되는 곳이며, 우리가 직접 정의할 수 없다.
-
필드를 캡슐화하여 필드의 데이터를 보호하고, Getter와 Setter를 통해서만 외부에 노출시키기 위함이다.
-
class Player {
var name = "madrigal" // (1)
get() = field.capitalize() // (2)
private set(value) {
field = value.trim() // (3)
}
}
-
name은 우리가 정의한 속성이다.
-
후원 필드backing field인
field
는 Getter/Setter가 사용하는 속성 데이터다. -
name 속성의 데이터를 저장한 후원 필드값을 변경한다. 즉, 자신이 선언된 속성의 후원 필드값을 변경한다.
Note
|
후원 필드(backing field)
후원 필드는 Getter와 Setter가 사용하는 속성 데이터다. 해당 속성을 사용하는 코드에서는 후원 필드를 직접 참조할 수 없고, 자동 실행되는 게터를 통해서만 속성 데이터를 받을 수 있다. |
Note
|
산출 속성(computed property)
클래스 속성을 정의하면 후원 필드를 생성하는데, 산출 속성computed property의 경우에는 다르다. 산출 속성은 다른 속성이나 변수 등의 값으로 자신의 값을 산출하는 속성이다. 즉, 값을 저장할 필요가 없으므로 코틀린 컴파일러는 후원 필드를 생성하지 않는다. |
Getter/Setter
-
Kotlin은 우리가 정의한 속성에 대해 필드field와 게터Getter/세터Setter가 자동 생성된다.
-
속성의 데이터를 읽거나 쓰는 방법을 우리가 지정하기 원할 때는 커스텀 Getter와 Setter를 정의할 수 있다.
-
이를 Getter와 Setter의 오버라이딩overriding이라고 한다.
Override Getter/Setterclass Player { var name = "madrigal" get() = field.capitalize() // overriding (1) set(value) { // (2) field = value.trim() // (3) } }
-
field
키워드는 Kotlin이 자동으로 관리해주는 후원 필드backing field를 참조한다. -
Setter는 속성이
var
일 때만 정의할 수 있다.
-
-
Getter
val player = Player()
player.name = "estragon" // (1)
-
setter는 대입 연산자를 사용해서 속성에 값을 지정할 때 자동 호출된다.
-
Getter는 모든 속성에 대해 자동 생성된다.
-
Getter는 속성을 참조할 때 자동 호출된다.
Setter
val player = Player()
println(player.name + "TheBrave") // (1)
-
getter는 속성을 참조할 떄 자동 호출된다.
-
Setter는 속성이
var
일 때만 자동 생성된다. -
Setter는 대입 연산자를 사용해서 속성에 값을 지정할 때 자동 호출된다.
Race Condition
var weapon: Weapon?
fun printWeaponName() {
if (weapon != null) {
println(weapon.name) // smart casting is impossible
}
}
-
위 코드는 보면 weapon은 nullable한 속성이지만 조건문을 통해 null이 발생할 수 없다.
-
하지만 스마트 캐스팅smart casting이 일어나지 않는다.
-
스마트 캐스팅이란 상황에 맞게 컴파일러가 똑똑하게 타입을 변환해 주는 것을 말한다.
-
-
null 체크하는 코드와 println 코드 사이에 weapon 속성 값이 변경될 가능성이 여전히 있으므로 에러가 된다.
-
이러한 상황을 경합 상태race condition라고 한다.
-
경합 상태는 특정 코드의 데이터를 프로그램의 다른 코드에서 동시에 변경할 때 발생하며, 이로 인해 예기치 않은 결과를 초래할 수 있다.
-
Package
-
Java는 기본적으로 패키지 가시성을 사용한다.
-
가시성 제한자가 없는 메서드, 필드, 클래스는 같은 패키지에 있는 클래스에서만 사용하능하다는 뜻이다.
-
-
Kotlin은 패키지 가시성이 없다.
-
같은 패키지에 있는 클래스, 함스, 속성 등은 기본적으로 상호할 수 있어서 굳이 별도의 가시성을 가질 필요가 없다.
-
-
Kotlin은 Java에 없는
internal
가시성을 지원한다.-
이것은 같은 모듈module에 있는 클래스, 함수, 속성끼리 상호 사용할 수 있다는 것을 뜻한다.
-
internal
이 지정된 클래스와 이 클래스의 함수나 속성은 bytecode 파일에서public
이 된다.
-
8. Initialization
Constructor
Primary constructor
class Payment(
_productName: String, // (1)
_unitPrice: Int,
_count: Int,
_isUsingPoint: Boolean
) {
val name = _productName
get() = field.capitalize()
val total = _unitPrice * _count
private val isUsingPoint = _isUsingPoint
}
-
밑줄이 있는 변수는 임시 변수를 나타낸다. 임시 변수는 한 번 이상 참조될 필요가 없는 변수이며, 1회용이라는 것을 나타내기 위해 이름 앞에 밑줄을 붙힌다.
Note
|
임시 변수에
_ prefix를 사용하는 것에 대한 생각
|
class Payment(
_productName: String,
_unitPrice: Int,
_count: Int,
private val isUsingPoint: Boolean // (1)
) {
val name = _productName
get() = field.capitalize()
var total = _unitPrice * _count
}
-
기본 생성자에 속성을 정의할 수 있으며, var나 val을 추가해야 한다. 이러한 코드는 클래스 속성과 생성자 매개변수의 두 가지 역할을 모두 하게 되므로 코드의 중복도 줄여 준다.
class C private constructor(a: Int) { ... } // (1)
-
기본 생성자에 접근제어자를 설정하려면 명시적으로
constructor
키워드를 추가해야 한다. 기본값은public
이다.
Secondary constructor
-
보조 생성자에서는 속성을 정의할 수 없다.
class Payment(
_productName: String,
_unitPrice: Int,
_count: Int,
private var isUsingPoint: Boolean
) {
val name = _productName
get() = field.capitalize()
var total = _unitPrice * _count
constructor(name: String) : this( // (1)
name,
0,
1,
isUsingPoint = false // (2)
)
constructor(name: String, price: Int) : this(
name,
price,
1,
isUsingPoint = false
) {
if (name == "포인트테스트상품") isUsingPoint = true // (3)
}
}
-
this
키워드는 다른 생성자를 말하며, 여기서는 기본 생성자를 뜻한다. -
인자를 그대로 넘기지 않고
isUsingPoint
를 설정해서 전달했는데, 이러한 방법을 지명 인자named argument라고 한다.
(속성에만 적용이 가능하며, 임시 변수는 지명 인자 사용이 불가능하다) -
속성을 초기화하는 대안으로 보조 생성자를 사용하면 편리하다. (속성을 변경하려면
var
로 선언되어 있어야 한다)
Tip
|
Named arguments
|
Property initialization
-
생성자에 기본 인자 설정이 가능하다.
Default propertiesclass Payment( _productName: String _unitPrice: Int, _count: Int = 1, // (1) private val isUsingPoint: Boolean ) { val name = _productName get() = field.capitalize() var total = _unitPrice * _count constructor(name: String) : this( name, 0, isUsingPoint = false) }
-
생성자를 정의할 때 인자의 기본값을 지정할 수 있다.
-
-
기본 인자 설정은 기본 생성자, 보조 생성자 모두 가능하다.
-
클래스의 속성에 기본값 설정이 가능하다.
class Payment( val name: String val price: Int = 0 ) { val receipt = getRecentReceipt() // (1) private fun getRecentReceipt() = File("data/receipt.txt") .readText() .split("\r\n") .first() }
-
인스턴스가 생성될 때 가장 최근 영수증 정보를 가져온다.
-
Initializer block
-
Kotlin에서는
init
키워드를 통해 클래스의 초기화 블록initializer block을 정의할 수 있다. -
전제 조건 검사는 생성자나 속성보다는 초기화 블록에서 하는 것이 좋다. 초기화 블록은 어떤 생성자를 통해 호출되든 인스턴스가 생성될 때마다 자동으로 호출되어 실행된다.
class Product(
val name: String,
val price: Int
) {
init {
require(price > 0, { "가격은 0보다 커야 합니다." }) // (1)
}
constructor(name: String) : this(name, 1)
}
-
사전 조건이 false가 되면
IllegalArgumentException
이 발생된다.
Initializer order
-
여러 가지의 초기화 코드(기본 생성자, 보조 생성자, 초기화 블록)에서 같은 속성이 참조될 때, 초기화가 처리되는 순서가 중요하다.
-
아래 코드를 디컴파일된 바이트 코드를 보면 다음과 같다.
class Player(_name: String, val health: Int) { // (1) val race = "DWARF" // (2) var town = "Bavaria" val name = _name val alignment: String private var age = 0 init { println("initializaing player") // (3) alignment = "GOOD" } constructor(_name: String) : this(_name, 100) { town = "The shire" // (4) } }
public final class Player { @NotNull private final String race; @NotNull private String town; @NotNull private final String name; @NotNull private final String alignment; private int age; private final int health; public Player(@NotNull String _name, int health) { super(); this.health = health; // (1) this.race = "DWARF"; // (2) this.town = "Bavaria"; this.name = _name; String var3 = "initializaing player"; // (3) boolean var4 = false; System.out.println(var3); this.alignment = "GOOD"; } public Player(@NotNull String _name) { this(_name, 100); this.town = "The shire"; // (4) } }
-
기본 생성자에 정의된 속성의 인자값 지정
-
클래스 내부에 정의된 속성의 초깃값 지정
-
초기화 블럭에서 속성에 초깃값 지정 및 함수 호출/실행
-
보조 생성자에서 속성의 초깃값 지정 및 기본 생성자 호출/생성
-
-
초기화 블록에서 사용되는 모든 속성은 소스 코드에서 초기화 블록이 정의되기 전에 초기화되어야 한다.
class Player { init { val healthBonus = health.times(3) // (1) } val health = 100 }
-
health 초기화 코드는 아랫줄에 있으므로 컴파일 에러가 발생한다.
-
-
컴파일러는 초기화 블록에서 속성을 사용하는 함수와 비교하면서까지 속성의 초기화 순서를 검사하지 않는다.
class Player { val name: String private fun firstLetter() = name[0] init { println(firstLetter()) // (1) name = "Madrigal" // (2) } }
-
에러 없이 정상적으로 컴파일된다.
-
Player를 초기화 할 때, name이 초기화가 되지 않으므로
println
에서 NPE가 발생한다.
-
Late initialization
delegation의 lazy 참고
9. Inheritance
-
상속Inheritance은 타입 간의 계층적인 관계를 정의하기 위해 사용할 수 있는 객체지향 원리다.
-
subclass는 상속해주는 클래스(superclass)의 모든 속성과 함수를 공유한다.
-
Kotlin의 클래스는 기본적으로 서브 클래스를 만들 수 없게 되어 있다.
// kotlin class Room // java public final class Room {}
-
서브 클래스를 가질 수 있게 하려면 해당 클래스에
open
키워드를 지정해야 한다.open class Room
-
-
서브 클래스를 정의할 때는 클래스 이름 다음에 콜론을 추가하고 슈퍼 클래스의 생성자를 호출한다.
open class Room(val name: String) class TownSquare : Room("Town Square")
-
override
키워드를 사용하여 상속받은 속성이나 함수를 오버라이딩overriding할 수 있다.open class Room(val name: String) { open fun load() = "empty" // (1) } class TownSquare : Room("Town Square") { override fun load() = "not empty" // (2) }
-
Kotlin에서는 서브 클래스에서 오버라이딩하는 슈퍼 클래스의 함수에도
open
키워드를 지정해야 한다. -
override
키워드를 사용하여 슈퍼 클래스의 함수를 오버라이딩 할 수 있다.
-
-
서브 클래스의 오버라이딩 함수나 속성은 기본적으로
open
이 되므로, 서브 클래스에서는 언제든 오버라이딩이 가능하다.open class Room(val name: String) { open fun load() = "empty" } open class TownSquare : Room("Town Square") { // (1) final override fun load() = "not empty" // (2) }
-
서브 클래스의 서브 클래스를 만들기 위해서는 클래스에
open
이 필요하다. -
final
키워드를 통해 하위 클래스에서 오버라이딩을 막을 수 있다.
-
-
protected
키워드를 사용하여 가시성을 지정할 수 있다. -
super
키워드를 통해 슈퍼 클래스의 속성을 참조할 수 있다.open class Room(val name: String) { protected open val level = 1 } class TownSquare : Room("Town Square") { override val level = super.level + 2 }
-
상속을 통해 다형성polymorphism을 구현할 수 있다.
val room: Room = TownSquare()
-
Kotlin에서는 오버라이딩 하기 위해
open
,override
키워드를 사용해야 한다. 어찌 보면 번거롭게 생각될 수도 있겠지만, 이렇게 함으로써 무의미하게 서브 클래스를 생성하고 속성과 함수를 오버라이딩 당하는 것을 막을 수 있다.
Note
|
|
Type check
-
is
키워드를 통해 현재 객체가 특정 타입인지 검사할 수 있다.val room = Room("Foyer") room is Room // true room is TownSquare // false val townSquare = TownSquare() townSquare is Room // true (1) townSquare is TownSquare // true
-
서브 클래스의 인스턴스는 슈퍼 클래스의 타입도 된다. (다형성)
-
-
Kotlin의 모든 non-null 클래스는 자동으로
Any
라는 최상위 슈퍼 클래스로부터 상속받는다.-
타입 변환type casting을 사용하면 우리가 지정한 타입으로 객체를 사용 할 수 있다.
(변환된 타입의 속성 참조나 함수 호출을 할 수 있는 것이지 해당 객체가 갖는 값을 변환하는 것이 아니다)
-
-
as
키워드를 통해 타입 변환이 가능하다.fun print(any: Any) { val isPlayerAOrMyRoom = if (any is Player) { true } else { (any as Room).name == "MyRoom" } }
-
타입 변환은 유용하지만 우리가 안전하게 사용해야 한다.
Note
|
JDK 17에서의 타입 체크
Java에서 JEP 394: Pattern Matching for instanceof를 통해 Kotlin과 비슷한 문법을 사용할 수 있다. 이 때, Kotlin의 스마트 캐스팅과 같이 컴파일 단계에서 클래스 타입 추론이 된다. |
Tip
|
Any 클래스
Kotlin을 사용하면 서로 다른 플랫폼의 애플리케이션을 만들 수 있다. 즉, JVM에서 실행되는 애플리케이션이나 JVM 없이 실행되는 네이티브 애플리케이션, 자바스크립트, Http 서블릿 등으로 만들 수 있다.
|
Smart Casting
fun print(any: Any) {
val isPlayerAOrMyRoom = if (any is Player) {
any.name == "A" // smart casting
} else {
(any as Room).name == "MyRoom"
}
}
-
위 코드를 보면
any.name == "A"
에 타입 변환없이 name 속성을 참조했다. -
Kotlin 컴파일러는 any 객체 타입이 Player 타입 비교 이후에 Player 인 것을 알고 있으므로 스마트 캐스팅smart casting이라는 타입 변환이 일어난다.
-
즉, 우리가 직접 타입 변환을 하지 않아도 된다.
10. Delegation
-
프로그래밍에서 위임delegation의 기원은 오브젝트 합성object composition으로부터다.
-
오브젝트 합성을 좀 더 재사용할 수 있게 하려면 새로운 패턴인 위임 패턴delegation pattern으로 통합된다.
이 패턴은 오브젝트가 헬퍼 오브젝트를 가질 수 있게 하며, 이 헬퍼 오브젝트는 델리게이트delegate라고 불린다.
-
Kotlin에서 위임한다는 것을 나타낼 때는
by
키워드를 사용한다.by
다음에 위임받을 일을 처리하는대리자delegate를 지정한다. 대리자로는 커스텀 함수나 코틀린 표준 라이브러리 함수를 사용할 수 있다.
lateinit
-
null이 아님을 확신하지만 초기화 시점에 할당을 안하는 속성일 경우
notNull
val name: String by Delegates.notNull<String>() fun main(args: Array<String>) { name = "jun" println(name) }
-
notNull
에 의한 변수 선언이 어색하게 들리지 않은가? 코틀린팀도 같은 생각을 했고, 같은 목표 달성을 위해 코틀린 1.1에서lateinit
이라는 간단한 키워드를 추가한 이유다. 단순히 지연 초기화에 대해서 나타내기 때문에 그냥lateinit
가 됐다.lateinit var name: String fun main(args: Array<String>) { name = "jun" println(name) }
-
-
인스턴스의 생성 시점에 속성을 초기화 할 수 없을 땐 지연 초기화를 활용할 수 있다.
-
lateinit
키워드를 사용한다.-
이 키워드는 우리 스스로가 책임지고 해당 속성을 사용하기 전에 초기화해야 한다는 것을 뜻한다.
-
초기화되기 전에 사용된다면
UninitializedPropertyAccessException
이 발행된다. -
다른 타입의 객체를 참조하므로 기본 타입(예,
Int
)이 될 수 없다. -
var
이면서 non-null 타입이어야 한다. -
커스텀 게터/세터를 정의할 수 없다
-
-
Kotlin의 표준 라이브러리인
isInitialized
함수를 사용하여 초기화 되었는지 확인할 수 있다. -
대안으로 nullable 타입의 속성을 사용할 수 있지만, 모든 코드에서 null 체크를 해야하므로 코드 작성이 번거로울 수 있다.
-
lateinit
키워드는 클래스 속성 외에 최상의 수준 속성과 함수의 지역 변수에도 사용될 수 있다.
class Wheel {
lateinit var alignment: String // (1)
fun initAlignment() {
alignment = "Good"
}
fun printAlignment() {
if (::alignment.isInitialized) println(alignment) // (2)
}
}
-
선언시점에 초기화하지 않아도 컴파일 에러가 발생하지 않는다.
-
속성의 값이 아니라 참조를 전달해야 하므로
::
를 붙혀야 한다.
lazy
-
lateinit
,Delegates.notNull()
모두 var 속성에서만 동작한다. 그렇다면 val 속성을 사용할 때는 무엇을 사용해야 하는가?-
lazy
를 사용한다. 위 방법과는 달리 선언 시에 변수 초기화 방법을 지정해야 한다. -
하지만 이 변수 초기화 함수는 사용할 때까지 호출되지 않는다.
-
-
변수나 속성이 최초 사용될 때까지 초기화를 연기할 수 있다.
-
Kotlin에서 늦 초기화lazy initialization는 위임delegation 메커니즘을 사용해서 구현한다.
-
Kotlin 표준 라이브러리인
lazy
함수를 대리자로 사용하여 초기화를 위임한다. -
언어 자체에서 lazy 계산법을 기본적으로 제공하지는 않지만, 코틀린 표준 라이브러리와 델리게이트 속성이라는 언어 기능의 일부로 제공한다.
-
by lazy vs lateinit: https://stackoverflow.com/questions/36623177/kotlin-property-initialization-using-by-lazy-vs-lateinit
val hometown by lazy { selectHometown() }
private fun selectHometown() = File("towns.txt")
.readText()
.split("\r\n")
.first()
Delegates.Observable
-
속성의 값 변경을 살펴봐야 하며, 변경이 일어나는 즉시 뭔가를 수행해야 하는 경우
var name: String by Delegates.observable("init value") {
property, before, after -> println(`속성 ${property.name}`(을)를 "$before"에서 "$after"로 변경한다.)
}
fun main(args: Array<String>) {
name = "jun"
name = "kim"
}
Delegaters.vetoable
-
값 변경을 거부할 수 있게 하는 표준 델리게이트
Tip
|
I forbid의 라틴어인 veto는 공식적인 행동을 일방적으로 막을 수 있는 힘이다.(예를 들어 주 경찰관히 사용하는 권력) |
var age: Int by Delegates.vetoable(1) {
property, before, after ->
println("${property.name} $before -> $after")
age > 0 && age < 150 // 1~149세만 변경을 허용
}
delegated map
-
맵 위임은 위임과 함께 제공되는 멋진 기능 중 하나
-
맵 위임은 함수/클래스 생성자에서 여러 파라미터 대신에 하나의 파라미터로 맵을 전달할 수 있다.
-
https://kotlinlang.org/docs/delegated-properties.html#storing-properties-in-a-map
커스텀 델리게이트
-
직접 델리게이트를 만들 수 있다.
-
var 속성에 대해 델리게이트를 만들려면
ReadWriteProperty
인터페이스를 구현한다. -
델리게이트를 사용할 땐 함수가 있어야 한다. 확장한 클래스를 익명 오브젝트로 생성하는 inline 함수를 만들어서 사용할 수 있다.
로컬 델리게이트
-
코틀린 1.1부터는 로컬 속성에 대해서도 델리케이트가 가능하다.
클래스 위임
data class PersonInfo(
val fName: String,
val lName: String,
val dob: LocalDate,
)
data class Person(
val fName: String,
val lName: String,
val dob: LocalDate,
val id: UUID
)
data class PersonInfo(
override val fName: String,
override val lName: String,
override val dob: LocalDate,
) : PersonDefinition
data class Person(
private val data: PersonInfo,
val id: UUID
) : PersonDefinition by data
interface PersonDefinition {
val fName: String
val lName: String
val dob: LocalDate
}
11. Another type of Class
Object
-
object
키워드를 사용하여 싱글톤singleton 객체를 정의할 수 있다.-
프로그램이 실행되는 내내 수시로 변하는 상태 정보를 지속적으로 유지 관리할 필요가 있다면 싱글톤 사용을 고려하자
-
싱글톤은 하나의 인스턴스만 생성되는 것을 말한다.
-
싱글톤은 시스템의 자원 사용과 부담을 줄이고 같은 객체를 공유할 수 있다는 장점이 있다.
-
다중 스레드multi-thread로 실행될 때는 반드시 하나의 객체만 생성되도록 동기화 처리를 해주어야 한다.
-
-
object
키워드를 사용하는 세 가지 방법이 있다.-
객체 선언object declaration
-
객체 표현식object expression
-
동반 객체companion object
-
Object declaration
object Game { // object declaration
init {
println("Game init")
}
}
-
객체 선언은 상태 관리에 유용하다.
-
객체 선언에는 초기화 블록이 포함될 수 있지만 생성자는 가질 수 없다.
-
최초로 사용되는 시점에 하나의 객체가 자동으로 생성되어 초기화된다.
Object expression
val abandonedTownSquare = object : TownSquare() {
override fun load() = "empty"
}
-
기존 클래스의 서브 클래스를 우리가 원하는 코드 안에 익명 클래스로 정의한 후 바로 인스턴스를 생성해서 사용할 수 있다.
-
위 코드를 보면 생성된 인스턴스가 val 변수에 저장하므로 싱글톤 객체가 된다. 해당 변수가 존재하는 동안만 사용 가능하다.
Companion object
class Job {
companion object {
private const JOB_NAME = "DailyJob"
fun getJobName() = JOB_NAME
}
}
-
동반 객체는 최상위 수준에서는 사용할 수 없고, 클래스 내부에 정의하여 사용한다.
-
클래스 내부에 정의된 객체 선언이라고 생각할 수 있다.
-
단 하나의 클래스에는 하나의 동반 객체만 포함될 수 있다.
-
포함 클래스의 인스턴스가 얼마나 많이 생성되든 동반 객체의 인스턴스는 하나만 생성된다.
-
동반 객체는 자신을 포함하는 클래스가 메모리에 로드될 때 같이 생성되며, 자신의 속성과 함수 중 하나가 사용될 때 초기화된다.
Nested Class
object Game {
private class GameInput(arg: String?) {
private val input arg ?: ""
}
}
-
다른 클래스 내부에 중첩된 클래스를 정의할 수 있다.
-
특정 객체에서만 필요하고 다른 코드에서는 사용하지 않을 때 활용할 수 있다.
-
외곽 클래스에서는 중첩된 클래스의 속성과 함수를 사용할 수 없다.
-
위 코드는 Java 코드로 디컴파일하면 클래스 내부에
private static final class GameInput
로 정의된다.
Data Class
-
데이터를 저장하기 위해 특별히 설계된 클래스이다.
-
다음과 같은 요구사항이 충족되야 한다.
-
최소한 하나의 매개변수를 갖는 기본 생성자를 가져야 한다.
-
기본 생성자의 매개변수에는 val 이나 var이 지정되어야 한다. 그래야만 속성이 생성되기 때문이다.
-
abstract
,open
,sealed
,inner
키워드를 지정할 수 없다.-
예를 들어
open
클래스로 정의한 객체가 있다면 equals와 hashCode를 구현해야 한다. 그렇지 않으면==
연산자를 사용할 때 객체 참조만 비교하게 된다.
-
-
-
데이터 클래스를 정의하면 속성에 맞게 처리되는
toString
,equals
,hashCode
함수들이 자동으로 생성된다.-
copy
함수도 생성해 준다. -
componentN 함수들도 생성해준다. 이를 통해 해체 선언destructuring declaration을 사용할 수 있다.
data class Grade(val name: String, val rate: Double) fun getNames(val grades: List<Grade>) = grades .map { (name, _) -> name } .toList()
-
Tip
|
componentN 함수
|
Note
|
Canonical method
카노니컬 메서드Canonical methods는
|
Value Class
-
데이터가 있는 불변 엔티티를 나타낸다.
-
value
키워드를 사용한다. -
단 하나의 프로퍼티만 가진다.
-
모든 프로퍼티는
val
이어야 한다. -
===
을 지원하지 않는다. -
inline class 였다.
-
@JvmInline
annotation
Enum
enum class Grade {
GOLD, SILVER
}
fun getRate(grade: Grade) = when (grade) {
GOLD -> 4.5
SILVER -> 2.0
}
-
열거형enumerated type은 enum 클래스로 정의할 수 있다.
-
Kotlin에서 enum 클래스의 각 항목에 대해 내부적으로 name과 ordinal 속성을 갖는다.
-
Java 클래스의 ordinal과 동일하다면.. 이펙티브 자바(item 35)에서는 ordinal 메서드 사용을 지양한다.
-
-
==
vs===
-
==
는equals()
동작이고,===
는 참조가 동일한지를 확인한다. -
CurrencyCode.KRW == currency
와 같은 코드는 문제 없다. -
but,
currency == CurrencyCode.KRW
와 같은 코드는 NPE가 발생한다.==
는 내부적으로equals()
로 동작하기 때문.-
라고 생각했지만, 정상 동작한다. 그냥 모두
==
를 사용하는 것이 편리할 것 같다. 굳이===
의 동작을 생각하게끔 코딩할 필요 없이.
-
-
enum class CurrencyLocation { RIGHT, LEFT }
enum class CurrencyCode(
val currency: String,
val symbol: String,
var locale: Locale,
val loc: CurrencyLocation = CurrencyLocation.LEFT
) {
JPY("yen", "¥", Locale.JAPAN, CurrencyLocation.RIGHT),
USD("dollar", "$", Locale.US),
KRW("원", "₩", Locale.KOREA)
}
Note
|
Enum naming convention
Java에서는 열거형 타입에 대해서 대문자 네이밍을 권장한다. 열거형 타입은 싱글톤으로 상수와 같이 사용되기 때문이다. 하지만 Kotlin에서는 상수 표기법 외에도 사용법에 따라 PascalCase도 괜찮다고 얘기한다. 처음에는 혼란스럽게 왜 이렇게 가이드했을까 생각하고 상수 표기법으로만 작성했는데, 코드를 작성하다가 Sealed Class를 만들면서 새로운 고민에 봉착했다.
|
Sealed Class
-
ADT(Algebraic data type, 대수적 데이터 타입)는 지정된 타입과 연관될 수 있는 서브 타입들의 폐집합(closed set)을 나타낼 수 있다.
-
enum 클래스도 ADT의 간단한 형태다.
-
-
enum 클래스를 포함해서 ADT의 장점은 우리가 모든 타입을 처리했는지 컴파일러가 검사할 수 있다는 것이다.
enum class Grade { GOLD, SILVER } fun getRate(grade: Grade) = when (grade) { // compile error GOLD -> 4.5 }
-
sealed 클래스는 자신의 서브 클래스 종류를 제한하기 위해 사용된다.
(sealed: 봉인을 한, 봉인하다, 밀봉하다.) -
sealed 클래스에 속하는 서브 클래스들은 일반 클래스이므로 인스턴스 갯수에 제한이 없다.
-
enum 클래스의 각 항목은 하나의 인스턴스만 생성된다.
-
-
sealed 클래스는 두 가지 형태로 사용할 수 있다.
-
첫번째 방법은 모든 서브 클래스들을 독립적으로 정의하고, sealed 클래스와 같은 코틀린 파일(.kt) 안에 둔다.
sealed class StudentStatus object NotEnrolled : StudentStatus() class Active(val courseId: String) : StudentStatus() object Graduated : StudentStatus()
val active = Active("kotlin01")
-
두번째 방법은 모든 서브 클래스들을 sealed 클래스 내부에 중첩된 클래스로 정의하는 방법이다.
sealed class StudentStatus { object NotEnrolled : StudentStatus() class Active(val courseId: String) : StudentStatus() object Graduated : StudentStatus() }
var active = StudentStatus.Active("Kotlin01")
-
두 예제 코드를 모두 제한된 수의 서브 클래스를 가지며, enum 클래스보다 더 다양한 처리를 할 수 있다.
-
object
로 선언된 객체는 인스턴스가 하나만 있으면 되기 때문이며,Active
클래스는 여러 인스턴스를 가질 수 있다.fun main(args: Array<String>) { val student = Student(StudentStatus.Active("Kotlin01")) // smart casting println(studentMessage(student.status)) } fun studentMessage(status: StudentStatus): String = when (status) { is StudentStatus.NotEnrolled -> "과정에 등록하세요." is StudentStatus.Active -> "${status.courseId} 과정에 등록하셨습니다." is StudentStatus.Graduated -> "졸업을 축하합니다." } class Student(var status: StudentStatue) sealed class StudentStatus { object NotEnrolled : StudentStatus() class Active(val courseId: String) : StudentStatus() object Graduated : StudentStatus() }
-
Operator overloading
-
Kotlin에는 여러가지 연산자가 있다.
-
Kotlin 컴파일러는
a + b
를 컴파일하여a.plus(b)
를 실행하도록 바이트코드로 생성한다. -
Kotlin의 연산자들을 함수이므로 오버로딩이 가능하다.
Table 1. 오버로딩 가능한 연산자 연산자 오버로딩 함수명 기능 +
plus
두 객체를 더한다.
+=
plusAssign
다른 객체와 더한 후 결과를 왼쪽 피연산자의 객체에 저장한다.
==
equals
두 객체가 같으면 ture, 아니면 false를 반환한다.
>
compareTo
왼쪽 객체가 오른쪽 객체보다 크면 true, 아니면 false를 반환한다.
[]
get
지정된 인덱스의 컬렉션 요소를 반환한다.
..
rangeTo
범위 객체를 생성한다.
in
contains
객체가 컬렉션에 있으면 true를 반환한다.
Type aliases
-
Type aliases는 기존 타입의 대체 이름을 제공한다.
typealias NodeSet = Set<Network, Node>
-
오브젝트로 함께 사용할 수 있다.
typealias UserFactory = User.Companion
12. Interface
-
인터페이스를 사용하면 여러 클래스들의 공통적인 속성과 행동을 나타낼 수 있다.
-
클래스는 어떻게(how) 구현하는가에 초점을 두지만, 인터페이스는 무엇(what)을 구현해야 하는지를 나타낸다.
-
추상클래스도 무엇(what)을 구현해야 하는지를 나타낸다는 관점에서 인터페이스와 비슷하지만, 추상 클래스는 서브 클래스를 가질 수 있고 생성자도 정의할 수 있다는 차이점이 있다.
-
-
헤더만 선언하고 몸체의 구현 코드가 없는 함수를 추상 함수abstract function 라고 한다.
interface Account { fun deposit(amount: Int): Int fun withdrawal(amount: Int): Int }
-
함수의 매개변수가 값이 아닌 타입(클래스나 인터페이스)인 경우는 무엇을 할 수 있는지 나타내는 것이지 어떻게 구현되는지는 나타내는 것이 아니다. 따라서 매개변수의 타입을 인터페이스로 지정하면 장점이 많다. (다형성)
interface Account { fun consolidate(account: Account): Account // 계좌 통합 }
인터페이스를 타입으로 사용하는 습관을 길러두면 프로그램이 훨씬 유연해질 것이다.
— Item 64 - 객체는 인터페이스를 사용해 참조하라
Effective Java 3/E -
인터페이스를 구현(implement)할 때는 상속과 동일하게 콜론(
:
)을 사용한다.class BankAccount : Account { ... }
Abstract Class
abstract class Payment {
abstract fun payment(amount: Long)
}
-
추상 클래스는 class 키워드 앞에
abstract
키워드를 추가하여 정의한다. -
추상 클래스는 인스턴스를 생성할 수 없다.
Note
|
인터페이스와 추상 클래스 차이
|
Tip
|
추천하는 방식은 언제나 인터페이스로 시작하는 것. 인터페이스가 좀 더 직관적이며 깨끗하고 좀 더 모듈식의 디자인을 허용함. |
13. Generic
-
제네릭generic은 클래스와 인터페이스의 매개변수 또는 함수의 매개변수와 반환 타입을 미리 확정하지 않고 정의한 후에 사용되는 시점에서 특정 타입을 지정할 수 있도록 해주는 기법을 말한다.
-
코드의 중복을 줄여준다.
-
컴파일 시점에서 사용 타입의 적합성을 확인할 수 있으므로 타입 안전을 보장해준다.
-
-
List
는 원시 타입raw type이라고 하며,<>
안에 지적된Int
타입을 제네릭 타입generic type 이라고 한다.val listOfInts: List<Int> = listOf(1, 2, 3)
-
Kotlin의 다른 타입처럼 제네릭 타입도 차입 추론type inference을 지원한다.
-
T
말고 다른 명칭을 사용할 수 있으며, 표준화된 명칭을 따르는 것이 좋다.-
E
(Entity, Element),K
(Key),N
(Number),T
(Type),V
(Value),R
(Return),X
(Exception) -
참고: Effective Java 3/E "Item 68, 일반적으로 통용되는 명명 규칙을 따르라"
-
-
제네릭 클래스
class LootBox<T>(_item: T) { // 전리품 상자 (1) private var loot: T = _item }
-
T
가 제네릭 타입 매개변수이며,<>
안에 지정한다.
-
-
제네릭 함수
class LootBox<T>(_item: T) { private var loot: T = _item var open = false fun fetch(): T? { return loot.talkIf { open } } }
-
제네릭에 고차 함수higher-order function 사용하기
class LootBox<T>(item: T) { private var loot: T = item var open = false fun <R> fetch(lootModFunction: (T) -> R): R? { // (1) return lootModFunction(loot).takeIf { open } } }
-
(T) → R
을 함수 타입function type 이라고 한다.Notehigher-order function다른 함수를 매개변수로 받거나 반환할 수 있는 함수를 고차 함수라고 한다. 고차 함수는 인자로 받은 함수를 필요한 시점에 호출하거나 클로저closure를 생성하여 반환한다.
-
-
타입 제약type constraint을 지정할 수 있다.
class LootBox<T : Loot>(item: T) { // (1) ... } open class Loot(val value: Int) class Fedora(val name: String, value: Int) : Loot(value) class Coin(value: Int) : Loot(value)
-
T
에Loot
를 지정하면Loot
클래스 및 서브 클래스만 매개변수 타입으로 사용될 수 있다.
-
-
가변인자는
vararg
키워드를 사용한다.class LootBox<T : Loot>(vararg item: T) { // (1) var open = false private var loot: Array<out T> = item // (2) operator fun get(index: Int): T? = loot[index].takeIf { open } // (3) fun fetch(item: Int): T? { return loot[item].takeIf { open } } }
-
배열로 처리된다.
-
out
키워드는T
타입을 포함해서T
타입의 서브 타입도 타입 인자가 될 수 있다는 것을 뜻한다. -
인덱스 연산자(
[]
)를 오버로딩하는 get 함수를 정의하면fetch
함수를 사용하지 않아도 loot 배열을 읽을 수 있다.lootBoxOne.fetch(1) lootBoxOne[1]
-
in, out
-
Kotlin에서는
in
과out
키워드를 사용하여 제네릭 타입 매개변수를 더 다양한 방법으로 조정할 수 있다.-
T
: 별도의 Wildcard 정의가 없이 read/write 모두 가능 -
in T
: Java의 ? super T와 같음. input의 약자이며 write 만 가능 -
out T
: Java의 ? extends T와 같음. output의 약자이며 read 만 가능
-
-
제네릭 타입 매개변수에
out
키워드를 지정한 것을 공병형covariance 이라고 하고,in
키워드를 지정한 것을 반공변형contravariance 이라고 한다. -
in
,out
키워드는 컴파일러가<>
로 나타낸 제네릭 타입 간의 슈퍼-서브 관계가 있더라도 인식하지 못하는 문제를 해결해준다.AS-ISinternal class LocalTest { @Test fun test() { var child1: Generic<Child1> = Generic(Child1("child1")) var parent: Generic<Parent> = Generic(Child2("child2")) parent = child1 // 컴파일 오류 발생 (1) } } class Generic<T>(var value: T) open class Parent(var value: String) class Child1(value: String) : Parent(value) class Child2(value: String) : Parent(value)
-
c1은
Generic<Child1>
, c2는Generic<Parent>
타입으로 서로 다른 것으로 간주되기 때문에 컴파일 에러가 된다.TO-BEinternal class LocalTest { @Test fun test() { var child1: Generic<Child1> = Generic(Child1("child1")) var parent: Generic<Parent> = Generic(Child2("child2")) parent = child1 val value: Child1 = parent.value println(value) } } class Generic<out T>(val value: T) // (1) open class Parent(var value: String) class Child1(value: String) : Parent(value) class Child2(value: String) : Parent(value)
-
out
키워드를 통해 T 와 T 의 자식 클래스를 포함한 제네릭 타입을 받을 수 있다.
-
reified keyword
-
제네릭으로 인라인 함수에서 사용되며, 런타임에 타입 정보를 알고 싶을 때 사용된다.
-
컴파일된 JVM 바이드코드에는 제네릭 타입 매개변수의 정보가 수록되지 않고 소거된다.
-
제네릭 타입 매개변수가 지정된 클래스로 인스턴스를 생성할 때 어떤 타입의 인자가 사용되었는지 알기 위해 타입을 검사할 수 없다.
따라서 제네릭 타입 매개변수가 지정된 클래스를 타입 검사에 사용될 수 없다.val list = listOf(1, 2) if (list if List<String>) { // compile error (1) println("This is List<String>") }
-
List<String>
에<String>
은 소거되므로 정확한 타입을 검사할 수 없기 때문에 컴파일 에러가 발생한다.
-
-
-
이와 같은 문제를 해결하기 위해 코틀린은 제네릭 타입 매개변수를 컴파일러가 실페 타입으로 변경해 주는 기능을 지원한다. 이것을 제네릭 타입 매개변수의 실체화reification라고 하며,
reified
키워드를 사용한다. -
reified
키워드를 사용하면 제네릭 매개변수로 전달된 인자의 타입과 제네릭 클래스 인스턴스의 타입을 런타임 시에 검사할 수 있다. -
reified
키워드를 사용한 타입 매개변수의 실체화는inline
키워드가 지정된 인라인 함수에서만 가능하다.-
원래 인라인 함수를 다른 함수를 인자로 받는 고차 함수의 실행 성능을 높이기 위해 필요하다.
-
그러나 타입 매개변수의 실체화에서는 제네릭 타입 매개변수를 실제 타입으로 교체하기 위해 사용된다.
-
인라인 함수에는
reified
키워드가 지정된 타입 매개변수를 하나 이상 지정할 수 있다. -
인라인 함수에는
reified
키워드가 지정되지 않은 일반 타입의 매개변수도 정의할 수 있다.
-
@Test
fun test1() {
reifiedTest { "test" }.let { println(it) }
reifiedTest { "test" }.let { println(it) }
reifiedTest { 2 }.let { println(it) }
reifiedTest { 2L }.let { println(it) }
}
inline fun <reified T> reifiedTest(func: () -> T): T {
val list = listOf(1, 1L, "123")
val random = list.shuffled().first()
return if (random is T) { // (1)
random
} else {
func()
}
}
-
reified
키워드를 추가함으로써 바이트코드에T
는 전달된 인자의 타입이 삽입된다.
14. Extension
-
확장extension은 기존 타입의 정의를 직접 변경하지 않고 새로운 기능을 추가할 수 있게 해준다.
-
Extensions는 정적(static)으로 처리된다.
-
이미 클래스에 Extension과 동일한 시그니처로 맴버가 있을 경우 맴버가 우선이다.
-
선택적인 import가 가능하다.
-
클래스의 맴버로 선언하면 해당 클래스 내에서만 범위가 결정된다.
-
일반 함수처럼 확장 함수도 정의된 곳 외에 다른 곳에서 사용되지 않는다면
private
으로 지정하다.
-
Extension function
-
확장 함수 정의하기
fun String.addEnthusiasm(amount: Int = 1) = this + "!".repeat(amount)
-
확장 함수를 추가할 타입(수신자 타입receiver type)도 같이 지정해야 한다.(위 코드에서는
String
이 수신자 타입이다) -
확장 함수는 최소한의 스코프에 사용해야 할 것 같다. 전역으로 사용하게 되면 변경 포인트가 방대해지면서 관리하기 어려워진다.
-
-
함수의 확장은 클래스 상속 없이도 가능하다. 그러나 확장 함수의 호출 가능 범위를 넓히기 위해서 상속과 함께 사용될 수 있다.
fun Any.easyPrint() = println(this)
-
Any
의 확장 함수를 추가하면Any
의 모든 하위 클래스에 대해 호출될 수 있다.
-
-
상속 관계에서의 확장 함수
import org.junit.jupiter.api.Test internal class Tests { @Test fun test() { printSpeak(Feline()) // "일반적인 고양잇과 소리" printSpeak(Cat()) // "일반적인 고양잇과 소리" } } open class Feline fun Feline.speak() = "일반적인 고양잇과 소리" class Cat : Feline() fun Cat.speak() = "야옹!!" fun printSpeak(feline: Feline) { println(feline.speak()) // (1) }
-
printSpeak
에 전달된 파라미터가Feline
이기 때문에Feline.speak()
가 두 번 호출된다. 이것이 상속 관계와 확장 함수의 차이다.
-
-
제네릭 확장 함수
-
아래와 같이
this
를 반환하면 연쇄 호출(체이닝)이 가능하다.fun Any.easyPrint(): Any { println(this) return this }
-
하지만 위 코드는
Any
를 반환하므로 하위 클래스들에서 연쇄 호출이 어렵고 컴파일 에러가 발생한다.fun <T> T.easyPrint(): T { println(this) return this }
-
위 코드와 같이 제네릭 타입 매개변수를 사용하면 메서드 체이닝이 가능하다.
-
모든 타입에 사용될 수 있도록
let
은 제네릭 함수로 정의되어 있다.public inline fun <T, R> T.let(block: (T) -> R): R { // (1) return block(this) }
-
inline
키워드가 지정되어 있다. 왜냐하면 람다를 인자로 받는 확장 함수를 인파인으로 처리하면 메모리 사용의 부담을 줄일 수 있기 때문이다.
-
-
-
Type aliases와 함께 사용하면 불필요한 타입을 만드는 것을 피할 수 있다.
typealias AccountNumber = String fun AccountNumber.masked(): String { return "${this.substring(0..2)}****${this.takeLast(4)}" }
val accountNumber: AccountNumber = "123456789" println(accountNumber) println(accountNumber.masked())
-
nullable한 타입에도 확장함수를 사용할 수 있다.
fun String?.printlnWithDefault(default: String) = println(this ?: default)
-
확장 함수는 바이트코드에서 static 메서드로 변환된다.
Extension property
val String.masked
get() = { "${it.substring(0..2)}****${it.takeLast(4)}" }
-
확장 속성은 산출 속성처럼 후원 필드를 갖지 않는다.
-
따라서 속성에서 반환된 값을 산출하는 get을 반드시 정의해야 한다.
-
확장 속성은 후원 필드를 갖지 않으므로 초기화할 수 없기 때문이다.
-
따라서 var 대신 val을 지정하고 원하는 값을 반환하는 get을 정의해야 한다.
Dispather reveiver
-
확장 함수가 선언된 클래스의 인스턴스를 디스패치 리시버라 함
class Dispather {
val dispatcher: Dispatcher = this // (1)
fun Int.extension() {
val receiver: Int = this // (2)
val dispather: Dispather = this@Dispatcher // (3)
}
}
-
클래스 내에서
this
는 클래스의 인스턴스 -
확장 함수 내에서
this
는 좋은 구문이 있는 유틸리티 함수의 첫 번째 파라미터 같은 리시버 타입의 인스턴스를 뜻한다? -
디스패처 리시버
infix
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that) // (1)
"key" to "value"
-
Tuples.kt
에to
infix 함수가 정의되어 있으며, 이를 통해Pair
를 직관적으로 생성할 수 있다.
-
하나의 파라미터만 가진 (일반 혹은 확장) 함수는
infix
로 표기할 수 있다. -
infix
키워드는 하나의 인자를 갖는 확장 함수와 클래스 함수 모두에 사용할 수 있다. -
함수 호출 문법을 간결하게 해준다.
-
함수에 infix 키워드를 지정한 것을 중위 함수infix function라고 한다.
-
중위 표기법과 함쎄 사용할 수 있다.
-
-
중위 함수를 호출할 때는 수신자 객체와 함수 호출 사이의 점(
.
)은 물론이고 인자 주위의 괄호도 생략할 수 있다. -
Kotlin의 비트 연산자bitwise operator는
shl
,shr
,ushr
,and
,or
,xor
,inv
와 같은 중위함수로 정의되어 있다.
15. Functional Programming
-
함수형 프로그래밍
-
람다 대수를 기반으로 1950년대에 개발되었다.
-
일반적으로 상업용 소프트웨어보다는 학계에서 많이 사용되지만, 기본 원리는 어떤 언어에도 유용하다.
-
컬렉션을 사용하도록 설계된 소수의 고차 함수(higher-order function)가 반환하는 데이터에 의존한다.
-
함수를 다른 타입으로 취급한다. → first-class functions
-
-
함수형 프로그램을 구형하는 함수의 유형에는 크게 세 가지가 있다.
-
변환transform:
map
,flatMap
, …-
변환 함수는 입력 컬렉션의 변경 요소를 갖는 새로운 컬렉션을 생성하고 반환한다.
-
입력 컬렉션은 변경하지 않는다.
-
-
필터filter:
filter
,filterNot
, …-
true 또는 false를 반환하는 술어 함수(predicate function)을 인자로 받는다.
-
-
결합combine:
zip
,fold
, …-
서로 다른 컬렉션을 인자로 받아서 모든 요소들이 합쳐진 새로운 컬렉션을 반환한다.
-
fold
는 최초 누적값을 인자로 받는다. -
fold
는 누적 값을 유지하면서 컬렉션을 반복한다. -
reduce
는fold
와 비슷하게 누적기를 통해 컬렉션에서 반복을 줄이지만 초깃 값은 없다.
-
-
-
공식 문서: Package kotlin.collections
Why to use a functional programming?
-
명령형 프로그래밍에서는 항상 해당 시점의 상태를 저장하는 변수들을 많이 생성해야 한다.
-
상태를 저장하는 변수를 변경하는 일이나, 새로 만든 값을 추가하는 것을 빠르지는 경우가 종종 생길 수 있다.
-
작업 단계가 추가될 때마다 이런 유형의 실수가 생일 가능성이 커진다.
-
-
함수형 코드 구현에서는 새로운 축적 변수를 정의할 필요가 없다.
-
단, 연쇄 호출의 각 단계마다 개로운 컬렉션이 임시로 생성된다는 부담이 생긴다.
-
그러나 연쇄 호출의 작업마다 내부적으로 새로운 컬렉션에 값을 축적하므로 프로그래머의 실수는 거의 생기지 않는다
-
-
확장성이 좋다.
-
명령형 프로그래밍에서는 for 루프와 축적값 및 변수등이 추가되야한다.
List<String> names = new ArrayList<>(); for (Map.Entry<String, String> value : users.entrySet()) { names.add(String.format("%s [%s]", value.getKey(), value.getValue(); }
-
함수형 프로그래밍에서는 추가로 수행할 함수를 연쇄 호출에 추가만 하면 된다.
val names = users .map { "${it.key} [${it.value}]" }; }
-
Sequence
-
List
,Set
,Map
과 같은 컬렉션 타입들은 조기 컬렉션eager collection이라고 한다.-
이 컬렉션 타입의 인스턴스가 생서오딜 때는 자신이 포함하는 요소나 항목이 추가되므로 바로 사용될 수 있기 때문이다.
-
-
다른 컬렉션으로는 지연 컬렉션lazy collection이 있다.
-
변수가 최초 사용될 때 초기화되는 지연 초기화와 유사하게 필요할 때만 값이 생성된다.
-
지연 컬렉션 타입은 더 좋은 성능을 제공한다(특히 매우 큰 컬렉션을 사용할 때).
-
-
Kotlin은 시퀀스sequence라는 내정된 지연 컬렉션 타입을 제공한다. (ref)
-
인덱스를 사용하지 않으며, 크기 정보도 유지하지 않는다.
-
-
시퀀스는 저장되는 항목의 갯수에 제한이 없다. → 무한 수열
-
시퀀스를 사용할 때는 새로운 값이 요청될 때마다 참조되는 반복자 함수iterator function를 정의한다.
generateSequence(0) { it + 1 } .onEach { println("value: $it" }
-
만일 이 코드를 실행하면 onEach 함수가 끝나지 않고 영원히 실행될 것이다.
-
-
1000개의 소수를 얻기 위해서는 얼마나 많은 수를 검사해야 할까?
-
이런 경우에 시퀀스를 사용하면 완벽하다.
val oneThousandPrimes = generateSequence(3) { it + 1 } .filter { it.isPrime() } .talk(1000)
-
Note
|
Collections vs. Sequences
Collections은 기본적으로 Eager evaluation으로 동작하고, Sequences는 Lazy evaluation으로 동작한다.
다음은 공식 문서에 나와있는 예제 코드이다. 이 코드를 통해 조급한 평가와 지연 평가의 동작 순서를 보자. Iterable example
filter: The (1) filter: quick filter: brown filter: fox filter: jumps filter: over filter: the filter: lazy filter: dog length: 5 (2) length: 5 length: 5 length: 4 length: 4 Lengths of first 4 words longer than 3 chars: (3) [5, 5, 5, 4] 위 예제 코드에서 볼 수 있듯이 조급한 평가를 하는 iterable의 경우엔 코드 선언시부터 모든 엘리먼트에 대해서 다음은 지연 평가를 하는 sequence 의 예제이다. Sequence example
Lengths of first 4 words longer than 3 chars (3) filter: The (1) filter: quick (1) length: 5 (2) filter: brown length: 5 filter: fox filter: jumps length: 5 filter: over length: 4 [5, 5, 5, 4] 재미난 점은 |
-
함수형 프로그래밍에서는 새로운 컬렉션을 자주 생성해야 한다.
-
함수의 연쇄 호출에 따른 중간 과정의 결과를 임시 컬렉션에 저장해야 하기 때문이다.
-
그러나 시퀀스는 그렇지 않으며, 대형 컬렉션에 사용할 수 있는 신축성 있는 메커니즘을 제공한다.
-
16. Performance Test
val listInNanos = measureNanoTime {
// 측정할 코드
}
-
Kotlin은 코드 성능을 알려주는 유틸리티 함수인
measureNanoTime
과measureTimeInMillis
를 제공한다. -
measureNanoTime
은 10억분의 1초 단위의 시간을 반환한다. -
measureTimeInMillis
는 1000분의 1초 단위의 시간을 반환한다.
17. Kotlin-Java interoperability
-
Kotlin 코드는 Java bytecode로 컴파일된다. 이것은 곧 Java와 상호운용interoperability이 된다는 것을 뜻한다.
-
Kotlin은 javascript로도 컴파일이 가능하다.
-
-
각 언어에 사용할 수 있는 annotation을 활용하면 Kotlin이 제공하는 기능을 최대한 사용할 수 있다.
-
annotation은 Kotlin compiler가 compile시 사용한다.
Null check
-
Java의 모든 객체는 언제든지 null이 될 수 있다.
-
Java 코드에서 반환하는 null이 될 수 있는
String
은String!
으로 나타낸다. -
String!
은 Kotlin의String
또는String?
타입 모두 될 수 있다는 것을 뜻한다. -
이것을 플랫폼 타입 이라고 한다.
-
우리가 코드 작성시에 사용하는 것은 아니며, IDE와 문서에서 자바 타입을 나타내기 위한 목적이다.
-
-
Java 코드에
@Nullable
어노테이션을 사용하면 Kotlin에서String?
타입으로 간주된다.import org.jetbrains.annotations.Nullable; public class Jhava { @Nullable public String getNull() { return null; } }
Type
-
Kotlin 타입은 Java 타입과 일대일로 매핑된다.
-
Kotlin에서는 기본 타입을 포함해서 모든 타입이 객체다.
fun main(args: Array<String>) {
val point: Int = 123
println(point.javaClass) // print 'int'
}
-
런타임시
point
변수는 Java의 기본 타입은int
로 변환된다. -
Kotlin에서는 기본 타입도 객체를 사용하므로 객체지향의 장점을 활용할 수 있다.
-
Kotlin은 런타임 시, 성능 향상을 위해 상황에 따라 자동으로 Java의 기본 타입으로 매핑시켜 준다.
Getter/Setter
-
Kotlin은 속성의 데이터를 갖는 후원 필드의 접근을 제한하며, 자동 생성된 게터와 세터를 통해서만 외부에서 사용할 수 있다.
JVM annotation
-
@JvmName
-
@file:
-
-
@JvmOverloads
-
@JvmField
-
@JvmStatic
-
@Throws
-
FunctionN
18. Coroutine
Coroutine
들어가기 앞서
코루틴(coroutine)은 루틴의 일종으로서, 협동 루틴이라 할 수 있다(코루틴의 "Co"는 with 또는 togather를 뜻한다). 상호 연계 프로그램을 일컫는다고도 표현가능하다. 루틴과 서브 루틴은 서로 비대칭적인 관계이지만, 코루틴들은 완전히 대칭적인, 즉 서로가 서로를 호출하는 관계이다. 코루틴들에서는 무엇이 무엇의 서브루틴인지를 구분하는 것이 불가능하다. 코루틴 A와 B가 있다고 할 때, A를 프로그래밍 할 때는 B를 A의 서브루틴으로 생각한다. 그러나 B를 프로그래밍할 때는 A가 B의 서브루틴이라고 생각한다. 어떠한 코루틴이 발동될 때 마다 해당 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다.
코루틴
코루틴은 컴퓨터 프로그램 구성 요소 중 하나로 비선점형 멀티태스킹(non-preemptive multitasking)을 수행하는 일반화한 서브루틴(subroutine)이다. 코루틴은 실행을 일시 중단(suspend)하고 재개(resume)할 수 있는 여러 진입 지점(entry point)을 허용한다.
- 서브루틴subroutine
-
-
여러 명령어를 모아 이름을 부여해서 반복 호출을 할 수 있게 정의한 프로그램 구성 요소(a.k.a. 함수).
-
객체지향 언어에서는 메서드도 서브루틴이라 할 수 있음.
-
서브루틴에 진입하는 방법은 오직 한 가지 뿐.
-
해당 함수를 호출하면 서브루틴의 맨 처음부터 실행 시작.
-
시작될 때 마다 활성 레코드activation record가 스택에 할당되면서 서브루틴 내부의 로컬 변수 등이 초기화 됨.
-
-
서브루틴 안에서 여러 번 return 을 사용할 수 있음.
-
서브루틴이 실행을 중단하고 제어를 호출한쪽caller에게 돌려주는 지점은 여럴 있을 수 있음.
-
다만 서브루틴에서 반환되고 나면 활성 레코드가 스택에서 사라지기 때문에 실행 중이던 모든 상태를 잃어버림.
-
서브루틴을 여러 번 반복 실행해도 항상 같은 결과를 얻게 됨(side-effect가 있지 않는 한)
-
-
-
- 비선점형non-preemptive
-
-
멀티태스킹의 각 작업을 실행하는 참여자들의 실행을 운영체제가 강제로 일시 중단시키고 다른 참여자를 실행하게 만들 수 없다는 뜻.
-
각 참여자들이 서로 자발적으로 협력해야만 비선점형 멀티태스킹이 제대로 작동할 수 있음.
-
- 멀티태스킹multitasking
-
-
여러 작업을 동시에 수행하는 것처럼 보이거나, 실제로 동시에 수행하는 것.
-
Coroutine
-
코루틴coroutine은 코틀린kotlin과 이름이 비슷해서 코틀린 기능이라고 생각할 수 있지만, 여러 언어에서 지원하는 개념.
-
concurrency design pattern.
-
코틀린 팀은 코루틴을
경량 스레드: Light-weighted thread
로 정의.-
한 thread에서 다수의 coroutine을 수행할 수 있음과 Context Switching이 필요없기 때문에 이렇게 부름
-
-
코루틴은 코드 블록을 실행하고 비슷한 라이프 사이클을 가졌지만 반환 값이나 예외를 사용해 완료할 수 있는 아주 가벼운 스레드다.
-
기술적으로 코루틴은 중지 가능한 계산의 인스턴스며, 일시 중단할 수 있는 계산이다.
-
코루틴은 특정 스레드에 바인딩되지 않으며, 한 스레드에서 일시 중지하고 다른 스레드에서 재개할 수 있다.
코루틴을 사용하면…
-
Thread 보다 리소스를 더 효율적으로 사용하면서 더 쉽게 작동.
-
Thread는 아니지만 비동기적인asynchronous 프로그래밍이 가능하게 만들어줌.
-
Thread는 '중단block'되지만, Coroutine은 '보류suspend' 됨.
-
Thread보다 더 좋은 성능을 제공.
-
Thread는 중단될 때 중단이 풀릴 때까지 아무 일도 할 수 없음.
-
코루틴은 Thread에 의해 실행되며, 코루틴을 실행하는 스레드를 중단시키지 않음.
-
대신에 보류된 함수를 실행하는 Thread는 다른 Coroutine을 실행하는 데 사용될 수 있음.
-
내부적으로 실행이 보류되는 함수를
suspend
키워드로 나타냄.
-
@startuml [Component] --> Interface1 [Component] -> Interface2 @enduml
Thread vs Coroutine
-
아래 예제는
Thread.sleep
을 사용하 I/O 계산을 시뮬레이팅한다.코루틴이 없는 간단한 예제 1import kotlin.concurrent.thread fun main(args: Array><String>) { thread { Thread.sleep(1_000) println("World!") } print("Hello ") Thread.sleep(2_000) }
-
보다 예쁜 코드는 아래와 같다.
fun main(args: Array<String>) { var computation = thread { Thread.sleep(1_000) println("World!") } print("Hello ") computation.join() // (1) }
-
이 메서드가 완료되기를 기다리므로, 예측한 시간을 기다리는 것보다 훨씬 똑똑한 방식이다.
-
-
스레드는 JVM에서 비동기 동시 애플리케이션의 빌딩 블록
-
JVM 스레드는 대부분 (프로세서 내의 코어 같은)하드웨어 스레드에 의해 백업된다.
-
하드웨어 스레드는 여러 소프트웨어 스레드(JVM 스레드는 소프트웨어 스레드의 일종이다)를 지원할 수 있지만, 오직 하나의 소프트웨어 스레드만이 주어진 시간에 실행된다.
-
OS는 각 하드웨어 스레드에서 실행되는 소프트웨어 스레드를 결정하고 생존한 스레드 사이를 빠르게 전환하므로, 여러 소프트웨어 스레드가 동시에 실행되는 것처럼 보이게 한다. (라운드로빈?)
-
JVM 스레드는 매루 빠르고 반응이 좋지만 비용이 크다.
-
각 스레드는 생성, 처분, 컨텍스트 스위치 시 CPU 타임과 메모리를 소모한다.
-
이 비용이 상대적으로 높기 때문에 JVM 애플리케이션은 많은 수의 스레드를 가질 수 없다.
-
-
현재의 JVM 애플리케이션에서 스레드를 생성하고 파괴하는 것은 나쁜 습관 습관으로 간주된다.
-
대신 스레드를 관리하고 재사용해 생성과 처분의 비용을 줄일 수 있는 추상적인 Excutor를 사용한다.
fun main(args: Array<String>) { val executor = Executors.newFixedThreadPool(1024) repeat(10_000) { executor.submit { Thread.sleep(1_000) println(".") } executor.shutdown() } }
-
Kotlin의 Coroutine
-
Kotlin 1.1부터 코루틴 API 제공
-
Kotlin 1.3부터 표준 라이러리에 정식 포함
-
코루틴을 사용하려면 코루틴 확장 라이브러리가 필요하다.
-
코루틴 라이브러리에서 제공하는
async
함수를 사용하면 코루팀을 생성할 수 있다.
Suspending functions
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
fun fetchCharacterData(): Deferred<CharacterGenerator.CharacterData> { // (1)
return GlobalScope.async { // (2)
val apiData = URL(API_URL).readText()
CharacterGenerator.fromApiData(apiData)
}
}
-
Deferred
는 우리가 요청할 때까지 데이터를 반환하지 않는다. -
async
는 하나의 인자로 람다를 받으며, 람다에 백그라운드에서 처리할 작업을 지정한다.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class AppService {
fun onCreate() {
GlobalScope.launch(Dispatchers.Main) { // (1) (2)
characterData = fetchCharacterData().await() (4)
displayData() // (3)
}
}
}
-
launch
함수는 코루팀을 생성하며,launch
함수는 블록안에 지정한 람다(코루틴 코드)를 시작시킨다. -
launch
함수의 파라미터에는 해당 작업이 실행되는 스레드를 나타낸다.Dispatcher.Main
은 안드로이드의 UI 스레드이다. -
이 코드를 안드로이드로 예를 들었을 때,
displayData()
함수는 UI를 변경시키는 작업이므로, UI 스레드를 지정시켰다. -
코루틴 컨텍스트의 기본 인자는
CommonPool
이다. 이것은 코루틴이 실행될 때 사용될 수 있는 백그라운드 스레드 풀이다.
따라서await
를 호출할 때 해당 작업은 CommonPool의 스레드 중 하나를 사용한다.
launch vs async/await
-
async
,launch
함수를 coroutine builder function 이라고 한다.-
이 함수들은 특정 방법으로 작업을 수행하도록 코루틴을 설정한다.
-
-
launch
는 우리가 지정한 작업을 올바르게 수행하는 코루틴을 빌드한다. -
async
는 지연된(아직 완료되지 않은) 작업을 나타내는Deferred
를 반환하는 코루팀을 빌드한다.-
즉, 해당 작업이 바로 시작되어 끝나는 것이 아니다.
-
-
Deferred
타입은await
함수를 제공한다.-
await
함수는 우리가 원하는 작업 수행 시점에 호출한다. -
await
함수는 지연된 작업이 완료될 때까지 다음에 할 작업을 보류한다.
-
-
Deferred
는 Java의Future
와 유사한 방법으로 동작한다.
yield
reactive vs coroutine
-
리액티브 프로그래밍은 현재의 프로그래밍 패러다임으로, 변화의 전파에 대해 말한다.
-
즉, 일련의 상태를 월드로 표현하는 대신 리팩티브 프로그래밍 모델 행동으로 표현한다.
-
-
리액티브 프로그래밍은 데이터 스트림과 변화의 전파를 중심으로 하는 비동기 프로그래밍 패러다임이다.
-
간단히 말하자면 데이터/데이터 스트림에 영향을 주는 모든 변경점을 관련된 당사자에게 전파하는 프로그램을 리액티브 프로그램으로 부른다.
-
-
리액티브 매니페스토Reactive Manifesto(https://www.reactivemanifesto.org/)는 다음과 같은 네가지 리액티브 원리를 정의하느 문서다.
-
반응Responsive
-
복원Resilient
-
탄력Elastic
-
메시지 중심Message-driven
-
Fiber
WebClient with Coroutines
suspending extension 함수인 awaitBody()
를 활용할 수 있다.
val htmlResponse = webClient.get()
.uri("https://www.baeldung.com/")
.retrieve()
.awaitBody<String>()
retrieve()
함수는 API 요청의 응답 코드가 2xx일 경우에만 반환하고, 나머지는 예외를 던진다. 다양한 응답 코드에 대한
핸들링이 필요하다면 awaitExchange()
확장 함수를 활용할 수 있다.
val response: ResponseEntity<String> = webClient.get()
.uri("https://www.baeldung.com/")
.awaitExchange()
.awaitEntity()
위와 같은 코드에서는 API 응답이 ResponseEntity
로 반환되므로, 상태 코드에 따른 처리가 가능해진다.
@GetMapping("/payments/{id}/")
suspend fun fundPayment(@PathVariable id: String): PaymentView {
val
return PaymentView()
}
Flow
fun simple(): List<Int> = listOf(1, 2, 3)
fun main() {
simple().forEach { println(it) }
}
fun simple(): Sequence<Int> = sequence {
for (i in 1..3) {
Thread.sleep(100)
yield(i)
}
}
fun main() {
simple().forEach { println(it) }
}
-
첫번째 코드와 동일하지만 각 숫자를 출력할 때마다 100ms를 기다림
-
(위 코드 기준으로) 메인 스레드를 스탑함
suspend fun simple(): List<Int> {
delay(100)
return listOf(1, 2, 3)
}
fun main() = runBlocking<Unit> {
simple().forEach { println(it) }
}
suspend fun simple(): Flow<Int> = flow{
for (i in 1..3) {
delay(100)
emit(i)
}
}
fun main() = runBlocking<Unit> {
simple().collect { println(it) }
}
-
List<Int>
를 반환하면 한번에 모든 값을 반환함 -
동기식으로 계산된 값에
Sequence<Int>
를 사용하것처럼 비동기식으로 계산되는 값의 스트림을 나타내기 위해Flow<Int>
를 사용할 수 있음 -
Flow는 cold stream.
-
collect가 호출될때까지 실행되지 않음
-
-
multiple flow로 동작하도록 하지 않는이상 sequential하게 동작함
cold stream vs. hot stream
Coroutine Context
-
코루틴은 항상 컨텍스트에서 실행된다.
-
asyncsk launch같은 코루틴 빌더는 기본적으로 DefaultDispatcher 디스페처를 사용한다(현재 코루틴 버전 0.2.1에서는 DefaultDispatcher와 CommonPool은 동일하다.)
-
코루틴 컨텍스트는 값을 보유할 수도 있다.
Channel
-
두 코루틴이 통신할 수 있는 방법
-
Deferred<T>: 값 하나는 가능하다 시쿼스나 스트림을 불가능
fun main(args: Array<String>) = runBlocking { val result = CompletableDeferred<String>() val world = launch { delay(500) result.complete("World") } val hello = launch { println("Hello ${result.await()}") } hello.join() world.join() }
-
Channel
fun main(args: Array<String>) = runBlocking { val channel = Channel<String>() val world = launch { delay(500) channel.send("World") } val hello = launch { println("Hello ${result.receive()}") } hello.join() world.join() }
-
당연히 이러한 코드는 채널의 의도된 용도가 아니다.
-
-
-
일반적으로 단일 혹은 여러 코루틴은 채널로 메시지를 보낸다.
fun main(args: Array<String>) = funBlocking<Unit> { val channel = Channel<Char>() val sender = launch { repeat(1000) { delay(10) channel.send('.') delay(10) channel.send(',') } channel.close() } for (msg in channel) { // (1) println(mst) } sender.join() }
-
채널 자체는 반복자이므로 for 블록에서 사용할 수 있다.
-
-
위 코드는 produce 빌드를 사용해서 더 간단하게 작성할 수 있다.
fun dotsAndCommas(size: Int) = produce { // (1) repeat(size) { delay(10) channel.send('.') delay(10) channel.send(',') } } fun main(args: Array<String>) = funBlocking<Unit> { val channel = dotsAndCommas(1000) for (msg in channel) { // (1) println(mst) } }
-
produce 빌더는 수신만을 위한 채널 타입 ReceiveChannel<T>를 반환한다.
-
-
채널 파이프라인
-
채널이 있을 때 파이프라인과 같은 관련된 패턴을 가질 수 있다.
-
파이프라인은 유닉스 파이프나 엔터프라이즈 인티그레이션 패턴EIP, Enterprise Integration Pattern 같이 소비자와 생산자를 연결하는 일련의 채널이다.
-
Actor
-
비동기 코드를 다를 때의 주요 관심사는 변경 가능한 상태를 처리하는 방법
-
액터는 메시지를 통해 외부 월드 및 다른 액터와 상호작용하는 일종의 오브젝트다.
-
액터 오브젝트는 메시지를 통해 외부적으로 수정 및 접근할 수 있지만, 직접 할 수는 없는 private 내부 변경 가능한 상태를 가진다.
ETC
-
Iterator
-
Mono, Flux, Flow, Sequence
-
Sequence는 코루틴 빌더가 아닌 kotlin 에서 제공하는 빌더
-
suspend
Appendix A: TODO
Appendix B: Spring Framework in Kotlin
Validation
-
Kotlin에서 속성과 Java의 필드는 다르므로, 어노테이션을 필드에 추가하려면
@field:
을 활용해야 한다. -
val, var 모두 적용 가능하다.
-
https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets
Appendix C: Test
Kotest
-
Kotlin 기반의 멀티플랫폼 테스트 프레임워크
-
Test Framework / Assertions Library / Property Testing 의 subproject로 구성
-
BDD 스타일을 포함한 총 10가지 테스트 스타일 지원
-
BehaviorSpec (like Spock)
-
DescribeSpec (like RSpec)
-
FetureSpec (like Cucumber)
-
-
Assertions
-
JUnit assertion 대비 직관적인 문법
-
다양한 type에 특화된 Matcher 제공
-
JSON, Android 용 별도 Matcher 모듈 제공(실제 JSON content 기준으로 검증 가능)
-
-
JVM위에서 Kotest는 JUnit Platform 의존성 필요
MockK
-
Kotlin 기반의 Mocking 라이브러리
-
Mockito와 흡사하게 기존적인 mocking feature 지원
-
Kotlin 특화 기능 지원
-
Mocking Kotlin singleton object
-
Mocking suspend function (coroutine)
-
Mocking extension function
-
Accessing property backing field
-
-
Annotation기반의 mock 객체 생성 지원
-
Strict vs. Relaxed mock
-
Strict: 명시되지 않은 동작이 호출될 경우 exception 발생 (default)
-
Relaxed: 명시되지 않은 동작이 호출될 경우 기본값 반환
-
Examples
-
BehaviorSpec
-
테스트 시나리오가 복잡하고 다양한 전제조건이 필요한 경우에 사용
-
Service/Controller 대상의 unit/integration test
-
-
DescribeSpec
-
테스트 시나리오가 단순하고 전제조건이 불필요한 경우에 사용
-
Utility class 혹은 외부 서비스 client 용 class 대상의 unit test
-
Appendix D: Kotlin version
1.3
-
Coroutine이 표준 라이브러리에 정식 포함
1.5
-
value class
-
sealed interface
Appendix E: Code Quality Tools
Tip
-
-
alternative: ksp(Kotlin Symbol Processing)
-