코틀린 입문 스터디 Week2 자료

mook2_y2
22 min readJan 20, 2019

--

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 2주차 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

Nullability

1. Nullable types

  • Null은 이를 고안한 Tony Hoare 조차 “Billion Dollar Mistake”라고 언급할 정도로 NPE (NullPointerException)라는 골치 아픈 문제를 만들었다. (관련 링크 : https://kwangyulseo.com/2016/10/16/null%EA%B3%BC-nullreferenceexception/) 이에 대한 Kotlin을 포함한 최신 언어들의 개선 방안은 이 문제를 Run-time Exception에서 Compile-time error로 바꾸는 것이다. 이를 통해 컴파일 과정에서 문제를 파악하여 사전에 예방할 수 있다.
  • Nullable type인 변수를 역참조 (dereference) 하고자 할 경우 (ex: Non-nullable 에 대해 정의된 확장함수(Extension)나 , Non-nullable 타입을 인자로 받는 함수) 컴파일 에러가 발생한다. 이를 처리하기 위해서는 조건문을 사용하여 명시적으로 null 여부를 체크하거나, Safe access expression(?)을 쓰거나, Not-null assertion(!!)을 사용해야 한다.
  • 한편 조건문을 통해 null 체크한 뒤에 컴파일 에러가 발생하지 않는 것은 smart casts 에 따라 자동으로 Non-nullable로 형변환되기 때문이다. (관련 링크 : https://link.medium.com/XPKeQSHTBT 6번 항목)
  • Safe access expression(?)을 쓸 경우 역참조하는 변수가 null인 경우 null을 반환한다. 이에 대한 디폴트값을 주기 위해서는 Elvis operator(?:)를 사용한다. Elvis operator는 Groovy 언어에서 영감을 받아 차용된 된 개념이다.
  • Elvis operator(?:)는 Kotlin 연산자 우선순위(Operator precedence)에 따라 동작하므로 다른 연산자와 함께 사용할 때 주의해야 한다. (관련 링크 : https://kotlinlang.org/docs/reference/grammar.html 의 Expressions > Precedence 항목)
  • Not-null assertion(!!)의 경우 개발자가 논리적으로 절대 null이 발생할 가능성이 없다고 확신할 때를 제외하면 가능한 피하는 것이 좋다. 이는 결국 기존 Java의 문제점인 Run-time 상에서의 NPE를 허용하는 것이기 때문이다. 하지만 그럼에도 Not-null assertion(!!) 이 좋은 것은 Java에 비해 가독성 측면에서 NPE가 발생할 가능성이 있는 지점을 명시적으로 보여주기 때문이다.
  • Kotlin은 Int, Char, Boolean 등에 대해 Primitive Type과 Wrapper class(Reference Type)를 구분하지 않는다. Java처럼 int와 Integer를 구분하지 않으며 오직 Int만 가지고 컴파일시에 자동으로 상황에 맞게 Primitive Type 또는 Wrapper class로 변환한다. 한편, Java에서 Primitive Type은 Null을 가질 수 없으므로 Int?와 같이 Nullable 타입으로 지정하는 경우 컴파일시에 무조건 Wrapper Class로 변환된다. (관련 링크 : https://tourspace.tistory.com/115)

2. Nullable types under the hood

  • Kotlin에서 Nullable type과 Non-nullable type은 Java8에서 도입된 Optional 클래스가 아니라, RetentionPolicy.CLASS (런타임시 적용되지 않음)인 Java annotation 형태로 (@Nullable, @NotNull) 변환되어 적용된다. 이에 따라 성능 오버헤드가 발생하지 않으며 Null 문제를 해결할 수 있다는 장점이 있다. (관련 링크 : https://yookeun.github.io/java/2017/01/13/java-annotation/, https://kotlinlang.org/docs/reference/java-interop.html 의 nullability annotation 부분)
  • List<Int?> 는 non-nullable list of nullable values 이고, List<Int>?는 nullable list of non-nullable values 이다.

3. Checking wheter string is null or empty 실습 코드 작성시 고려할 점

  • Extension을 정의하는 String은 nullable일까 non-nullable일까?
  • Extension의 반환 타입은 무엇일까? (답변 영상에서는 1주차에서 배운 Function with expression body를 통해 타입 추론으로 반환 타입 명시를 생략하였으나 연습시 우선 반환 타입을 명시하고 시작해봅시다.)
  • 1주차에서 배운 Smart Casts를 활용하여 코드를 간소화하기 위해서 비교문 순서를 어떻게 하는게 좋을까?

4. Safe casts

  • Unsafe casts (as) : as 키워드는 Type casting 시에 사용되며 캐스팅하려는 변수가 null 또는 다른 타입인 경우 컴파일 오류는 발생하지 않지만 런타임시에 ClassCastException이 발생한다.
  • Safe casts (as?) : as? 키워드는 캐스팅하려는 변수가 null 또는 다른 타입인 경우 null이 반환되며 런타임 오류가 발생하지 않는다.
  • as, as? 키워드 관련 링크 : https://link.medium.com/9aeBJooOET 의 1번, 2번 부분
  • 런타임 오류 관련 링크 : https://cocomo.tistory.com/209

5. Safe casts 실습 코드 작성시 고려할 점

val s:Int = 1
println(s as Int?)
  • 위와 같이 s에 대해 명시적으로 Non-nullable Int로 타입을 지정해주더라도 s as Int?는 런타임시에 null을 반환하지 않고 동일한 타입으로 인식한다. (Optional Class와 annotation 방식의 차이, Sub-typing과 관련하여 생각해볼 점)

6. Importance of nullability

  • Null은 reference type에 저장된 값이 없음을 표현하기 위해 필요한 개념이지만 런타임시에 예상치 못하게 NPE가 발생한다는 문제가 있어 anti-pattern 으로도 불린다. (관련 링크 : https://ko.wikipedia.org/wiki/%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4)
  • Kotlin은 Nullable/Non-nullable type, 몇 개의 Operator (?, ?:, !!, as?), smart casts 등 몇가지 기능을 통해 Nullability 문제를 효과적으로 다룰 수 있도록 설계 되었다. 이를 통해 null이 가지고 있던 문제를 효과적으로 통제하고 추적할 수 있도록 하였으며 (anti-pattern to legitimate pattern), Null을 First-class citizen으로 다룰 수 있도록 했다. (관련 링크 : https://link.medium.com/iDMpJXqcFT)
  • Java8에서 도입된 Optional 방식에 비해 Kotlin 방식이 좋은 점은 앞서 언급한 성능 오버헤드 이슈 외에도, 런타임시에 Nullable type과 Non-nullable type이 동일하게 동작하여 변수 할당에 있어 유연하다는 점이다. (관련 링크 : https://en.wikipedia.org/wiki/Subtyping)
  • 이와 같은 Kotlin의 Nullability를 다루는 방식이 효과적이고 유용한 것으로 실무 개발에서 검증되고 있다.

Functional Programming

1. Lambdas

//1. 기본 형태 (Lambda expression을 mapValues 확장함수의 인자로 받음)pairList.filter({ i:Employee? -> i?.city == "PRAGUE" }) //2. Lambda를 괄호 뒤에 씀 
employees.filter() { i:Employee? -> i?.city == "PRAGUE" }
//3. 괄호가 비는 경우 괄호 생략 (Groovy 에서 고안하여 검증되었고 차용한 아이디어)
employees.filter { i:Employee? -> i?.city == "PRAGUE" }
//4. Lambda의 인자 타입 생략
employees.filter { i -> i?.city == "PRAGUE" }
//5. Lambda의 인자가 1개인 경우 인자 생략하고 it으로 처리
employees.filter { it?.city == "PRAGUE" }
  • Lambda가 인자로 Map.Entry나 Pair를 받는 경우 아래와 같이 코드를 간소화할 수 있다.
//1. 기본 형태 
map.mapValues { entry -> "${entry.value}!"}
//2. Destructuring syntax (인자를 Pair 형태로 명시하여 바로 value 접근 가능)
map.mapValues { (key, value) -> "$value!"}
//3. 사용되지 않는 인자를 "_" 처리하여 사용되지 않음을 명시 & 네이밍 고민안해도 됨
map.mapValues { (_, value) -> "$value!" }

2. Common Operations on a collections

  • Kotlin 라이브러리는 Java Collections에 대한 다양한 operation 확장함수를 제공한다.

filter / partition / count

  • filter : Lambda로 정의된 predicate를 만족하는 원소들로 구성된 Collection을 반환하며 만족하는 원소가 없다면 null을 반환.
  • partition : Lambda로 정의된 predicate를 만족하는 원소들과, 만족하지 않는 원소들로 나누어 Pair of Collectoins를 반환. 상황에 따라 만족하지 않는 원소들도 필요한 경우 filter 대신 partition 사용.
  • count: Lambda로 정의된 predicate를 만족하는 원소 개수를 반환.
  • predicate : boolean test(T) 메소드가 정의된 인터페이스로 T 타입 데이터에 대한 특정 조건 부합 여부를 확인하여 bool을 반환 (관련 링크 : https://m.blog.naver.com/PostView.nhn?blogId=deepplin&logNo=220490253913&proxyReferer=https%3A%2F%2Fwww.google.com%2F)

map / flatMap

  • map : Collection의 모든 원소 각각에 대해 Lambda로 정의된 transform을 통해 변경된 원소들로 구성된 Collection을 반환. 반환되는 Collection의 원소개수는 대상 Collection의 원소개수와 같음.
  • flatMap : map의 transform이 List를 반환하는 경우, map operation 후에 flatten (list of list를 flat하게 바꿈) 처리를 하여 반환

any / all / none

  • any : Lambda로 정의된 predicate를 만족하는 원소가 1개 이상 존재하는지 여부(Boolean) 반환
  • all : Lambda로 정의된 predicate를 모든 원소가 만족하는지 여부(Boolean) 반환
  • none : Lambda로 정의된 predicate를 모든 원소가 만족하지않는지 여부(Boolean) 반환

find / firstOrNull / first / lastOrNul / last

  • find / firstOrNull : Collection의 원소들 중에서 Lambda로 정의된 predicate를 만족하는 가장 첫번째 원소를 반환. 만족하는게 없는 경우 null 반환
  • first : find와 동일한 기능을 하나 만족하는게 없는 경우 NoSuchElementException 발생. 대신 first의 반환값은 non-nullable임이 보장되어 safe access, non-null assertion 등 불필요함. Lambda(predicate)가 없는 경우 Collection의 첫번째 원소 반환.
  • lastOrNull : Collection의 원소들 중에서 Lambda로 정의된 predicate를 만족하는 가장 마지막 원소를 반환. 만족하는게 없는 경우 null 반환
  • last : lastOrNull과 동일한 기능을 하나 만족하는게 없는 경우 NoSuchElementException 발생. 대신 last의 반환값은 non-nullable임이 보장되면 Lambda(predicate)가 없는 경우 Collectoin의 마지막 원소 반환

groupBy / associateBy

  • groupBy : Lambda로 정의된 argument를 기준으로 Collection의 원소들을 그룹화 하여 나눈 기준과 기준에 따라 분류된 Collection으로 구성된 Map 반환
  • associateBy : groupBy와 유사하나 나눈 기준 마다 1개의 원소만을 반환. 그룹별 원소가 unique하다고 판단될 경우 Collection 없이 반환되므로 편리하나, unique 하지 않을 경우 가장 마지막 원소를 제외하고 지워짐.

Zip

  • 2개의 Collection에 대해 같은 index의 원소들끼리 Pair로 만든 Collection 반환. collection1.zip(collection2) 형태로 사용함.
  • 2개의 Collection 중 원소 개수가 적은 쪽을 기준으로 하여 많은 쪽 Collection의 뒷 원소들은 버려짐.

maxBy / minBy

  • maxBy : Lambda로 정의된 argument를 기준으로 비교하여 가장 큰 값을 가지는 원소를 반환.
  • minBy : Lambda로 정의된 argument를 기준으로 비교하여 가장 작은 값을 가지는 원소를 반환
  • 공통적으로 해당 기준의 max/min이 2개 이상일 경우 앞쪽 index가 반환되며, Collection이 비어있는 경우 null이 반환됨.

distinct

  • Collection에서 중복되는 원소를 제거하고 unique한 원소들만으로 구성된 Collection을 반환함.

3. Operations Quiz — I / Operations Quiz — II

  • Map에서 key에 해당되는 value를 조회할 때 map["key"] 는 해당 key가 없는 경우 null을 반환하며, map.getValue("key") 는 해당 key가 없는 경우 NoSuchElementException을 반환하고, map.getOrElse("key") { } 는 해당 key가 없는 경우 lambda로 정의된 argument를 디폴트값으로 하여 반환함. (lambda 호출과 연산은 key가 없는 경우에만 진행되므로 불필요한 연산을 줄일 수 있음)
  • Lambda 속에 Lambda가 중첩되어 사용되거나, 여러 Collection operation 확장함수를 연달아 사용하여 Lambda가 여러번 사용될 때, it은 의미를 혼동하게할 가능성이 있으므로 인자를 명시적으로 네미밍하는 것을 장려.
  • Collection operation은 매우 다양하며 동일한 연산을 위해 가장 간단하고 명확한 operation을 선택해야함. 아래 2개 코드는 사실상 동일한 연산이지만 후자가 보다 명료함.
/**
* 1번.
* Collection의 원소들을 2개씩 Pair로 묶은 모든 가능한 조합에 대해
* 두 원소의 age의 차이가 가장 큰 Pair를 찾음
*/
val allPossiblePairs = heroes
.flatMap { firstHero ->
heroes.map{
secondHero -> firstHero to secondHero
}
}
val (oldest, youngest) = allPossiblePairs
.maxBy { it.first.age - it.second.age }!!
println(oldest.name)
/**
* 2번.
* 1번은 사실상 가장 age가 큰 원소와 가장 age가 작은 원소를 찾는 것과 동일
*/
val oldestHero = heroes.maxBy { it.age }
val youngestHero = heroes.minBy { it.age }
println(oldestHero?.name)

4. Function Types

  • Lambda, 명시적인 익명함수 정의, 그리고 다음 강의(5. Member Reference)에서 배울 함수의 reference를 통해 함수를 Variable에 저장할 수 있으며 이 때 Function Type이 정의된다. 변수 타입 추론과 마찬가지로 타입 추론도 가능하다.
val isEven : (Int) -> Boolean = { i : Int -> i % 2 == 0 } 
val isEven = { i : Int -> i % 2 == 0 } //타입 추론
isEven(5) // 함수를 변수에 저장하는 경우 변수를 함수처럼 사용할 수 있음
  • Function Type 역시 non-nullable/nullable 이 존재한다. Nullable인 경우 null이 할당될 수 있으며, null 여부를 명시적으로 확인하거나 safe access(?)를 통해 사용해야함.
val f : (() -> Int)? = null //nullable function type으로 null 할당 가능
if (f != null) { f() } //null 여부를 명시적으로 확인하여 smart cast한뒤 사용
f?.invoke()?.compareTo(5) //safe access 방식으로 사용
  • lambda를 정의하며 바로 호출할 수 있는데 이 경우 사용됨을 명시하기 위해 run을 사용할 수 있다.
{ println("hey") }() 
run { println("hey") } //위 코드와 동일하게 동작하며 보다 명시적이다.

5. Member References

  • 앞서 Collection operation에 대해 lambda를 사용한 이유는 operation이 함수 (ex: predicate, 연산을 거친 argument)를 인자로 사용하기 때문이다. 한편, Kotlin에서 선언적인 function을 variable에 저장하고 인자로 사용할 수 없지만 lambda 외에 function reference는 variable에 저장하고 인자로 사용할 수 있다.
fun isEven(i:Int):Boolean = i % 2 == 0
val predicate = isEven() // fun을 통해 선언한 함수는 변수에 저장 못하며 오류발생
val predicate2 = ::isEven // function reference는 변수에 저장 가능
val predicate3 = {i:Int -> isEven(i)} // lambda 형태로 변수에 저장 가능
  • 이미 선언된 함수를 인자로 사용하고자 하는 경우 lambda보다 function reference 방식이 보다 코드가 간소화된다.
//Android 코드 예시 (정의된 클릭 리스너 함수를 버튼 어댑터에 인자로 넣어야 하는 경우)// 1. lambda 방식
val buttonAdapter = ButtonAdapter(buttons, {button: Button -> buttonClicked(button) })

// 2. function reference 방식
val buttonAdapter = ButtonAdapter(buttons, ::buttonClicked)
  • Class에 memeber function이 있고 이에 대한 member function reference를 변수에 저장하고자 할 때, Class로 생성한 특정 객체에 대한 member reference를 저장하면 bound reference라 하고, 특정 객체가 아닌 Class에 대한 memeber reference 혹은 top-level function의 reference는 non-bound reference라고 한다.
class Person(val name:String, val age:Int){
fun isOlder(ageLimit: Int) = age > ageLimit
val jeff = Person("Jeff", 35)
// 1. bound reference
val agePredicate = jeff::isOlder
println(agePredicate(21))
// 2. non-bound reference (변수에 저장된 함수 호출시 객체를 인자로 넣어야 함)
val agePredicate2 = Person::isOlder
println(agePredicate2(jeff, 21))

6. Return from Lambda

  • Kotlin에서 return은 기본적으로 fun 키워드로 명시된 곳에 대응된다. 한편, lambda는 fun 키워드로 선언되지 않으므로 lambda내부에 return을 사용할 때 주의해야 한다. labeling 또는 return을 사용하지 않는 방법이 있고, local function을 정의하거나, 명시적인 익명함수를 사용하는 방법이 있다.
/**
* 1. 아래 코드에서 원소가 0인 경우 해당 원소에 대한 transform 결과물이 []가 되는
* 것이 아니라 duplicateNonZero의 반환값이 []가 되어 버림
*/
fun duplicateNonZero(list:List<Int>):List<Int>{
return list.flatMap {
if(it==0) return listOf()
listOf(it, it)
}
}
// 2. lambda에 l@, @l 라벨링을 통해 return 이 대응되어야 할 지점 설정
fun duplicateNonZeroLabel(list:List<Int>):List<Int>{
return list.flatMap l@{
if(it==0) return@l listOf()
listOf(it, it)
}
}

// 3. lambda에 return을 사용하지 않음
fun duplicateNonZeroNoReturn(list:List<Int>):List<Int>{
return list.flatMap {
if(it==0) listOf()
else listOf(it, it)
}
}
// 4. local function (fun 키워드 선언)과 function reference를 사용
fun duplicateNonZeroLocalFunction(list:List<Int>):List<Int>{
fun duplicateNonZeroElement(e: Int): List<Int>{
if(e==0) return listOf()
return listOf(e, e)
}
return list.flatMap(::duplicateNonZeroElement)
}
// 5. anonymous function을 정의 (fun 키워드 선언)
fun duplicateNonZeroAnonymousFunction(list:List<Int>):List<Int>{
return list.flatMap(fun (e): List<Int>{
if(e==0) return listOf()
return listOf(e,e)
})
}
  • lambda와 명시적인 익명함수는 bytecode level에서는 동일하나 return에 대한 대응 지점만 달라진다. (Kotlin이 return을 fun 키워드 기준으로 대응시키는 규칙 때문)

7. Is Kotlin a functional language

  • Kotlin은 Haskell처럼 순수한 functional language는 아니며, 최신 언어들은 OOP, Structural, Functional Programming 등 다양한 패러다임의 아이디어를 조합하여 만들어졌다. Kotlin의 class와 interface 부분은 OOP 아이디어 기반이며, lambda, immutability, high-order function , immutability부분은 FP 아이디어 기반이다.
  • Kotlin의 FP 아이디어 기반의 특징들을 잘 활용하면 실무적 관점에서 함수형 스타일로 코딩할 수 있으며 이는 충분한 이점을 제공한다.

--

--