1. 소개
Kotlin에서 함수 유형 및 람다 표현식 사용 Codelab에서는 repeat()
와 같은 다른 함수를 매개변수로 받거나 반환하는 함수인 고차 함수에 관해 알아봤습니다. 고차 함수는 더 적은 코드로 정렬이나 필터링과 같은 일반적인 작업을 실행하는 데 도움이 되므로 컬렉션과 특히 관련이 높습니다. 컬렉션에 관해 자세히 알아봤으므로 이제 고차 함수를 다시 살펴보겠습니다.
이 Codelab에서는 컬렉션 유형에 사용할 수 있는 다양한 함수(예: forEach()
, map()
, filter()
, groupBy()
, fold()
, sortedBy()
)를 알아봅니다. 이 과정에서 람다 표현식을 사용하는 연습을 추가로 하게 됩니다.
기본 요건
- 함수 유형 및 람다 표현식에 관한 지식
repeat()
함수와 같은 후행 람다 문법에 관한 지식List
와 같은 Kotlin의 다양한 컬렉션 유형에 관한 지식
학습할 내용
- 람다 표현식을 문자열에 삽입하는 방법
List
컬렉션에서forEach()
,map()
,filter()
,groupBy()
,fold()
,sortedBy()
등 다양한 고차 함수를 사용하는 방법
필요한 항목
- Kotlin 플레이그라운드에 액세스할 수 있는 웹브라우저
2. forEach() 및 람다를 사용한 문자열 템플릿
시작 코드
다음 예에서는 베이커리의 쿠키 메뉴를 나타내는 List
를 사용하고, 고차 함수를 사용하여 다양한 방식으로 메뉴 형식을 지정합니다.
먼저 초기 코드를 설정합니다.
- Kotlin 플레이그라운드로 이동합니다.
main()
함수 위에Cookie
클래스를 추가합니다.Cookie
의 각 인스턴스는name
과price
, 쿠키에 관한 기타 정보가 포함된 메뉴의 항목을 나타냅니다.
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
fun main() {
}
Cookie
클래스 아래main()
외부에서 다음과 같이 쿠키 목록을 만듭니다. 유형은List<Cookie>
로 추론됩니다.
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
val cookies = listOf(
Cookie(
name = "Chocolate Chip",
softBaked = false,
hasFilling = false,
price = 1.69
),
Cookie(
name = "Banana Walnut",
softBaked = true,
hasFilling = false,
price = 1.49
),
Cookie(
name = "Vanilla Creme",
softBaked = false,
hasFilling = true,
price = 1.59
),
Cookie(
name = "Chocolate Peanut Butter",
softBaked = false,
hasFilling = true,
price = 1.49
),
Cookie(
name = "Snickerdoodle",
softBaked = true,
hasFilling = false,
price = 1.39
),
Cookie(
name = "Blueberry Tart",
softBaked = true,
hasFilling = true,
price = 1.79
),
Cookie(
name = "Sugar and Sprinkles",
softBaked = false,
hasFilling = false,
price = 1.39
)
)
fun main() {
}
forEach()
로 목록 반복
처음으로 알아볼 고차 함수는 forEach()
함수입니다. forEach()
함수는 매개변수로 전달된 함수를 컬렉션의 각 항목에 한 번 실행합니다. 이는 repeat()
함수 또는 for
루프와 비슷하게 작동합니다. 람다는 컬렉션의 각 요소에 실행될 때까지 첫 번째 요소에 실행된 후 두 번째 요소에 실행되는 방식으로 실행됩니다. 메서드 서명은 다음과 같습니다.
forEach(action: (T) -> Unit)
forEach()
는 (T) -> Unit
유형의 함수인 단일 작업 매개변수를 사용합니다.
T
는 컬렉션에 포함된 데이터 유형에 상응합니다. 람다가 단일 매개변수를 사용하므로 이름을 생략하고 it
으로 매개변수를 참조할 수 있습니다.
forEach()
함수를 사용하여 cookies
목록의 항목을 출력합니다.
main()
에서 후행 람다 문법을 사용하여cookies
목록에서forEach()
를 호출합니다. 후행 람다가 유일한 인수이므로 함수를 호출할 때 괄호를 생략할 수 있습니다.
fun main() {
cookies.forEach {
}
}
- 람다 본문에서
it
을 출력하는println()
문을 추가합니다.
fun main() {
cookies.forEach {
println("Menu item: $it")
}
}
- 코드를 실행하고 출력을 살펴봅니다. 출력되는 것은 모두 유형의 이름(
Cookie
)과 객체의 고유 식별자이며 객체의 콘텐츠는 출력되지 않습니다.
Menu item: Cookie@5a10411 Menu item: Cookie@68de145 Menu item: Cookie@27fa135a Menu item: Cookie@46f7f36a Menu item: Cookie@421faab1 Menu item: Cookie@2b71fc7e Menu item: Cookie@5ce65a89
문자열에 표현식 삽입
처음으로 문자열 템플릿을 알아볼 때 달러 기호($
)를 변수 이름과 함께 사용하여 문자열에 삽입하는 방법을 확인했습니다. 하지만 이는 점 연산자(.
)와 결합하여 속성에 액세스할 때는 예상대로 작동하지 않습니다.
forEach()
호출에서$it.name
을 문자열에 삽입하도록 람다의 본문을 수정합니다.
cookies.forEach {
println("Menu item: $it.name")
}
- 코드를 실행합니다. 클래스의 이름
Cookie
와 객체의 고유 식별자, 그리고.name
이 삽입되는 것을 볼 수 있습니다.name
속성의 값은 액세스되지 않습니다.
Menu item: Cookie@5a10411.name Menu item: Cookie@68de145.name Menu item: Cookie@27fa135a.name Menu item: Cookie@46f7f36a.name Menu item: Cookie@421faab1.name Menu item: Cookie@2b71fc7e.name Menu item: Cookie@5ce65a89.name
속성에 액세스하여 이를 문자열에 삽입하려면 표현식이 필요합니다. 중괄호로 묶어 문자열 템플릿의 표현식 부분을 만들 수 있습니다.
람다 표현식은 여는 중괄호와 닫는 중괄호 사이에 배치됩니다. 속성에 액세스하고 수학 연산을 실행하고 함수를 호출할 수 있으며 람다의 반환 값은 문자열에 삽입됩니다.
이름이 문자열에 삽입되도록 코드를 수정해 보겠습니다.
it.name
을 중괄호로 묶어 람다 표현식으로 만듭니다.
cookies.forEach {
println("Menu item: ${it.name}")
}
- 코드를 실행합니다. 출력에는 각
Cookie
의name
이 포함됩니다.
Menu item: Chocolate Chip Menu item: Banana Walnut Menu item: Vanilla Creme Menu item: Chocolate Peanut Butter Menu item: Snickerdoodle Menu item: Blueberry Tart Menu item: Sugar and Sprinkles
3. map()
map()
함수를 사용하면 컬렉션을 동일한 수의 요소로 구성된 새 컬렉션으로 변환할 수 있습니다. 예를 들어 map()
함수에 각 Cookie
항목에서 String
을 만드는 방법을 알려 준다면 map()
은 List<Cookie>
를 쿠키의 name
만 포함된 List<String>
으로 변환할 수 있습니다.
베이커리의 대화형 메뉴를 표시하는 앱을 작성한다고 가정해 보겠습니다. 사용자가 쿠키 메뉴가 표시되는 화면으로 이동할 때 논리적인 방식(예: 이름 다음에 가격 표시)으로 데이터가 제공되는 것을 원할 수 있습니다. map()
함수를 사용하여 관련 데이터(이름, 가격)로 형식이 지정된 문자열 목록을 만들 수 있습니다.
main()
에서 이전 코드를 모두 삭제합니다.fullMenu
라는 새 변수를 만들고cookies
목록에서map()
을 호출한 결과와 동일하게 설정합니다.
val fullMenu = cookies.map {
}
- 람다 본문에서
it
의name
과price
를 포함하도록 형식이 지정된 문자열을 추가합니다.
val fullMenu = cookies.map {
"${it.name} - $${it.price}"
}
fullMenu
의 콘텐츠를 출력합니다.forEach()
를 사용하면 됩니다.map()
에서 반환된fullMenu
컬렉션에는List<Cookie>
가 아닌List<String>
유형이 있습니다.cookies
의 각Cookie
는fullMenu
의String
에 상응합니다.
println("Full menu:")
fullMenu.forEach {
println(it)
}
- 코드를 실행합니다. 출력이
fullMenu
목록의 콘텐츠와 일치합니다.
Full menu: Chocolate Chip - $1.69 Banana Walnut - $1.49 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Sugar and Sprinkles - $1.39
4. filter()
filter()
함수를 사용하면 컬렉션의 하위 집합을 만들 수 있습니다. 예를 들어 숫자 목록이 있다면 filter()
를 사용하여 2로 나눌 수 있는 숫자만 포함된 새 목록을 만들 수 있습니다.
map()
함수의 결과는 항상 동일한 크기의 컬렉션을 생성하지만 filter()
는 원래 컬렉션과 같거나 작은 크기의 컬렉션을 생성합니다. map()
과 달리 결과 컬렉션도 동일한 데이터 유형을 가지므로 List<Cookie>
를 필터링하면 또 다른 List<Cookie>
가 생성됩니다.
map()
, forEach()
와 마찬가지로 filter()
는 단일 람다 표현식을 매개변수로 사용합니다. 람다는 컬렉션의 각 항목을 나타내는 단일 매개변수를 가지며 Boolean
값을 반환합니다.
컬렉션의 각 항목의 경우 다음과 같습니다.
- 람다 표현식의 결과가
true
이면 항목은 새 컬렉션에 포함됩니다. - 결과가
false
이면 항목은 새 컬렉션에 포함되지 않습니다.
이는 앱에서 데이터의 하위 집합을 가져오려는 경우 유용합니다. 예를 들어 베이커리에서 메뉴의 별도 섹션에 소프트 베이크 쿠키를 강조표시하려고 한다고 가정해 보겠습니다. 항목을 출력하기 전에 먼저 cookies
목록을 filter()
할 수 있습니다.
main()
에서softBakedMenu
라는 새 변수를 만들고cookies
목록에서filter()
를 호출한 결과로 설정합니다.
val softBakedMenu = cookies.filter {
}
- 람다 본문에서 불리언 표현식을 추가하여 쿠키의
softBaked
속성이true
와 같은지 확인합니다.softBaked
가Boolean
자체이므로 람다 본문에는it.softBaked
만 포함하면 됩니다.
val softBakedMenu = cookies.filter {
it.softBaked
}
forEach()
를 사용하여softBakedMenu
의 콘텐츠를 출력합니다.
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- 코드를 실행합니다. 메뉴는 전과 동일하게 출력되지만 소프트 베이크 쿠키만 포함됩니다.
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79
5. groupBy()
groupBy()
함수를 사용하면 함수에 따라 목록을 맵으로 변환할 수 있습니다. 함수의 고유한 각 반환 값은 결과 맵의 키가 됩니다. 각 키의 값은 고유한 반환 값을 생성한 컬렉션의 모든 항목입니다.
키의 데이터 유형은 groupBy()
에 전달된 함수의 반환 유형과 동일합니다. 값의 데이터 유형은 원래 목록의 항목 목록입니다.
이는 개념화가 어려울 수 있으므로 간단한 예를 먼저 들어 보겠습니다. 전과 같은 숫자 목록이 있다고 할 때 홀수 또는 짝수로 그룹화합니다.
2
로 나누고 나머지가 0
인지 1
인지 확인하여 숫자가 홀수인지 짝수인지 확인할 수 있습니다. 나머지가 0
이면 짝수입니다. 나머지가 1
이면 홀수입니다.
이 작업은 모듈로 연산자(%
)를 사용하면 됩니다. 모듈로 연산자는 표현식의 왼쪽에 있는 피제수를 오른쪽의 제수로 나눕니다.
나누기 연산자(/
)와 같이 나누기 결과를 반환하는 대신 모듈로 연산자는 나머지를 반환합니다. 이는 숫자가 짝수인지 홀수인지 확인하는 데 유용합니다.
groupBy()
함수는 { it % 2 }
람다 표현식과 함께 호출됩니다.
결과 맵에는 0
와 1
이라는 두 개의 키가 있습니다. 각 키의 값은 List<Int>
유형입니다. 키 0
의 목록에는 모든 짝수가 포함되어 있으며 키 1
의 목록에는 모든 홀수가 포함되어 있습니다.
사진을 찍은 대상이나 위치별로 사진을 그룹화하는 사진 앱이 실제 사용 사례가 될 수 있습니다. 베이커리 메뉴에서는 쿠키의 소프트 베이크 여부에 따라 메뉴를 그룹화해 보겠습니다.
groupBy()
를 사용하여 softBaked
속성을 기반으로 메뉴를 그룹화합니다.
- 이전 단계에서
filter()
호출을 삭제합니다.
삭제 코드
val softBakedMenu = cookies.filter {
it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
cookies
목록에서groupBy()
를 호출하여 결과를groupedMenu
라는 변수에 저장합니다.
val groupedMenu = cookies.groupBy {}
it.softBaked
를 반환하는 람다 표현식을 전달합니다. 반환 유형은Map<Boolean, List<Cookie>>
입니다.
val groupedMenu = cookies.groupBy { it.softBaked }
groupedMenu[true]
값이 포함된softBakedMenu
변수와groupedMenu[false]
값이 포함된crunchyMenu
변수를 만듭니다.Map
의 첨자 지정 결과가 null을 허용하므로 Elvis 연산자(?:
)를 사용하여 빈 목록을 반환할 수 있습니다.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
- 코드를 추가하여 소프트 쿠키 메뉴를 출력한 다음 크런치 쿠키 메뉴를 출력합니다.
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
println("${it.name} - $${it.price}")
}
- 코드를 실행합니다.
groupBy()
함수를 사용하면 속성 중 하나의 값에 따라 목록을 두 개로 분할할 수 있습니다.
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Crunchy cookies: Chocolate Chip - $1.69 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Sugar and Sprinkles - $1.39
6. fold()
fold()
함수는 컬렉션에서 단일 값을 생성하는 데 사용됩니다. 주로 총가격을 계산하거나 목록의 모든 요소를 합산하여 평균을 구하는 등의 작업에 가장 많이 사용됩니다.
fold()
함수는 다음 두 매개변수를 사용합니다.
- 초깃값. 데이터 유형은 함수를 호출할 때 추론됩니다. 즉, 초깃값
0
은Int
로 추론됩니다. - 초깃값과 동일한 유형의 값을 반환하는 람다 표현식
람다 표현식에는 추가 매개변수 두 개가 있습니다.
- 첫 번째는 누산기라고 합니다. 데이터 유형이 초깃값과 동일합니다. 이를 누계라고 생각하면 됩니다. 람다 표현식이 호출될 때마다 누산기는 람다가 이전에 호출된 때의 반환 값과 동일합니다.
- 두 번째는 컬렉션의 각 요소와 동일한 유형입니다.
앞서 본 다른 함수와 마찬가지로 람다 표현식은 컬렉션의 각 요소에 호출되므로 fold()
를 모든 요소를 합산하는 간결한 방법으로 사용할 수 있습니다.
fold()
를 사용하여 모든 쿠키의 총가격을 계산해 보겠습니다.
main()
에서totalPrice
라는 새 변수를 만들고cookies
목록에서fold()
를 호출한 결과와 같게 설정합니다. 초깃값으로0.0
을 전달합니다. 유형은Double
로 추론됩니다.
val totalPrice = cookies.fold(0.0) {
}
- 람다 표현식에 두 매개변수를 모두 지정해야 합니다. 누산기에
total
을 사용하고 컬렉션 요소에cookie
를 사용합니다. 매개변수 목록 뒤에 화살표(->
)를 사용합니다.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
- 람다 본문에서
total
및cookie.price
의 합계를 계산합니다. 이 값은 반환 값으로 추론되고 다음에 람다가 호출될 때total
에 전달됩니다.
val totalPrice = cookies.fold(0.0) {total, cookie ->
total + cookie.price
}
- 가독성을 위해 문자열로 형식이 지정된
totalPrice
값을 출력합니다.
println("Total price: $${totalPrice}")
- 코드를 실행합니다. 결과는
cookies
목록의 가격 합계와 같아야 합니다.
... Total price: $10.83
7. sortedBy()
컬렉션에 관해 처음 알아볼 때 sort()
함수는 요소를 정렬하는 데 사용할 수 있다고 배웠습니다. 그러나 이는 Cookie
객체 컬렉션에서는 작동하지 않습니다. Cookie
클래스에는 여러 속성이 있으며 Kotlin은 개발자가 어떤 속성(name
, price
등)으로 정렬하려는지 알 수 없습니다.
이러한 경우 Kotlin 컬렉션은 sortedBy()
함수를 제공합니다. sortedBy()
를 사용하면 정렬 기준으로 사용하려는 속성을 반환하는 람다를 지정할 수 있습니다. 예를 들어 price
를 기준으로 정렬하려면 람다는 it.price
를 반환합니다. 값의 데이터 유형에 자연스러운 정렬 순서가 있는 경우(문자열은 알파벳순으로 정렬되고 숫자 값은 오름차순으로 정렬됨) 해당 유형의 컬렉션과 마찬가지로 정렬됩니다.
sortedBy()
를 사용하여 쿠키 목록을 알파벳순으로 정렬합니다.
main()
에서 기존 코드 뒤에alphabeticalMenu
라는 새 변수를 추가하고cookies
목록에서sortedBy()
를 호출하는 것과 동일하게 설정합니다.
val alphabeticalMenu = cookies.sortedBy {
}
- 람다 표현식에서
it.name
을 반환합니다. 결과 목록은 계속List<Cookie>
유형이지만name
을 기준으로 정렬됩니다.
val alphabeticalMenu = cookies.sortedBy {
it.name
}
alphabeticalMenu
의 쿠키 이름을 출력합니다.forEach()
를 사용하여 각 이름을 새 줄에 출력할 수 있습니다.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
println(it.name)
}
- 코드를 실행합니다. 쿠키 이름은 알파벳순으로 출력됩니다.
... Alphabetical menu: Banana Walnut Blueberry Tart Chocolate Chip Chocolate Peanut Butter Snickerdoodle Sugar and Sprinkles Vanilla Creme
8. 결론
축하합니다. 컬렉션에서 고차 함수를 사용할 수 있는 방법을 보여주는 몇 가지 예를 살펴봤습니다. 정렬 및 필터링과 같은 일반적인 작업은 코드 한 줄로 실행할 수 있어 프로그램이 더 간결해지고 표현력도 풍부해집니다.
요약
forEach()
를 사용하여 컬렉션의 각 요소를 반복할 수 있습니다.- 표현식을 문자열에 삽입할 수 있습니다.
map()
은 종종 다른 데이터 유형의 컬렉션으로 컬렉션의 항목 형식을 지정하는 데 사용됩니다.filter()
는 컬렉션의 하위 집합을 생성할 수 있습니다.groupBy()
는 함수 반환 값을 기준으로 컬렉션을 분할합니다.fold()
는 컬렉션을 단일 값으로 변환합니다.sortedBy()
는 지정된 속성별로 컬렉션을 정렬하는 데 사용됩니다.