본문 바로가기

코틀린

코틀린(Kotlin) - 컬렉션 API : filter, map, all, any, count, find, groupBy, flatMap, flatten

람다 함수형 프로그래밍은 컬렉션(Collection)을 다룰 때 상당히 편리하고 막강합니다.

대부분의 작업에 라이브러리 함수를 활용할 수 있고, 그로 인해 코드를 아주 간결하게 만들 수 있습니다.

그러므로 이번에는, 컬렉션을 다루는 코틀린 표준 라이브러리들을 살펴보도록 하겠습니다.

 

참고로, 코틀린에서 제공하는 컬렉션 API는 새롭게 추가된 것이 아닌, 기존의 java, C#, 그루비, 스칼라 등 람다를 지원하는 대부분의 언어에서 사용하는 것들과 동일합니다.

필수적인 함수 : filter, map

filter와 map은 컬렉션을 활용할 때 기반이 되는 함수로, 대부분의 컬렉션 연산을 이 두 함수를 통해 표현할 수 있습니다.

 

filter

filter 함수는 컬렉션을 iteration 하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는(조건에 맞는) 원소만 필터링하는 기능을 합니다.

filter의 결과는, 입력 컬렉션의 원소 중에서 주어진 조건문에 만족하는 원소만으로 이루어진 새로운 컬렉션 입니다.

 

/* 리스트에서 짝수만 뽑아내는 예제 */

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter { it % 2 == 0 }) //짝수만 필터링
[2, 4]

 

그림으로 표현하자면 아래와 같습니다.

 

filter 함수는 조건을 만족하는 모든 원소로만 이루어진 새로운 컬렉션입니다. 

 

다른 예제로, 나이가 30살 이상인 사람만 추려내기 위해서 filter를 사용해보았습니다.

 

/* 나이가 30살 이상인 사람만 뽑아내는 예제 */

>>> val people = listOf(Person("안드로이드", 29), Person("코틀린", 30))
>>> println(people.filter { it.age >= 30 })
[Person(name=코틀린, age=30)]

 

이렇듯, filter 함수는 컬렉션에서 원치 않는 원소들을 제거해줄 수 있습니다.

하지만, filter는 원소를 변환할 수는 없습니다. 원소를 변환하기 위해서는 map 함수를 사용해야 합니다.

 

 

map

map 함수는 각 원소를 원하는 형태로 변환하는 기능을 하며, 변환한 결과를 모아서 새 컬렉션을 만듭니다.

결과는 원본 리스트와 원소 개수는 같지만, 각 원소는 주어진 람다(함수)에 따라 변환된 새로운 컬렉션입니다. 

 

/* 각 원소의 제곱으로 모인 리스트를 만드는 map 예제 */

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it }) //제곱 만들기 (1x1, 2x2, 3x3, 4x4)
[1, 4, 9, 16]

 

map 함수는 람다를 컬렉션의 모든 원소에 적용한 결과로 이루어진 새로운 컬렉션입니다.

 

예로, 사람 리스트가 아닌 이름 리스트를 출력하고 싶다면 map으로 사람 리스트를 이름 리스트로 변환하면 됩니다.

 

/* 사람 리스트 -> 이름 리스트 변환 예제 */

>>> val people = listOf(Person("안드로이드", 29), Person("코틀린", 30))
>>> println(people.map { it.name })
[안드로이드, 코틀린]

 

이 예제를 람다의 멤버 참조를 사용해서 더 멋지게 작성할 수도 있습니다.

 

/* 멤버 참조 문법 */
people.map(Person::name)

 

이제 우리는 이런 함수들을 연쇄시켜서, 원하는 결과를 쉽게 얻어낼 수 있습니다!

예를 들어, 30살 이상인 사람의 이름을 출력하려는 경우

 

>>> people.filter { it.age >= 30 }.map(Person::name)
[코틀린]

 

이와 같이, 컬렉션 함수를 연쇄시켜 아주 쉽게 원하는 결과를 얻어내었습니다!

 

