이번 글의 핵심은 코틀린의 기본 단위인 함수를 이해하는 것입니다. 그런데 코틀린 함수를 제대로 이해하려면 먼저 "문(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)』을 참고하여 작성했습니다.
'Language > Kotlin' 카테고리의 다른 글
| [Kotlin] 자바 개발자를 위한 코틀린 온보딩 (2) - 변수: val/var와 문자열 템플릿으로 이해하기 (1) | 2026.09.06 |
|---|---|
| [Kotlin] 코틀린 함수 호출 다루기: 이름 있는 인자부터 vararg, infix까지 (0) | 2026.09.06 |
| [Kotlin] 코틀린 확장 함수와 확장 프로퍼티 (0) | 2026.09.06 |