본문 바로가기

Language/Kotlin

[Kotlin] 자바 개발자를 위한 코틀린 온보딩 (1) - 함수: 문과 식부터 이해하기

이번 글의 핵심은 코틀린의 기본 단위인 함수를 이해하는 것입니다. 그런데 코틀린 함수를 제대로 이해하려면 먼저 "문(statement)"과 "식(expression)"의 구분부터 짚고 넘어가야 합니다. 자바에서는 당연히 문이었던 것들이 코틀린에서는 식으로 취급되고, 이 차이가 함수를 정의하는 방식(블록 본문 vs 식 본문)까지 그대로 이어지기 때문입니다.

 

문과 식의 구분부터 시작해서, 이 개념이 함수 정의 방식에 어떻게 이어지는지까지 살펴보겠습니다.

 

문과 식의 기본 구분

  • 문(Statement): 실행되지만 값을 만들어내지 않습니다.
  • 식(Expression): 평가되어 값을 만들어냅니다.

자바에서는 if, when(switch), try 같은 제어 구조가 전부 문이었습니다. 값을 만들어내려면 삼항 연산자(? :) 같은 별도의 식을 써야 했죠.

// 자바
int max;
if (a > b) {
    max = a;
} else {
    max = b;
}
// 또는
int max = (a > b) ? a : b;

코틀린은 이 중 상당수를 식으로도 쓸 수 있게 만들었습니다.

 

코틀린에서 식으로 취급되는 것들

if

val max = if (a > b) a else b

삼항 연산자 없이도 if 자체가 값을 반환합니다. 코틀린에는 삼항 연산자가 따로 없는데, if가 식으로 동작하기 때문에 굳이 필요하지 않은 것입니다.

 

분기가 여러 줄이어도 됩니다. 이때는 블록의 마지막 식이 그 분기의 값이 됩니다.

val max = if (a > b) {
    println("a가 더 큽니다")
    a          // 이 블록의 값은 a
} else {
    println("b가 더 크거나 같습니다")
    b          // 이 블록의 값은 b
}

이 "블록의 값 = 마지막 식"이라는 규칙은 뒤에서 다룰 when, try-catch, 그리고 함수 본문에도 동일하게 적용되는 코틀린의 일관된 원칙입니다.

 

when

자바의 switch에 대응하는 when도 식으로 쓸 수 있습니다.

val result = when (x) {
    1 -> "one"
    2 -> "two"
    else -> "other"
}

 

when을 식으로 쓸 때는 완전성(exhaustiveness) 검사가 들어갑니다. 컴파일러가 모든 경우의 수를 커버하는지 확인하고, 그렇지 못하면 컴파일 에러를 냅니다.

val result = when (x) {
    1 -> "one"
    2 -> "two"
}   // 에러! else가 없어서 x가 1, 2가 아닐 때의 값이 정의되지 않음

 

단, enum class나 sealed class처럼 컴파일러가 가능한 하위 타입/값을 전부 알고 있는 경우는 예외입니다. 모든 케이스를 다 나열했다면 else 없이도 완전성을 인정받습니다.

enum class Direction { NORTH, SOUTH, EAST, WEST }

fun describe(d: Direction) = when (d) {
    Direction.NORTH -> "북쪽"
    Direction.SOUTH -> "남쪽"
    Direction.EAST -> "동쪽"
    Direction.WEST -> "서쪽"
    // else 불필요: enum의 모든 값을 다뤘음을 컴파일러가 확인
}

 

같은 when을 문으로만 쓸 때(값을 대입하지 않을 때)는 이 완전성 검사가 강제되지 않습니다. 케이스 몇 개를 빼먹어도 컴파일은 됩니다. 값을 만들어내야 하는 식일 때만 "모든 경우가 다뤄졌는가"가 문제가 되는 것입니다.

참고: 자바도 14부터(JEP 361) switch 표현식을 지원합니다. case L -> 형태와 yield로 값을 반환할 수 있게 되어 코틀린의 when 식과 상당히 비슷한 모양이 됐습니다. 다만 완전성 검사 방식이나 패턴 매칭 지원 범위 등 세부 동작은 다르므로, "겉보기엔 비슷하지만 규칙까지 같지는 않다" 정도로 이해하면 됩니다.

 

try-catch

val num = try { 
	s.toInt() 
} catch (e: NumberFormatException) { 
	0 
}

 

여전히 문인 것들

for, while 루프는 값을 반환하지 않으므로 코틀린에서도 여전히 문입니다.

val x = for (i in 1..10) {}  // 컴파일 에러! for는 값이 없음

 

판단하는 팁

"이걸 val에 대입할 수 있는가?"를 기준으로 생각하면 편합니다.

  • val x = if (...) ... else ... → 가능 → 식으로 쓰인 것
  • val x = for (...) { ... } → 불가능 → 문

 

자바 개발자가 걸려 넘어지는 포인트

if를 식으로 쓰려면 모든 분기가 값을 만들어야 하고, 반드시 else가 있어야 합니다. 그래야 컴파일러가 "이 식은 항상 값을 반환한다"고 보장할 수 있기 때문입니다. 자바에서 if만 쓰고 else를 생략하는 습관이 있다면, 식으로 쓸 때 컴파일 에러를 만나게 됩니다.

val max = if (a > b) a  // 에러! else가 없어서 값이 보장되지 않음