그렇다면, 이제 리스트에서 가장 나이 많은 사람의 이름을 알고 싶다고 가정하겠습니다.

음~먼저 사람 리스트의 나이 최댓값을 구하고, 그 최댓값과 나이가 같은 사람을 반환하면 되겠죠?

 

람다를 사용하면 이런 코드를 쉽게 작성할 수 있습니다.

 

/* filter '내부'에서 maxBy 이용 [비효율적 코드] */
people.filter { it.age == people.maxBy(Person::age)!!.age }

/* filter '외부'에서 maxBy 이용 [효율적 코드] */
val maxAge = people.maxBy(Person::age)!!.age
people.filter { it.age == maxAge }

 

첫 번째 코드는 filter 안에서 최댓값 구하는 작업을 계속 반복하기 때문에, 만약 100명의 리스트가 있다면 filter를 돌면서 100번 최댓값 연산을 수행하기 때문에 상당히 비효율적입니다.

그에 비해, 두 번째 코드는 최댓값을 한 번만 계산하게 만든 코드이기 때문에 훨씬 더 개선되고 효율적인 코드라고 할 수 있습니다.

따라서, 무작정 코드량을 줄이려고 사용하기보다는, 불필요한 반복이 없는지 등... 내부적인 동작에 대해서 고려한 후에 코드를 작성하시는 게 더 좋은 코드가 될 수 있을 것 같습니다. ^^

 

filter와 map 함수를 Map(Key, Value) 에도 적용할 수 있습니다.

Map(Key, Value)의 경우 키와 값을 처리하는 함수가 따로 존재합니다.

  • Key 추출 : filterKeys, mapKeys
  • Value 추출 : filterValues, mapValues
>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() }) //값을 대문자로 변환
{0=ZERO, 1=ONE}

all, any, count, find : 컬렉션 조건 함수

컬렉션에 대해 자주 수행하는 조건 함수들에 대해서 살펴보도록 하겠습니다.

 

api description return type
all 컬렉션의 모든 원소가 조건을 만족하는지 판단 Boolean
any 컬렉션의 원소 중에, 조건을 만족하는 원소가 하나라도 있는지 판단 Boolean
count 조건을 만족하는 원소의 개수를 반환 Int
find 조건을 만족하는 첫번째 원소를 반환 <T>

 

이런 함수들의 동작을 살펴보기 위해, 예제로 나이가 30살 미만인지 판단하는 코드를 구현해보겠습니다.

 

all, any

 

val under30 = { p:Person -> p.age < 30 }

//모든 원소가 만족하는지 판단하려면 all 함수를 사용합니다.
>>> val people = listOf(Person("안드로이드", 25), Person("코틀린", 33))
>>> println(people.all(under30))
false

//하나라도 만족하는 원소가 있는지 판단하려면 any 함수를 사용합니다.
>>> println(people.any(under30))
true

 

팁으로, !all과 !any를 사용할 순 있지만, 
다만 ( !all = any ), ( !any = all ) 와 같은 경우에는 앞에 ! 연산자를 눈치 채지 못하는 경우가 있기 때문에 가독성을 높이기 위해 가급적이면 any와 all 앞에 ! 를 붙이지 않는 편이 낫습니다!

 

count

조건을 만족하는 원소의 개수를 구할 때는 count를 사용합니다.

 

val under30 = { p:Person -> p.age < 30 }

>>> val people = listOf(Person("안드로이드", 25), Person("코틀린", 33))
>>> println(people.count(under30))
1

 

함수를 효과적으로 사용하기 : count와 size

count 함수가 있다는 사실을 잊어버리고, 컬렉션을 필터링한 결과의 크기를 size로 가져오는 경우가 종종 있습니다.

>>> println(people.filter(under30).size)
1

하지만, 이렇게 처리하면 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생겨버리게 됩니다.
반면에, count는 조건을 만족하는 원소의 개수만을 추적하지 원소를 따로 저장하지는 않기 때문에,
따라서 count가 훨씬 더 효율적이라고 할 수 있습니다.

