안녕하세요, 반갑습니다!
코틀린으로 안드로이드 앱을 개발하다보면, 자바와 비슷한 것이 많아 편함을 느낍니다.
익숙하죠. 자바만 알고 있다면 유추해서 코틀린으로도 해당 기능을 구현할 수 있으니까요.
그래도 코틀린은 역시 다른 언어임을 느낄 수 있습니다.
람다식 및 컬렉션에 대해 자바보다 훨씬 간단하고 효율적으로 구현할 수 있거든요.
오늘은 그 중 컬렉션에 활용할 수 있는 filter, map, all, any 등 함수형 API에 대해 정리해봤습니다.
코틀린 함수형 API
filter, map, all, any,count, find, maxByOrNull, groupBy
먼저 모든 함수를 사용해보기 전에, 데이터 클래스를 하나 정의해놓도록 하겠습니다.
//데이터 저장용도로만 사용하는 클래스 Movies 정의
data class Movies(
var title:String,
var rating:Float) {
}
단순하게 영화 제목과 별점의 정보를 가지는 클래스를 정의했습니다.
정보만을 저장하기 위해 사용하는 용도로 만드는 클래스는 data 키워드를 붙일 수 있습니다.
그리고 아래와 같이 데이터를 넣었습니다.
val movieInfo = listOf<Movies>(
Movies("기생충", 9.07f),
Movies("세자매", 8.83f),
Movies("스푸트닉", 4.78f),
Movies("내겐 너무 어려운 연애", 7.75f),
Movies("페어웰", 7.91f),
Movies("어니스트 씨프", 9.55f)
)
이제 이 클래스를 기반으로 모든 함수를 사용해보고, 어떤차이가 있는지 살펴보겠습니다.
필수형 함수
필수형 함수라고 굳이 구분한 이유는 대부분의 컬렉션에 대한 연산을 이 함수들로 처리할 수 있기 때문입니다.
함수는 filter() 와 map() 입니다.
1. filter()
var movie = movieInfo.filter { it.rating > 8.0f }
movie.forEach {
println("평점 8점 이상의 영화 : ${it}")
}
8.0 이상의 평점을 가진 영화가 어떤 것들인지 찾는 로직입니다.
결과를 보면, 정확하게 8.0이상의 영화들이 모아진 것을 확인할 수 있습니다.
아주 간단하죠?
2. map()
var movie = movieInfo.filter { it.rating > 8.0f }.map { it.title }
movie.forEach {
println(it)
}
역시 평점 8.0 이상의 영화를 조회하는 로직입니다.
흠 filter 와 별다를 게 없는 것 같네요.
하지만 일단 결과를 보면 그 차이를 확실히 알 수 있습니다.
어떤가요? filter() 와 map() 의 차이가 구분되시나요?
간단히 요약하면, filter() 는 컬렉션을 true를 반환하는 원소만 모읍니다.
그래서 결과는 주어진 술어를 만족하는 원소만으로 이루어진 새로운 컬렉션이 되는 것이죠.
하지만 원소를 변형할 수는 없습니다.
그러기 위해 사용하는 것이 바로 map() 입니다.
.map { it.tile } 은 술어를 만족하는 요소들 중에서 제목만을 따로 뽑아 새로운 컬렉션을 만듭니다.
이렇게 하면 영화 이름만 모인 리스트로 바꿀 수 있습니다.
그 외 유용한 함수
이제 정리해볼 함수들은 코드를 더 이해하기 쉽고 깔끔하게 작성하는데 도움이 되는 함수들입니다.
사용해보면서 정말 유용하다는 걸 느낄 수 있었습니다.
1. all()
val isRatingOver8 = { m:Movies -> m.rating > 8.0f} //술어
println("모두 평점 8점 이상의 영화들인가요? : ${movieInfo.all(isRatingOver8)}")
평점 8점 이상의 영화들이 있는지 확인하는 로직입니다.
결과는 true / false 입니다.
딱 모든 원소가 술어를 만족하는지 그 여부만 알 수 있습니다.
all() 은 모든 원소가 술어를 만족하는지 확인하는 함수입니다.
2.any()
val isRatingUnder8 = { m:Movies -> m.rating < 8.0f} //술어
println("평점 8점 미만의 영화가 있나요? : ${movieInfo.any(isRatingUnder8)}")
평점 8 점 미만의 영화가 있는지 확인하는 로직입니다.
all() 과 같이 결과값은 true / false 입니다.
술어를 만족하는 원소가 1개라도 있는지 궁금할때는 any() 를 사용하면 됩니다.
3.count()
val isRatingOver8 = { m:Movies -> m.rating > 8.0f} //술어
println("평점 8점 이상의 영화는 몇 편이 있나요? : ${movieInfo.count(isRatingOver8)}")
평점 8점 이상의 영화가 몇 편인지 확인하는 로직입니다.
결과에서 보듯이 술어를 만족하는 값이 몇개인지 확인할 수 있습니다.
그런데 보통 컬렉션의 크기를 구할때 size() 가 제일 먼저 떠오르곤 합니다.
하지만 size() 와 비교하여 count() 를 쓰는 것이 조금 더 효율적입니다.
4.find()
val firtUnder8 = movieInfo.find { it.rating < 8.0f }
println("평점 8점이 안되는 첫 번째 영화는 ${firtUnder8}")
평점 8점이 안되는 영화 중 가장 첫 번째에 있는 영화를 조회하는 로직입니다.
입력한 데이터와 결과를 보면, 평점 8점이 안되는 영화 중 가장 처음에 있는 값이 반환됨을 알 수 있습니다.
6.groupBy()
groupBy() 는 특정 값을 기준으로 여러 그룹으로 나누는 함수이다.
따라서 기존 데이터 클래스에 기준이 되는 멤버변수를 만들었습니다.
data class MoviesForGroup(
var title:String,
var rating:Float,
var isDomestic:Boolean) //추가, 국내영화인지, 해외영화인지
그리고 데이터를 추가해줬습니다.
val movieInfo = listOf<MoviesForGroup>(
MoviesForGroup("기생충", 9.07f, true),
MoviesForGroup("세자매", 8.83f, true),
MoviesForGroup("스푸트닉", 4.78f, false),
MoviesForGroup("내겐 너무 어려운 연애", 7.75f, false),
MoviesForGroup("페어웰", 7.91f, false),
MoviesForGroup("어니스트 씨프", 9.55f, false)
)
이제 그룹지어 볼까요?
val movieGroup = movieInfo.groupBy { it.isDomestic }
movieGroup.forEach {
println("${it}")
}
isDomestic 값을 기준으로 그룹이 나누어 집니다.
isDomestic 값을 기준으로 그룹지어지는 것을 볼 수 있습니다.
GroupBy() 는 key와 value 로 이루어지는 Map 으로 결과를 반환합니다.
그럼 key 는 isDomestic 이 되고, value 가 요소가 되겠지요?
확실히 이렇게 컬렉션에 대한 코틀린 함수들을 정리해보니 자바에서 많이 개선되었음을 느낄 수 있습니다.
감사합니다!