코틀린 완벽 가이드
개요
-
인텔리제이네 내장된 코틀린 런타임 라이브러리는 자동 업데이트 되므로 고정된 버전이 팔요하다면 라이브러리 폴더를 카피한 후 관리가 필요하다.
-
빌드시스템으로는 그레들이나 그루비가 있지만 이 책에서는 인텔리제이 내장 빌드시스템을 사용한다.
-
jvm버전은 jre 바전과 같거나 낮게 설정해야 한다.
-
Tool > kotlen > kotlen > repl 로간단히 코드를 실행해볼 수 있다. -
온라인 플레이그라운드에서 테스트해볼 수도 있다.
-
이클립스에서 사용하려면 코틀린 플러그인을 직접설치해야한다.
-
식별자를 역인용부호로 감싸서 쓸수 가 있는데 불가피하게 예약어를 식별자로 쓸 경우를 위한 것이다.
-
val는 불변변수 var은 가변변수
-
코틀린은 a = b = c 와 같은 연쇄 대입문은 쓸 수 없다
-
자바에서는 원시타입을 감싼 확실한 박싱타입이 있지만 코틀린은 문맥에따라 알아서 박싱타입을 결정한다.
타입
-
타입 에너테이션으루안붙이면 int 나 long으로 알어서 추론하지만, 사용자가 정확히 byte나 short으로 정의해줘도 된다.
-
부동소수점 리터럴으누자동으로 double이 되지만 f를 븥이면 float이 된다
-
코틀린은 비트연산을 특수문자기호 대신 예약어롤 사용해서 처리해야 한다.
-
Chat 타입은 문자열 한글자
-
Char타입에 산술연산을 하면 인코딩 키 순서상의 키값에 연산을 해준다.
-
정수형에서 더 큰 범위에 타입에 작은 타입을 담는건 허용되며 아닐경우에는 타입변환필요하다.
-
andor연선은&&||연산보다 우선순위가 높다.
문자열
-
템플릿 문자열을 지원한다 js처럼 역인용부호로 안싸도 된다
-
따움표 세개와
trimIndent를 사용하면 여러줄 리터럴 문자를 사용가능하다. -
자바에서는
.equals로 문자열을 비교하지만 코틀린은 그럴필요없다==으로 편의성 증가
배열
emptyArray나arrayOf로 정햐진 길이의 배열생성
val squares = Array(size) { (it + 1)*(it + 1) }
val squares = Array(size) { (it + 1)*(it + 1) }로 생성가능 it은 람다의 this 표현.
*외에도 IntArry나 CharArray 같은 내장함수 사용가능
- 배열은
copyOf메서드로 복사 가능하다
문자열과 달리 배열에 대한 ==와 != 연산자는 원소 자체를 비교하지 않고 참조를 비교한다.
intArrayOf(1, 2, 3) == intArrayOf(1, 2, 3) // false
- 배열 내용을 동등 비교하고 싶으면
contentEquals()함수를 사용하라.
intArrayOf(1, 2, 3).contentEquals(intArrayOf(1, 2, 3)) // true
모듈
-
import디렉티브를 사용해야kotlin.math.PI라는 전체 이름이 아니라 PI라는 간단한 이름으로 이 값을 사용할 수 있다. -
코틀린에서 함수의 파라미터는 무조건 재할당 할 수 없다.
-
callByValue callByReference 규칙은 js와 비슷하다
-
Unit 타입은 void에 해당하는 코를린의 타입이다
fun circleArea(radius: Double): Double = PI * radius * radius 와 같이 한즐로 정의할수 있다.
함수
- 이름 붙은 인자는 위치가 아니라 파라미터의 이름을 명시함으로써 인자를 전달하는 방식이다. 예를 들어 rectangleArea() 호출을 다음과 같이 할 수도 있다.
rectangleArea(width = w, height = h)`
- 심지어는 다음 코드처럼 호출해도 된다.
rectangleArea(height = h, width = w)
이름 붙은 인자를 사용하면 인자의 실제 순서는 중요하지 않다. 따라서 방금 본 두 함수 호출은 모두 rectangleArea(w, h)와 똑같은 뜻이다.
오버로딩시 상위타입의 인자를 쓸수록 덜 구체적인 함수로 인식한다.
-
코틀린에서는 * 이 스프레드 연산자다.
-
한 함수에 둘 이상의 vararg선언은 할수없다.
접근자
-
private 는 같은 파일내에서만 사용 가능한 함수다.
-
internal은 같은 모듈 네에서만 사용가능하며.
-
public은 전역에서 사용 가능하다.
-
코틀린은 함수 내에서 함수 선언이 가능
패키지 선언 과 import
-
파일 제일 위에 패키지명을 기입하면 파일이 속하는 패키지를 정의할 수 있다.
-
다른 경로의 패키지 코드를 사용하려면 패키지의 풀패스를 전부 적어줘야한다
-
import 구문을 사용하면 패키지 풀패스를 적지않고 다른 패키지의 코드사용가능.
-
소스파일트리와 페키지계층구조는 틀리다. 하지만 파일트리와 패키지계픙을 같게 하는게 편하고 일반적이다.
-
As 가능
import foo.readInt as fooReadInt
import bar.readInt as barReadInt
if 식
코틀린은 삼항 연산자 대신 if문을 식으로 쓸수있다
Range
val twoDigits = 10 until 100 // 10..99와 같음. 100은 포함되지 않음
ep 2 // 15, 13, 11, 9
- 리턴문과 함께 쓰인 when
fun hexDigit(n: Int): Char {
when {
n in 0..9 -> return '0' + n
n in 10..15 -> return 'A' + n – 10
else -> return '?'
}
}
- = 을 이용하면 리턴문 생략 가능
- when 뒤에 괄호 있는 방식과 없는 방식 으로 사용가능
fun numberDescription(n: Int): String = when {
n == 0 -> "Zero"
n == 1 || n == 2 || n == 3 -> "Small"
n in 4..9 -> "Medium"
n in 10..100 -> "Large"
n !in Int.MIN_VALUE until 0 -> "Negative"
else -> "Huge"
}
fun numberDescription(n: Int, max: Int = 100): String = when (n) {
0 -> "Zero"
1, 2, 3 -> "Small"
in 4..9 -> "Medium"
in 10..max -> "Large"
!in Int.MIN_VALUE until 0 -> "Negative"
else -> "Huge"
}
이터레이터 Iterator
- 코틀린에서는 문자열에 대한 루프를 직접 수행가능.
- 코틀린에거는 어떤 객체든 iterable 함수를 제공하면 루프를 돌릴수 있다
- 레이블 붙은 break / continue 가능
tailrec
- 재귀 함수 앞에 tailrec 키워드를 붙이면 컴파일러가 재귀함수를 비재귀 함수로 바꿔준다. 조건은 함수의 끝이 항상 재귀호출이어야 한다.
try 식
- if식과 마찬가지로 try 도 식으로 쓸수ㅜ있다
- try finally르르식으로ㅠ사용항경우 파이널리는 리턴값에 영향을 미치지ㅡ않는다
클래스
코틀린의 클래스는 자기자신의 참조를 위해 this를 꼭 쓰지 않아도 되며 메서드 내의변수와 이름이 겹칠때만 구분해서 쓰면 된다.
코틀린에서는 한파일에 여러 클래스를 만들어 써도 된다.
class Person(firstName: String, familyName: String) {
val fullName = "$firstName $familyName"
}
- 생성자 사용 코드
fun main() {
val person = Person("John", "Doe") // 새 Person 인스턴스 생성
println(person.fullName) // John Doe
}
- 인스턴스 생성
class Person(fullName: String) {
// error: property must be initialized or be abstract
val firstName: String
val familyName: String
init {
val names = fullName.split(" ")
if (names.size == 2) {
firstName = names[0]
familyName = names[1]
}
}
}
-
생성될때 init 스코프 안에 코드가 실행되며 상태 초기화를 시킨다. 중복선언 가능하며 순서대로 실행된다. 초기화 안된 상태에 대해서는 컴퍼일러가 오류메세지로 알려준다.
-
하지만 코틀린은 간단하게 생성자 파라미터의 값을 멤버 프로퍼티로 만들 수 있는 방법을 제공한다.
class Person(val firstName: String, familyName: String) {
// firstName은 생성자 파라미터를 가리킴
val fullName = "$firstName $familyName"
fun printFirstName() {
println(firstName) // firstName은 멤버 프로퍼티를 가리킴
}
}
- 본체가 없는 클래스도 많이 사용되며 권장된다
class Person(val firstName: String, val familyName: String = "")
- 하지만 constructor가 완전히 없는건 아니다 부 생성자로 사용하여 오버로드할수있다
- 부생성자를 이용하여 주 생성자를 호출하고 있다
class Person(val fullName: String) {
constructor(firstName: String, familyName: String):
this("$firstName $familyName")
}
-
부생성자는 주생성자나 다른 부 생성자를 호출할수 있지만 부생성자의 파라미터에는 val/var 키워드를 쓸 수 없다
-
주생성자의 가시성을 지정하려면 constructor 키워드를 꼭 명시해야 한다.
class Empty private constructor() {
fun showMe() = println("Empty")
}
내포된 클래스
- 내포된 클래스에 inner를 붙이면 자신을 둘러싼 외부 클래스의 현재 인스턴스에 접근할 수 있다.
class Person(val firstName: String, val familyName: String) {
inner class Possession(val description: String) {
fun showOwner() = println(fullName())
}
private fun fullName() = "$firstName $familyName"
}
// 여기서 내부(inner)2 클래스 생성자를 호출할 때 person.Possession("Wallet")처럼 외부 클래스 인스턴스를 지정해야 한다.
fun main() {
val person = Person("John", "Doe")
// Possession 생성자 호출
val wallet = person.Possession("Wallet")
wallet.showOwner() // John Doe
}
- this 에 실벽자 이용하기
- 이너나 아우터 의this는 생략 가능하지만 아래와 같이 명시할수도있다.
- 한정시킨 this 식에서 @ 기호 다음에 오는 식별자는 외부 클래스의 이름이다.
class Person(val firstName: String, val familyName: String) {
inner class Possession(val description: String) {
fun getOwner() = this@Person
}
}
- 함수 안에서도 클래스 정의가 가능하며 함수내에서마누사용 가능하다
- 지역 클래스 내에서도 클로저가 동작한다
- 지역 클래스의 내포된 클래스는 항상 inner 클래스여야 한다
nullable 타입
코틀린의 타입시스템은 널이될수있는 타입과 그렇지 않은 타입을 구분할 수 있으며, 기본적으로 모든 참조타입은 널이 될수없는 타입이다.
- 코틀린에서 String? 같은 타입은 널이 될 수 있는 타입(nullable type)이라고 불린다.
fun isBooleanString(s: String?) = s == "false" || s == "true"
- 안전한 호출 연산자
println(readLine()?.toInt()?.toString(16))
- 엘비스연산자는 nullable값에 디폴트를 제공
fun sayHello(name: String?) {
println("Hello, " + (name ?: "Unknown"))
}
val currentName = name ?: return "Unknown"
늦은 초기화 lateinit (!!와 비슷하다)
- 원시타입에 lateinit은 사용 불가능하다.
- 코틀린에서 String 은 참조타입이다.
class Content {
lateinit var text: String // var 로 선언해야함
fun loadFile(file: File) {
text = file.readText()
}
}
커스텀 접근자(get, set)와 뒷받침하는 필드
- 뒷받침하는 필드 참조는 field라는 키워드를 사용하며 접근자의 본문 안에서만 유용하다.
- lateinit 프로퍼티는 커스텀접근자를 .생성할수 없다
class Person(val firstName: String, val familyName: String, age: Int) {
val age: Int = age
get(): Int {
println("Accessing age")
return field
}
}
- private 로 명시하면 클래스 밖에서는 변경할수 없다
class Person(name: String) {
var lastChanged: Date? = null
private set // Person 클래스 밖에서는 변경할 수 없다
..
}
val text by lazy {
File("data.txt").readText()
}
초기값 세팅을 미룰때 by lazy
-
불변에 초기화를 늦출 필요가 있을때
by lazy를 쓰면 편리lateinit은 항상 가변에만 할수있고 원시자료형에는 안된다. -
by lazy는 스마트캐스트 안됨
객체선언이 위와같이 가능하며 타입과 값 모두다로 사용가능
object Application {
val name = "My Application"
override fun toString() = name
fun exit() { }
}
import Application.exit
fun main() {
println(Application.name) // 전체 이름을 사용
exit() // 간단한 이름을 사용
}
- 하지만 객체의 모든 멤버가 필요할 때 임포트 문으로 임포트할 수는 없다.
- 이런 제약을 가하는 이유는 객체 정의 안에는 다른 클래스 정의와 같이 toString()이나 equals()와 같은 공통 메서드 정의가 들어있기 때문
import Application.* // Error
- 코틀린은 유틸리티 객체가 필요없음
- 내포객체 동반객체라고 불림 다 자신을 둘러싼객체의 private에도 접근 가능
- companion 지시어로 편리하게 동반객체 사용할수 있다.
class Application private constructor(val name: String) {
companion object Factory {
fun create(args: Array<String>): Application? {
val name = args.firstOrNull() ?: return null
return Application(name)
}
}
}
fun main(args: Array<String>) {
val app = Application.create(args) ?: return
println("Application started: ${app.name}")
}
- 객체도 식으로 표현할수 있다.
- 하지만 객체 자체는 함수 안에 지역객체로 정의할수는 없다.
val button = object : Clickable {
override fun click() {
println("버튼이 클릭됨")
}
}
-
똑같은 함수가 리턴한 완전 똑같은 객체가 있다고 해도 둘의 타입은 다르다.
-
객체식(익명객체)은 지역선언이나 비공개 선언에만 사용할수 있다
- 다른파일에서 사용하면 리턴되느누익명갹체를 Any로 추론해버린다.
- public 으로 정상적으로 사용하려면 명시적인 클래스로 선언하거나, interface 를 구현한 객체로 정의하면 된다.
-
interface앞에fun을 붙인 메서드가 하나뿐인 인터페이스를 사용하면 SAM 인터패이스라고 하는 람다표현삭으로 객채를 정의하여 사용헐수 있다.
val obj: MySAMInterface = { name -> println("Hello, $name!") }
fun main() {
obj.greet("Bob") // Hello, Bob!
}
- 마지막 인자가 람다 표현식이면 괄호 밖에서 사용할수 있다.
fun main() {
val lessThan: (Int, Int) -> Boolean = { a, b -> a < b }
println(lessThan(1, 2)) // true
}
fun main() {
val evalAtZero: ((Int) -> (Int)) -> Int = { f -> f(0) }
println(evalAtZero { n -> n + 1 }) // 1
println(evalAtZero { n -> n - 1 }) // -1
}
- 사용하지 않는 람다파라미터를 밑즐로 표현 가능
fun main() {
println(check("Hello") { _, c ->c.isLetter() })
- 꼭 람다식을 쓰지않고 익명함수를 써도 된다
- 람다식 단축표현을 써도 된다
fun isCapitalLetter(c: Char) = c.isUpperCase() && c.isLetter()
fun main() {
println(check("Hello") { c -> isCapitalLetter(c) }) // false
// 또는
println(check("Hello") { isCapitalLetter(it) }) // false
// 또는
println(check("Hello", fun(c: Char): Boolean { return isCapitalLetter(c) })) // false
}
- 하지만 코틀린에는 이미 존재하는 함수 정의를 함수 타입의 식으로 사용할 수 있는 더 단순한 방법이 있다. 호출 가능 참조(callable reference)를 사용하면 된다.
fun main() {
println(check("Hello", ::isCapitalLetter)) // false
}
fun main() {
val isJohn = Person("John", "Doe")::hasNameOf
println(isJohn("JOHN")) // true
println(isJohn("Jake")) // false
}
class Person(val firstName: String, val familyName: String) {
fun hasNameOf(name: String) = name.equals(firstName, ignoreCase = true)
}
fun main() {
val isJohn = Person("John", "Doe")::hasNameOf
println(isJohn("JOHN")) // true
println(isJohn("Jake")) // false
}
-
오버로드 되는 메서드는 추론할수 없으므로 구체타입을 애너테이션 해줘야함
-
바인딩된 호출 가능 참조는 게터 세터에도 접근할수 있음
inline 키워드
-
함수정의 앞에 inline 키워드를 붙이면 컴파일시 함수 호출 자리가 코드가 그대로 인라인되는 방식으로 바뀐다. 코드가 늘어나지만 성능이점이 생김.
-
다만 인라인 함수의 인자로 받는 고차함수도 다 함께 인라인됨을 유의해야한다. 이를 막으려면 noinline 키워드로 방지할 수 있다.
-
인라인 함수에는 비공개 맴버를 넣지 못한다. (비공개 함수가 인라인되면 코드가 노출되어 버리므로)
-
람다함수의 리턴문은 람다 함수 자체가 아닌 자신을 둘러싼 상위 함수에 대해 리턴되므로 주의.
확장함수
-
확장 함수는 어떤 클래스의 멤버인 것처럼 호출할 수 있는 (그러나 실제로는 멤버가 아닌) 함수를 뜻한다.
-
확장함수는 원래 객체의 비공개 맴버나 함수에는 접근할 수 없다.
-
클래스 본문에 정의함 확장함수는 클래스 본문에 들어있는 다른 함수들과 마찬가지로 클래스의 비공개 멤버에 마음대로 접근할 수 있다.
class Person(val name: String, private val age: Int) {
// Ok: age에 접근할 수 있음
fun Person.showInfo() = println("$name, $age")
}
-
확장함수는 바인딩된 호출 가능 참조로도 사용가능하다.
-
확장함수와 원래 클래스맴버와 이름이 겹칠경우 원래 클래스맴버가 우선 이다.
interface Truncated {
val truncated: String
val original: String
}
private fun String.truncator(max: Int) = object: Truncated {
override val truncated
get() = if (length <= max) this@truncator else substring(0, max)
override val original
get() = this@truncator
}
fun main() {
val truncator = "Hello".truncator(3)
}
널이 될 수 있는 수신 객체 타입
?를 붙이면 된다.
fun String?.truncate(maxLength: Int): String? {
if (this == null) return null
}
확장 프로퍼티는 게터나 세터를 이용해서 정의
var IntArray.midValue
get() = this[midIndex]
set(value) {
this[midIndex] = value
}
동반객체에도 확장가능
fun IntRange.Companion.singletonRange(n: Int) = n..n
람다 수신객체 지정함수란 Int.(Int) -> Int
- 람다에 적용시킬 객체를 this로 하는 람다함수를 적용시킨다.
fun aggregate(numbers: IntArray, op: Int.(Int) -> Int): Int {
var result = numbers.firstOrNull()
?: throw IllegalArgumentException("Empty array")
for (i in 1..numbers.lastIndex) result = result.op(numbers[i])
return result
}
fun main() {
val numbers = intArrayOf(1, 2, 3, 4)
println(aggregate(numbers, Int::plus)) // 10
}
영역함수
-
영역 함수가 기본적으로 하는 일은 여러분이 인자로 제공한 람다를 간단하게 실행해주는 것이다.
-
전체적으로는
run,let,with,apply,also라는 다섯 가지 표준 영역 함수가 있다.
class Address {
var zipCode: Int = 0
var city: String = ""
var street: String = ""
var house: String = ""
fun post(message: String): Boolean {
"Message for {$zipCode, $city, $street, $house}: $message"
return readLine() == "OK"
}
}
fun main() {
val isReceived = Address().run {
// Address 인스턴스를 this로 사용할 수 있다
zipCode = 123456
city = "London"
street = "Baker Street"
house = "221b"
post("Hello!") // 반환값
}
if (!isReceived) {
println("Message is not delivered")
}
}
- with도 비슷한 역할을 하는데 단지 run 처럼 확장되는 형식이 아니다. 아래처럼
fun main() {
val address = run {
val city = readLine() ?: return
val street = readLine() ?: return
val house = readLine() ?: return
Address(city, street, house)
}
println(address.asText())
}
-
let도run과 비슷 하지만 this를 사용하지 않고 받고it을 사용한다. -
apply도 비슷하지만 리턴이 없다 -
also도 비슷하지만it이 사용된다. -
내부확장객체를 사용할때
with를 사용하면 문맥컨텍스트로 인한 문제를 해결하는 트릭이된다
클래스
Enum 클래스
enum class
enum class Direction {
NORTH, SOUTH, WEST, EAST;
}
fun main() {
println(Direction.WEST.name) // WEST
println(Direction.WEST.ordinal) // 2
}
enum class RainbowColor(val isCold: Boolean) {
RED(false), ORANGE(false), YELLOW(false),
GREEN(true), BLUE(true), INDIGO(true), VIOLET(true);
val isWarm get() = !isCold
}
fun main() {
println(RainbowColor.BLUE.isCold) // true
println(RainbowColor.RED.isWarm) // true
}
Data 클래스
- 데이터 클래스는 동등비교를 기본 비교연산자
==으로 할수 있게 해준다equals나hashcode,toString를 알아서 만들어준다. - 데이터클래스는 카피메서드도 제공한다.
구조분해선언
- 이름 매핑이 아닌 순서매핑임을 주의해야함
val (firstName, familyName, age) = person
- for 루프에서 구조 분해를 사용할 수도 있다.
val pairs = arrayOf(1 to "one", 2 to "two", 3 to "three")
for ((number, name) in pairs) {
println("$number: $name")
}
- 아직 코틀린은 중첩된 구조분해를 할 수 없다.
인라인 클래스
- 래퍼클래스를 이용해 어답터패턴을 구현하면 편리하지만 비용 성능 문제가 생긴다. 코틀린은 이를 위해 인라인클래스를 제공한다.
- @JvmInline을 value class 앞에 반드시 붙여줘야 한다.
@JvmInline
value class Dollar(val amount: Int) // amount의 단위는 센트
@JvmInline
value class Euro(val amount: Int) // amount의 단위는 센트
- 인라인 클래스의 주생성자에는 불변 프로퍼티 하나만 선언할 수 있다.
- 인라인 클래스의 프로퍼티는 접근자로서만 명시할수 있다
- 부호없는정수타입은 인라인 클래스로 구현한것이다
- 부호가 있는 타입과 없는 타입은 호환 되지는 않지만 캐스팅은 할 수 있다
컬랙션
리스트
-
코틀린 컬렉션 타입은 기본적으로 네 가지로 분류할 수 있다.
배열,이터러블(iterable),시퀀스(sequence),맵(map)이다. -
불변컬렉션은 공변성을 보장한다. 하지만 가변컬렉션은 공변성을 허용하지 않는다.
-
리스트는
링크드리스트,해시셋,링크드해시셋,트리셋이 있다
맵
-
맵은 키 벨류 기반 자료형이로 맵 자체는 컬렉션의 하위집합아니지만 컬렉션처럼 사용할수 있다.
- entries, values, keys 를사용하여
-
map도Pair를 사용하여 세팅할수도 있다 -
맵의 표준 구현에는
HashMap,LinkedHashMap,TreeMap등이 있다. 이들의 성질은 각각에 대응하는Set클래스의 성질과 비슷하다. -
객체에
compareTo가 구현되어 있으면 같은 객체끼리 비교 연산이 가능해진다. -
emptyList()/emptySet(): 불변인 빈 리스트/집합 인스턴스를 생성한다. -
listOf()/setOf(): 인자로 제공한 배열에 기반한 불변 리스트 / 집합 인스턴스를 만든다. -
listOfNotNull(): 널인 값을 걸러내고 남은 원소들로 이뤄진 새 불변 리스트를 만든다. -
mutableListOf()/mutableSetOf(): 가변 리스트/집합의 디폴트 구현 인스턴스를 만든다. -
arrayListOf(): 새로운 ArrayList를 만든다. -
hashSetOf()/linkedSetOf()/sortedSetOf():HashSet/LinkedHashSet/TreeSet의 새 인스턴스를 만든다.
시퀀스
- 시퀀스는 지연계산이다.
println(sequenceOf(1, 2, 3).iterator().next()) // 1
println(listOf(10, 20, 30).asSequence().iterator().next()) // 10
println(
mapOf(1 to "One", 2 to "Two").asSequence().iterator().next()
) // 1=One
- 무한 시퀀스(단, 값 오버플로가 발생하면 음수와 양수를 왔다갔다 함): 1, 2, 4, 8, …
- null 을 반환하면 시퀀스가 끝난다.
val powers = generateSequence(1) { it * 2 }
// 유한 시퀀스: 10, 8, 6, 4, 2, 0
val evens = generateSequence(10) { if (it >= 2) it - 2 else null }
- 시퀀스를 만들면 유예기능계산을 할수 있으며 아래와 같이 조합된 시퀀스를 생성 가능
val numbers = sequence {
yield(0)
yieldAll(listOf(1, 2, 3))
yieldAll(intArrayOf(4, 5, 6).iterator())
yieldAll(generateSequence(10) { if (it < 50) it * 3 else null })
} println(numbers.toList()) // [0, 1, 2, 3, 4, 5, 6, 10, 30, 90]
sequence()/yield()/yieldAll()로 만들어진 이 시퀀스 빌더는 실제로는 유예 가능 계산(suspendable computation)이라는 강력한 코틀린 기능의 예다. 다중 스레드 환경에서는 이 기능이 아주 유용하다.
-
컬렉션 자료형들은 서로 다른 자료형으로 쉽게 변한가능한 메서드를 지니고 있다.
-
컬렉션의
for의 대안으로forEach를 쓸수있고 람다함수 연결이 가능하다. -
forEachIndexed로 루프의 인덱스를 얻는 방식으로도 사용할 수 있다.
-
equals()메서드를 제대로 구현해야contains()/containsAll()이 잘 작동한다는 점에 유의하라. 여러분이 직접 작성한 클래스의 인스턴스를 컬렉션 원소로 사용하는 경우, 필요에 따라 내용을 기반으로 동등성 비교를 하는equals()를 반드시 작성해야 한다. -
single()함수는 싱글턴 컬렉션의 원소를 반환한다. 컬렉션이 비어있거나 원소가 두 개 이상이면single()은 예외를 던진다. 안전한 버전인singleOrNull()은 동일한 경우 예외 대신 널을 반환한다.
val list = listOf(42)
println(list.single()) // 42 출력 (요소가 하나뿐이므로 정상 동작)
val emptyList = emptyList<Int>()
println(emptyList.single()) // 예외 발생 (컬렉션이 비어 있음)
val multipleList = listOf(1, 2, 3)
println(multipleList.single()) // 예외 발생 (요소가 여러 개 있음)
all()함수는 컬렉션의 모든 원소가 주어진 술어를 만족하면 true를 반환한다. 배열, 이터러블, 시퀀스, 맵 등의 모든 컬렉션 객체에 대해 이 함수를 적용할 수 있다. 맵의 경우 맵 엔트리가 술어에 전달된다.
println(listOf(1, 2, 3, 4).all { it < 10 }) // true
none메사드는all과 반대다.count는 틍정 조건의 아이템 개수 반환, 무한시퀀스에서 사용시 에러- 이터러블 객체에
joinToString을 하면 커스터마이징된 형태의 패턴으로 변환가능 reduce,jointo,reduceIndexed등을 지원fold,foldIndexed를 사용하면 초기값 지정 가능filter도 사용가능filterNot,filterKeys,filterValuesmap,mapIndexed,mapNotNullTo
println(
listOf(listOf(1, 2), setOf(3, 4), listOf(5)).flatten()
) // [1, 2, 3, 4, 5]
println(Array(3) { arrayOf("a", "b") }.flatten()) // [a, b, a, b, a, b]
associateWith()로 구현하며, 이 함수는 원래 컬렉션을 키의 근원으로 사용해 새로운 맵을 만들어준다.- 배열에는 associateWith()를 적용할 수 없다.
println(
listOf("red", "green", "blue").associateWith { it.length }
) // {red=3, green=5, blue=4}
println(
generateSequence(1) { if (it > 50) null else it*3 }
.associateWith { it.toString(3) }
) // {1=1, 3=10, 9=100, 27=1000, 81=10000}
subList는 원본배열을 참조하는 뷰 를 리턴하지만 slice는 새 리스트를 만든다.
// 0, 1, 4, 9, 16, 25
println(List(6) { it*it }.slice(2..4)) // [4, 9, 16]
-
배열 데에터의 경우는 sliceArray를 사용한다.
-
take()와 takeLast() 함수는 이터러블이나 배열에서 원소를 주어진 개수만큼 추출한다. take()는 맨 앞에서부터, takeLast()는 맨 뒤에서부터 개수를 센다.
println(List(6) { it*it }.take(2)) // [0, 1]
println(List(6) { it*it }.takeLast(2)) // [16, 25]
- chunked
// 0, 1, 4, 9, 16, 25, 36, 49, 64, 81
val list = List(10) { it*it }
println(list.chunked(3)) // [[0, 1, 4], [9, 16, 25], [36, 49, 64], [81]]
// 0, 1, 4, 9, 16, 25, 36, 49, 64, 81
val list = List(10) { it*it }
println(list.chunked(3) { it.sum() }) // [5, 50, 149, 81]
- windowed
println(list.windowed(3, 2, true))
// [[1, 2, 3], [3, 4, 5], [5, 6, 7], [7]]
-
sorted,sortedDesending,soredArray,soredArrayDesending -
soredWith는Comparator를 인자로 받고compareBy는Comparator를 간편하게 만들 수 있는 함수다. -
reversed, -
asReversed는 배열이 아닌 리스트만 적용가능하며 가변 참조로 리탄한다 -
shuffled는 리스트에만 가능하며shuffle를 카피 리턴 하지 않고 원본배열의 순서를 바꿔준다.
IO 시스템
import java.io.*
fun main() {
FileWriter("data.txt").use { it.write("One\nTwo\nThree") }
// One
FileReader("data.txt").buffered().use { println(it.readLine()) }
// One Two Three
FileReader("data.txt").use { println(it.readText().replace('\n', ' ')) }
// [One, Two, Three]
println(FileReader("data.txt").readLines())
}
- 반면
BufferedReader에는 각 줄로 이뤄진 시퀀스를 돌려주는lineSequence()가 있다.
FileReader("data.bin").buffered().use {
for (line in it.lineSequence()) println(line)
}
-
꼭
buffred를 쓰지 않더라도useLines나forEachLines를 사용할수 있다. -
copyTo()함수를 사용하면 한 스트림에서 다른 스트림으로 데이터를 전달할 수 있다. -
use의 사용은tryfinally로 버퍼를 자동으로 닫아주는 역할을 한다.
스트림생성
-
bufferedReaders()/bufferedWriter()확장 함수를 사용하면 지정한 File 객체에 대해BufferedReader/BufferedWriter인스턴스를 만들 수 있다. -
이진 파일을 처리하고 싶다면
inputStream()/outputStream()을 사용해 적절한 스트림을 생성하면 된다. -
inputStream을 이용하면 문자열이나 바이트어레이에서 직접 스티림으루 얻어 사용할수 있다.
URL 유틸리티는
- Url주소로부터 네트워크를 통해 데이터를 읽어오는 도우미이다.
fun URL.readText(charset: Charset = Charsets.UTF_8): String
fun URL.readBytes(): ByteArray
- 스트림을 쓰지 않고도 파일을 쓸수 있도록 제공한다.
import java.io.File
fun main() {
val file = File("data.txt")
file.writeText("One")
println(file.readText()) // One
file.appendText("\nTwo")
println(file.readLines()) // [One, Two]
file.writeText("Three")
println(file.readLines()) // [Three]
}
- 메서드들 예시
import java.io.File
fun main() {
File("my/nested/dir").mkdirs()
val root = File("my")
println("Dir exists: ${root.exists()}") // true
println("Simple delete: ${root.delete()}") // false
println("Dir exists: ${root.exists()}") // true
println("Recursive delete: ${root.deleteRecursively()}") // true
println("Dir exists: ${root.exists()}") // false
}
-
copyTo,copyRecurcivily를 사용하면 파일을 복사하거나 하위디렉터리까지 복사한다. -
File().walk로 디렉토리 순회방식을 결정 가능하다. -
createTempFile()/createTempDir()l함수를 사용해 임시 파일이나 디렉터리를 만들 수 있다.
클래스계층 이해하기
상속 예제
- 상속은 항상 콜론으로 표시
class FlyingVehicle : Vehicle() {
fun takeOff() {
println("Taking off")
}
fun land() {
println("Landed")
}
}
-
class 키워드 앞에 open 을 넣어야 하는데 이는. 상속에 열려있다는 뜻이다.
- (해당 클래스가 부모 클래스가 될수 있다는 것)
-
인라인 클래스는 상속할수도 없고 상속받을수도 없다
-
동반객체는 클래스를 상속할수 있다. 하지만 객체를 상속할순 없다.
open class Vehicle {
open fun start() {
println("I'm moving")
}
fun stop() {
println("Stopped")
}
}
class Car : Vehicle() {
override fun start() {
println("I'm riding")
}
}
-
메서드 역시 오버라이드 하려면 open키워드와 override 키워드가 필요.
-
메서드는 오바라이드 되지만, 확장은 장착 타이핑에 딸려있는 확장만 취급한다.
open class Vehicle {
open fun start() {
println("I'm moving")
}
}
fun Vehicle.stop() {
println("Stopped moving")
}
class Car : Vehicle() {
override fun start() {
println("I'm riding")
}
}
fun Car.stop() {
println("Stopped riding")
}
fun main() {
val vehicle: Vehicle = Car()
vehicle.start() // I'm riding
vehicle.stop() // Stopped moving
}
- 오버라이드하는 멤버를 final로 선언하면 더 이상 하위 클래스가 이 멤버를 오버라이드할 수 없다.
open class Car : Vehicle() {
final override fun start() = "I'm riding a car"
}
- 프로퍼티도 open override로 오바라이드 가능
open class Animal {
open val sound: String = "Generic Sound"
}
class Dog : Animal() {
override val sound: String = "Bark"
}
- 아래 처럼 가능
open class Person(val name: String, val age: Int)
class Student(name: String, age: Int, val university: String) : Person(name, age)
fun main() {
Student("Euan Reynolds", 25, "MIT")
}
- 상위 클래스로 생성자 넘기기
- Student 클래스에서 부생성자를 사용하고 싶다면 어떻게 해야 할까? 이 경우에는 위임 호출을 생성자 시그니처 바로 뒤에 위치시켜야 한다.
- 클래스에 주생성자가 없이 부생성자로 위임호출을 할때 이렇게 ㅜ한다. 주생성자가 있으면 부생성자에서는 위엠 르허출을 할 수 없다.
- 자식클래스가 부모 클래스의 여러 생성자를 사용하고 싶다면 부 생성자를 사용하는 방법뿐이 없다.
open class Person(val name: String, val age: Int)
class Student : Person {
val university: String
constructor(name: String, age: Int, university: String) :
super(name, age) {
this.university = university
}
}
-
코르린 상속에는 this 유출 문제가 있다.
- 메소드는 오버러이드 되지만 init 코드는 상위 클래스부터 실행되기 때문이다.
-
is 연산자는 자바의 instanceof 연산자와 매우 비슷하다.
- instanceof 연산자는 null에 대해 항상 false를 반환하지만, is의 결과는 연산자 오른쪽에 있는 타입이 널이 될 수 있는지 여부에 따라 결과가 달라진다.
-
When 에서도 사용가능
val objects = arrayOf("1", 2, "3", 4)
var sum = 0
for(obj in objects) {
when (obj) {
is Int -> sum += obj // 여기서 obj는 Int 타입이다
is String -> sum += obj.toInt() // 여기서 obj는 String 타입이다
}
}
println(sum) // 10
-
커스텀점군자가 있는 속성은 스마트 캐스팅이 안됨
-
열림 멤버 클래스의 프로퍼티의 경우 스마트 캐스팅 안됨
-
by 프로퍼티 위임이 없는 불변 지역변수는 항상 스마트캐스팅 가능
-
as 사용 예제
- as는 예외를 던지지만 as?는 null을 돌려준다.
val o: Any = 123
println((o as Int) + 1) // 124
println((o as? Int)!! + 1) // 124
println((o as? String ?: "").length) // 0
println((o as String).length) // java.lang.ClassCastException
equals 메서드 hashCode 메서드
-
== → 값이 같은지 확인 (equals() 호출)
-
=== → 동일한 객체인지 확인 (메모리 주소 비교)
-
아래 예제에서 index() 호출은 Address 객체를 찾을 수 있고 2를 반환한다.
-
==와 != 연산자가 공통으로 equals() 메서드를 사용한다는 점에 유의하라.
// Address에 정의한 equals 함수
override fun equals(other: Any?): Boolean {
if (other !is Address) return false
return city == other.city &&
street == other.street &&
house == other.house
}
-
equals()메서드의 커스텀 구현은 대응하는hashCode()와 서로 잘 조화돼야 한다. 두 구현은 서로 연관이 있어야 하고,equals()가 같다고 보고하는 두 객체는 항상 같은hashCode()를 반환해야 한다.- 해시코드를 사용하는 자료형의 경우 동등성을 검사할때 해시 동일성을 먼저 검사하기 때문이다.
-
배열에는 자체적인 내용 기반 동등성 구현이 없기 때문에 생성된 코드가
contentEquals()와contentHashCode()를 사용한다(또는 다차원 배열인 경우contentDeepEquals()과contentDeepHashCode()를 사용한다). -
IDE에는.equals 나 hashcide를 자동으로 구현해주는 기능이 있다.
-
추상클래스에도 생성자가 있을수 있지만 상속관계를 사용될때마다 실행되며 직접 인스턴스를 생성할 수는 없다.
-
추상 멤버는 명시적으로 open을 지정할 필요가 없다. 기본 상태가 오픈이다.
-
인터페이스 멤버는 디폴트가 추상 멤버다. 따라서 (뒤의 코드처럼) 구현을 제공하지 않으면
abstract변경자가 자동으로 붙은 것처럼 간주된다. 명시적으로abstract를 붙일 수도 있지만 불필요하다.
interface Vehicle {
val currentSpeed: Int
fun move()
fun stop()
}
봉인된 클래스
- 봉인된 클래스는 인스턴스를 생성할수 없고 상속할수 없다 sealed 키워드를 클래스 앞에 븥여 선언한다.
- 봉인된 클래스는 같은 파일 내에서만 상속 가능하고 스스로 인스턴스르루만들수 없다
- Enum은 단순한 값 모음이고, Sealed Class는 더 복잡한 계층 구조를 가질 수 있는 확장 가능한 개념!
- 한파일에 모든 타임을 컴파일러가 다 알수 있기때문에 when에 else가 필요없는 타입을 만들수 있다.
- 전략패턴을.만들기에 용이하다
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result() // 단일 인스턴스
}
val success = Result.Success("데이터 로드 완료!")
val error = Result.Error("네트워크 오류 발생")
val loading = Result.Loading
handleResult(success) // 출력: 성공: 데이터 로드 완료!
handleResult(error) // 출력: 오류: 네트워크 오류 발생
handleResult(loading) // 출력: 로딩 중...
제네릭
- 프로퍼티나 함수에 타입 파라미터를 추가하면 프로퍼티나 함수 자체를 제네릭으로 만들 수 있다.
- 제네릭 클래스에서와 달리 프로퍼티나 함수를 제네릭으로 선언할 때는 타입 파라미터를 fun이나 val/var 바로 뒤에 위치시킨다는 점에 유의하라.
- 객체 선언이나 클래스 멤버 프로퍼티에는 타입파라미터를 사용할 수 없다.
fun <T> TreeNode<T>.addChildren(vararg data: T) {
data.forEach { addChild(it) }
}
fun <T> TreeNode<T>.walkDepthFirst(action: (T) -> Unit) {
children.forEach { it.walkDepthFirst(action) }
action(data)
}
val <T> TreeNode<T>.depth: Int
get() = (children.asSequence().map { it.depth }.maxOrNull() ?: 0) + 1
fun main() {
val root = TreeNode("Hello").apply {
addChildren("World", "!!!")
}
println(root.depth) // 2
}
바운드 제약
- 타입파라미터는 바운드 제약을 걸수 있는데 타입스크립트의 extends와 비슷한 개념이다.
- 아래처럼 바운드가 자신보다 앞에 있는 타입 파라미터를 가리킬 수도 있다. 이런 바운드를 사용해 트리 원소를 가변 리스트에 추가하는 함수를 정의할 수 있다.
fun <T, U : T> TreeNode<U>.toList(list: MutableList<T>) {
walkDepthFirst{ list += it }
}
where절을 이용한 복잡한 제약 바운드- 여러개의 제약조건 바운딩을 걸 수 있다.
interface Named {
val name: String
}
interface Identified {
val id: Int
}
// 이름과 식별자가 모두 있는 객체 레지스트리를 정의하고 싶다고 가정하자.
class Registry<T> where T : Named, T : Identified {
val items = ArrayList<T>()
}
- 아래 코드에서 *는 기본적으로 알지 못하는 타입을 뜻하며, 타입 인자 하나를 대신한다
list is List<*>
map is Map<*, *>
구체화한 타입 파라미터 와 타입소거 문제
-
reified는 제네릭 타입이 런타임에도 유지되도록 해주는 기능이다.- inline 함수와 함께 사용하며,
T::class.java(리플랙션 가능하다는 말 같음) 같이 타입 정보 접근이 가능
- inline 함수와 함께 사용하며,
-
구체화한 타입 파라미터를 사용하면 타입 소거 문제를 해결할 수 있다.
- 타입소거 문제는 제네릭타입이 런타임에는 제거되는 것을 말하며, 런타임에서 타입에러를 발생시킬수 있다.
-
구체화는 제네릭 타입파라미터를 런타임에 유지시켜주는거류말한다
-
인라인한 함수에 대해서는 타입인자에
reified를 사용할수 있으며 이를 이용해 타입구체화가 가능하다.
변성이란
배열과 가변 컬렉션은 타입 인자의 하위 타입 관계를 유지하지 않는다.
예를 들어 String은 Any의 하위 타입이지만, Array<String>은 Array<Any>의 하위 타입으로 간주되지 않는다(그렇다고 Array<Any>가 Array<String>의 하위 타입으로 간주되지도 않는다).
반대로 List 나 Set 같은 불변 컬렉션의 경우, 타입 파라미터의 하위 타입 관계가 컬렉션 타입에서도 유지된다. 예를 들어 List<String>은 List<Any>의 하위 타입이다.
디폴트로 어떤 제네릭 타입의 타입 인자를 서로 다른 타입으로 대치한 타입들은 서로 하위 타입 관계가 없는 것으로 간주된다. 타입 인자들 사이에 하위 타입 관계가 있는 경우에도 역시 서로 아무 관계도 없는 타입인 것으로 간주된다. 이런 경우 해당 제네릭 타입이 무공변(invariant)이라고 말한다
아래 코드는 가변컬렌션이므로 무공변이다.
val stringNode = TreeNode<String>("Hello")
val anyNode: TreeNode<Any> = stringNode
anyNode.addChild(123)
val s = stringNode.children.first() // ???
- 함수의 반환타입 공변적이다.
- 반환타입으로 타입을 사용하는건 "생산자"다.
open class Animal {
open fun sound() = "Some sound"
}
class Dog : Animal() {
override fun sound() = "Bark"
}
// 공변적인 반환 타입
fun getAnimal(): Animal = Dog() // Dog가 Animal의 서브타입이므로 OK
fun main() {
val animal: Animal = getAnimal()
println(animal.sound()) // "Bark" 출력됨
}
- 함수의 인자타입은 반공변적이다.
- 인자타입으로 타입을 사용하는건 "소비자"다.
val f1: (Dog) -> Unit = ::acceptAnimal // 오류 발생!
val f2: (Animal) -> Unit = ::acceptDog // 가능!
-
가변자료형이라도
add처럼 아이템을 추가하는 기능이 없다면 공변이다. -
아래처럼 불변 자료형이라도 타입이 생산자로 쓰이지 않으면 무공변이다.
interface Set<T> {
fun contains(element: T): Boolean
}
제네릭 타입 X<T,…>이 타입 파라미터 T에 대해 in out 키워드 사용하기
-
X가 생산자 역할을 한다. 이 경우 T를 공변적(out)으로 선언할 수 있고, A가 B의 하위 타입이면 X도 X의 하위 타입이 된다.
-
X가 소비자 역할을 한다. 이 경우 T를 반공변적(in)으로 선언할 수 있고, B가 A의 하위 타입이면 X가 X의 하위 타입이 된다.
-
나머지 경우, X는 T에 대해 무공변이다(기본적으로 제네릭은 무공변이므로)
-
“*” 로 표시되는 스타 프로젝션은 타입 인자가 타입 파라미터의 바운드 안에서 아무 타입이나 될 수 있다는 사실을 표현한다. 코틀린 타입 파라미터는 상위 바운드만 허용하기 때문에 타입 인자에 스타 프로젝션을 사용하면 타입 인자가 해당 타입 파라미터를 제한하는 타입의 하위 타입 중 어떤 것이든 관계없다는 뜻이다.
Type Alias
typealias IntPredicate = (Int) -> Boolean
typealias IntMap = HashMap<Int, Int>
// 이제 이렇게 정의한 이름을 =의 오른쪽에 있는 타입 대신 쓸 수 있다.
fun readFirst(filter: IntPredicate) =
generateSequence{ readLine()?.toIntOrNull() }.firstOrNull(filter)
애너테이션
애너테이션이란 선언에 메타데이터를 엮어서 활용하는 것이다.
- 코틀린 애너테이션을 식에 적용할 수도 있다. 예를 들어 내장
@Suppress애너테이션을 사용하면 소스 파일의 특정 식에 대한 컴파일러 경고를 끌 수 있다.
val s = @Suppress("UNCHECKED_CAST") objects as List<String>
- 같은 구성 요소에 애너테이션을 여럿 붙이고 싶다면 각괄호([])로 애너테이션들을 감쌀 수 있다.
@[Synchronized Strictfp] // @Synchronized @Strictfp와 같은 역할을 함
fun main() { }
- 애너테이션을 주생성자에 적용하고 싶을 때는 명시적으로 주생성자의 인자 목록 앞에 constructor 키워드를 붙여야 한다.
class A @MyAnnotation constructor ()
- 애너테이션을 정의하려면 클래스 앞에
annotation이라는 변경자를 붙여야 한다.
annotation class MyAnnotation
// 애너테이션 사용
@MyAnnotation fun annotatedFun() { }
- 일반 클래스와 달리 애너테이션 클래스에는 멤버나 부생성자, 초기화 코드가 없다.
annotation class MyAnnotation {
val text = "???" // Error
}
-
일반 클래스와 달리 애너테이션 클래스에는 멤버나 부생성자, 초기화 코드가 없다.
-
하지만 코틀린 1.3부터는 내포된 클래스, 인터페이스, 객체(동반 객체 포함)를 애너테이션 본문에 넣을 수 있다.
annotation class MyAnnotation(val value: String) {
// ✅ 내포된 클래스 허용
class NestedClass
// ✅ 인터페이스 허용
interface NestedInterface {
fun doSomething()
}
// ✅ 객체 선언 허용 (동반 객체 포함)
object NestedObject {
fun log() = println("Nested Object")
}
companion object {
fun companionMethod() = println("Companion Object")
}
}
- 애너테이션 파라미터를 항상 val로 선언해야 한다는 점에 유의하라.
annotation class MyAnnotation(val text: String)
@MyAnnotation("Some useful info") fun annotatedFun() { }
-
애너테이션 클래스는 애너테이션용도로 써야되며, 따라서 직접 인스턴스를 만들수는 없다.
-
애너테이션 파라미터로 사용할 수 있는 타입의 종류를 다음과 같이 제한한다.
- Int, Boolean, Double 등 원시 타입
- String
- 이넘
- 다른 애너테이션
- 클래스 리터럴 (KClass)
- 위에 나열한 타입들로 이뤄진 배열
-
코틀린 1.2부터는 애너테이션 인자로 각괄호([])를 사용해 더 간결하게 배열을 만들 수 있다.
- 각괄호를 사용한 배열 표현의 경우 현재는 애너테이션에서만 사용할 수 있다.
annotation class Dependency(val componentNames: Array<String>)
@Component(dependency = Dependency(["I/O", "Log"]))
class Main
-
KClass는 코틀린에서 리플렉션을 사용할 때 필요한 클래스이며, 주로 클래스 정보 조회, 동적 인스턴스 생성, 애노테이션 처리, DI 등에 활용된다.
-
사용 지점 대상을 사용하는 애너테이션을 [] 구문으로 묶을 수 있다. 이런 경우에는 대상을 모든 애너테이션에 적용하게 된다. 따라서 다음과 같은 정의는
getter에 애너테이션A와B적용
class Person(@get:[A B] val name: String)
// 다음과 같다.
class Person(@get:A @get:B val name: String)
receiver라는 대상을 사용하면 확장 함수나 프로퍼티의 수신 객체에 애너테이션을 붙일 수 있다.
class Person(val firstName: String, val familyName: String)
fun @receiver:A Person.fullName() = "$firstName $familyName"
- file을 대상으로 사용해 전체 파일에 대해 애너테이션을 붙일 수 있다. 다른 패키지 임포트나 패키지 지시자보다 더 앞인 파일의 시작 부분에 이런 애너테이션을 붙여야 한다.
@file:JvmName("MyClass") // 이 줄은 파일 맨 앞에 있어야 함
...
fun main() {
println("main() in MyClass")
}
애너테이션 생성에 관련된 애너테이션 (애너테이션 클래스 정의시 사용된다)
-
@Retention애너테이션은 애너테이션이 저장되고 유지되는 방식을 제어한다.- 디폴트로 코틀린 애너테이션은 RUNTIME으로 유지 시점이 정의된다.
-
SOURCE: 이 애너테이션은 컴파일 시점에만 존재하며 컴파일러의 바이너리 출력(JVM의 경우 바이트코드가 저장된 클래스 파일)에는 저장되지 않는다.
- BINARY: 이 애너테이션은 컴파일러의 바이너리 출력에 저장되지만, 런타임에 리플렉션 API로 관찰할 수는 없다.
- RUNTIME: 이 애너테이션은 컴파일러의 바이너리 출력에 저장되며 런타임에 리플렉션 API를 통해 관찰할 수도 있다.
-
@MustBeDocumented은 api문서에 해당 애너테이션이 붙으면 api에 자동으로 포함시켜준다. -
데코레이션과 차이점
- 애너테이션은 직접적인 동작 변경을 하지 않지만, 다른 라이브러리나 프레임워크와 결합되어 동작을 변경하는 방식으로 사용될 수 있다. 이런 방식이 바로 데코레이터처럼 동작하는 것이다.
-
@Target은 애너테이션을 어떤 언어 요소에 붙일 수 있는지 지정한다. AnnotationTarget 이넘에 정의된 다음 상수들을 vararg로 지정하면 된다.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) // 요러케 하면 된다는 뜻이다.
annotation class MultiTargetAnnotation
@Suppress애너테이션을 사용하면 지정한 이름의 컴파일러 경고를 표시하지 않게 할 수 있다. 이 애너테이션은 식이나 파일을 포함하는 모든 대상에 붙일 수 있다. 예를 들어, 여러분이 코드가 맞다고 확신한다면 이 애너테이션을 사용해 타입 캐스팅과 관련한 불필요한 경고를 막을 수 있다.
리플렉션
-
리플렉션 API는 클래스, 함수, 프로퍼티의 런타임 표현에 접근할 수 있게 해주는 타입, 함수, 프로퍼티 모음이다.
-
리플랙션은 크게 두가지 그룹으로 나눌수 있다
- 호출가능그룹 (함수)
- 지정자그룹 (클래스, 프로퍼티)
-
Main클래스와 연관된 애너테이션을 가져오고 싶다면. 클래스 리터럴의annotations프로퍼티를 통해 정보를 얻을 수 있다.
fun main() {
val component = Main::class.annotations
.filterIsInstance<Component>()
.firstOrNull() ?: return
-
코틀린 리플랙션의 지정자는 두가지 변종이있다.
KClass<T>KTypeParameter
-
KClassAPI를 살펴보자
val isAbstract: Boolean
val isCompanion: Boolean
val isData: Boolean
val isFinal: Boolean
val isInner: Boolean
val isOpen: Boolean
val isSealed: Boolean
println(String::class.isInstance("")) // true
println(String::class.isInstance(12)) // false
println(String::class.isInstance(null)) // false
- 다음 코드는 리플렉션을 사용해 Person 클래스의 인스턴스를 만들고, 그 인스턴스의 fullName() 함수를 호출한다.
class Person(val firstName: String, val familyName: String) {
fun fullName(familyFirst: Boolean): String = if (familyFirst) {
"$familyName $firstName"
} else {
"$firstName $familyName"
}
}
fun main() {
val personClass = Class.forName("Person").kotlin
val person = personClass.constructors.first().call("John", "Doe")
val fullNameFun = personClass.members.first { it.name == "fullName" }
println(fullNameFun.call(person, false))
}
KClass가 객체 선언을 표현 하는 경우constructors프로퍼티는 항상 빈 컬렉션을 반환한다. 실제 인스턴스를 얻고 싶으면objectInstance프로퍼티를 사용해야 한다.
object O {
val text = "Singleton"
}
fun main() {
println(O::class.objectInstance!!.text) // Singleton
}
KClass인스턴스가 객체를 표현하지 않으면objectInstance프로퍼티도 null이다.
fun main() {
println(Child::class.supertypes) // [Parent, IParent]
}
supertype프로퍼티는 클래스가 직접 상속한 상위 타입만 돌려준다- 호출 가능(callable) 요소라는 개념은 어떤 결과를 얻기 위해 호출할 수 있는 함수나 프로퍼티를 함께 묶어준다. 리플렉션 API에서는
KCallable<out R>이라는 제네릭 인터페이스를 통해 호출 가능 요소를 표현한다.
import kotlin.reflect.KCallable
val simpleVal = 1
val Int.extVal get() = this
class A {
val Int.memberExtVal get() = this
}
fun main() {
fun printParams(callable: KCallable<*>) {
println(
callable.parameters.joinToString(prefix = "[", postfix = "]") {
it.type.toString()
}
)
}
// []
printParams(::simpleVal)
}
Callable의parameters예시
import kotlin.reflect.KCallable
val simpleVal = 1
val Int.extVal get() = this
class A {
val Int.memberExtVal get() = this
}
fun main() {
fun printParams(callable: KCallable<*>) {
println(
callable.parameters.joinToString(prefix = "[", postfix = "]") {
it.type.toString()
}
)
}
// []
printParams(::simpleVal)
// [kotlin.Int]
printParams(Int::extVal)
// [A, kotlin.Int]
printParams(A::class.members.first { it.name == "memberExtVal" })
}
KParameter인터페이스는 멤버 및 확장 선언의 수신 객체나 함수/생성자의 파라미터에 대한 정보를 포함한다.
import kotlin.reflect.full.primaryConstructor
class Example(val x: Int, val y: String)
fun main() {
val constructor = Example::class.primaryConstructor!! // 기본 생성자 리플렉션
val params = constructor.parameters // KParameter 리스트 가져오기
for (param in params) {
println("이름: ${param.name}, 타입: ${param.type}")
}
}
DSL
-
DSL이란 도메인 특화언어로 특정영역이나 기능을 위해 만들어진 언어를 뜻한다
-
DSL의 강점은 단순성이다.
-
연산자 오버로딩도 DSL의 일종이다 (+ × ÷ -)
-
“*” 연산자(이 연산자는 times() 함수에 해당된다)를 String과 Int의 쌍에 대해 확장함으로써 정의한다. 이로 인해 다음과 같은 코드를 작성할 수 있다.
operator fun String.times(n: Int) = repeat(n)
println("abc" * 3) // abcabcabc
- “+” 연산자의 경우
val x = 1.plus(2) // 1 + 2와 같음
- not() 관습을 사용하면 보색 관계를 ! 연산자로 표현할 수 있다.
enum class Color {
BLACK, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, WHITE;
operator fun not() = when (this) {
BLACK -> WHITE
RED -> CYAN
GREEN -> MAGENTA
BLUE -> YELLOW
YELLOW -> BLUE
CYAN -> RED
MAGENTA -> GREEN
WHITE -> BLACK
}
}
++는inc()--는dec()a + b는a.plus(b)a - b는a.minus(b)a * b는a.times(b)a / b는a.div(b)a % b는a.rem(b)a .. b는a.rangeTo(b)a in b는b.contains(a)a !in b는!b.contains(a)
중위 표현 DSL
- 아래와 같은 호출을 가능하게 하려면 함수 앞에
infix변경자를 붙여야 한다.
val pair1 = 1 to 2 // 중위 호출
val pair2 = 1.to(2) // 일반적인 호출
- 예를 들어 표준 to 함수 구현은 다음과 같다.
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
-
코틀린에서 || 연산과 && 연산은 우선순위가 틀리다. 하지만 and 나 or 연산은 우건순위가 같다.
-
plusAssign()를 구현하면 아래와 같은 연산이 가능해진다.
var numbers = mutableListOf(1, 2, 3)
// 변수에 저장된 참조를 바꿔야 할까, 객체 내부를 바꿔야 할까?
numbers += 4 // Error
println(numbers)
-
plusAssign()등의 복합 대입 연산자 함수가 없는 경우 복합 대입문을 이행 연산자와 대입을 사용한 연산으로 해석한다.- 예를 들어
a += b의 경우plus()가 있으면a = a.plus(b)로,a -= b의 경우minus()가 있으면a = a.minus(b)로 복합 대입문을 해석한다.
- 예를 들어
-
함수 타입의 값은 자동으로
invoke()멤버가 생긴다. 하지만 원한다면 임의의 타입에 대해invoke()를 정의할 수 있다.
operator fun <K, V> Map<K, V>.invoke(key: K) = get(key)
val map = mapOf("I" to 1, "V" to 5, "X" to 10)
println(map("V")) // 5
println(map("L")) // null
-
구조분해는
componemntN()을 구현하면 된다.take사용가능 불변리스트의 경우는 이미componentN이 디폴트로 구현되어 있다. -
iterator를 구현하면for문을 적용할 수 있다.
위임 프로퍼티 (by)
-
위임 프로퍼티를 사용하면 간단한 문법적인 장식 뒤에 커스텀 프로퍼티 접근 로직을 구현할 수 있다.
-
필요하다면 다른
lock 객체를 사용해서 여러분이 원하는 동기화 그룹을 만들수 있다.
private val lock = Any()
val text by lazy(lock) { File("data.txt").readText() }
- 또는
LazyThreadSafetyModeEnum 상수를 통해 세 가지 기본 구현 중 하나를 선택할 수 있다.- SYNCHRONIZED: 프로퍼티 접근을 동기화한다. 따라서 한 번에 한 스레드만 프로퍼티 값을 초기화할 수 있다(이 구현이 디폴트다).
- PUBLICATION: 초기화 함수가 여러 번 호출될 수 있지만 가장 처음 도착하는 결과가 프로퍼티 값이 되도록 프로퍼티 접근을 동기화한다.
- NONE: 프로퍼티 접근을 동기화하지 않는다. 이 방식을 선택하면 다중 스레드 환경에서 프로퍼티의 올바
디폴트로 구현되어 있는 위임 프로퍼티
-
by notNull의 경우lateinit와 비슷하지만 원시타입에 대해서도 작용할수 있다. -
observable()함수를 사용하면 프로퍼티 값이 변경될 때 통지를 받을 수 있다.observable()은 초깃값과 람다를 인자로 받는다.
import kotlin.properties.Delegates.observable
class Person(name: String, val age: Int) {
var name: String by observable(name) { _, old, new ->
println("Name changed: $old to $new")
}
}
-
vetoable()함수도 비슷한 위임을 만든다. 이 함수는 초깃값과 Boolean을 반환하는 람다를 인자로 받는다.- 프로퍼티 값을 변경하려고 시도할 때마다 값을 변경하기 직전에 이 람다가 호출되고, 람다가 true를 반환하면 실제 값 변경이 일어난다. 람다가 false를 반환하면 값이 바뀌지 않고 그대로 남는다.
-
맵을 위임 객체로 사용할 수 있으므로, 변경 가능한 맵을 통해 가변 프로퍼티를 정의할 수도 있다.
class CartItem(data: MutableMap<String, Any?>) {
var title: String by data
var price: Double by data
var quantity: Int by data
}
-
커스텀 프로퍼티 위임 만들기
- 사실상 구현은 CachedProperty 의 getValue만 구현하면 위임프로퍼티가됨
- CachedPropertyProvider 가 필요한 이유는 추가 로직을 구현하기 위함
- 추가 로직을 구현하려면 provideDelegate 가 필요
- 아래 예제는 Nocache 어노테이션을 체크하기 위한 예
-
커스텀 위임구현과 프로바이더델리케이션을 이용한 캐시방지 어노테이션의 예이다.
- provideDelegate를 통해 위임 인스턴스르르제어할 수 있다
R.() -> T는 특정 객체®를 리시버로 하는 람다 표현식이며, 위임 구현 시 객체의 속성 값을 동적으로 제공하는 규약 역할을 한다고 볼 수 있다.
// 프로퍼티에 위치시킬수 있는 NoCache 어노케이션 선언
@Target(AnnotationTarget.PROPERTY)
annotation class NoCache
// 아래 cache 함수가 바로 CachedProperty를 사용하지 않고 중간단계에서 Cache 어너테이션 확인을 해준다.
class CachedPropertyProvider<in R, out T : Any>(
val initializer: R.() -> T
) {
operator fun provideDelegate(
receiver: R?,
property: KProperty<*>
): CachedProperty<R, T> {
if (property.annotations.any{ it is NoCache }) {
throw IllegalStateException("${property.name} forbids caching")
}
return CachedProperty(initializer)
}
}
// getValue가 구현되어 있는 CachedProperty클래스
// 이런식으로 getValue가 구현되어 있고 수신객체위임을 사용해 구현된 class는 위임 프로퍼티로 사용할수 있다.
class CachedProperty<in R, out T : Any>(val initializer: R.() -> T) {
private val cachedValues = HashMap<R, T>()
operator fun getValue(receiver: R, property: KProperty<*>): T {
return cachedValues.getOrPut(receiver) { receiver.initializer() }
}
}
// by cache 로 사용할수 있는 집입함수
fun <R, T : Any> cached(initializer: R.() -> T) = CachedPropertyProvider(initializer)
고차함수와 DSL
- 중위함수를 이용하면 아래와 같은 그럴듯한 dsl을 만들수 있다
val nums = listOf(2, 8, 9, 1, 3, 6, 5)
val query = from(nums) where { it > 3 } select { it*2 } orderBy { it }
println(query.items.toList())
@DslMarker는 암시적 수신 객체의 누출만 막을 수 있다는 점에 유의하라. 필요한 경우에는 명시적인 this를 써서 외부 수신 객체를 가져와 사용할 수 있다.
동시성과 코루틴
스레드 안전성을 위해 블럭연산을 하면 성능에 안좋은 영향을 줄수 있다.
그러므로 더 효율적인 대안으로 비동기 프로그래밍을 할수 있게 도와주는 코루틴이 있다.
블러킹은 응답을 기다리며 아무일도 안하지만, 코로틴은 기다리는 동안 다른일을 할 수 있다.
코루틴은 블로킹과 비동기의 두가지 장점을 다 갖는다.
대부분의 코루틴 기능이 별도 라이브러리로 제공되기 때문에 명시적으로 프로젝트 설정에 이를 추가해야 한다.
-
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3 -
아래의
delay()함수는 코루틴 라이브러리에 정의된 일시 중단 함수다. 이 함수는 Thread.sleep()과 비슷한 일을 한다. 하지만 delay()는 현재 스레드를 블럭시키지 않고 자신을 호출한 함수를 일시 중단시키며 스레드를 (다른 일시 중단된 함수를 다시 계속 실행하는 등의) 다른 작업을 수행할 수 있게 풀어준다.
suspend fun foo() {
println("Task started")
delay(100)
println("Task finished")
}
- 코틀린은 일반 함수가 일시 중단 함수를 호출하는 것을 금지한다.
- 코루틴을 쓰려면 꼭 suspend로 선언해야함
fun foo() {
println("Task started")
delay(100) // error: delay is a suspend function
println("Task finished")
}
코루틴 빌더(coroutine builder)
-
현실적인 경우 동시성 코드의 동작을 제어하고 싶기 때문에 공통적인 생명 주기(life cycle)와 문맥이 정해진 몇몇 작업(task)이 정의된 구체적인 영역 안에서만 동시성 함수를 호출한다. (이런 구체적 영역을 제공하기 위해) 코루틴을 실행할 때 사용하는 여러 가지 함수를 코루틴 빌더(coroutine builder)라고 부른다.
-
병렬로 실행되므로 뭐가 먼자 끝날지는 매번 틀려진다
- 메인 함수에 슬립이 있는 이유는 코루틴 스레드의 실행을 보장하기 위해서다(메인지 종료되면 코루틴도 종료되므로).
- 아래는 예시일 뿐이다, 보통은 이렇게 쓰지는 않는다.
import kotlinx.coroutines.*
import java.lang.System.*
fun main() { // main이 suspend 함수가 아님에 유의
val time = currentTimeMillis()
GlobalScope.launch {
delay(100)
println("Task 1 finished in ${currentTimeMillis() - time} ms")
}
GlobalScope.launch {
delay(100)
println("Task 2 finished in ${currentTimeMillis() - time} ms")
}
Thread.sleep(200)
}
- 아래 경우에는 main()을 suspend로 표시해서 두 Deferred 작업에 대해 직접 await() 메서드를 호출했다. 출력은 기대한 대로 다음과 같다. abcabcabc
import kotlinx.coroutines.*
suspend fun main() {
val message = GlobalScope.async {
delay(100)
"abc"
}
val count = GlobalScope.async {
delay(100)
1 + 2
}
delay(200)
val result = message.await().repeat(count.await())
println(result) // abcabcabc
}
runBlocking & coroutineScope
-
runBlocking()은 블러킹 호출과 넌블러킹 호출 사이의 다리 역할을 하기 위해 고안된 코루틴 빌더이다. (위 예제들처럼 sleep을 걸 필요가 없도록)- 경우에 따라서는 코루틴이 어떤 연산을 수행하는 도중에만 실행되길 바랄 수도 있다. 동시성 작업 사이의 부모 자식 관계로 인해 이런 실행 시간 제한이 가능하다.
runBlocking은 메인스레드가 끝나지 않게 블러킹 해준다.
-
runBlocking()을 다른 코루틴 안에서 사용하면 안 된다. 테스트나 메인 함수에서 최상위 빌더로 사용하는 등의 경우에만runBlocking()을 써야 한다. -
coroutineScope()가 일시 중단 함수라 현재 스레드를 블럭시키지 않는다.
import kotlinx.coroutines.*
fun main() {
runBlocking {
println("Custom scope start")
coroutineScope {
launch {
delay(100)
println("Task 1 finished")
}
launch {
delay(100)
println("Task 2 finished")
}
}
println("Custom scope end")
}
}
- 코루틴은
job,dispetcher를 통해 취소하거나 스레드 연관을제어한다
GlobalScope.launch {
// 현재 잡을 얻고 "Task is active: true"를 출력
println("Task is active: ${coroutineContext[Job.Key]!!.isActive}")
}
- 기본적으로
launch(),async()등의 표준 코루틴 빌더에 의해 만들어지는 코루틴은 현재 문맥을 이어받는다.
import kotlinx.coroutines.*
private fun CoroutineScope.showName() {
println("Current coroutine: ${coroutineContext[CoroutineName]?.name}")
}
fun main() {
runBlocking {
showName() // Current coroutine: null
launch(coroutineContext + CoroutineName("Worker")) {
showName() // Current coroutine: Worker
}
}
}
잡생명주기
-
잡의 기본 상태는 활성화 상태다.
-
신규 상태의 잡에 대해
start()나join()메서드를 호출하면 잡이 시작되면서 활성화 상태가 된다. 다음 예제를 보자.
import kotlinx.coroutines.*
fun main() {
runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
println("Job started")
}
delay(100)
println("Preparing to start...")
job.start()
}
}
- 위 예제는 자식 코루틴의 시작을 부모 코루틴이 메시지를 호출한 뒤로 미룬다. 출력은 다음과 같다.
Preparing to start...
Job started
- 아래는 기본상태로 실행되었을때의 동작
fun main() {
runBlocking {
val job = coroutineContext[Job.Key]!!
val jobA = launch { println("This is task A") }
val jobB = launch { println("This is task B") }
jobA.join()
jobB.join()
println("${job.children.count()} children running")
}
}
- 결과는 다음과 같다.
This is task A
This is task B
0 children running
await은join과 비슷하지만await은 결과값을 받아올수 있다.job.cancel()로 잡을 취소할수 있다- 50밀리초 안에 읽우면 실행되고 아니면 에러처리된다
import kotlinx.coroutines.*
import java.io.File
fun main() {
runBlocking {
val asyncData = async { File("data.txt").readText() }
try {
val text = withTimeout(50) { asyncData.await() }
println("Data loaded: $text")
} catch (e: Exception) {
println("Timeout exceeded")
}
}
}
-
코루틴을 실행할때 스레드를 제어가능하게 해주는 기능을 디스패처라고 한다
-
다음은 스레드풀 다섯개를 만들어서 스레드를 만들어 실행해보는 예제이다
import kotlinx.coroutines.*
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.atomic.AtomicInteger
fun main() {
val id = AtomicInteger(0)
val executor = ScheduledThreadPoolExecutor(5) { runnable ->
Thread(
runnable,
"WorkerThread-${id.incrementAndGet()}"
).also { it.isDaemon = true }
}
executor.asCoroutineDispatcher().use { dispatcher ->
runBlocking {
for (i in 1..3) {
launch(dispatcher) {
println(Thread.currentThread().name)
delay(1000)
}
}
}
}
}
- delay()는 실행기가 별도의 스레드를 만들게 한다. 따라서 이 코드는 다음과 같은 결과를 출력한다.
WorkerThread-1
WorkerThread-2
WorkerThread-3
- 코틀린 익셉션 핸들러 처리 예제
import kotlinx.coroutines.*
suspend fun main() {
val handler = CoroutineExceptionHandler{ _, exception ->
println("Caught $exception")
}
GlobalScope.launch(handler) {
launch {
throw Exception("Error in task A")
println("Task A completed")
}
}
}
동시성 통신
-
동시성 통신은 채널에서 처리된다.
-
채널에 메세지가 꽉차있는데, 다른 처리가 필요하면 코루틴은 자동으로 일시정지된다.
-
채널의 대기 처리는 옵션 3개가 있다.
- Channel.UNLIMIT 필요해따라 버퍼가 늘어남
- Channel.RENDEZVOUS 보내는 순서와 받는 순서가 보장되는 랑데뷰 채널 생성
- Channel.CONFLATED 버퍼가 하나로 유지되고 리시버가 바빠서 받아가지 못했는데 Send되면 원래 있던값은 소실되어 덮어쓰임
-
한 채널로 부터 여러 리시브가 메세지를 읽어오는 패턴을 팬아웃이라고 한다.
-
코로틴에는 누티커라고 하는 특별한 랑데뷰 채널이 있으며 일정시간마다 메세지를 발생시킨다.
코테스트
-
코테스트와 인텔리제이의 궁합을 좋게해주는 플러그인을 설치하면 좋다
-
코테스트 예제 1
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.StringSpec
class NumbersTest : StringSpec({
"2 + 2 should be 4" { (2 + 2) shouldBe 4 }
"2 * 2 should be 4" { (2 * 2) shouldBe 4 }
})
- 코테스트 예제 2
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.WordSpec
class NumbersTest2 : WordSpec({
"1 + 2" should {
"be equal to 3" { (1 + 2) shouldBe 3 }
"be equal to 2 + 1" { (1 + 2) shouldBe (2 + 1) }
}
})
- 코테스트 예제 3
- should() 호출을 When() 또는
when()`5으로 감싸면 테스트 계층을 3단계로 구성할 수도 있다.
- should() 호출을 When() 또는
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.WordSpec
class NumbersTest2 : WordSpec({
"Addition" When {
"1 + 2" should {
"be equal to 3" { (1 + 2) shouldBe 3 }
"be equal to 2 + 1" { (1 + 2) shouldBe (2 + 1) }
}
}
})
- 코테스트 예제 4
import io.kotlintest.shouldBe
import io.kotlintest.specs.FeatureSpec
class NumbersTest7 : FeatureSpec({
feature("Arithmetic") {
val x = 1
scenario("x is 1 at first") { x shouldBe 1 }
feature("increasing by") {
scenario("1 gives 2") { (x + 1) shouldBe 2 }
scenario("2 gives 3") { (x + 2) shouldBe 3 }
}
}
})
-
BDD스타일 지원
src/main과 동등한 레벨로src/test를 만들어서 ide에서 테스트 디렉토리로 지정해서 사용한다.
-
픽스처 기능 제공, 다른 테스트 라이브러리에 있는 비슷한 기능으로서, 특별한건 없어보인다.
Ktor를이용한 웹 개발(코털로 읽지 말고, 케이 토르 라고 읽으라고 한다)
-
Ktor는 여러 가지 클라이언트 서버 애플리케이션이 연결된 시스템의 개발을 편하게 도와주려는 목적으로 만들어진 프레임워크다. 이런 클라이언트나 서버 애플리케이션으로는 브라우저, 모바일 클라이언트, 웹 애플리케이션이나 서비스 등이 있다.
-
Ktor의 주 응용 분야는 HTTP 프로토콜을 사용해 데이터를 교환하는 웹 애플리케이션 위주다.
-
간단한 사용법 예
package com.example.plugins
import io.ktor.routing.*
import io.ktor.http.*
import io.ktor.application.*
import io.ktor.response.*7
import io.ktor.request.*
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
- 라우팅 예
package com.example.plugins
import io.ktor.html.*
import kotlinx.html.*
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
fun Application.configureTemplating() {
routing {
get("/html-dsl") {
call.respondHtml {
body {
h1 { +"HTML" }
ul {
for (n in 1..10) {
li { +"$n" }
}
}
}
}
}
}
}
- Htmlmdsl 사용 예
package com.example
import io.ktor.routing.*
import io.ktor.http.*
import io.ktor.html.*
import kotlinx.html.*
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.request.*
import com.example.plugins.*
import kotlin.test.*
import io.ktor.server.testing.*
class ApplicationTest {
@Test
fun testRoot() {
withTestApplication({ configureRouting() }) {
handleRequest(HttpMethod.Get, "/").apply {
assertEquals(HttpStatusCode.OK, response.status())
assertEquals("Hello World!", response.content)
}
}
}
}
- ktor 예제코드
- 서버관련기능을 변경하려면 설정에서 서버관련기능을 추가하고
Applixatuon.configureHttp함수를 추가 변경하면 된다.
- 서버관련기능을 변경하려면 설정에서 서버관련기능을 추가하고
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
configureRouting()
configureTemplating()
configureHTTP()
}.start(wait = true)
}
실제 기능 설정은 Application.configureHTTP() 확장 함수 안에서 install(Compression)과 같이 해당 기능을 설치한다. 이 안에서 압축 방식과 우선순위를 설정한다.
fun Application.configureHTTP() {
install(Compression) {
gzip {
priority = 1.0
}
deflate {
priority = 10.0
minimumSize(1024) // 적용 조건
}
}
}
routing {
get("hello/{userName}") {
call.respondHtml {
body {
h1 {+"Hello, ${call.parameters["userName"]}"}
}
}
}
}
-
세그먼트 변수로 활용할수 있지만 변수활용이 필요없을경우
*로 대체해도 된다 -
빈 경로 세그먼트와 매치될 수도 있는 선택적인 파라미터가 필요하다면 어떻게 할까? 파라미터 이름 바로 뒤에 ?를 추가하면 된다. 다음 라우팅은 /hello/John과 /hello에 모두 매치된다
routing {
get("/hello/{userName?}") {
val userName = call.parameters["userName"] ?: "모르는 분"
call.respondHtml {
body {
h1 { +"Hello, $userName" }
}
}
}
}
- 파라미터 이름 뒤에 마침표 세 개(
...)를 덧붙이면 경로의 뒤쪽에 있는 모든 세그먼트와 매치된다. 이 경우 Parameters 클래스의getAll()메서드를 사용해 모든 세그먼트를 List로 얻을 수 있다.- 예를 들어
/calc/+/123/456처럼 연산과 정수를 받아서 간단한 산술 연산을 수행하는 서비스를 생각해보자.
- 예를 들어
routing {
get("/calc/{data...}") {
val data = call.parameters.getAll("data") ?: emptyList()
}
- 함수의 두 번째 인자 값에 따라 상태 301(Moved Permanently)이나 302(Found)를 사용한 HTTP 리디렉션(redirection)을 지원한다.
routing {
get("/") {
call.respondRedirect("index") // 302 Found
}
get("index") {
call.respondText("Main page")
}
}
코틀린 ktor 세션 사용 예
import io.ktor.server.application.*
import io.ktor.server.sessions.*
import io.ktor.server.response.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.http.*
data class UserSession(val userId: String, val username: String)
fun Application.module() {
install(Sessions) {
cookie<UserSession>("SESSION_COOKIE") {
cookie.path = "/" // 모든 경로에서 쿠키 사용 가능
cookie.maxAgeInSeconds = 3600 // 1시간 유지
cookie.httpOnly = true // 클라이언트에서 JS로 접근 불가 (보안 강화)
}
}
routing {
// **로그인 처리**
post("/login") {
val params = call.receiveParameters()
val username = params["username"]
val password = params["password"]
// 실제 서비스에서는 DB에서 사용자 검증해야 함
if (username == "admin" && password == "password") {
call.sessions.set(UserSession(userId = "123", username = username))
call.respondText("Login Successful!")
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid Credentials")
}
}
// **현재 로그인한 사용자 정보 확인**
get("/me") {
val session = call.sessions.get<UserSession>()
if (session != null) {
call.respondText("Logged in as: ${session.username}")
} else {
call.respond(HttpStatusCode.Unauthorized, "Not Logged In")
}
}
// **로그아웃 (세션 삭제)**
get("/logout") {
call.sessions.clear<UserSession>()
call.respondText("Logged Out!")
}
}
}
fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}