Kotlin/자바 개발자를 위한 코틀린 입문

5. 추가적으로 알아두어야 할 코틀린 특성

PHM 2023. 7. 29. 10:31

19. 코틀린의 이모저모

1. Type Alias 와 as import

- 긴 이름의 클래스 혹은 함수 타입이 있을 때 축약하거나 더 좋은 이름을 쓰고 싶다!

typealias FruitFilter = (Fruit) -> Boolean
fun filterFruits2(fruit: List<Fruit>, filter: FruitFilter) {
    
}

// 이름 긴 클래스를 컬렉션에 사용할 때도 간단히 줄일 수 있다.
data class UltraSuperGuardianTribe(
    val name: String
)
typealias USGTMap = Map<String, UltraSuperGuardianTribe>

 

- 다른 패키지의 같은 이름 함수를 동시에 가져오고 싶다면?!
- as import : 어떤 클래스나 함수를 임포트할 때 이름을 바꾸는 기능

import com.lannstark.lec19.a.printHelloWorld as printHelloWorldA
import com.lannstark.lec19.b.printHelloWorld as printHelloWorldB

fun main() {
    printHelloWorldA()
    printHelloWorldB()
}

 

2. 구조분해와 componentN 함수

- 구조분해 : 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는 것

val person = Person("박현민", 100)
val (name, age) = person

    ㆍ순서가 정해져있다. 첫번째  프로퍼티를 첫번째 변수, 두번째 프로퍼티를 두번째 변수에 넣어준다.

- Data Class는 componentN이란 함수도 자동으로 만들어준다!

val name = person.component1()
val age = person.component2()

 

- Data Class가 아닌데 구조분해를 사용하고 싶다면, componentN 함수를 직접 구현해줄 수도 있다.

class Person2(
    val name: String,
    val age: Int
) {
    operator fun component1(): String {
        return this.name
    }
    
    operator fun component2(): Int {
        return this.age
    }
}

 

val map = mapOf(1 to "A", 2 to "B")
for ((key, value) in map.entries) {
}

- 이 문법 역시 구조분해 이다.

 

3. Jump와 Label 

- return / break / continue

    ㆍreturn : 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환된다.

    ㆍbreak : 가장 가까운 루프가 제거된다

    ㆍcontinue : 가장 가까운 루프를 다음 step으로 보낸다.

- for 문 및 while 문에서 break, continue 기능은 동일하다.

    ㆍ단, forEach 문과 함께 break 또는 continue를 사용할 수 없다.

- forEach문과 함께 break 또는 continue를 꼭 쓰고 싶다면?!

run {
    numbers.map { number -> number + 1 }
        .forEach { number ->
            if (number == 3) {
                return@run	// break 역할
            }
            println(number)
        }
}
numbers.map { number -> number + 1 }
    .forEach { number ->
        if (number == 3) {
            return@forEach	// continue 역할
        }
        println(number)
    }

- break, continue를 사용할 때엔 가급적 익숙한 for문 사용을 추천한다.

 

- 코틀린에는 라벨이라는 기능이 있다.

- 특정 expression에 라벨이름@ 을 붙여 하나의 라벨로 간주하고 break, continue, return 등을 사용하는 기능

loop@ for(i in 1..100) {
    for(j in 1..100) {
        if(j == 2) {
            break@loop
        }
        println("$i $j")
    }
}

- 라벨을 사용한 Jump는 사용하지 않는 것을 강력 추천한다.

    ㆍ복잡도가 증가하고 유지보수가 어려워진다.

 

4. TakeIf와 TakeUnless

fun getNumberOrNull(): Int? {
    return if (number <= 0) {
        null
    }else {
        number
    }
}

- Kotlin에서는 method chaning을 위한 특이한 함수를 제공한다.

fun getNumberOrNullV2(): Int? {
    return number.takeIf { it > 0 }
}

- takeIf : 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환된다.

 

fun getNumberOrNullV3(): Int? {
    return number.takeUnless { it <= 0 }
}

- takeUnless : 주어진 조건을 만족하지 않으면 그 값이, 그렇지 않으면 null이 반환된다.

 

정리

- 타입에 대한 별칭을 줄 수 있ㄷ는 typealias 라는 키워드가 존재한다.

- Import 당시 이름을 바꿀 수 있는 as import 기능이 존재한다.

- 변수를 한 번에 선언할 수 있는 구조분해 기능이 있으며 componentN 함수를 사용한다.

- for문, while문과 달리 forEach에는 break와 continue를 사용할 수 없다.

- takeIf와 takeUnless를 활용해 코드양을 줄이고 method chaning 을 활용할 수 있다.

 

20. 코틀린의 scope function

1. scope function이란 무엇인가?

- scope : 영역 / fucntion : 함수

- scope function : 일시적인 영역을 형성하는 함수

fun printPerson(person: Person?) {
//    if(person != null) {
//        println(person.name)
//        println(person.age)
//    }
    person?.let {
        println(it.name)
        println(it.age)
    }
}

- Safe Call (?.) 을 사용 : person이 null이 아닐때에 let을 호출

- let : scope function의 한 종류

    ㆍ확장함수. 람다를 받아, 람다 결과를 반환한다.

public inline fun <T, R> T.let(blcok: (T) -> R): R {
    return block(this)
}

 

