Theme:

코틀린의 함수는 자바보다 훨씬 유연하다. 기본값 파라미터, 명명 인자, 단일 표현식 함수 같은 기능들은 코드를 간결하게 만들어주고, 메서드 오버로딩의 필요성을 크게 줄여준다. 면접에서는 이런 기능들을 "왜 이렇게 설계했는지"까지 물어보는 경우가 많으니, 하나씩 짚어보자.

함수 선언의 기본

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 — 꼬리 재귀를 반복문으로 최적화. 재귀 호출이 마지막 연산이어야 함
댓글 로딩 중...