코틀린의 함수는 자바보다 훨씬 유연하다. 기본값 파라미터, 명명 인자, 단일 표현식 함수 같은 기능들은 코드를 간결하게 만들어주고, 메서드 오버로딩의 필요성을 크게 줄여준다. 하나씩 짚어보겠습니다.

함수 선언의 기본

KOTLIN
// 기본 형태
fun add(a: Int, b: Int): Int {
    return a + b
}

// 반환 타입이 Unit이면 생략 가능
fun greet(name: String) {
    println("안녕, $name")
}

코틀린의 함수는 fun 키워드로 선언한다. 파라미터에는 타입 명시가 필수이고, 반환 타입은 추론 가능한 경우(단일 표현식) 생략할 수 있다.

단일 표현식 함수 — 한 줄이면 중괄호를 없앤다

함수 본문이 하나의 표현식이면 중괄호와 return을 생략할 수 있다.

KOTLIN
// 일반 형태
fun double(x: Int): Int {
    return x * 2
}

// 단일 표현식 함수 — 반환 타입도 추론 가능
fun double(x: Int) = x * 2

// if 표현식도 가능
fun max(a: Int, b: Int) = if (a > b) a else b

// when 표현식도 가능
fun describe(obj: Any) = when (obj) {
    is Int -> "정수"
    is String -> "문자열"
    else -> "기타"
}

가독성이 좋아서 코틀린 코드에서 자주 사용되는 패턴이다. 다만, 본문이 복잡해지면 오히려 가독성이 떨어지므로 적절히 판단해야 한다.

기본값 파라미터 — 오버로딩을 줄이는 방법

자바에서는 다양한 호출 방식을 지원하려면 여러 개의 오버로딩 메서드가 필요했다.

JAVA
// 자바 — 오버로딩 세 개
public String greet(String name) { return greet(name, "안녕"); }
public String greet(String name, String greeting) { return greet(name, greeting, "!"); }
public String greet(String name, String greeting, String punctuation) {
    return greeting + ", " + name + punctuation;
}

코틀린은 기본값 하나로 해결한다.

KOTLIN
// 코틀린 — 하나의 함수
fun greet(
    name: String,
    greeting: String = "안녕",
    punctuation: String = "!"
): String = "$greeting, $name$punctuation"

// 다양한 호출 방식
greet("코틀린")                        // "안녕, 코틀린!"
greet("코틀린", "반갑다")               // "반갑다, 코틀린!"
greet("코틀린", "반갑다", "~")          // "반갑다, 코틀린~"

@JvmOverloads — 자바에서도 쓰려면

자바에서 코틀린의 기본값 파라미터를 사용하려면 @JvmOverloads를 붙여야 한다. 이 어노테이션이 오버로딩 메서드를 자동으로 생성해준다.

KOTLIN
@JvmOverloads
fun greet(
    name: String,
    greeting: String = "안녕",
    punctuation: String = "!"
): String = "$greeting, $name$punctuation"

명명 인자(Named Arguments) — 순서를 바꿔서 호출

파라미터 이름을 지정하면 순서에 관계없이 호출할 수 있다.

KOTLIN
fun createUser(
    name: String,
    age: Int,
    email: String = "",
    isAdmin: Boolean = false
): String = "$name($age)"

// 명명 인자 사용
createUser(name = "홍길동", age = 25)
createUser(age = 30, name = "김철수")                 // 순서 변경 가능
createUser(name = "박지은", age = 28, isAdmin = true) // 중간 파라미터 건너뛰기

명명 인자의 핵심 장점은 두 가지다.

  1. 가독성 — 호출 코드만 봐도 각 인자의 의미를 알 수 있다
  2. ** 유연성** — 기본값이 있는 중간 파라미터를 건너뛸 수 있다
KOTLIN
// 이름 없이 호출하면 무슨 의미인지 알기 어려움
sendEmail("hello@test.com", "제목", true, false, 3)

// 명명 인자로 호출하면 의미가 명확
sendEmail(
    to = "hello@test.com",
    subject = "제목",
    isHtml = true,
    hasAttachment = false,
    retryCount = 3
)

vararg — 가변 인자

KOTLIN
fun printAll(vararg messages: String) {
    for (msg in messages) {
        println(msg)
    }
}

printAll("안녕", "하세요", "코틀린")

배열을 vararg에 전달 — spread 연산자(*)

KOTLIN
val words = arrayOf("Hello", "World")
printAll(*words)                          // spread 연산자로 풀어서 전달
printAll("Hi", *words, "Kotlin")          // 다른 인자와 섞어서 사용 가능

vararg의 위치 제한

vararg는 보통 마지막 파라미터로 두지만, 명명 인자를 쓰면 중간에도 가능하다.

KOTLIN
fun format(vararg parts: String, separator: String = ", "): String {
    return parts.joinToString(separator)
}

format("사과", "바나나", "체리")                      // "사과, 바나나, 체리"
format("사과", "바나나", separator = " | ")            // "사과 | 바나나"

지역 함수 — 함수 안의 함수

코틀린에서는 함수 내부에 함수를 정의할 수 있다. 특정 함수 안에서만 쓰이는 로직을 캡슐화할 때 유용하다.