- 람다를 사용해 일시적인 영역을 만들고, 코드를 더 간결하게 만들거나, method chaning에 활용하는 함수를 scope function이라고 한다.

 

 

2. scope function의 분류

- 절대 외우지마라!

  it 사용 this 사용
람다의 결과 리턴 let run
객체 그자체 리턴 also apply
  with ( 확장함수 X )  

    ㆍthis : 생략이 가능한 대신, 다른 이름을 붙일 수 없다.

    ㆍit : 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.

val value1 = person.let { p ->
    p.age
}

val value2 = person.run {
    age
}

    ㆍ let 은 일반함수를 받는다.

    ㆍrun 은 확장함수를 받는다.

* 확장함수에서는 본인 자신을 this로 호출하고, 생략할 수 있었다.

 

val person = Person("박현민", 100)
with(person) {
    println(name)
    println(this.age)
}

- with(파라미터, 람다) : this를 사용해 접근하고, this는 생략 가능하다.

 

3. 언제 어떤 scope function을 사용해야 할까?

1. let

- 하나 이상의 함수를 call chain 결과 호출할 때

val strings = listOf("APPLE", "CAR")
strings.map { it.length }
    .filter { it > 3 }
//        .let(::println)
    .let { lengths -> println(lengths) }

 

- non-null 값에 대해서만 code block을 실행시킬 때

val length = str?.let {
    println(it.uppercase())
    it.length
}

    ㆍ제일 많이 사용

 

- 일회성으로 제한된 영역에 지역 변수를 만들 때

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first()
    .let {firstItem ->
        if (firstItem.length >= 5) firstItem else "!$firstItem!"
    }.uppercase()
println(modifiedFirstItem)

    ㆍ주로 쓰지 않음

 

2. run

- 객체 초기화와 반환 값의 계산을 동시에 해야 할 때

val person = Person("박현민", 100).run(personRepository::save)

    ㆍ객체를 만들어 DB에 바로 저장하고, 그 인스턴스를 활용할 때

val person = Person("박현민",100).run {
    hobby = "독서"
    personRepository.save(this)
}

    ㆍ개인적으로는 잘 사용하지 않는다.

- 반복되는 생성 후처리는 생성자, 프로퍼티, init blolck으로 넣는 것이 좋다.

val person = personRepository.save(Person("박현민", 100))

 

3. apply

- apply 특징 : 객체 그 자체가 반환된다.

- 객체 설정을 할 때에 객체를 수정하는 로직이 call chain 중간에 필요할 때

 

- Test Fixture를 만들 때

 

fun createPerson(
    name: String,
    age: Int,
    hobby: String
): Person {
    return Person(
        name = name,
        age = age,
    ).apply {
        this.hobby = hobby
    }
}
val person = Person("박현민", 100)
person.apply { this.growOld() }
    .let{ println(it) }

 

4. also

- also 특징 : 객체 그 자체가 반환된다.

- 객체를 수정하는 로직이 call chain 중간에 필요할 때

mutableListOf("one", "two", "three")
    .also { println("four 추가 이전 지금 값: $it") }
    .add("four")

 

5. with

- 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때

return with(person) {
    PersonDto(
        name = name,
        age = age,
    )
}

    ㆍthis를 생략할 수 있어 필드가 많아도 코드가 간결해진다.

 

4. scope function과 가독성

- scope function을 사용한 코드가 그렇지 않은 코드보다 가독성 좋은 코드일까?!

// 1번 코드
if(person != null && person.isAdult) {
    view.showPerson(person)
} else {
    view.showError()
}

// 2번 코드
person?.takeIf { it.isAdult }
    ?.let(view::showPerson)
    ?: view.showError()

    ㆍ1번 코드 : 전통적인 if와 else를 활용

    ㆍ2번 코드 : scope function을 활용한 코틀린스러운 코드

        ▷ view.showPerson() 이 null을 반환한다면?! - elivs 연산자에서 view.showError 가 출력

            showPerson도 불리고 showError도 불리는 버그가 생길 수 있다.

 

- 개인적(강사)으로 1번 코드가 훨씬 좋은 코드라고 생각한다.

    ㆍ1. 구현 2는 숙련된 코틀린 개발자만 더 알아보기 쉽다.

           어쩌면 숙련된 코틀린 개발자도 잘 이해하지 못할 수 도 있다.

    ㆍ2. 구현 1의 디버깅이 쉽다.

    ㆍ3. 구현이 1이 수정도 더 쉽다.

- 사용 빈도가 적은 관용구는 코드를 더 복잡하게 만들고 이런 관용구들을 한 문장 내에서 조합해 사용하면 복잡성이 훨씬 증가한다.

- 하지만 scope function을 사용하면 안되는 것도 아니다! 적절한 convention을 적용하면 유용하게 활용할 수 있다.


정리

- 코틀린의 scope function은 일시적인 영역을 만들어 코드를 더 간결하게 하거나, method chain에 활용된다.

- scope function의 종류에는 let / run / also / apply / with 가 있었다.

- scope function을 사용한 코드는 사람에 따라 가독성을 다르게 느낄 수 있기 때문에, 함께 프로덕트를 만들어 가는 팀끼리 convention을 잘 정해야 한다.

 


강의에서 다루지 못한 내용

- 제네릭

- 리플렉션

- 복잡한 함수형 프로그래밍

- DSL

- 동시성 프로그래밍 (코루틴)