Kim-Baek 개발자 이야기

Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍 본문

개발/java basic

Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍

김백개발자 2025. 12. 24. 08:45
반응형

이 글을 읽으면: Java의 Stream API보다 강력한 Kotlin 컬렉션 함수를 배울 수 있습니다. map, filter, groupBy 등 함수형 프로그래밍으로 데이터를 우아하게 처리하는 방법을 실전 예제로 마스터하세요.


📌 목차

  1. 들어가며 - Java 컬렉션의 불편함
  2. 컬렉션 생성 - 불변 vs 가변
  3. List 컬렉션 완벽 정리
  4. Set과 Map
  5. 함수형 연산 - map, filter, reduce
  6. 고급 연산 - groupBy, partition, flatMap
  7. Sequence - 성능 최적화
  8. 실전 패턴 모음
  9. 마무리 - 다음 편 예고

들어가며 - Java 컬렉션의 불편함

Java로 데이터 처리할 때 이런 경험 있으신가요?

// Java - 장황한 컬렉션 처리
List<User> users = Arrays.asList(
    new User("규철", 30),
    new User("영희", 25),
    new User("철수", 35)
);

// 1. 30세 이상 필터링
List<User> adults = new ArrayList<>();
for (User user : users) {
    if (user.getAge() >= 30) {
        adults.add(user);
    }
}

// 2. 이름만 추출
List<String> names = new ArrayList<>();
for (User user : adults) {
    names.add(user.getName());
}

// 3. 대문자로 변환
List<String> upperNames = new ArrayList<>();
for (String name : names) {
    upperNames.add(name.toUpperCase());
}

// Java 8 Stream - 조금 나아짐
List<String> result = users.stream()
    .filter(u -> u.getAge() >= 30)
    .map(User::getName)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Kotlin은 이렇게 간단합니다:

// Kotlin - 한 줄로 끝
val users = listOf(
    User("규철", 30),
    User("영희", 25),
    User("철수", 35)
)

val result = users
    .filter { it.age >= 30 }
    .map { it.name.uppercase() }

println(result)  // [규철, 철수]

오늘은 Kotlin의 강력한 컬렉션 API를 완전히 마스터해보겠습니다!


컬렉션 생성 - 불변 vs 가변

불변 컬렉션 (읽기 전용)

기본적으로 불변 컬렉션을 사용하세요!

fun main() {
    // List - listOf
    val numbers = listOf(1, 2, 3, 4, 5)
    // numbers.add(6)  // ❌ 컴파일 에러 (불변)
    
    // Set - setOf
    val uniqueNumbers = setOf(1, 2, 2, 3, 3, 3)
    println(uniqueNumbers)  // [1, 2, 3] (중복 제거)
    
    // Map - mapOf
    val scores = mapOf(
        "국어" to 90,
        "영어" to 85,
        "수학" to 95
    )
    println(scores["국어"])  // 90
}

가변 컬렉션 (읽기/쓰기)

fun main() {
    // MutableList
    val numbers = mutableListOf(1, 2, 3)
    numbers.add(4)
    numbers.remove(2)
    println(numbers)  // [1, 3, 4]
    
    // MutableSet
    val items = mutableSetOf("사과", "바나나")
    items.add("오렌지")
    items.add("사과")  // 중복이므로 추가 안 됨
    println(items)  // [사과, 바나나, 오렌지]
    
    // MutableMap
    val scores = mutableMapOf("국어" to 90)
    scores["영어"] = 85
    scores["수학"] = 95
    println(scores)  // {국어=90, 영어=85, 수학=95}
}

빈 컬렉션 생성

fun main() {
    // 불변 빈 컬렉션
    val emptyList = emptyList<String>()
    val emptySet = emptySet<Int>()
    val emptyMap = emptyMap<String, Int>()
    
    // 가변 빈 컬렉션
    val mutableList = mutableListOf<String>()
    val mutableSet = mutableSetOf<Int>()
    val mutableMap = mutableMapOf<String, Int>()
}

Java 스타일 vs Kotlin 스타일

// Java - 타입 중복, 길고 복잡함
List<String> list = new ArrayList<>();
list.add("사과");
list.add("바나나");