KOTLIN
fun processUser(user: User) {
    // 지역 함수 — 바깥 함수의 파라미터에 접근 가능
    fun validate() {
        require(user.name.isNotBlank()) { "이름이 비어있습니다" }
        require(user.age > 0) { "나이는 양수여야 합니다" }
    }

    validate()  // 검증 로직 호출
    // ... 나머지 처리
}

지역 함수는 바깥 함수의 변수와 파라미터에 접근할 수 있다(클로저). 다만 너무 중첩하면 가독성이 떨어지므로, 한 단계 정도만 사용하는 게 좋다.

infix 함수 — 연산자처럼 호출하기

tountil처럼 중위 표기법으로 호출할 수 있는 함수다.

KOTLIN
// to도 사실 infix 함수다
val pair = "이름" to "코틀린"   // Pair("이름", "코틀린")

// 직접 정의하기
infix fun Int.times(str: String) = str.repeat(this)

println(3 times "코틀린! ")  // "코틀린! 코틀린! 코틀린! "

infix 함수의 조건

  1. 멤버 함수이거나 확장 함수여야 한다
  2. 파라미터가 정확히 하나 여야 한다
  3. 기본값 파라미터나 vararg를 가질 수 없다
KOTLIN
// 가독성 있는 DSL 만들기
infix fun <T> T.shouldBe(expected: T) {
    if (this != expected) {
        throw AssertionError("기대값: $expected, 실제값: $this")
    }
}

// 테스트 코드에서 활용
val result = calculator.add(2, 3)
result shouldBe 5   // 읽기 쉬운 테스트

꼬리 재귀(tailrec) — StackOverflow 방지

재귀 함수는 깊이가 깊어지면 StackOverflow가 발생한다. tailrec을 붙이면 컴파일러가 반복문으로 최적화해준다.

KOTLIN
// 일반 재귀 — 깊은 호출 시 StackOverflow 위험
fun factorial(n: Long): Long {
    if (n <= 1) return 1
    return n * factorial(n - 1)   // 재귀 호출 후 곱셈이 남아 있음
}

// 꼬리 재귀 — 컴파일러가 반복문으로 변환
tailrec fun factorialTailrec(n: Long, acc: Long = 1): Long {
    if (n <= 1) return acc
    return factorialTailrec(n - 1, n * acc)  // 재귀 호출이 마지막 연산
}

tailrec의 조건

꼬리 재귀가 되려면 재귀 호출이 함수의 마지막 연산 이어야 한다.

KOTLIN
// tailrec 가능 — 재귀 호출이 마지막
tailrec fun findFixedPoint(x: Double = 1.0): Double =
    if (Math.abs(x - Math.cos(x)) < 1e-10) x
    else findFixedPoint(Math.cos(x))

// tailrec 불가 — 재귀 호출 후 + 1 연산이 남아 있음
// tailrec fun bad(n: Int): Int = if (n == 0) 0 else bad(n - 1) + 1

컴파일러가 조건을 만족하지 않으면 경고를 띄워주므로, tailrec을 붙이고 경고가 없는지 확인하면 된다.

실무에서의 tailrec 활용 예시

KOTLIN
// 리스트에서 특정 조건을 만족하는 첫 번째 요소 찾기
tailrec fun <T> findFirst(
    list: List<T>,
    index: Int = 0,
    predicate: (T) -> Boolean
): T? {
    if (index >= list.size) return null
    if (predicate(list[index])) return list[index]
    return findFirst(list, index + 1, predicate)
}

// GCD (최대공약수) 계산
tailrec fun gcd(a: Int, b: Int): Int =
    if (b == 0) a else gcd(b, a % b)

함수 타입과 고차 함수 맛보기

코틀린에서 함수는 일급 시민이다. 변수에 담고, 파라미터로 넘기고, 반환값으로 쓸 수 있다.

KOTLIN
// 함수 타입 — (파라미터 타입) -> 반환 타입
val double: (Int) -> Int = { it * 2 }
val isPositive: (Int) -> Boolean = { it > 0 }

// 함수를 파라미터로 받기
fun applyOperation(x: Int, operation: (Int) -> Int): Int {
    return operation(x)
}

println(applyOperation(5, double))     // 10
println(applyOperation(5) { it * 3 })  // 15 — 트레일링 람다

고차 함수와 람다에 대한 자세한 내용은 별도의 글에서 다루겠다.

정리

  • 단일 표현식 함수= expr 형태로 간결하게 작성. 반환 타입 추론 가능
  • ** 기본값 파라미터** — 오버로딩을 줄여준다. 자바 호환은 @JvmOverloads
  • ** 명명 인자** — 가독성과 유연성. 중간 파라미터를 건너뛸 수 있다
  • vararg — 가변 인자. 배열 전달 시 spread 연산자(*) 사용
  • 지역 함수 — 함수 내부 정의. 바깥 변수에 접근 가능(클로저)
  • infix — 중위 표기법. 파라미터 1개, 기본값/vararg 불가
  • tailrec — 꼬리 재귀를 반복문으로 최적화. 재귀 호출이 마지막 연산이어야 함
댓글 로딩 중...