| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- effectivejava
- 이펙티브자바
- 김영한
- 자바스크립트
- Kotlin
- 자바
- 카카오
- Sort
- 티스토리챌린지
- Spring
- 이차전지관련주
- Effective Java 3
- 오블완
- 예제로 배우는 스프링 입문
- 스프링 핵심원리
- JavaScript
- 알고리즘
- 알고리즘정렬
- k8s
- 함수형프로그래밍
- 클린아키텍처
- 스프링부트
- java
- 스프링
- ElasticSearch
- kubernetes
- 이펙티브 자바
- 스프링핵심원리
- 엘라스틱서치
- Effective Java
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 람다와 고차 함수 완벽 가이드 | 함수형 프로그래밍 심화 본문

이 글을 읽으면: 함수를 값처럼 다루는 강력한 함수형 프로그래밍을 마스터할 수 있습니다. 람다 표현식, 고차 함수, 수신 객체 지정 람다를 활용해 더 간결하고 우아한 코드를 작성하는 방법을 실전 예제로 배워보세요.
📌 목차
- 들어가며 - 함수를 값처럼
- 람다 표현식 기본
- 고차 함수 - 함수를 인자로, 함수를 반환
- it 키워드와 underscore
- 수신 객체 지정 람다
- inline, crossinline, noinline
- 실전 DSL 패턴
- 마무리 - 다음 편 예고
들어가며 - 함수를 값처럼
Java로 이벤트 리스너를 작성할 때 이런 경험 있으신가요?
// Java - 익명 클래스로 콜백 구현
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("클릭됨!");
}
});
// Java 8 람다 - 조금 나아짐
button.setOnClickListener(v -> {
System.out.println("클릭됨!");
});
// 함수형 인터페이스 정의도 필요
@FunctionalInterface
public interface Callback {
void execute();
}
Kotlin은 이렇게 간단합니다:
// Kotlin - 람다가 일급 시민
button.setOnClickListener {
println("클릭됨!")
}
// 함수를 변수에 저장
val onClick: () -> Unit = {
println("클릭됨!")
}
// 함수를 인자로 전달
fun performAction(action: () -> Unit) {
action()
}
performAction { println("실행!") }
실제 프로젝트에서 람다를 사용한 사례:
// 데이터 처리 파이프라인
val result = users
.filter { it.age >= 18 } // 람다로 조건
.map { it.name.uppercase() } // 람다로 변환
.sortedBy { it.length } // 람다로 정렬 기준
.take(5)
// 비동기 처리
launch {
val data = fetchData()
updateUI { showData(data) } // 람다로 UI 업데이트
}
// DSL 스타일
html {
body {
h1 { +"제목" }
p { +"내용" }
}
}
오늘은 Kotlin의 람다와 고차 함수를 완전히 마스터해보겠습니다!
람다 표현식 기본
람다 문법
기본 형식: { 파라미터 -> 본문 }
fun main() {
// 가장 간단한 람다
val greet = { println("안녕하세요!") }
greet() // 안녕하세요!
// 파라미터가 있는 람다
val sum = { a: Int, b: Int -> a + b }
println(sum(10, 20)) // 30
// 여러 줄 람다 (마지막 표현식이 반환값)
val calculate = { x: Int, y: Int ->
val result = x * y
println("계산 중...")
result // 반환값
}
println(calculate(5, 3)) // 15
}
타입 추론
fun main() {
// 타입 명시
val add: (Int, Int) -> Int = { a, b -> a + b }
// 타입 추론 (파라미터 타입 생략)
val multiply = { a: Int, b: Int -> a * b }
// 사용
println(add(10, 20)) // 30
println(multiply(5, 3)) // 15
}
함수 타입
fun main() {
// 파라미터 없고 반환 없음
val action: () -> Unit = { println("실행") }
// 파라미터 있고 반환 있음
val transform: (String) -> Int = { it.length }
// 여러 파라미터
val combine: (String, Int) -> String = { str, num ->
"$str: $num"
}
// Nullable 반환
val findUser: (Long) -> User? = { id ->
if (id > 0) User(id, "규철") else null
}
}
data class User(val id: Long, val name: String)
Java와 비교
// Java - 함수형 인터페이스 필요
@FunctionalInterface
interface Operation {
int execute(int a, int b);
}
// 사용
Operation add = (a, b) -> a + b;
int result = add.execute(10, 20);
// Kotlin - 함수 타입 직접 사용
val add: (Int, Int) -> Int = { a, b -> a + b }
val result = add(10, 20)
고차 함수 - 함수를 인자로, 함수를 반환
함수를 인자로 받기
// 고차 함수 정의
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
// 다양한 연산 전달
println(calculate(10, 5, { a, b -> a + b })) // 15
println(calculate(10, 5, { a, b -> a - b })) // 5
println(calculate(10, 5, { a, b -> a * b })) // 50
// 미리 정의된 람다
val divide: (Int, Int) -> Int = { a, b -> a / b }
println(calculate(10, 5, divide)) // 2
}
후행 람다 (Trailing Lambda)
마지막 파라미터가 람다면 괄호 밖으로 빼낼 수 있습니다.
fun repeat(times: Int, action: () -> Unit) {
for (i in 1..times) {
action()
}
}
fun main() {
// 일반 호출
repeat(3, { println("안녕!") })
// 후행 람다 (권장)
repeat(3) {
println("안녕!")
}
// 안녕!
// 안녕!
// 안녕!
}
함수를 반환하기
fun makeMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor }
}
fun main() {
val double = makeMultiplier(2)
val triple = makeMultiplier(3)
println(double(5)) // 10
println(triple(5)) // 15
// 직접 호출
println(makeMultiplier(10)(5)) // 50
}
실전 예제 - 로깅 데코레이터
fun <T> withLogging(
name: String,
block: () -> T
): T {
println("[$name] 시작")
val startTime = System.currentTimeMillis()
val result = block()
val elapsed = System.currentTimeMillis() - startTime
println("[$name] 완료 (${elapsed}ms)")
return result
}
fun main() {
val result = withLogging("데이터 처리") {
// 시간이 걸리는 작업
Thread.sleep(100)
"완료"
}
println("결과: $result")
// [데이터 처리] 시작
// [데이터 처리] 완료 (100ms)
// 결과: 완료
}
실전 예제 - 재시도 로직
fun <T> retry(
times: Int = 3,
delay: Long = 1000,
block: () -> T
): T {
var lastException: Exception? = null
repeat(times) { attempt ->
try {
return block()
} catch (e: Exception) {
lastException = e
println("시도 ${attempt + 1} 실패: ${e.message}")
if (attempt < times - 1) {
Thread.sleep(delay)
}
}
}
throw lastException!!
}
fun main() {
var count = 0
try {
val result = retry(times = 3, delay = 100) {
count++
if (count < 3) {
throw Exception("아직 실패")
}
"성공!"
}
println("최종 결과: $result")
} catch (e: Exception) {
println("모든 시도 실패")
}
}
it 키워드와 underscore
it - 단일 파라미터의 암시적 이름
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// 명시적 파라미터명
val doubled1 = numbers.map { number -> number * 2 }
// it 사용 (파라미터가 하나일 때)
val doubled2 = numbers.map { it * 2 }
println(doubled2) // [2, 4, 6, 8, 10]
// 체이닝에서 it
val result = numbers
.filter { it % 2 == 0 } // 짝수만
.map { it * it } // 제곱
.sum()
println(result) // 20 (4 + 16)
}
it 중첩 시 주의
fun main() {
val nested = listOf(
listOf(1, 2, 3),
listOf(4, 5, 6)
)
// ❌ 혼란스러운 코드
val bad = nested.map {
it.filter { it > 2 } // 두 번째 it이 무엇?
}
// ✅ 명시적 이름 사용
val good = nested.map { list ->
list.filter { number -> number > 2 }
}
println(good) // [[3], [4, 5, 6]]
}
underscore - 사용하지 않는 파라미터
fun main() {
val map = mapOf(
"국어" to 90,
"영어" to 85,
"수학" to 95
)
// 키는 사용하지 않고 값만 필요
val scores = map.map { (_, score) -> score }
println(scores) // [90, 85, 95]
// 여러 파라미터 중 일부만 사용
val pairs = listOf(1 to "a", 2 to "b", 3 to "c")
val letters = pairs.map { (_, letter) -> letter }
println(letters) // [a, b, c]
}
수신 객체 지정 람다
기본 개념
람다 내부에서 객체의 멤버를 직접 접근할 수 있습니다.
fun buildString(builder: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.builder() // StringBuilder의 멤버 함수 호출
return sb.toString()
}
fun main() {
val result = buildString {
// 여기서 this는 StringBuilder
append("안녕")
append("하세요")
appendLine("!")
append("Kotlin")
}
println(result)
// 안녕하세요!
// Kotlin
}
apply - 객체 설정
객체를 설정하고 그 객체를 반환합니다.
data class User(
var id: Long = 0,
var name: String = "",
var email: String = ""
)
fun main() {
val user = User().apply {
id = 1
name = "규철"
email = "user@example.com"
}
println(user)
// User(id=1, name=규철, email=user@example.com)
}
with - 객체로 여러 작업
객체로 여러 작업을 수행하고 마지막 표현식을 반환합니다.
fun main() {
val numbers = mutableListOf(1, 2, 3)
val result = with(numbers) {
add(4)
add(5)
sum() // 마지막 표현식 반환
}
println(numbers) // [1, 2, 3, 4, 5]
println(result) // 15
}
run - apply + with
객체로 작업 수행 후 결과를 반환합니다.
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("규철", 30)
val description = person.run {
"이름: $name, 나이: $age"
}
println(description) // 이름: 규철, 나이: 30
}
also - 체이닝 중 부가 작업
객체에 부가 작업을 수행하고 객체 자체를 반환합니다.
fun main() {
val numbers = mutableListOf(1, 2, 3)
.also { println("초기 리스트: $it") }
.also { it.add(4) }
.also { println("4 추가 후: $it") }
.also { it.add(5) }
println("최종: $numbers")
// 초기 리스트: [1, 2, 3]
// 4 추가 후: [1, 2, 3, 4]
// 최종: [1, 2, 3, 4, 5]
}
let vs apply vs run vs also vs with
data class User(var name: String, var age: Int)
fun main() {
val user = User("규철", 30)
// let - it 사용, 결과 반환
val length = user.let {
println("User: ${it.name}")
it.name.length
}
// apply - this 사용, 객체 반환
user.apply {
name = "김규철"
age = 31
}
// run - this 사용, 결과 반환
val description = user.run {
"$name ($age세)"
}
// also - it 사용, 객체 반환
user.also {
println("로그: ${it.name}")
}
// with - this 사용, 결과 반환
with(user) {
println("$name, $age")
}
}
비교 표
함수 객체 참조 반환 주 용도
| let | it | 결과 | null 체크, 변환 |
| apply | this | 객체 | 객체 설정 |
| run | this | 결과 | 객체로 계산 |
| also | it | 객체 | 부가 작업 (로깅) |
| with | this | 결과 | 객체로 여러 작업 |
inline 함수
inline이란?
함수 호출을 함수 본문으로 대체하여 성능을 향상시킵니다.
// 일반 함수
fun normalFunction(action: () -> Unit) {
action()
}
// inline 함수
inline fun inlineFunction(action: () -> Unit) {
action()
}
fun main() {
// 일반 함수 - 람다가 객체로 생성됨
normalFunction {
println("일반")
}
// inline 함수 - 람다 코드가 직접 삽입됨 (객체 생성 없음)
inlineFunction {
println("인라인")
}
}
inline의 장점 - non-local return
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// inline 함수인 forEach에서 return 가능
numbers.forEach {
if (it == 3) return // main 함수에서 리턴!
println(it)
}
println("이 줄은 실행 안 됨")
}
// 출력:
// 1
// 2
noinline - 일부만 inline 제외
inline fun doSomething(
inlined: () -> Unit,
noinline notInlined: () -> Unit
) {
inlined()
// noinline 람다는 변수에 저장 가능
val stored = notInlined
stored()
}
crossinline - non-local return 금지
inline fun runAsync(crossinline action: () -> Unit) {
Thread {
action() // non-local return 불가
}.start()
}
fun main() {
runAsync {
// return // ❌ 컴파일 에러
println("비동기 실행")
}
}
실전 예제 - 측정 함수
inline fun <T> measureTime(
name: String,
block: () -> T
): T {
val start = System.currentTimeMillis()
val result = block()
val elapsed = System.currentTimeMillis() - start
println("[$name] ${elapsed}ms")
return result
}
fun main() {
val result = measureTime("복잡한 계산") {
(1..1000000).sum()
}
println("결과: $result")
}
실전 DSL 패턴
HTML DSL
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML {
private val children = mutableListOf<String>()
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
children.add(body.toString())
}
override fun toString() = "<html>\n${children.joinToString("\n")}\n</html>"
}
@HtmlDsl
class Body {
private val children = mutableListOf<String>()
fun h1(text: String) {
children.add("<h1>$text</h1>")
}
fun p(text: String) {
children.add("<p>$text</p>")
}
override fun toString() = "<body>\n${children.joinToString("\n")}\n</body>"
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
fun main() {
val page = html {
body {
h1("Kotlin DSL")
p("강력한 DSL을 만들 수 있습니다")
p("타입 안전하게!")
}
}
println(page)
}
테스트 DSL
class TestSuite(val name: String) {
private val tests = mutableListOf<Test>()
fun test(name: String, block: () -> Unit) {
tests.add(Test(name, block))
}
fun run() {
println("=== $name ===")
tests.forEach { it.run() }
}
}
class Test(val name: String, val block: () -> Unit) {
fun run() {
try {
block()
println("✓ $name")
} catch (e: AssertionError) {
println("✗ $name: ${e.message}")
}
}
}
fun describe(name: String, init: TestSuite.() -> Unit) {
val suite = TestSuite(name)
suite.init()
suite.run()
}
fun assertEquals(expected: Any?, actual: Any?) {
if (expected != actual) {
throw AssertionError("Expected: $expected, Actual: $actual")
}
}
fun main() {
describe("계산기 테스트") {
test("덧셈") {
assertEquals(4, 2 + 2)
}
test("뺄셈") {
assertEquals(0, 2 - 2)
}
test("실패 테스트") {
assertEquals(5, 2 + 2) // 실패
}
}
}
설정 DSL
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var ssl: Boolean = false
fun database(init: DatabaseConfig.() -> Unit) {
val db = DatabaseConfig()
db.init()
println("DB 설정: $db")
}
}
class DatabaseConfig {
var url: String = ""
var username: String = ""
var password: String = ""
override fun toString() = "Database(url=$url, user=$username)"
}
fun server(init: ServerConfig.() -> Unit): ServerConfig {
val config = ServerConfig()
config.init()
return config
}
fun main() {
val config = server {
host = "example.com"
port = 443
ssl = true
database {
url = "jdbc:postgresql://localhost/mydb"
username = "admin"
password = "secret"
}
}
println("서버: ${config.host}:${config.port}, SSL: ${config.ssl}")
}
마무리 - 다음 편 예고
오늘 배운 것 ✅
- 람다 표현식 - { 파라미터 -> 본문 }
- 고차 함수 - 함수를 인자로, 함수를 반환
- it 키워드 - 단일 파라미터의 암시적 이름
- 수신 객체 지정 람다 - apply, run, with, let, also
- inline 함수 - 성능 최적화와 non-local return
- DSL 패턴 - 타입 안전한 빌더
다음 편에서 배울 것 📚
9편: 확장 함수와 프로퍼티 완벽 가이드 | 기존 클래스 확장하기
- Extension Functions 심화
- Extension Properties
- infix 함수로 DSL 만들기
- 연산자 오버로딩
- 제네릭 확장 함수
- 실전 유틸리티 라이브러리 만들기
실습 과제 💪
// 1. 고차 함수 만들기
// - 조건을 만족할 때까지 재시도
// - 실행 시간 측정 데코레이터
// 2. 수신 객체 지정 람다 활용
// - 설정 빌더 패턴
// - HTML 생성기
// 3. DSL 만들기
// - JSON 빌더
// - SQL 쿼리 빌더
// 4. inline 함수
// - 성능 측정 함수
// - 트랜잭션 헬퍼
자주 묻는 질문 (FAQ)
Q: 람다와 익명 함수의 차이는?
A: 람다는 간결한 문법, 익명 함수는 명시적 반환 타입 가능. 대부분 람다를 씁니다.
Q: apply와 also 중 뭘 써야 하나요?
A: 객체 설정은 apply (this), 부가 작업은 also (it)를 쓰세요.
Q: inline은 항상 좋나요?
A: 아닙니다! 고차 함수에만 유용하고, 일반 함수는 코드 크기만 늘어납니다.
Q: DSL은 언제 만드나요?
A: 설정, 테스트, 빌더 등 반복적인 패턴이 있을 때 유용합니다.
관련 글
- Kotlin 컬렉션 완벽 가이드 (이전 편)
- Kotlin 확장 함수와 프로퍼티 완벽 가이드 (다음 편)
- Kotlin 함수형 프로그래밍 고급
💬 댓글로 알려주세요!
- 어떤 고차 함수를 자주 사용하시나요?
- DSL을 만들어본 경험이 있나요?
- 이 글이 도움이 되셨나요?
'개발 > java basic' 카테고리의 다른 글
| Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍 (1) | 2025.12.24 |
|---|---|
| Kotlin Null Safety 완벽 가이드 | ?, ?., ?:, !! 마스터하기 (0) | 2025.12.23 |
| Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기 (0) | 2025.12.22 |
| Kotlin 클래스와 객체 완벽 가이드 | OOP의 Kotlin 스타일 (0) | 2025.12.22 |
| Kotlin 제어 구조 완벽 가이드 | if, when, for, while 마스터하기 (0) | 2025.12.21 |