Map<String, Integer> map = new HashMap<>();
map.put("국어", 90);
map.put("영어", 85);
// Kotlin - 간결하고 타입 추론
val list = mutableListOf("사과", "바나나")

val map = mutableMapOf(
    "국어" to 90,
    "영어" to 85
)

List 컬렉션 완벽 정리

기본 연산

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // 크기
    println(numbers.size)  // 5
    
    // 인덱스 접근
    println(numbers[0])     // 1
    println(numbers.first()) // 1
    println(numbers.last())  // 5
    
    // 안전한 접근
    println(numbers.getOrNull(10))  // null
    println(numbers.getOrElse(10) { 0 })  // 0
    
    // 포함 확인
    println(numbers.contains(3))  // true
    println(3 in numbers)         // true
    
    // 인덱스 찾기
    println(numbers.indexOf(3))  // 2
}

슬라이싱

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
    // 처음 3개
    println(numbers.take(3))  // [1, 2, 3]
    
    // 마지막 3개
    println(numbers.takeLast(3))  // [8, 9, 10]
    
    // 처음 3개 제외
    println(numbers.drop(3))  // [4, 5, 6, 7, 8, 9, 10]
    
    // 범위로 자르기
    println(numbers.slice(2..5))  // [3, 4, 5, 6]
    
    // 조건에 맞는 동안만
    println(numbers.takeWhile { it < 5 })  // [1, 2, 3, 4]
}

정렬

fun main() {
    val numbers = listOf(5, 2, 8, 1, 9, 3)
    
    // 오름차순
    println(numbers.sorted())  // [1, 2, 3, 5, 8, 9]
    
    // 내림차순
    println(numbers.sortedDescending())  // [9, 8, 5, 3, 2, 1]
    
    // 커스텀 정렬
    data class Person(val name: String, val age: Int)
    
    val people = listOf(
        Person("규철", 30),
        Person("영희", 25),
        Person("철수", 35)
    )
    
    // 나이순 정렬
    println(people.sortedBy { it.age })
    // [Person(name=영희, age=25), Person(name=규철, age=30), Person(name=철수, age=35)]
    
    // 나이 내림차순
    println(people.sortedByDescending { it.age })
}

리스트 조작

fun main() {
    val list1 = listOf(1, 2, 3)
    val list2 = listOf(4, 5, 6)
    
    // 합치기
    println(list1 + list2)  // [1, 2, 3, 4, 5, 6]
    
    // 중복 제거
    val duplicates = listOf(1, 2, 2, 3, 3, 3)
    println(duplicates.distinct())  // [1, 2, 3]
    
    // 뒤집기
    println(list1.reversed())  // [3, 2, 1]
    
    // 섞기
    println(list1.shuffled())  // 랜덤 순서
}

Set과 Map

Set - 중복 없는 컬렉션

fun main() {
    val set1 = setOf(1, 2, 3)
    val set2 = setOf(3, 4, 5)
    
    // 합집합
    println(set1 + set2)  // [1, 2, 3, 4, 5]
    println(set1.union(set2))  // [1, 2, 3, 4, 5]
    
    // 교집합
    println(set1.intersect(set2))  // [3]
    
    // 차집합
    println(set1 - set2)  // [1, 2]
    println(set1.subtract(set2))  // [1, 2]
}

Map - 키-값 쌍

fun main() {
    val scores = mapOf(
        "국어" to 90,
        "영어" to 85,
        "수학" to 95
    )
    
    // 값 가져오기
    println(scores["국어"])  // 90
    println(scores.get("국어"))  // 90
    println(scores.getValue("국어"))  // 90 (없으면 예외)
    println(scores.getOrDefault("과학", 0))  // 0
    
    // 키/값 목록
    println(scores.keys)    // [국어, 영어, 수학]
    println(scores.values)  // [90, 85, 95]
    
    // 순회
    for ((subject, score) in scores) {
        println("$subject: $score점")
    }
    
    // 필터링
    val highScores = scores.filter { it.value >= 90 }
    println(highScores)  // {국어=90, 수학=95}
}

가변 Map 조작

