코딩마을방범대

코틀린 기초 (4) - 추가적으로 알아두어야 할 코틀린 특성 본문

💡 백엔드/Kotlin

코틀린 기초 (4) - 추가적으로 알아두어야 할 코틀린 특성

신짱구 5세 2025. 8. 17. 18:45
728x90

 

 

코틀린의 이모저모

 

1. Type Alias와 as import

  • typealias: 타입에 대해 미리 선언하여, 이후 축약된 키워드로 사용 가능하게 한다.

    ex. 타입과 반환 타입을 그대로 기입할 경우 코드가 지저분 해보이는 상황이 있을 수 있다.
    이럴 경우 typealias 라는 키워드를 통해 타입에 대해 미리 정의가 가능하다.
// 사용 전
fun filterFruits(fruits: List<Fruit>, filter: (Fruit) -> (Boolean) {

// typealias 사용 후
typealias FruitFilter = (Fruit) -> Boolean
fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {
긴 클래스를 컬렉션에 사용할 때도 타입 별칭으로 사용 가능하다.
data class UltraSuperGuardianTribe(
    val name: String
)
typealias USGTMap = Map<String, UltraSuperGuardianTribe>​

 


 

 

  • as import: 어떤 클래스나 함수를 import 할 때 이름을 바꾸는 기능
/* 
    a와 b라는 다른 패키지에 있는 동일한 함수명을 가져와도 
    이름을 변경했으므로 호출 시 오류가 발생하지 않는다. 
*/
import com.test.a.printHelloWorld as printHelloWorldA
import com.test.b.printHelloWorld as printHelloWorldB

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

 

 


 

 

 

 

2. 구조분해와 componentN 함수

  • 구조분해: 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는 것
data class Person(
    val name: String,
    val age: Int
)

val person = Person("최태현", 100)
val (name, age) = person
 Data Class 는 componentN 이란 함수를 자동으로 만들어준다.
componentN은 프로퍼티를 차례대로 가져올 수 있다.
name, age 등 필드명을 인식하는 것이 아니라 순서로 기억한다.

위 코드는 사실 상 아래 코드와 같이 component 함수를 호출해준 것이다.

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

 


 

 

componentN 함수를 직접 구현하는 방법

componentN 함수는 연산자의 속성을 가지고 있기 때문에, 연산자 오버로딩처럼 operator 키워드를 붙여줘야 한다.

class Person(
    val name: String,
    val age: Int
) {
    operator fun component1(): String {
        return this.name
    }	
    operator fun component2(): Int {
    	return this.age
    }
}
for ((key, value) in map.entries({​

 

(idx, value) 등의 문법 역시 구조분해다.

 

 


 

 

 

 

3. Jump와 Label

  • return: 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환된다.
  • break: 가장 가까운 루프가 제거된다.
  • continue: 가장 가까운 루프를 다음 step으로 보낸다.

 

forEach 문에서는 continue나 break를 사용할 수 없으므로, 아래와 같이 작성해야 한다.

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

// continue 를 사용하고 싶은 경우
numbers.forEach { number ->
    if(number == 2){
        return@forEach
    }
}

// break 를 사용하고 싶은 경우
run {
    numbers.forEach { number ->
        if(number == 2){
            return@run
        }
    }
}

 


 

 

Label 기능

아래 코드는 제일 바깥 for 문에 loop라는 라벨을 붙여줬기 때문에 break@loop 시 제일 바깥 for문이 종료된다.

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

 

 


 

 

 

 

4. TakeIf 와 TakeUnless

  • takeIf: 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null 이 반환됨
// 사용 전
fun getNumberOrNull(): Int? {
    return if (number <= 0) {
        null
    } else {
        number
    }
}

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

 


 

 

  • takeUnless: 주어진 조건을 만족하지 않으면 그 값이, 그렇지 않으면 null이 반환된다.
fun getNumberOrNullV2(): Int? {
	return number.takeUnless { it <= 0 }
}

 

 


 

 

 

 

코틀린의 scope function

 

1. scope function 이란?

  • scope(영역)function(함수): 일시적인 영역을 형성하는 함수
  • 람다를 사용해 일시적인 영역을 만들고 코드를 더 간결하게 만들거나, method chaining 에 활용하는 함수를 말한다.
// 전
fun printPerson(person: Person){
    if (person != null){
        println(person.name)
        println(person.age)
    }
}
// 후
fun printPerson(person: Person){
    person?.let {
        println(it.name)
        println(it.age)
    }
}

 

 


 

 

 

 

2. scope function 의 분류

함수 객체 참조 반환값 사용 예시
let it 람다의 마지막 줄 null-safe 코드, 변수 스코프 제한
run this 객체 초기화 및 값 반환
with 객체 여러 프로퍼티 설정
apply 수신 객체 자체 객체 초기화 및 설정
also it 부수 효과(side-effect), 로깅
* let과 apply의 차이
class Person {
    var name: String = ""
    var age: Int = 0
}

// 1. apply: 객체 설정 후 자기 자신(객체)을 반환
val person1 = Person().apply {
    name = "John"
    age = 30
}
// person1은 Person 객체

// 2. let: 객체 작업 후 람다의 결과물을 반환
val result = Person().let {
    it.name = "John"
    it.age = 30
    "이름: ${it.name}"
}
// result는 String 타입의 "이름: John"​

 


 

 

💡 this와 it의 차이

  • it: 생략이 불가능한 대신, 다른 이름을 붙일 수 있다.
  • this: 생략이 가능한 대신, 다른 이름을 붙일 수 없다.
// it
val value1 = person.let { p ->
    p.age
}

// this
val value2 = person.run {
    age
}

 

* 이런 차이가 발생한 이유?
// let: 파라미터명이 block 이고, T 타입의 단일 인수를 받아서 R이라는 결과를 반환한다.
public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}
// run: 파라미터명이 block 이고, T에 대한 확장함수를 받아 R이라는 결과를 반환한다.
public inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}​

 

 


 

 

 

 

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

 

let: 

 // ex. 하나 이상의 함수를 call chain 결과로 호출할 때
val strings = listOf("APPLE", "CAR")
strings.map { it.length }
    .filter { it > 3 }
    .let(::println)

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

// ex. non-null 값에 대해서만 code block 을 실행시킬 때
val length = str?.let {
    println(it.uppercase())
    it.length
}

// ex. 일회성으로 제한된 영역에 지역 변수를 만들 때
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first()
	// firstItem 라는 임시변수 생성
    .let { firstItem ->
        if (firstItem.length >= 5) firstItem else "!$firstItem!"
    }.uppercase()

 


 

 

run: 

// ex. 객체 초기화와 반환 값의 계산을 동시에 해야 할 때
val person = Person("최태현", 100).run(personRepository::save)
//응용편
val person = Person("최태현", 100).run {
    hobby = "독서"
    personRepository.save(this)
}
// 개인적으로는 잘 사용하지 않고, 반복되는 생성 후 처리는 생성자,프로퍼티,init block으로 넣는 것이 좋다.
// 생성자가 길어지는 경우엔 run 을 쓰면 깔끔해보이긴 함
val person = personRepository.save(Person("최태현", 100))

 


 

 

apply:

// ex. 객체 설정을 할 때에 객체를 수정하는 로직이 call chain 중간에 필요할 때
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) }

 


 

 

also:

// ex. 객체를 수정하는 로직이 call chain 중간에 필요할 때
mutableListOf("one", "two", "three")
    .also { println("four 추가 이전 지금 값: $it") }
    .add("four")

 


 

 

with

  • 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass 함수를 만들기 어려울 때
return with(person) {
    PersonDto(
        // with가 아니었다면 name = person.name 으로 작성했어야함
        name = name,
        age = age
    )
}



 

 


 

 

 

 

4. scope function 과 가독성

개인적인 의견으로는 scope function이 더 가독성이 떨어지는 것 같다.

코틀린이 익숙하지 않은 개발자에겐 해석 오류가 생길 수 있고, 디버깅 또한 복잡하다.

// 사용 전
if (person != null && person.isAdult){
    view.showPerson(person)
} else {
    view.showError()
}

// 사용 후
person?.takeIf ( it.isAdult }
    ?.let(view::showPerson)
    ?: view.showError()

 

 

 

 

 

728x90