코딩마을방범대
코틀린 기초 (4) - 추가적으로 알아두어야 할 코틀린 특성 본문
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
'💡 백엔드 > Kotlin' 카테고리의 다른 글
| 코틀린 기초 (3) - 코틀린에서의 FP (5) | 2025.08.17 |
|---|---|
| 코틀린 기초 (2) - 코틀린에서의 OOP (5) | 2025.08.12 |
| 코틀린 기초 (1) - 코틀린의 변수와 코드 제어 (5) | 2025.08.10 |