fun main() {
    val scores = mutableMapOf(
        "국어" to 90,
        "영어" to 85
    )
    
    // 추가/수정
    scores["수학"] = 95
    scores.put("과학", 88)
    
    // 없을 때만 추가
    scores.putIfAbsent("국어", 100)  // 이미 있으므로 무시
    
    // 삭제
    scores.remove("영어")
    
    // 조건부 수정
    scores.compute("수학") { _, value ->
        (value ?: 0) + 5  // 100
    }
    
    println(scores)
}

함수형 연산 - map, filter, reduce

filter - 조건에 맞는 요소만

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
    // 짝수만
    val evens = numbers.filter { it % 2 == 0 }
    println(evens)  // [2, 4, 6, 8, 10]
    
    // 5보다 큰 수
    val greaterThan5 = numbers.filter { it > 5 }
    println(greaterThan5)  // [6, 7, 8, 9, 10]
    
    // 조건에 안 맞는 것만 (반대)
    val odds = numbers.filterNot { it % 2 == 0 }
    println(odds)  // [1, 3, 5, 7, 9]
}

map - 변환

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // 제곱
    val squares = numbers.map { it * it }
    println(squares)  // [1, 4, 9, 16, 25]
    
    // 문자열로 변환
    val strings = numbers.map { "숫자: $it" }
    println(strings)  // [숫자: 1, 숫자: 2, ...]
    
    data class Person(val name: String, val age: Int)
    val people = listOf(
        Person("규철", 30),
        Person("영희", 25)
    )
    
    // 이름만 추출
    val names = people.map { it.name }
    println(names)  // [규철, 영희]
}

체이닝 - filter + map

fun main() {
    data class Product(val name: String, val price: Int, val category: String)
    
    val products = listOf(
        Product("노트북", 1500000, "전자제품"),
        Product("마우스", 30000, "전자제품"),
        Product("책상", 200000, "가구"),
        Product("의자", 150000, "가구")
    )
    
    // 전자제품 중 5만원 이상인 것의 이름만
    val result = products
        .filter { it.category == "전자제품" }
        .filter { it.price >= 50000 }
        .map { it.name }
    
    println(result)  // [노트북]
}

reduce - 누적 계산

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // 합계
    val sum = numbers.reduce { acc, num -> acc + num }
    println(sum)  // 15
    
    // 곱셈
    val product = numbers.reduce { acc, num -> acc * num }
    println(product)  // 120
    
    // fold - 초기값 지정 가능
    val sumWith100 = numbers.fold(100) { acc, num -> acc + num }
    println(sumWith100)  // 115 (100 + 15)
}

실전 예제 - 주문 처리

data class Order(
    val id: Long,
    val customerName: String,
    val amount: Int,
    val status: String
)

fun main() {
    val orders = listOf(
        Order(1, "규철", 50000, "완료"),
        Order(2, "영희", 30000, "완료"),
        Order(3, "철수", 100000, "취소"),
        Order(4, "규철", 25000, "완료")
    )
    
    // 완료된 주문의 총 금액
    val totalAmount = orders
        .filter { it.status == "완료" }
        .sumOf { it.amount }
    
    println("총 매출: ${totalAmount}원")  // 105000원
    
    // 고객별 구매 금액
    val customerTotals = orders
        .filter { it.status == "완료" }
        .groupBy { it.customerName }
        .mapValues { (_, orders) -> orders.sumOf { it.amount } }
    
    println(customerTotals)  // {규철=75000, 영희=30000}
}

고급 연산 - groupBy, partition, flatMap

groupBy - 그룹화

fun main() {
    data class Student(val name: String, val grade: Int, val score: Int)
    
    val students = listOf(
        Student("규철", 1, 90),
        Student("영희", 2, 85),
        Student("철수", 1, 88),
        Student("민수", 2, 92)
    )
    
    // 학년별 그룹화
    val byGrade = students.groupBy { it.grade }
    println(byGrade)
    // {1=[Student(규철, 1, 90), Student(철수, 1, 88)], 
    //  2=[Student(영희, 2, 85), Student(민수, 2, 92)]}
    
    // 학년별 평균 점수
    val averageByGrade = students
        .groupBy { it.grade }
        .mapValues { (_, students) ->
            students.map { it.score }.average()
        }
    
    println(averageByGrade)  // {1=89.0, 2=88.5}
}

