함수형 프로그래밍(Functional Programming, FP)은 함수를 일급 객체로 다루며, 순수 함수와 불변성을 중심으로 하는 프로그래밍 패러다임이다. Kotlin은 객체지향과 함수형 프로그래밍을 모두 지원하는 멀티 패러다임 언어로, 강력한 함수형 프로그래밍 기능을 제공한다.
이 글에서는 Kotlin에서의 함수형 프로그래밍 기본 개념부터 고급 개념까지 차례대로 살펴보자.
1. 함수형 프로그래밍 기본 개념
1.1 일급 함수 (First-Class Functions)
Kotlin에서 함수는 일급 객체다. 즉, 함수를 변수에 할당하고, 다른 함수의 매개변수로 전달하며, 함수의 반환값으로 사용할 수 있다.
// 함수를 변수에 할당
val add: (Int, Int) -> Int = { a, b -> a + b }
// 함수를 매개변수로 전달
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int {
return operation(x, y)
}
fun main() {
val result = calculate(5, 3, add)
println(result) // 8
// 람다 표현식 직접 전달
val result2 = calculate(10, 2) { a, b -> a - b }
println(result2) // 8
}
1.2 고차 함수 (Higher-Order Functions)
고차 함수는 다른 함수를 매개변수로 받거나 함수를 반환하는 함수다.
// 함수를 매개변수로 받는 고차 함수
fun applyOperation(numbers: List<Int>, operation: (Int) -> Int): List<Int> {
return numbers.map(operation)
}
// 함수를 반환하는 고차 함수
fun createMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// 각 숫자를 제곱
val squared = applyOperation(numbers) { it * it }
println(squared) // [1, 4, 9, 16, 25]
// 3배 곱하는 함수 생성
val triple = createMultiplier(3)
val tripled = applyOperation(numbers, triple)
println(tripled) // [3, 6, 9, 12, 15]
}
1.3 순수 함수 (Pure Functions)
순수 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 부수 효과(side effect)가 없는 함수다.
// 순수 함수 - 부수 효과 없음
fun multiply(a: Int, b: Int): Int = a * b
// 순수하지 않은 함수 - 외부 상태에 의존
var counter = 0
fun impureIncrement(): Int {
counter++ // 부수 효과: 외부 상태 변경
return counter
}
// 순수한 버전
fun pureIncrement(current: Int): Int = current + 1
fun main() {
// 순수 함수는 항상 같은 결과
println(multiply(3, 4)) // 12
println(multiply(3, 4)) // 12
// 순수하지 않은 함수는 매번 다른 결과
println(impureIncrement()) // 1
println(impureIncrement()) // 2
// 순수한 버전 사용
var count = 0
count = pureIncrement(count)
println(count) // 1
}
1.4 불변성 (Immutability)
함수형 프로그래밍에서는 데이터의 불변성을 유지하는 것이 중요하다.
// 불변 데이터 클래스
data class Person(val name: String, val age: Int) {
// 새로운 객체를 반환하는 메서드
fun withAge(newAge: Int): Person = this.copy(age = newAge)
}
// 불변 리스트 조작
fun addToList(list: List<Int>, element: Int): List<Int> {
return list + element // 새로운 리스트 반환
}
fun main() {
val person = Person("김철수", 25)
val olderPerson = person.withAge(26) // 새 객체 생성
println(person) // Person(name=김철수, age=25)
println(olderPerson) // Person(name=김철수, age=26)
val numbers = listOf(1, 2, 3)
val newNumbers = addToList(numbers, 4)
println(numbers) // [1, 2, 3]
println(newNumbers) // [1, 2, 3, 4]
}
2. Kotlin의 함수형 프로그래밍 도구들
2.1 람다 표현식과 익명 함수
// 람다 표현식
val square: (Int) -> Int = { x -> x * x }
val squareShort: (Int) -> Int = { it * it } // 단일 매개변수는 it 사용 가능
// 익명 함수
val cube = fun(x: Int): Int { return x * x * x }
// 여러 매개변수를 가진 람다
val combine: (String, String) -> String = { a, b -> "$a $b" }
fun main() {
println(square(5)) // 25
println(cube(3)) // 27
println(combine("Hello", "World")) // Hello World
}
2.2 컬렉션 함수형 메서드
Kotlin은 컬렉션에 대한 강력한 함수형 메서드들을 제공한다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// map: 각 요소를 변환
val doubled = numbers.map { it * 2 }
println(doubled) // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// filter: 조건에 맞는 요소만 선택
val evens = numbers.filter { it % 2 == 0 }
println(evens) // [2, 4, 6, 8, 10]
// reduce: 컬렉션을 하나의 값으로 축약
val sum = numbers.reduce { acc, n -> acc + n }
println(sum) // 55
// fold: 초기값과 함께 축약
val product = numbers.fold(1) { acc, n -> acc * n }
println(product) // 3628800
// flatMap: 평면화된 매핑
val words = listOf("hello", "world")
val characters = words.flatMap { it.toList() }
println(characters) // [h, e, l, l, o, w, o, r, l, d]
// 체이닝: 여러 연산을 연결
val result = numbers
.filter { it % 2 == 1 } // 홀수만
.map { it * it } // 제곱
.filter { it > 10 } // 10보다 큰 것만
println(result) // [25, 49, 81]
}
2.3 함수 합성 (Function Composition)
// 함수 합성을 위한 확장 함수
infix fun <A, B, C> ((A) -> B).then(f: (B) -> C): (A) -> C = { f(this(it)) }
fun main() {
val addOne: (Int) -> Int = { it + 1 }
val multiplyByTwo: (Int) -> Int = { it * 2 }
val toString: (Int) -> String = { it.toString() }
// 함수들을 합성
val combined = addOne then multiplyByTwo then toString
println(combined(5)) // "12" (5 + 1 = 6, 6 * 2 = 12, 12.toString())
}
3. 고급 함수형 프로그래밍 개념
3.1 커링 (Currying)
개념
- 여러 인자를 받는 함수 → 인자 하나만 받는 함수들의 체인으로 분리하는 기법
f(a, b, c)→f(a)(b)(c)- 함수 합성을 더 단순하게 만들 수 있음
// 일반 함수
fun add(a: Int, b: Int): Int = a + b
// 커링된 함수
fun addCurried(a: Int): (Int) -> Int = { b -> a + b }
fun main() {
val add5 = addCurried(5) // (Int) -> Int
println(add5(10)) // 15
}
의의
- 인자를 고정해놓고 새로운 함수(Partial Application)를 쉽게 만들 수 있음
- 예: “모든 값에 +5를 더하는 함수” 같은 걸 즉석에서 생성
4. 실전 활용 예제
4.1 데이터 변환 파이프라인
함수형 프로그래밍의 장점을 보여주는 실제 사용 사례다.
data class User(val id: Int, val name: String, val age: Int, val email: String)
data class UserDto(val name: String, val email: String)
fun main() {
val users = listOf(
User(1, "김철수", 25, "kim@example.com"),
User(2, "이영희", 17, "lee@example.com"),
User(3, "박민수", 30, "park@example.com"),
User(4, "최지은", 16, "choi@example.com")
)
val adultUserDtos = users
.filter { it.age >= 18 } // 성인만 필터링
.map { UserDto(it.name, it.email) } // DTO로 변환
.sortedBy { it.name } // 이름순 정렬
println(adultUserDtos)
// [UserDto(name=김철수, email=kim@example.com), UserDto(name=박민수, email=park@example.com)]
}
4.2 에러 처리와 함수 합성
sealed class ValidationError : Exception() {
object EmptyName : ValidationError()
object InvalidEmail : ValidationError()
object TooYoung : ValidationError()
}
fun validateName(name: String): Result<String> =
if (name.isBlank()) Result.failure(ValidationError.EmptyName)
else Result.success(name)
fun validateEmail(email: String): Result<String> =
if (email.contains("@")) Result.success(email)
else Result.failure(ValidationError.InvalidEmail)
fun validateAge(age: Int): Result<Int> =
if (age >= 18) Result.success(age)
else Result.failure(ValidationError.TooYoung)
fun createUser(name: String, email: String, age: Int): Result<User> {
return validateName(name)
.flatMap { validName ->
validateEmail(email).flatMap { validEmail ->
validateAge(age).map { validAge ->
User(0, validName, validAge, validEmail)
}
}
}
}
data class User(
val id: Long,
val name: String,
val age: Int,
val email: String
)
fun main() {
val validUser = createUser("김철수", "kim@example.com", 25)
println(validUser) // Success(User(id=0, name=김철수, age=25, email=kim@example.com))
val invalidUser = createUser("", "invalid-email", 16)
println(invalidUser) // Failure(ValidationError$EmptyName@...)
}
5. Arrow 라이브러리와 고급 함수형 프로그래밍
Kotlin에서 더 고급 함수형 프로그래밍을 위해서는 Arrow 라이브러리를 사용할 수 있다.
5.1 Option 타입
import arrow.core.*
fun divide(a: Int, b: Int): Option<Double> =
if (b == 0) None else Some(a.toDouble() / b)
fun main() {
val result1 = divide(10, 2)
.map { it * 2 }
.getOrElse { 0.0 }
println(result1) // 10.0
val result2 = divide(10, 0)
.map { it * 2 }
.getOrElse { 0.0 }
println(result2) // 0.0
}
5.2 Either 타입
import arrow.core.*
sealed class DivisionError {
object DivisionByZero : DivisionError()
}
fun safeDivide(a: Int, b: Int): Either<DivisionError, Double> =
if (b == 0) DivisionError.DivisionByZero.left()
else (a.toDouble() / b).right()
fun main() {
val success = safeDivide(10, 2)
.map { it * 2 }
println(success) // Right(10.0)
val failure = safeDivide(10, 0)
.map { it * 2 }
println(failure) // Left(DivisionError$DivisionByZero@...)
}
6. 함수형 프로그래밍의 장단점
장점
- 예측 가능성: 순수 함수는 같은 입력에 대해 항상 같은 출력
- 테스트 용이성: 부수 효과가 없어 단위 테스트가 쉬움
- 병렬 처리: 불변 데이터로 인해 스레드 안전성 확보
- 코드 재사용성: 고차 함수와 함수 합성으로 코드 재사용 증가
- 버그 감소: 불변성과 순수 함수로 인한 버그 감소
단점
- 학습 곡선: 새로운 사고방식에 대한 학습 필요
- 성능 오버헤드: 불변 객체 생성으로 인한 메모리 사용량 증가 가능
- 디버깅: 고차 함수와 체이닝으로 인한 디버깅 복잡성
7. 기존 고급 개념들
7.1 모나드 (Monad)
개념
- 값을 어떤 “컨텍스트”에 담아두고 안전하게 꺼내 쓰는 방법을 추상화한 것
- FP에서 가장 흔한 모나드는
Optional/Maybe,List,Result,Future등
👉 세 가지 속성
- 타입 생성자: 값을 담는 껍데기 (예:
Result<T>) - unit(=return): 값을 껍데기 안에 넣는 것
- bind(=flatMap): 껍데기 안 값을 꺼내서 새로운 껍데기에 넣는 함수 실행
예시 (Kotlin – Result 모나드)
fun parseInt(str: String): Result<Int> =
runCatching { str.toInt() }
fun reciprocal(x: Int): Result<Double> =
if (x == 0) Result.failure(ArithmeticException("0으로 나눌 수 없음"))
else Result.success(1.0 / x)
fun main() {
val result = parseInt("10")
.flatMap { reciprocal(it) }
println(result) // Success(0.1)
}
의의
- 에러 처리, 비동기 처리, null 안전성을 함수 합성 패턴에 녹여낼 수 있음
- 문서 속
Result.success/failure→ 전형적인 모나드 활용 사례
3. 모나드와 부수효과
- 순수 함수만 합성 가능하지만, 현실은 I/O, 예외, 상태 변경 등 부수효과가 필수
- 모나드는 부수효과를 안전하게 컨테이너 안에 가둔 뒤, 합성 가능하게 만들어줌
- 예시
List모나드 → 여러 값의 연산을 합성Future/Deferred모나드 → 비동기 연산 합성
- IO 모나드 (Haskell) → I/O 부수효과 합성
4. 추가 개념 (간단 소개)
🔹 부분 적용 (Partial Application)
- 커링과 비슷하지만, 함수를 분해하지 않고 일부 인자만 고정하는 기법
fun power(base: Int, exp: Int): Int = Math.pow(base.toDouble(), exp.toDouble()).toInt()
val square: (Int) -> Int = { power(it, 2) }
🔹 Functor
map이 가능한 컨테이너List,Optional,Result등
Result.success(5).map { it * 2 } // Success(10)
🔹 Applicative
- Functor보다 강력, 여러 컨테이너 값을 조합 가능
- Kotlin에서는 Arrow 라이브러리(.ap) 같은 기능 제공
정리
- 커링: 다인자 함수 → 1인자 함수들의 합성으로 쪼개기 (합성/부분 적용 용이)
- 모나드: 값을 컨텍스트 안에 안전하게 담고, 합성을 가능하게 하는 추상화 (Result, Future 등)
- Functors & Applicatives: 모나드로 가기 전 단계, map/ap을 통한 함수 적용
결국 이 모든 것은 “부수효과를 통제하면서도, 순수 함수 합성의 장점을 살리는 방법”
'Kotlin' 카테고리의 다른 글
| Kotlin + Spring Boot: 비동기 멀티홉 환경에서 예외를 유실하지 않는 전략 (0) | 2026.03.08 |
|---|---|
| Kotlin/Gradle 빌드 시 OOM 발생(?) (부제: Kotlin IR, KAPT를 다시 까보자) (0) | 2025.12.22 |
| Kotlin 제너릭 완전 정복: 클래스 vs 함수, 타입 추론까지 (0) | 2025.10.05 |