find

find는 조건을 만족하는 첫 번째 원소를 반환하고, 만족하는 원소가 없다면 null을 반환합니다.

 

val under30 = { p:Person -> p.age < 30 }

>>> val people = listOf(Person("안드로이드", 25), Person("코틀린", 33))
>>> println(people.find(under30))
Person(name=안드로이드, age=25)

 

find는 조건에 만족하는 첫번째 원소를 반환해주기 때문에 firstOrNull과 기능이 동일합니다.

그렇기 때문에, 조건을 만족하는 원소가 없으면 null이 나온다는 사실을 좀 더 명확하게 표현하고 싶다면 firstOrNull을 사용해도 무방합니다. ^^

groupBy : 리스트를 여러 그룹으로 이루어진 맵으로 변경

컬렉션의 모든 원소를 어떤 특성에 따라 여러 그룹으로 나누고 싶은 경우가 있습니다.

이런 분류 특성을 파라미터로 전달하면 컬렉션을 자동으로 구분해주는 함수가 있으면 참~ 편리할 것입니다.

groupBy 함수가 그런 역할을 합니다.

 

예를 들어, 사람을 나이에 따라 분류해보겠습니다. 

 

/* groupBy 예제 */

>>> val people = listOf(
          Person("안드로이드", 25),
          Person("코틀린", 30),
          Person("자바", 30))
>>> println(people.groupBy { it.age })

{ 25=[Person(name=안드로이드, age=25)],
  30=[Person(name=코틀린, age=30), Person(name=자바, age=30)] }

 

이 연산의 결과는 아래와 같은 맵(Map)입니다.

  • Key : 원소를 구분하는 특성 (예제에서는 age)
  • Value : Key 값에 따른 각 그룹 리스트 (예제에서는 Person 객체 리스트)

그림으로 표현하면 아래와 같습니다.

 

groupBy 함수를 적용한 결과

 

눈치채셨을 수도 있겠지만, 각 그룹은 리스트입니다.
따라서, 위의 groupBy 결과 타입은 Map<Int, List<Person>>이 됩니다.

flatMap과 flatten : 중첩된 컬렉션 안의 원소 처리

flatMap은 주어진 람다를 컬렉션의 모든 객체에 적용하고, 적용한 결과로 얻어지는 여러 리스트를 한 리스트로 flat 하게 만드는 함수입니다.

즉, map을 처리하고 난 다음의 결과가 list인 경우, 이 list의 원소를 다시 펼쳐서 하나의 list로 만듭니다.

말이 조금 어렵고 정신없을 수 있지만, "리스트의 리스트를 처리할 때 쓰는 함수" 로 기억해주시면 쉬울 것 같습니다!

 

예제를 하나 살펴보도록 하겠습니다.

 

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })

 

위 코드는 다음과 같은 두 단계를 수행합니다.

  1. it.toList()를 이용하여 해당 원소로 이루어진 리스트가 만들어진다. => list("abc"), list("def")
  2. flatMap이 list의 원소를 flat 하게 모든 원소로 이루어진 단일 리스트를 반환한다. => list("a", "b", "c", "d", "e", "f")

그리하여 출력의 결과는 다음과 같습니다.

 

[a, b, c, d, e, f]

 

리스트 원소를 사용해서 특별히 변환해야 할 내용이 없다면, 단순히 리스트의 리스트를 평평하게 펼치기만 하는 flatten 함수를 사용할 수 있습니다.

마무리

지금까지 코틀린 표준 라이브러리가 제공하는 몇 가지 컬렉션 연산 함수를 살펴보았습니다.

물론 이 외에도 더 많은 함수가 있기 때문에, 대부분의 경우 원하는 함수를 잘 찾으셔서 활용하시면 직접 코드로 로직을 구현하는 것보다 더 빠르고 쉽게 문제를 해결할 수 있을 것 같습니다. ^^