partition - 둘로 나누기

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
    // 짝수와 홀수로 분리
    val (evens, odds) = numbers.partition { it % 2 == 0 }
    
    println("짝수: $evens")  // [2, 4, 6, 8, 10]
    println("홀수: $odds")   // [1, 3, 5, 7, 9]
}

flatMap - 평탄화

fun main() {
    val nested = listOf(
        listOf(1, 2, 3),
        listOf(4, 5),
        listOf(6, 7, 8, 9)
    )
    
    // 중첩 리스트를 평탄화
    val flattened = nested.flatMap { it }
    println(flattened)  // [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    // 변환 + 평탄화
    val result = nested.flatMap { list ->
        list.map { it * 2 }
    }
    println(result)  // [2, 4, 6, 8, 10, 12, 14, 16, 18]
}

실전 예제 - 문자열 처리

fun main() {
    val text = "Kotlin is awesome! Kotlin is fun!"
    
    // 단어 빈도수
    val wordCount = text
        .lowercase()
        .split(" ", "!", ".")
        .filter { it.isNotEmpty() }
        .groupBy { it }
        .mapValues { (_, words) -> words.size }
        .toList()
        .sortedByDescending { it.second }
    
    println(wordCount)
    // [(kotlin, 2), (is, 2), (awesome, 1), (fun, 1)]
}

associate - Map 생성

fun main() {
    data class User(val id: Long, val name: String)
    
    val users = listOf(
        User(1, "규철"),
        User(2, "영희"),
        User(3, "철수")
    )
    
    // id를 키로, name을 값으로
    val userMap = users.associate { it.id to it.name }
    println(userMap)  // {1=규철, 2=영희, 3=철수}
    
    // name을 키로, User를 값으로
    val nameMap = users.associateBy { it.name }
    println(nameMap["규철"])  // User(id=1, name=규철)
}

Sequence - 성능 최적화

List vs Sequence

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    
    // List - 즉시 평가 (각 연산마다 새 리스트 생성)
    val listResult = numbers
        .map { 
            println("map: $it")
            it * 2 
        }
        .filter { 
            println("filter: $it")
            it > 10 
        }
        .take(2)
    
    println("=== Sequence ===")
    
    // Sequence - 지연 평가 (필요할 때만 계산)
    val seqResult = numbers.asSequence()
        .map { 
            println("map: $it")
            it * 2 
        }
        .filter { 
            println("filter: $it")
            it > 10 
        }
        .take(2)
        .toList()
}

언제 Sequence를 사용할까?

fun main() {
    // ✅ Sequence 사용 권장
    // 1. 대용량 데이터
    val largeData = (1..1_000_000).asSequence()
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .take(100)
        .toList()
    
    // 2. 체인이 긴 경우
    val result = (1..100).asSequence()
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .filter { it > 50 }
        .map { it.toString() }
        .toList()
    
    // ❌ Sequence 불필요
    // 작은 데이터 + 간단한 연산
    val simple = listOf(1, 2, 3, 4, 5)
        .filter { it % 2 == 0 }
        .map { it * 2 }
}

generateSequence - 무한 시퀀스

fun main() {
    // 피보나치 수열
    val fibonacci = generateSequence(0 to 1) { (a, b) ->
        b to (a + b)
    }.map { it.first }
    
    // 처음 10개만
    println(fibonacci.take(10).toList())
    // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    
    // 1부터 시작하는 무한 수열
    val infiniteNumbers = generateSequence(1) { it + 1 }
    
    // 100 이하만
    println(infiniteNumbers.takeWhile { it <= 100 }.toList())
}

실전 패턴 모음

패턴 1: 데이터 집계

data class Sale(
    val date: String,
    val product: String,
    val amount: Int,
    val quantity: Int
)

fun main() {
    val sales = listOf(
        Sale("2024-01", "노트북", 1500000, 2),
        Sale("2024-01", "마우스", 30000, 5),
        Sale("2024-02", "노트북", 1500000, 1),
        Sale("2024-02", "키보드", 100000, 3)
    )
    
    // 월별 총 매출
    val monthlyTotal = sales
        .groupBy { it.date }
        .mapValues { (_, sales) ->
            sales.sumOf { it.amount * it.quantity }
        }
    
    println(monthlyTotal)
    // {2024-01=3150000, 2024-02=1800000}
    
    // 제품별 판매량
    val productQuantity = sales
        .groupBy { it.product }
        .mapValues { (_, sales) ->
            sales.sumOf { it.quantity }
        }
    
    println(productQuantity)
    // {노트북=3, 마우스=5, 키보드=3}
}