물론 if를 문으로만 쓸 때는(값을 대입하지 않을 때는) 자바처럼 else 없이 써도 무방합니다.

 

 

함수 선언의 기본 골격

이제 이 "문/식" 개념이 코틀린의 기본 단위인 함수에 어떻게 이어지는지 살펴보겠습니다. 먼저 함수를 선언하는 기본 골격부터 짚고 넘어가겠습니다.

 
fun 함수이름(파라미터명: 타입, 파라미터명: 타입): 반환타입 {
    // 본문
}

 

fun 키워드로 시작해서, 함수 이름, 괄호 안 파라미터 목록, 콜론(:) 뒤에 반환 타입이 옵니다. 자바와 비교하면 반환 타입의 위치가 다릅니다.

 
// 자바: 반환 타입이 함수 이름보다 앞에 옵니다
int max(int a, int b) { ... }
 
// 코틀린: 반환 타입이 파라미터 목록보다 뒤, 콜론 뒤에 옵니다
fun max(a: Int, b: Int): Int { ... }

 

자바는 "반환 타입 → 함수 이름 → 파라미터" 순서지만, 코틀린은 "fun → 함수 이름 → 파라미터 → 반환 타입" 순서입니다. 이 어순 자체가 코틀린 코드를 처음 읽을 때 낯설게 느껴지는 지점 중 하나입니다.

 

이 골격 안에서 본문을 채우는 방식(블록으로 쓸지, 식 하나로 쓸지)과 파라미터·반환 타입을 다루는 세부 규칙을 아래에서 하나씩 다루겠습니다.

 

 

함수도 같은 맥락: 블록 본문 vs 식 본문

"제어 구조가 식이 될 수 있다"는 개념은 함수 정의에도 그대로 이어집니다. 함수 본문이 결국 하나의 식으로 축약 가능하다면, { return ... }을 쓰지 않고도 = 식 형태로 짧게 쓸 수 있습니다.

 

블록 본문 함수 (Block body)

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}
  • 중괄호로 감싸고, 명시적으로 return을 씁니다.
  • 반환 타입을 생략할 수 없습니다. 컴파일러가 추론하지 않습니다.

 

식 본문 함수 (Expression body)

fun max(a: Int, b: Int): Int = if (a > b) a else b

 

= 뒤에 바로 식이 오고, return은 쓰지 않습니다. 반환 타입도 생략할 수 있습니다.

fun max(a: Int, b: Int) = if (a > b) a else b  // Int로 자동 추론

 

주의할 점 1: 식 본문에서 return을 쓰면 안 됩니다

fun max(a: Int, b: Int) = return if (a > b) a else b  // 에러!

 

주의할 점 2: 타입 추론이 항상 되는 건 아닙니다

재귀 함수처럼 컴파일러가 추론하지 못하는 경우에는 반환 타입을 명시해야 합니다.

fun factorial(n: Int): Int = if (n <= 1) 1 else n * factorial(n - 1)
// 재귀 함수는 반환 타입 명시가 필수입니다.

 

주의할 점 3: 블록 본문에서 if를 식으로 안 쓰면 값이 사라집니다

fun max(a: Int, b: Int): Int {
    if (a > b) a else b  // 이 줄은 그냥 무시됩니다! return이 없으니 값이 버려짐
}

블록 본문에서는 반드시 return으로 명시해야 함수의 반환값이 됩니다. if가 식이라는 것과, 그 값이 함수의 리턴값이 되는 것은 별개의 문제입니다.

 

함수 파라미터와 반환 타입: 적는 법과 생략 규칙

파라미터 타입은 생략 불가능합니다

자바와 마찬가지로, 코틀린도 함수 파라미터에는 타입을 반드시 명시해야 합니다.

fun greet(name) { ... }         // 에러! 타입 필수
fun greet(name: String) { ... } // OK

참고로 자바에서 타입 추론이 가능한 유일한 경우는 람다 파라미터입니다(함수형 인터페이스로부터 추론). 하지만 이는 일반 메서드 파라미터와는 다른 이야기이고, 코틀린의 함수 파라미터 역시 이 규칙에서 예외가 아닙니다.

 

반환 타입: 블록 본문은 생략 불가, 식 본문은 추론 가능

블록 본문 함수는 반환 타입을 생략할 수 없습니다. 단, 반환값이 없으면(Unit) 생략 가능합니다.

fun printSum(a: Int, b: Int) {  // Unit 생략, 자동으로 Unit
    println(a + b)
}
// 명시하면: fun printSum(a: Int, b: Int): Unit { ... }

식 본문 함수는 추론 가능하면 생략할 수 있지만, 재귀 함수처럼 추론이 안 되는 경우는 명시해야 합니다.

 

정리표

요소 생략 가능? 조건
파라미터 타입 불가능 항상 명시
반환 타입 (블록 본문) Unit일 때만 가능 값 반환 시 필수
반환 타입 (식 본문) 대부분 가능 재귀 등 추론 불가 시 필수

 

마무리

문과 식의 구분에서 출발해서, 코틀린의 기본 단위인 함수를 블록 본문과 식 본문으로 나누어 정의하는 방식까지 살펴봤습니다. 다음 편에서는 변수 선언(val/var)과 문자열 템플릿을 다뤄보겠습니다.

 


이 시리즈는 『코틀린 인 액션(Kotlin in Action)』을 참고하여 작성했습니다.