4장. 함수
프로그램은 결국 “같은 일을 반복하는 것“의 모음입니다.
값을 더하고, 인사말을 만들고,
입력이 올바른지 검사합니다.
이렇게 “어떤 일을 하는 코드 묶음“에
이름을 붙여 둔 것이 바로 함수입니다.
한 번 만들어 두면
필요할 때마다 이름만 불러서 다시 쓸 수 있습니다.
이 장에서는 코틀린에서 함수를 만드는 방법과,
함수를 더 편하게 쓰는 여러 장치를 배웁니다.
특히 기본 인자, 이름 붙은 인자처럼
자바에는 없던 편리한 기능들을 만나게 됩니다.
4.1 함수 선언하기
우리는 이미 함수를 하나 봤습니다.
바로 1장에서 만든 main 함수입니다.
fun main() {
println("Hello, World!")
}
이제 이 fun이 무엇인지 제대로 살펴봅시다.
함수(function)란
“어떤 일을 하는 코드 묶음에 이름을 붙인 것“입니다.
비유하자면 이렇습니다.
함수는 “자판기“와 비슷합니다.
값을 넣으면(입력) 결과가 나옵니다(출력).
매개변수와 반환 타입
함수를 만들 때는 fun이라는 키워드로 시작합니다.
간단한 예를 하나 보겠습니다.
fun add(a: Int, b: Int): Int {
return a + b
}
이 함수는 두 숫자를 받아 더한 값을 돌려줍니다.
한 조각씩 뜯어보겠습니다.
fun: 함수를 만들겠다는 표시add: 함수의 이름(a: Int, b: Int): 함수가 받는 값들: Int: 함수가 돌려주는 값의 타입return a + b: 실제로 돌려주는 값
여기서 함수가 받는 값을
매개변수(parameter)라고 부릅니다.
매개변수는 “함수 안에서만 쓰이는 변수“입니다.a와 b가 바로 그것입니다.
매개변수를 적을 때는 순서를 기억해 둡시다.
이름: 타입
2장에서 변수를 val 이름: 타입 순서로 적었죠.
매개변수도 똑같은 순서입니다.
함수가 돌려주는 값의 타입은
반환 타입(return type)이라고 합니다.
위 코드에서 add의 반환 타입은 Int입니다.
소괄호 뒤에 콜론(:)을 쓰고 적습니다.
실제로 값을 돌려줄 때는return 뒤에 그 값을 적습니다.
이제 만든 함수를 불러 써 봅시다.
함수를 불러 쓰는 것을 호출(call)이라고 합니다.
fun add(a: Int, b: Int): Int {
return a + b
}
fun main() {
val result = add(3, 5)
println(result)
}
8
호출할 때 넘기는 실제 값(3, 5)은
인자(argument)라고 부릅니다.
용어를 정리하면 이렇습니다.
- 매개변수 : 함수를 “만들 때” 정하는 받을 자리
- 인자 : 함수를 “부를 때” 실제로 넣는 값
자판기로 비유하면,
매개변수는 “동전 투입구“이고
인자는 “실제로 넣는 동전“입니다.
두 단어가 자주 섞여 쓰이지만,
뜻을 구분해 두면 나중에 도움이 됩니다.
참고로 자바의 같은 함수는 이렇게 생겼습니다.
// 자바
public int add(int a, int b) {
return a + b;
}
타입을 이름 앞에 쓰고,
클래스 안에 넣어야 한다는 점이 다릅니다.
Unit: 돌려줄 값이 없을 때
모든 함수가 값을 돌려주는 것은 아닙니다.
그냥 일만 하고 끝나는 함수도 많습니다.
예를 들어 화면에 출력만 하는 함수가 그렇습니다.
fun printHello(): Unit {
println("Hello")
}
여기서 Unit은
“돌려줄 값이 없다“를 뜻하는 타입입니다.
(2장에서 잠깐 만났던 그 타입입니다.)
그런데 이 Unit은 거의 생략합니다.
반환 타입을 안 적으면 자동으로 Unit이 됩니다.
fun printHello() { // 반환 타입 생략 = Unit
println("Hello")
}
자바를 아는 분이라면void가 있던 자리라고 생각하면 쉽습니다.
// 자바
public void printHello() {
System.out.println("Hello");
}
- 자바의
void= 코틀린의Unit - 다만 코틀린은 아예 생략해도 됨
단일 표현식 함수
앞에서 만든 add 함수를 다시 봅시다.
fun add(a: Int, b: Int): Int {
return a + b
}
이 함수는 하는 일이a + b라는 한 줄뿐입니다.
이렇게 몸통이 한 줄로 끝나는 함수는
더 짧게 쓸 수 있습니다.
중괄호와 return을 지우고,
등호(=)로 이어 붙이면 됩니다.
fun add(a: Int, b: Int): Int = a + b
이런 형태를
단일 표현식 함수(single-expression function)라고 합니다.
“함수의 결과가 표현식 하나로 정해진다“는 뜻입니다.= 오른쪽 값이 그대로 반환값이 됩니다.
여기서 한 걸음 더 나갈 수 있습니다.
반환 타입도 생략할 수 있습니다.
fun add(a: Int, b: Int) = a + b
a + b가 Int라는 것을
코틀린이 알아서 추론하기 때문입니다.
(2장에서 배운 타입 추론이 여기서도 쓰입니다.)
정리하면 같은 함수를
세 가지 방법으로 쓸 수 있습니다.
// 1. 기본 형태
fun add(a: Int, b: Int): Int {
return a + b
}
// 2. 단일 표현식
fun add(a: Int, b: Int): Int = a + b
// 3. 반환 타입까지 생략
fun add(a: Int, b: Int) = a + b
셋 다 완전히 똑같이 동작합니다.
짧은 함수는 단일 표현식으로,
긴 함수는 중괄호로.
이렇게 쓰면 코드가 한결 깔끔해집니다.
한 가지만 조심합시다.
공개용(라이브러리 등) 함수라면
반환 타입을 적어 두는 편이 읽기에 좋습니다.
타입이 눈에 보이면
“이 함수가 무엇을 돌려주는지”
한눈에 알 수 있기 때문입니다.
4.2 기본 인자와 이름 붙은 인자
함수를 쓰다 보면
“대부분 같은 값을 넘기는” 경우가 자주 생깁니다.
예를 들어 인사말을 만드는 함수를 생각해 봅시다.
fun greet(name: String, greeting: String): String {
return "$greeting, $name!"
}
인사말은 보통 “안녕하세요“입니다.
그런데 부를 때마다 매번 적어야 합니다.
greet("홍길동", "안녕하세요")
greet("이순신", "안녕하세요")
greet("강감찬", "안녕하세요")
같은 값을 계속 반복하니 번거롭습니다.
이 문제를 코틀린은 깔끔하게 풀어 줍니다.
기본 인자 (Default Parameter)
매개변수에 미리 값을 정해 둘 수 있습니다.
이것을 기본 인자(default parameter)라고 합니다.
매개변수 뒤에 =로 기본값을 적으면 됩니다.
fun greet(name: String, greeting: String = "안녕하세요"): String {
return "$greeting, $name!"
}
이제 인사말을 생략하면
자동으로 “안녕하세요“가 쓰입니다.
fun main() {
println(greet("홍길동"))
println(greet("이순신", "반갑습니다"))
}
안녕하세요, 홍길동!
반갑습니다, 이순신!
- 값을 안 넘기면 : 기본값 사용
- 값을 넘기면 : 넘긴 값 사용
필요한 것만 넘기면 되니
호출이 훨씬 간단해집니다.
이름 붙은 인자 (Named Argument)
매개변수가 많아지면
“어떤 값이 어디로 가는지” 헷갈립니다.
예를 들어 이런 함수가 있다고 합시다.
fun createUser(name: String, age: Int, isAdmin: Boolean): String {
return "$name / $age / $isAdmin"
}
이 함수를 이렇게 부르면
읽는 사람이 한 번에 이해하기 어렵습니다.
createUser("홍길동", 20, false)
false가 무엇을 뜻하는지
함수 정의를 열어 봐야 알 수 있습니다.
이럴 때 인자 앞에 이름을 붙일 수 있습니다.
이것을 이름 붙은 인자(named argument)라고 합니다.
createUser(
name = "홍길동",
age = 20,
isAdmin = false
)
이제 각 값이 무엇인지
코드만 봐도 바로 알 수 있습니다.
이름을 붙이면 순서를 바꿔도 됩니다.
createUser(
isAdmin = false,
name = "홍길동",
age = 20
)
이름으로 자리를 찾아가기 때문입니다.
기본 인자와 함께 쓰면 더욱 강력해집니다.
중간 값만 골라서 넘길 수 있습니다.
fun order(
item: String,
count: Int = 1,
giftWrap: Boolean = false
) {
println("$item / $count 개 / 포장: $giftWrap")
}
fun main() {
// count는 기본값, giftWrap만 지정
order("커피", giftWrap = true)
}
커피 / 1 개 / 포장: true
만약 이름을 안 붙였다면giftWrap만 골라 넘길 방법이 없었습니다.count부터 순서대로 다 채워야 했겠죠.
이름 붙은 인자는
“필요한 값만 콕 집어” 넘기게 해 줍니다.
Java의 메서드 오버로딩과 비교
자바에는 기본 인자가 없습니다.
그래서 “값을 생략할 수 있게” 만들려면
같은 이름의 함수를 여러 개 만들어야 했습니다.
// 자바
public String greet(String name) {
return greet(name, "안녕하세요");
}
public String greet(String name, String greeting) {
return greeting + ", " + name + "!";
}
이렇게 같은 이름으로
여러 버전을 만드는 것을
메서드 오버로딩(method overloading)이라고 합니다.
매개변수가 늘어날수록
만들어야 할 버전도 늘어납니다.
경우의 수가 많으면 아주 번거롭습니다.
코틀린은 이 모든 것을
기본 인자 한 줄로 대신합니다.
// 코틀린: 함수 하나면 충분
fun greet(name: String, greeting: String = "안녕하세요"): String {
return "$greeting, $name!"
}
두 방식을 나란히 비교하면 이렇습니다.
| 상황 | 자바 | 코틀린 |
|---|---|---|
| 값 생략을 허용하고 싶다 | 함수 여러 개(오버로딩) | 기본 인자 한 줄 |
| 인자 순서를 바꾸고 싶다 | 불가능 | 이름 붙은 인자 |
| 코드 양 | 많아짐 | 적음 |
즉, 자바에서 오버로딩으로 풀던 일을
코틀린은 기본 인자와 이름 붙은 인자로
더 간단히 해결합니다.
4.3 가변 인자
지금까지는 인자의 개수가 정해져 있었습니다.add(a, b)는 항상 두 개를 받았습니다.
그런데 개수가 정해지지 않은 경우도 있습니다.
예를 들어 여러 숫자를 모두 더하는 함수를
만든다고 생각해 봅시다.
두 개를 더할 때도, 다섯 개를 더할 때도
같은 함수를 쓰고 싶습니다.
vararg
이럴 때 쓰는 것이
가변 인자(variable arguments)입니다.
“개수가 변할 수 있는 인자“라는 뜻입니다.
매개변수 앞에 vararg를 붙입니다.
fun sum(vararg numbers: Int): Int {
var total = 0
for (n in numbers) {
total += n
}
return total
}
이제 숫자를 몇 개 넣든 상관없습니다.
fun main() {
println(sum(1, 2))
println(sum(1, 2, 3, 4, 5))
println(sum())
}
3
15
0
함수 안에서 numbers는
여러 값이 담긴 목록처럼 다뤄집니다.
그래서 for로 하나씩 꺼내 쓸 수 있습니다.
(반복문은 3장에서 다뤘습니다.)
사실 우리는 이미 vararg를 써 왔습니다.println도 여러 값을 받을 수 있고,listOf(1, 2, 3) 같은 것도 가변 인자를 씁니다.
자바에도 비슷한 기능이 있습니다.
바로 ... 표기입니다.
// 자바
public int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
역할은 같고,
표기 방식만 ...에서 vararg로 바뀐 셈입니다.
스프레드 연산자 (Spread Operator)
이미 목록(배열)에 값이 담겨 있을 때가 있습니다.
이 목록을 가변 인자에 통째로 넘기고 싶다면 어떻게 할까요?
그냥 넘기면 오류가 납니다.
val numbers = intArrayOf(1, 2, 3)
sum(numbers) // 오류: 배열 하나를 통째로 넘길 수 없음
sum은 Int 여러 개를 받는데,numbers는 “배열 하나“이기 때문입니다.
이때 배열 앞에 별표(*)를 붙입니다.
이 별표를 스프레드 연산자(spread operator)라고 합니다.
val numbers = intArrayOf(1, 2, 3)
sum(*numbers) // OK: 배열을 낱개로 펼쳐서 넘김
“펼친다(spread)“는 이름 그대로,
배열을 낱개의 값들로 풀어서 넘겨 줍니다.
비유하자면 이렇습니다.
묶음으로 포장된 물건을,
하나씩 낱개로 풀어서 건네는 것입니다.
낱개 값과 배열을 섞어서 넘길 수도 있습니다.
val middle = intArrayOf(2, 3, 4)
println(sum(1, *middle, 5))
15
1, 펼친 2, 3, 4, 그리고 5가
모두 이어져서 전달됩니다.
지금은 배열이 낯설어도 괜찮습니다.
목록을 다루는 방법은 5부에서 자세히 배웁니다.
여기서는 이렇게만 기억해 둡시다.
vararg: 개수가 정해지지 않은 인자를 받는다*: 이미 있는 배열을 낱개로 펼쳐서 넘긴다
4.4 함수 오버로딩
앞에서 자바의 오버로딩을 잠깐 봤습니다.
코틀린에도 오버로딩은 있습니다.
기본 인자로 많은 경우를 해결할 수 있지만,
오버로딩이 더 자연스러운 상황도 있기 때문입니다.
같은 이름의 여러 함수
같은 이름이지만
받는 값이 다른 함수를 여러 개 만들 수 있습니다.
이것을 함수 오버로딩(function overloading)이라고 합니다.
fun describe(value: Int): String {
return "정수: $value"
}
fun describe(value: String): String {
return "문자열: $value"
}
fun describe(value: Boolean): String {
return "참/거짓: $value"
}
이름은 모두 describe로 같지만,
받는 타입이 각각 다릅니다.
부를 때는 넘기는 값에 맞춰
코틀린이 알맞은 함수를 골라 줍니다.
fun main() {
println(describe(100))
println(describe("커피"))
println(describe(true))
}
정수: 100
문자열: 커피
참/거짓: true
이름이 같아도 헷갈리지 않는 이유는,
매개변수의 타입이나 개수가 다르기 때문입니다.
코틀린은 이 차이를 보고
“어떤 함수를 부른 것인지” 판단합니다.
오버로딩의 핵심은
“이름은 같되, 받는 값이 달라야 한다“입니다.
주의할 점이 하나 있습니다.
반환 타입만 다른 것으로는 오버로딩이 안 됩니다.
fun read(): Int { ... }
fun read(): String { ... } // 오류: 구분할 수 없음
호출하는 쪽에서는
넘기는 값만 보이고 반환 타입은 안 보입니다.
그래서 코틀린이 둘을 구분할 수 없습니다.
기본 인자와 오버로딩의 선택
여기서 자연스러운 질문이 떠오릅니다.
기본 인자와 오버로딩,
언제 무엇을 써야 할까요?
간단한 기준은 이렇습니다.
- 하는 일이 “같다” : 기본 인자
- 하는 일이 “다르다” : 오버로딩
앞의 greet 함수를 떠올려 봅시다.
인사말이 있든 없든 하는 일은 똑같습니다.
“인사말을 만든다“는 것이죠.
이런 경우는 기본 인자가 잘 맞습니다.
fun greet(name: String, greeting: String = "안녕하세요") =
"$greeting, $name!"
반면 describe는
받는 타입에 따라 하는 일이 다릅니다.
정수를 다룰 때와 문자열을 다룰 때가 다르죠.
이런 경우는 오버로딩이 어울립니다.
한 가지 더 조심할 점이 있습니다.
둘을 섞으면 헷갈리는 상황이 생길 수 있습니다.
fun log(message: String) {
println("로그: $message")
}
fun log(message: String, level: String = "INFO") {
println("[$level] $message")
}
fun main() {
log("서버 시작") // 어느 함수가 불릴까?
}
log("서버 시작")은
두 함수 모두에 들어맞습니다.
이럴 때 코틀린은
기본 인자 없이 더 딱 맞는 쪽을 고릅니다.
즉 첫 번째 함수가 불립니다.
하지만 읽는 사람은 헷갈립니다.
그래서 이런 애매한 조합은 피하는 편이 좋습니다.
기본 인자와 오버로딩을
억지로 섞지 않는 것이 안전합니다.
4.5 로컬 함수
함수는 보통 파일이나 클래스 안에 만듭니다.
그런데 함수 “안에” 함수를 만들 수도 있습니다.
이것을 로컬 함수(local function)라고 합니다.
“그 함수 안에서만 쓰는, 국소적인 함수“라는 뜻입니다.
함수 안의 함수
먼저 간단한 예를 보겠습니다.
fun outer() {
fun inner() {
println("안쪽 함수입니다")
}
inner()
}
inner는 outer 안에 들어 있습니다.
그래서 outer 밖에서는 inner를 부를 수 없습니다.
inner() // 오류: outer 밖에서는 보이지 않음
로컬 함수의 큰 장점이 하나 있습니다.
바깥 함수의 변수를 그대로 쓸 수 있다는 점입니다.
fun greetLoudly(name: String) {
fun shout() {
// 바깥의 name을 그대로 사용
println("$name!!!")
}
shout()
shout()
}
shout은 매개변수로 name을 받지 않았지만,
바깥 greetLoudly의 name을 바로 씁니다.
값을 다시 넘겨줄 필요가 없어서 편리합니다.
반복되는 검증 코드 정리하기
로컬 함수가 진짜 빛나는 순간은
“같은 검사를 여러 번 할 때“입니다.
사용자 정보를 저장하는 함수를 예로 들어 봅시다.
이름과 비밀번호가 비어 있으면 안 된다고 합시다.
먼저 로컬 함수 없이 써 보겠습니다.
fun saveUser(name: String, password: String) {
if (name.isEmpty()) {
throw IllegalArgumentException("이름이 비어 있습니다")
}
if (password.isEmpty()) {
throw IllegalArgumentException("비밀번호가 비어 있습니다")
}
println("$name 님의 정보를 저장했습니다")
}
isEmpty()는
“값이 비어 있는가“를 알려 주는 함수입니다.
문자열에 아무 글자도 없으면 true를 돌려줍니다.
throw는 오류를 발생시키는 명령입니다.
잘못된 값이 들어오면 프로그램을 멈춥니다.
(예외 처리는 8부에서 자세히 다룹니다.)
코드를 보면 검사 부분이 비슷하게 반복됩니다.
“비어 있으면 오류를 던진다“가 두 번 나옵니다.
이 반복을 로컬 함수로 묶어 봅시다.
fun saveUser(name: String, password: String) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw IllegalArgumentException("${fieldName}이(가) 비어 있습니다")
}
}
validate(name, "이름")
validate(password, "비밀번호")
println("$name 님의 정보를 저장했습니다")
}
검사 로직이 validate 한 곳에 모였습니다.
검사할 항목이 늘어나도 한 줄만 추가하면 됩니다.
validate(email, "이메일") // 항목이 늘어도 이렇게 한 줄
여기서 “이 검사 함수를 밖으로 빼면 안 되나?”
하고 생각할 수 있습니다.
물론 밖으로 뺄 수도 있습니다.
하지만 이 검사가 saveUser에서만 쓰인다면,
안에 두는 편이 더 깔끔합니다.
로컬 함수를 쓰면 좋은 점을 정리하면 이렇습니다.
- 반복되는 코드를 한곳에 모을 수 있다
- 그 함수 안에서만 쓰이니 밖을 어지럽히지 않는다
- 바깥 변수를 그대로 쓸 수 있어 편하다
함수 안에서만 반복되는 코드가 있다면,
로컬 함수를 떠올려 보세요.
다만 로컬 함수가 너무 길어지거나
여러 곳에서 쓰인다면,
그때는 밖으로 꺼내 독립된 함수로 만드는 것이 낫습니다.
4장을 마치며
이 장에서 우리는 다음을 배웠습니다.
fun으로 함수를 만들고,
매개변수와 반환 타입을 정하는 방법- 값을 돌려주지 않는 함수의 반환 타입은
Unit이며 대부분 생략한다는 점 - 몸통이 한 줄이면
단일 표현식 함수로 짧게 쓸 수 있다는 점 - 기본 인자와 이름 붙은 인자로
자바의 오버로딩을 간단히 대신하는 방법 vararg로 개수가 정해지지 않은 인자를 받고,*로 배열을 펼쳐 넘기는 방법- 함수 오버로딩과,
기본 인자와의 선택 기준 - 함수 안에 함수를 만들어
반복되는 코드를 정리하는 로컬 함수
특히 두 가지는 앞으로 계속 나옵니다.
“기본 인자”, 그리고 “단일 표현식 함수“입니다.
다음 장에서는
값을 묶어 다루는 컬렉션을 만나 보겠습니다.