패턴 2: 중복 제거 및 정렬

data class User(val id: Long, val name: String, val email: String)

fun main() {
    val users = listOf(
        User(1, "규철", "user1@example.com"),
        User(2, "영희", "user2@example.com"),
        User(1, "규철", "user1@example.com"),  // 중복
        User(3, "철수", "user3@example.com")
    )
    
    // ID 기준 중복 제거 + 이름순 정렬
    val uniqueSorted = users
        .distinctBy { it.id }
        .sortedBy { it.name }
    
    println(uniqueSorted)
}

패턴 3: 조건별 분류

data class Student(val name: String, val score: Int)

fun main() {
    val students = listOf(
        Student("규철", 95),
        Student("영희", 78),
        Student("철수", 88),
        Student("민수", 65),
        Student("수지", 92)
    )
    
    // 성적별 분류
    val gradeGroups = students.groupBy { student ->
        when {
            student.score >= 90 -> "A"
            student.score >= 80 -> "B"
            student.score >= 70 -> "C"
            else -> "D"
        }
    }
    
    println(gradeGroups)
    // {A=[규철(95), 수지(92)], C=[영희(78)], B=[철수(88)], D=[민수(65)]}
}

패턴 4: 체이닝 최적화

fun main() {
    val numbers = (1..1000).toList()
    
    // ❌ 비효율적 - 여러 번 순회
    val bad = numbers
        .filter { it % 2 == 0 }  // 첫 번째 순회
        .map { it * 2 }          // 두 번째 순회
        .filter { it > 100 }     // 세 번째 순회
    
    // ✅ 효율적 - Sequence 사용
    val good = numbers.asSequence()
        .filter { it % 2 == 0 }
        .map { it * 2 }
        .filter { it > 100 }
        .toList()  // 한 번만 순회
}

마무리 - 다음 편 예고

오늘 배운 것 ✅

  • 불변/가변 컬렉션 (listOf, mutableListOf)
  • List, Set, Map 기본 연산
  • 함수형 연산 - filter, map, reduce
  • 고급 연산 - groupBy, partition, flatMap
  • Sequence - 성능 최적화

다음 편에서 배울 것 📚

8편: 람다와 고차 함수 완벽 가이드 | 함수형 프로그래밍 심화

  • 람다 표현식 완벽 정리
  • 고차 함수 (함수를 인자/반환)
  • 수신 객체 지정 람다
  • inline, crossinline, noinline
  • 실전 DSL 패턴

실습 과제 💪

// 1. 데이터 필터링 및 변환
// - 상품 리스트에서 특정 조건 필터링
// - 가격 합계, 평균 계산

// 2. groupBy 활용
// - 주문 데이터를 고객별로 그룹화
// - 고객별 총 구매액 계산

// 3. Sequence 성능 비교
// - List vs Sequence 벤치마크
// - 대용량 데이터 처리

// 4. 실전 패턴
// - 로그 데이터 분석
// - 에러 빈도수 집계

자주 묻는 질문 (FAQ)

Q: List와 Sequence 중 뭘 써야 하나요?
A: 작은 데이터나 단순 연산은 List, 대용량이나 긴 체인은 Sequence를 쓰세요.

Q: map과 forEach의 차이는?
A: map은 새 리스트 반환, forEach는 반환값 없이 실행만 합니다.

Q: filter 여러 개 vs 조건 합치기?
A: 가독성이 좋으면 여러 개도 OK. Sequence면 성능 차이 없습니다.

Q: groupBy와 partition의 차이는?
A: groupBy는 여러 그룹, partition은 딱 두 그룹으로 나눕니다.


관련 글


💬 댓글로 알려주세요!

  • 어떤 컬렉션 함수가 가장 유용했나요?
  • Sequence를 실전에서 사용해본 경험이 있나요?
  • 이 글이 도움이 되셨나요?

태그: #Kotlin #컬렉션 #함수형프로그래밍 #map #filter #groupBy #Sequence

반응형
Comments