이 글은 Kotlin In Action 1–8장을 정리한 내용과 Coroutine에 대한 DevSummit2019 의 한 세션을 참고하여 작성한 글 입니다. 코드를 통한 설명을 추가할 예정입니다.
문(statement)과 식(expression)
- 식은 값을 만들어 내며 다른 식의 하위 요소로 계산에 참여할 수 있음
- 문은 자신을 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어내지 않음
- kotlin 에서 if문은 식
- ‘블록의 마지막 식이 블록의 결과'라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립
> 함수에 대해서는 성립하지 않음, 식이 본문인 함수는 블록을 본문으로 가질 수 없고 블록이 본문인 함수는 내부에 return문이 반드시 있어야 함
val, var
- val(value) — 변경 불가능한 참조를 저장, var(variable) — 변경가능한 참조
- val 참조 자체는 불변일지라도 그 참조가 가리키는 객체 내부 값 변경 가능
문자열 템플릿
“Hello, $name!”
- 자바의 문자열 접합 연산 (“Hello, “ + name + “!”) 과 동일한 기능을 하지만 더 간결하고, 자바 문자열 접합 연산을 사용한 식과 마찬가지로 효율적
- 컴파일된 코드는 StringBuilder를 사용하고 문자열 상수와 변수의 값을 append로 문자열 빌더 뒤에 추가
- 자바에서는 + 연산으로 문자열과 변수를 붙여도 컴파일러는 StringBuilder를 사용하는 바이트코드를 생성해줌
확장 함수와 확장 프로퍼티
확장 함수
- 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수
- 확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 됨
클래스 이름을 수신 객체 타입, 확장 함수가 호출되는 대상이 되는 값을 수신 객체라고 부름 - fun String.lastChar(): char = this.get(this.length-1)
String이 수신 객체 타입, this가 수신 객체 - 확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할수 있지만 확장 함수가 캡슐화를 깨지는 않음
> 클래스 안에서 정의한 메소드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private, protected 멤버를 사용할 수 없음 - 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 함
- 코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야 함. 따라서 임포트할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법
- 단지 정적 메소드 호출에 대한 문법적인 편의(syntatic sugar)일 뿐이므로 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수 있음
- 확장 함수가 정적 메소드와 같은 특징을 가지므로, 확장 함수를 하위 클래스에서 오버라이드할 수는 없음
- 어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출됨
(멤버 함수의 우선순위가 더 높음)
확장 프로퍼티
- 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐
- 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 함
vararg
- 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있음
- 가변 길이 인자는 메소드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능 (자바에서 …)
- 이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때, 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 함
> 스프레드 연산자 *를 배열 앞에 붙임
infix
- 중위 함수 호출 구문 사용 시 인자가 하나뿐인 메소드를 간편하게 호출
open, final, abstract 변경자
final
- 오버라이드 할 수 없음, 클래스 멤버의 기본 변경자
- 클래스의 기본적인 상속 가능 상태를 final로 함으로써 얻을 수 있는 큰 이익은 다양한 경우에 스마트 캐스트가 가능하다는 점
open
- 오버라이드 할 수 있음
abstract
- 반드시 오버라이드 해야 함
접근 변경자(access modifier, 가시성 변경자)
public, internal, protected, private
internal
- “모듈 내부에서만 볼 수 있음”이라는 뜻으로 모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미
- 자바에 internal과 딱 맞는 가시성이 없음, 패비지-전용 가시성은 모듈은 여러 패키지로 이뤄지며 서로 다른 모듈에 같은 패키지에 속한 선언이 들어 있을 수 있기 때문에 internal과 완전히 다름, 따라서 internal 변경자는 바이트코드상에서는 public이 됨
- 코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다는 점
inner class
- 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같음
- 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 함
람다
- 다른 함수에 넘길 수 있는 작은 코드 조각
- 함수를 선언할 필요가 없고 코드 블록을 직접 함수의 인자로 전달할 수 있음
- 람다가 포획한(captured) 변수: 람다 안에서 사용하는 외부 변수
- 람다를 실행 시점에 표현하는 데이터 구조는 람다에서 시작하는 모든 참조가 포함된 닫힌 객체 그래프를 람다 코드와 함께 저장해야 함. 그런 데이터 구조를 클로저라고 부름. 함수를 쓸모 있는 1급 시민으로 만들려면 포획한 변수를 제대로 처리해야 하고, 포획한 변수를 제대로 처리하려면 클로저가 꼭 필요하다. 그래서 람다를 클로저라고 부르기도 한다. 람다, 무명 함수, 함수 리터럴, 클로저를 서로 혼용하는 일이 많다.
- 람다가 변수를 포획하면 무명 클래스 안에 포획한 변수를 저장하는 필드가 생기며, 매 호출마다 그 무명 클래스의 인스턴스를 새로 만듦.
하지만 포획하는 변수가 없는 람다에 대해서는 인스턴스가 단 하나만 생김 - inline으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명클래스도 만들어지지 않음
> 대부분의 코틀린 확장 함수들은 inline이 붙어 있음 - 람다에는 무명 객체와 달리 인스턴스 자신을 가리키는 this가 없음,
람다 안에서 this는 그 람다를 둘러싼 클래스의 인스턴스
lazy
- 시퀀스를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있음
- 코틀린 지연 계산 시퀀스는 Sequence 인터페이스에서 시작
Sequence 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현할 뿐임, Sequence 안에는 iterator라는 단 하나의 메소드가 있고 이 메소드를 통해 시퀀스로부터 원소 값을 얻음 - Sequence 인터페이스의 강점은 그 인터페이스 위에 구현된 연산이 계산을 수행하는 방법 때문에 생김. 시퀀스의 원소는 필요할 때 비로소 계산됨. 따라서 중간 처리 결과를 저장하지 않고도 연산을 연쇄적으로 적용해서 효율적으로 계산을 수행할 수 있음
- 시퀀스의 원소를 차례로 이터레이션해야 한다면 시퀀스를 직접 써도 되지만 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메소드가 필요하다면 시퀀스를 리스트로 반환해야 함
- 시퀀스에 대한 연산은 중간 연산과 최종 연산으로 나뉨.
중간 연산은 다른 시퀀스를 반환하고
그 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 안다.
최종 연산은 결과를 반환.
결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터
일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체 - 중간 연산은 항상 지연 계산,
최종 연산을 호출하면 연기 됐던 모든 계산 수행 - 시퀀스의 경우 모든 연산은 각 원소에 대해 순차적으로 적용
run, with, apply
run
- 코드의 일부분을 블록으로 둘러싸 실행할 필요가 있을 때 사용
- 인자로 받은 람다를 실행해주는 라이브러리 함수
with
- 수신 객체 지정 람다
- 어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산 수행
- 파라미터가 2개 있는 함수, 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만듦, 인자로 받은 람다 본문에서는 this를 사용해 그 수신 객체에 접근
- 일반 함수를 일반 람다, 확장 함수를 수신 객체 지정 람다로 생각할 수 있음
apply
- 수신 객체 지정 람다
- with와 비슷하지만 유일한 차이는 apply는 항상 자신에게 전달된 객체를 반환한다는 점
- apply는 확장 함수로 정의돼 있음,
apply의 수신 객체가 전달받은 람다의 수신 객체가 됨 - 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야 하는 경우 유용
타입 시스템
- 타입: 타입은 분류, 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는연산의 종류 결정
- 코틀린의 널이 될 수 있는 타입은 널이 될 수 있는 타입과 널이 될 수 없는 타입을 구분하여 각 타입의 값에 대해 어떤 연산이 가능할지 명확히 이해할 수 있고, 실행 시점에 예외를 발생시킬 수 있는 연산을 판단할 수 있고, 그런 연산을 금지시킬 수 있음
- 자바에서 가져온 널 값을 널이 될 수 없는 코틀린 변수에 대입하면 실행 시점에 대입이 이뤄질 때 예외 발생
- 자바 클래스나 인터페이스를 코틀린에서 구현할 경우 널 가능성을 제대로 처리하는 일이 중요, 구현 메소드를 다른 코틀린 코드가 호출할 수 있으므로 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어줌
- 널 가능성 관련 지식은 코틀린이 자바의 박스 타입을 처리하는 방법을 이해할 때 중요한 역할을 함
안전한 호출 연산자 ?.
- 호출하려는 값이 null이 아니라면 ?.은 일반 메소드 호출처럼 작동하고
호출하려는 값이 null이면 이 호출은 무시되고 null이 결과 값이 됨
엘비스 연산자 ?:
- null 대신 사용할 디폴트 값을 지정할 때 사용
널 아님 단언 !!
- !!는 컴파일러에게 “나는 이 값이 null이 아님을 잘 알고 있고, 잘못 생각해서 예외가 발생해도 감수한다" 는 것
- 호출된 함수가 언제나 다른 함수에서 널이 아닌 값을 전달받는다는 사실이 분명하다면 굳이 널 검사를 다시 수행하고 싶지 않을 것, 이럴 때 사용
- !!를 널에 대해 사용해서 발생하는 예외의 스택 트레이스에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않음. 어떤 값이 널이었는지 확실히 하기 위해 여러 !! 단언문을 한 줄에 함께 쓰는 일은 피해야 함
let 함수
- 자신의 수신 객체를 인자로 전달받은 람다에 넘김
- 널이 될 수 있는 값에 대한 안전한 호출 구문을 사용해 let을 호출하되 널이 될 수 없는 타입을 인자로 받는 람다를 let에 전달
- 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우 흔히 사용됨
- let을 중첩시켜 처리하면 코드가 복잡해져서 알아보기 어려워지므로 일반적인 if를 사용해 모든 값을 한꺼번에 검사하는 것이 나음
lateinit
- 항상 var여야 함, val 프로퍼티는 final 필드로 컴파일 되며, 생성자 안에서 반드시 초기화 해야 함. 따라서 생성자 밖에서 초기화 해야 하는 나중에 초기화하는 프로퍼티는 항상 var여야 함.
- 나중에 초기화하는 프로퍼티는 널이 될 수 없는 타입이라 해도 더 이상 생성자 안에서 초기화 할 필요가 없음
타입 파라미터
- 타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반드시 물음표를 타입 이름 뒤에 붙여야 한다는 규칙의 유일한 예외
- 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한을 지정해야 함
> fun<T: Any> printHashCode(t: T) { … }
코틀린의 원시 타입
- 원시 타입의 변수는 그 값이 직접 들어가지만, 참조 타입의 변수에는 메모리상의 객체 위치가 들어감
- 코틀린의 Int 타입은 자바 int 타입으로 컴파일 됨, 이런 컴파일이 불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우 뿐이며 이 경우 Int의 래퍼 타입에 해당하는 java.lang.Integer 객체가 들어감
- null 참조를 자바 참조 타입의 변수에만 대입할 수 있기 때문에 널이 될 수 있는 코틀린 타입은 자바 원시 타입으로 표현할 수 없음. 그래서 코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 됨
- 제네릭 클래스의 경우 래퍼 타입을 사용, 어떤 클래스의 타입 인자로 원시 타입을 넘기면 코틀린은 그 타입에 대한 박스 타입 사용
> JVM은 타입 인자로 원시 타입을 허용하지 않기 때문
최상위 타입 Any, Any?
- 자바에서 Object가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입
- 내부에서 Any 타입은 java.lang.Object에 대응, 자바 메소드에서 Object를 인자로 받거나 반환하면 코틀린에서는 Any로 그 타입을 취급
- 모든 코틀린 클래스는 Any에 정의된 메서드를 상속받으며 toString, equals, hashCode라는 세 메소드가 들어 있음
Unit 타입: 코틀린의 void
- 코틀린 함수의 반환 타입이 Unit이고 그 함수가 제네릭 함수를 오버라이드 하지 않는다면 그 함수는 내부에서 자바 void 함수로 컴파일
- 코틀린의 Unit이 자바 void와 다른 점: Unit은 모든 기능을 갖는 일반적인 타입이며, void와 달리 Unit을 타입 인자로 쓸 수 있음, Unit 타입에 속한 값은 단 하나뿐이며, 그 이름도 Unit임. Unit 타입의 함수는 Unit 값을 묵시적으로 반환
> 제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환 타입으로 Unit을 쓸 때 유용 - 함수형 프로그래밍에서 전통적으로 Unit은 ‘단 하나의 인스턴스만 갖는 타입'을 의미해 왔고 바로 그 유일한 인스턴스의 유무가 자바 void와 코틀린 Unit을 구분하는 가장 큰 차이점
Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다
- Nothing 타입은 아무 값도 포함하지 않으므로 Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일타입 파라미터로만 쓸 수 있음
fun fail(message: String): Nothing { throw IllegalStateException(message)
}val address = company.address ?: fail("No address")
println(address.city)
- 컴파일러는 Nothing이 반환 타입인 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용, 위의 예제에서 컴파일러는 company.address가 널인 경우 엘비스 연산자의 우항에서 예외가 발생한다는 사실을 파악하고 address의 값이 널이 아님을 추론할 수 있음
컬렉션과 배열
kotlin.collections.Collection
- 컬렉션 안의 원소에 대해 이터레이션하고, 컬렉션의 크기를 얻고, 어떤 값이 컬렉션 안에 들어있는지 검사하고, 컬렉션에서 데이터를 읽는 여러 다른 연산 수행 가능, 하지만 원소를 추가하거나 제거하는 메소드는 없음
- MutableCollection: kotlin.collections.Collection을 확장하면서 원소 추가, 삭제, 컬렉션 안의 모든 원소 삭제 등의 메소드 제공
- 컬렉션 인터페이스 사용 시 읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션일 필요는 없다는 점을 항상 염두 해 둬야 함
>읽기 전용 컬렉션이 항상 스레드 안전(thread-safe)하지 않다는 점 - 자바 컬렉션 인터페이스의 구조를 그대로 옮겨 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스이지만 코틀린 상위 타입을 갖는 것처럼 취급함으로써 자바 호환성을 제공하는 한편 읽기 전용 인터페이스와 변경 가능 인터페이스를 분리
- 자바 표준 라이브러리에 속한 클래스의 인스턴스를 반환하기 때문에 내부는 변경 가능한 클래스이고, java.util.Collection을 파라미터로 받는 자바 메소드가 있다면 Collection이나 MutableCollection 값을 인자로 넘길 수 있음. 이 때 컬렉션을 변경하는 자바 메소드에 읽기 전용 Collection을 넘겨도 코틀린 컴파일러가 이를 막을 수 없음
배열
- 원시 타입인 원소로 이뤄진 배열에도 확장 함수를 똑같이 사용할 수 있지만 이런 함수가 반환하는 값은 배열이 아니라 리스트라는 점에 유의
- Array<Int>는 박싱된 정수의 배열(java.lang.Integer[])
- 코틀린은 원시 타입의 배열을 표현한ㄴ 별도 클래스를 각 원시 타입마다 하나씩 제공
: IntArray, ByteArray, CharArray, BooleanArray 등
> int[], byte[], char[] 등으로 컴파일 되며 이런 배열의 값들은 박싱하지 않고 가장 효율적인 방식으로 저장 됨
위임 프로퍼티
- 위임 프로퍼티를 사용하면 값을 뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있음
- 위임: 객체가 직접 작업을 수행하지 않고 다른 ㄷ우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴이며, 작업을 처리하는 도우미 객체를 위임 객체라고 함
class Foo { var p: Type by Delegate()
}
- p 프로퍼티는 접근자 로직을 다른 객체에게 위임, 위에서는 Delegate 클래스의 인스턴스를 위임 객체로 사용하고, by 뒤에 있는 식을 계산해서 위임에 쓰일 객체를 얻음
by lazy()를 사용한 프로퍼티 초기화 지연
- 지연 초기화: 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 흔히 쓰이는 패턴
- 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 지연 초기화 패턴을 사용할 수 있음
- 위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화
- lazy 함수는 getValue 메소드가 들어있는 객체를 반환, lazy를 by와 함께 사용해 위임 프로퍼티를 만들 수 있음
- lazy 함수의 인자는 값을 초기화할 때 호출할 람다
- lazy 함수는 기본적으로 스레드 안전
by
- by 오른쪽에 오는 객체를 위임 객체라고 부름
- 코틀린은 위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체의 프로퍼티를 읽거나 쓸 때마다 위임 객체의 getValue와 setValue를 호출
고차함수(High-order function)
- 람다나 함수 참조를 인자로 넘길 수 있거나 람다나 함수 참조를 반환하는 함수
- 코드 중복을 줄일 때 함수 타입이 상당히 도음, 코드의 일부분을 복사해 붙여 넣고 싶은 경우가 있다면 그 코드를 람다로 만들면 중복을 제거할 수 있을 것
inline: 람다의 부가 비용 없애기
- 코틀린이 보통 람다를 무명 클래스로 컴파일하지만 그렇다고 람다 식을 사용할 때마다 새로운 클래스가 만들어지지는 않음
- 람다가 변수를 포획하면 람다가 생성되는 시점마다 새로운 무명 클래스 객체가 생김
> 이 경우 실행 시점에 무명 클래스 생성에 따른 부가 비용이 듦
> 람다를 사용하는 구현은 똑같은 작업을 수행하는 일반 함수를 사용한 구현보다 덜 효율적 - inline 변경자를 어떤 함수에 붙이면 컴파일러는 그 함수를 호출하는 모든 문장을 함수 본문에 해당하는 바이트코드로 바꿔치기 해줌
> 인라인 된다: 함수를 호출하는 코드를 함수를 호출하는 바이트코드 대신에 함수 본문을 번역한 바이트코드로 컴파일한다는 뜻
fun <T> abc(value: Value, action: () -> T) :T { ...
}fun asdf () { ...
abc(somthing) {
... }}
- 람다를 넘기는 위의 경우 람다의 본문도 함께 인라이닝 됨
> 람다의 본문에 의해 만들어지는 바이트코드는 그 람다를 호출하는 코드 정의의 일부분으로 간주되기 때문에 코틀린 컴파일러는 그 람다를 함수 인터페이스를 구현하는 무명 클래스로 감싸지 않음
fun asdf(body: () -> Unit) { abc(something, body)
}
- 인라인 함수를 호출하는 코드 위치에서는 변수에 저장된 람다의 코드를 알 수 없으므로 람다 본문은 인라이닝되지 않고 abc 함수의 본문만 인라이닝
인라인 함수의 한계
- 인라인 함수의 본문에서 람다 식을 바로 호출하거나 람다 식을 인자로 전달받아 바로 호출하는 경우 그 람다를 인라이닝 할 수 있음, 그런 경우가 아니라면 컴파일러는 “Illegal usage of inline-parameter” 메시지와 함께 인라이닝을 금지 시킴
- 시퀀스는 람다를 저장해야하므로 람다를 인라인하지 않음, 따라서 지연 계산을 통해 성능을 향상시키려는 이유로 모든 컬렉션 연산에 sSequence를 붙여서는 안됨. 시퀀스 연산에서는 람다가 인라이닝 되지 않기 때문에 크기가 작은 컬렉션은 오히려 일반 컬렉션 연산이 더 성능이 좋을 수 있음
함수를 인라인으로 선언해야 하는 경우
- inline 키워드를 사용해도 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높음, 다른 경우는 주의 깊게 성능을 측정하고 조사해봐야 함
- 일반 함수 호출의 경우 JVM은 이미 강력하게 인라이닝을 지원, JVM은 코드 실행을 분석해서 가장 이익이 되는 방향으로 호출을 인라이닝하며 이런 과정은 바이트코드를 실제 기계어 코드로 번역하는 과정(JIT)에서 일어남. 이런 JVM의 최적화를 활용한다면 바이트코드에서는 각 함수 구현이 정확히 한 번만 있으면 되고, 그 함수를 호출하는 부분에서 따로 함수 코드를 중복할 필요가 없음. 반면 코틀린 인라인 함수는 바이트코드에서 각 함수 호출 지점을 함수 본문으로 대치하기 때문에 코드 중복이 생김.
+ 함수를 직접 호출하면 스택 트레이스가 더 깔끔해짐 - 람다를 인자로 받는 함수를 인라이닝하면 이익이 더 많음
_1 인라이닝을 통해 없앨 수 있는 부가 비용이 상당함: 함수 호출 비용을 줄일 뿐 아니라 람다를 표현하는 클래스와 람다 인스턴스에 해당하는 객체를 만들 필요도 없어짐
_2 현재의 JVM은 함수 호출과 람다를 인라이닝 해 줄 정도로 똑똑하지는 못함
_3 인라이닝을 사용하면 일반 람다에서는 사용할 수 없는 넌로컬 반환과 같은 몇 가지 기능을 사용할 수 있음 - inline 변경자를 함수에 붙일 때는 코드 크기에 주의를 기울여야 함. 인라이닝하는 함수가 큰 경우 함수의 본문에 해당하는 바이트코드를 모든 호출 지점에 복사해 넣고 나면 바이트코드가 전체적으로 아주 커질 수 있음
고차 함수 안에서 흐름 제어
- 람다 안의 return문: 람다를 둘러싼 함수로부터의 반환
_람다 안에서 return을 사용하면 람다로부터만 반환되는게 아니라 그 람다를 호출하는 함수가 실행을 끝내고 반환 됨.
_자신을 둘러싸고 있는 블록보다 더 바깥에 있는 다른 블록을 반환하게 만드는 return 문을 넌로컬 return이라 부름
_return이 바깥쪽 함수를 반환시킬 수 있는 때는 람다를 인자로 받는 함수가 인라인 함수인 경우 뿐 - 람다로부터 반환: 레이블을 사용한 return
_로컬 return과 넌로컬 return을 구분하기 위해 레이블을 사용해야 함
_람다 식에 레이블을 붙이려면 레이블 이름 뒤에 @문자를 추가한 것을 람다를 여는 { 앞에 넣으면 됨
_무명 함수를 통해 넌로컬 반환문을 여럿 사용해야 하는 코드 블록을 쉽게 작성할 수 있음 - 무명 함수: 기본적으로 로컬 return
fun foo(list: List<Something>) { list.forEach(fun (item) { if (item == "something") return println("$item is not something")
}
}
_return: fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환 시킴
_람다식은 fun 을 사용해 정의되지 않으므로 람다 본문의 return은 람다 밖의 함수를 반환시킴
_무명 함수는 fun을 사용해 정의되므로 그 함수 자신이 바로 가장 안쪽에 있는 fun으로 정의됨 > 무명 함수 본문의 return은 그 무명 함수를 반환시키고, 무명 함수 밖의 다른 함수를 반환시키지 못함
Coroutine
- 코루틴 빌더에 원하는 동작을 람다로 넘겨 코루틴을 만들어 실행하는 방식
- callback을 대체하는 방법
다른 스레드에서 작업 후 main으로 돌아올 때, 반환 받기 위해 동기를 맞춰야 하고 이럴 때 callback을 사용함. callback을 사용하면 boilerplate code가 발생하게 됨. 코루틴을 사용하면 코드를 심플하게 할 수 있음. thread 전환 없이 순차적인 코드를 작성하고 background 작업을 간단하게 할 수 있음.
kotlinx.coroutines.CoroutineScope.launch
- launch는 코루틴을 Job으로 반환하며, 만들어진 코루틴은 기본적으로 즉시 실행
- 원하면 launch가 반환한 Job의 cancel()을 호출해 코루틴 실행 중단 가능
- 다른 suspend 함수라면 해당 함수가 사용중인 CoroutineScope가 있겠지만, 그렇지 않은 경우 GlobalScope 사용 가능
- GlobalScope: 메인 스레드가 실행 중인 동안만 코루틴의 동작 보장
- 메인 스레드가 종료되면서 프로그램 전체가 끝나버리므로 GlobalScope()를 사용할 ㄸ는 주의가 필요하며, 이를 방지하려면 비동기적으로 launch를 실행하거나, launch가 끝날 때까지 기다려야 함
- runBlocking(): 코루틴의 실행이 끝날 떄까지 현재 스레드를 블록 시킴
CoroutineScope의 일반함수이므로 별도의 코루틴 ㅡ코프 객체 없이 사용 가능
kotlinx.coroutines.CoroutineScope.async
- async는 launch와 같은 일을 하지만 유일한 차이는 launch가 Job을 반환하는 반면 async는 Deffered를 반환한다는 점
- Deffered는 Job을 상속한 클래스로, Job은 아무 타입 파라미터가 없는데 Deffered는 타입 파라미터가 있는 제네릭 타입이라는 점과 Deffered 안에는 await() 함수가 정의돼 있다는 점이 다름
- Deffered의 타입 파라미터는 바로 Deffered 코루틴이 계산 하고 돌려주는 값의 타입
- Job은 Unitdmf ehffuwnsms Deffered<Unit> 이라 생각할 수도 있음
- async는 코드 블록을 비동기로 실행할 수 있고(제공하는 코루틴 컨텍스트에 따라 여러 스레드를 사용하거나 한 스레드 안에서 제어만 왔다 갔다 할 수도 있음), async가 반환하는 Deffered의 await을 사용해서 코루틴이 결과 값을 내놓을 때까지 기다렸다가 결과 값을 얻어낼 수 있음
- 스레드를 여럿 실행하는 병렬 처리와 달리 모든 async 함수들이 메인 스레드 안에서 실행된다는 점이 스레드를 사용한 병렬 처리와의 큰 차이
동시에 실행할 수 있는 스레드 개수가 한정된 경우 특히 코루틴과 일반 스레드를 사용한 비동기 처리 사이 차이가 커짐
CoroutineContext와 Dispatcher
- launch와 async는 CoroutineScope의 확장함수로 CoroutineScope에는 CoroutineContext 타입의 필드 하나만 가짐
- CoroutineContext는 실제로 코루틴이 실행 중인 여러 Job과 디스패처를 저장하는 일종의 맵
- 코틀린 런타임은 이 CoroutineContext를 사용해서 다음에 실행할 작업을 선정하고 어떻게 스레드에 배정할지에 대한 방법을 결정
- Dispatcher
_Dispatchers.Main
_Dispatchers.Default: shared background thread common pool 사용, CPU resource를 사용하는 연산이 많이 필요한 작업에 적합
_Dispatchers.IO: on-demand thread pool을 사용, file I/O나 socketI/O와 같이 I/O 집중적인 blocking 작업에 적합
_Dispatchers.Unconfined: 보통 코드에는 사용이 적합하지 않음,
첫 suspension 까지 실행된 후 빌더 함수가 return 되고, 특정 스레드나 pool에 제한 없이 suspending function에 사용되는 thread 에서 resume 됨 - launch, async, runblocking 등은 Coroutine Builder라고 불림: 코루틴을 만들어 줌
일시 중단 함수들
- delay(), yield()
- withContext: 다른 context로 코루틴 전환
- withTimeout: 코루틴이 정해진 시간 안에 실행되지 않으면 예외 발생
- withTimeoutOrNull: 코루틴이 정해진 시간 안에 실행되지 않으면 null return
- awaitAll: 모든 작업의 성공을 기다림, 작업 중 어느 하나가 예외로 실패하면 awaitAll도 그 예외로 실패
- joinAll: 모든 작업이 끝날 때까지 현재 작업 일시 중단
suspend
- suspend fun의 동작
_코루틴에 진입할 때와 코루틴에서 나갈 때 코루틴이 실행중이던 상태를 저장하고 복구하는 등의 작업을 할 수 있어야 함
_현재 실행중이던 위치를 저장하고 다시 코루틴이 재개될 때 해당 위치부터 실행을 재개할 수 있어야 함
_다음에 어떤 코루틴을 실행할지 결정(코루틴 컨텍스트에 있는 디스패처에 의해 수행)
참고
Kotlin IN ACTION — 드미트리 제메로프, 스베트라나 이사코바 지음, 오현석 옮김