Kotlin - 3.제어문, 반복문, 예외, 함수 다루기

Kotlin 문법 배우기

코틀린의 제어문, 반복문, 예외, 함수 다루기

1. 제어문

if 문

다음과 같은 자바 코드를 살펴보자. 아래 함수는 받는 int 인자가 0보다 작으면 예외를 던지는 함수이다.

private void validIntegerNotNegative(int num) {
    if (num < 0) {
        throw new IllegalArgumentException("%s 인자가 0보다 작을 수 없습니다.".formatted(num));
    }
}

이러한 함수를 코틀린으로 변환해보겠다.

fun validIntegerNotNegative(num: Int) {
    if (num < 0) {
        throw IllegalArgumentException("${num} 인자가 0보다 작을 수 없습니다.")
    }
}

코틀린으로 변환된 코드를 살펴보면 Exception을 던질 때 throw 앞에 new를 쓰지 않는 것 외에는 크게 차이는 없다. 즉 if 문법은 자바와 코틀린과 차이가 없다.
else가 있는 if 문 또한 자바와 코틀린과 문법의 차이는 없다.

하지만 if 문에서 자바와 코틀린과의 차이점은 존재한다. 바로 자바에서의 if 문은 Statement 이지만, 코틀린에서는 Expression 이라는 점이다.
Statement: 프로그램의 문장, 하나의 값으로 도출되지 않는다.
Expression: 하나의 값으로 도출되는 문장
따라서 자바에서는 if 문이 하나의 값으로 도출되지 않기 때문에 다음과 같은 코드는 문법상 잘못되었다고 판단한다.

String rank = if (score >= 80) {
        "Honor";
    } else {
        "Normal";
    }

자바에서는 위와 같이 동작하는 코드를 작성하기 위해 3항 연산자라는 것을 사용하게 된다. 3항 연산자는 하나의 값으로 취급하여 Expression 이면서 Statement 라고 볼 수 있다.

String rank = (score >= 80) ? "Honor" : "Normal";

하지만 코틀린에서는 if 문이 Expression 이라고 했다. 즉 자바에서 3항 연산자를 사용하는 것처럼 코틀린에서는 if 문을 그렇게 작성할 수 있다는 의미이다.
코틀린에서는 if 문을 3항 연산자처럼 사용할 수 있으므로 3항 연산자가 따로 존재하지 않는다.

val rank = if (score >= 80) {
    "Honor"
} else {
    "Normal"
}

코틀린에서의 팁으로, 어떠한 값이 특정 범위에 포함되어 있는지, 포함되어 있지 않은지 조건을 자바에서는 이렇게 작성했을 것이다.

if (0 <= score && score <= 100)

코틀린에서도 같은 식으로 작성할 수 있지만, 앞 포스팅에서 잠깐 다뤘던 ina..b 연산자로 간단하게 작성할 수 있다.

if (score in 0..100)
switch 문과 when

자바에서 switch 문은 다음과 같이 작성한다.

private String getGrade(int score) {
    switch (score / 10) {
        case 9:
            return "A";
        case 8:
            retun "B";
        case 7:
            return "C";
        default:
            return "D";
    }
}

이러한 switch 문을 코틀린에서는 다음과 같이 작성할 수 있다. 즉 코틀린에서는 switch 문을 when 이라는 새로운 문법을 사용해서 작성할 수 있다.

fun getGrade(score: Int): String {
    return when (score) {
        in 90..99 -> "A"
        in 80..89 -> "B"
        in 70..79 -> "C"
        else -> "D"
    }
}

when 문을 활용한 예제로, 다음과 같이 instanceof 키워드를 사용하는 자바 코드를 코틀린 코드로 다음과 같이 변환해볼 수 있다.

private boolean containsWithA(Object obj) {
    if (obj instanceof String) {
        return ((String) obj).contains("A");
    } else {
        return false;
    }
}

👇

fun containsWithA(obj: Any): Boolean {
    return when (obj) {
        is String -> obj.contains("A")
        else -> false
    }
}

또한 when 문은 받아오는 값이 없을 수도 있어, 이를 활용해 when 문법은 early return 처럼 사용할 수 있다. 아래의 인자로 주어진 int가 짝수, 홀수, 0인지 검증하는 자바 코드를 코틀린으로 변환해보겠다.

private void validNumberEvenOdd(int num) {
    if (num == 0) {
        System.out.println("주어진 숫자는 0입니다.");
        return;
    }
    
    if (num % 2 == 0) {
        System.out.println("주어진 숫자는 짝수입니다.");
        return;
    }

    System.out.println("주어진 숫자는 홀수입니다.");
}

👇

fun validNumberEvenOdd(num: Int) {
    when {
        num == 0 -> println("주어진 숫자는 0입니다.")
        num % 2 == 0 -> println("주어진 숫자는 짝수입니다.")
        else -> println("주어진 숫자는 홀수입니다.")
    }
}

2. 반복문

for each 문

다음과 같이 숫자가 들어있는 리스트를 하나씩 반복하여 출력하는 자바 코드를 코틀린으로 변환해보겠다.

List<Integer> numbers = Array.asList(1, 2, 3, 4, 5);
for (int number : numbers) {
    System.out.println(number);
}

👇

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers) {
    println(number)
}
전통적인 for 문

코틀린으로 위와 같은 동작을 하는 반복문을 전통적인 for 문으로 작성해보겠다.

val numbers = listOf(1, 2, 3, 4, 5)
for (i in 0..(numbers.size - 1)) {
    println(numbers[i])
}

그렇다면 만약 i 값이 내려가면서 반복하는 경우는 어떻게 작성해야 할까? 먼저 자바에서의 예시를 보여주고, 해당 예제 코드를 코틀린으로 변환해보겠다.

List<Integer> numbers = Array.asList(1, 2, 3, 4, 5);
for (int i = numbers.size() - 1; i >= 0; i--) {
    System.out.println(numbers.get(i));
}

👇

val numbers = listOf(1, 2, 3, 4, 5)
for (i in (numbers.size - 1) downTo 0) {    // 반복 인자가 내려갈 때는 downTo 사용
    println(numbers[i])
}

그 다음으로는 i 값이 2씩 커지는 경우라면 어떻게 작성할까? 이것 또한 자바에서의 예시를 보여주고, 해당 예제 코드를 코틀린으로 변환해보겠다.

List<Integer> numbers = Array.asList(1, 2, 3, 4, 5, 6);
for (int i = 0; i < numbers.size(); i+=2) {
    System.out.println(numbers.get(i));
}

👇

val numbers = listOf(1, 2, 3, 4, 5, 6)
for (i in 0..(numbers.size - 1) step 2) {   // n씩 올리면서 반복할 때는 step n 사용
    println(numbers[i])
}

위와 같은 반복문의 동작 원리로는, downTo나 step이나 모두 등차수열(Progression)을 만드는 함수이기 때문이다.

while 문

while 반복문으로 1부터 5까지 반복하여 출력하는 예제를 작성하고자 한다면 코틀린에서는 다음과 같이 작성한다.

var i = 1
while (i <= 5) {
    println(i)
    i++
}

while 문을 보면 문법은 자바와 완전히 동일하며, do-while 또한 동일하다.

3. 예외

try catch finally

주어진 문자열을 정수로 변환하고, 만약 정수로 변환할 수 없는 문자열일 경우 예외를 던지는 함수를 작성해본다고 해보자. 먼저 자바로 작성한다면 다음과 같이 작성할 것이다.

private int parseIntOrThrow(@NotNull String str) {
    try {
        return Integer.parseInt(str);
    } catch (NumberFormatException e) {
        throw new IllegalArgumentException("주어진 인자는 숫자가 아닙니다.");
    }
}

try catch finally의 문법은 자바와 코틀린과 동일하다.

fun parseIntOrThrow(str: String): Int {
    try {
        return str.toInt()  // 내부적으로는 parseInt를 호출함
    } catch (e: NumberFormatException) {
        throw IllegalArgumentException("주어진 인자는 숫자가 아닙니다.")
    }
}

또 다른 예제를 살펴보자. 이번 예제는 아까처럼 주어진 문자열을 정수로 변환하는데, 정수로 변환할 수 없는 경우라면 null을 반환하는 함수를 작성해본다고 하자. 먼저 자바로 작성해보겠다.

private Integer parseIntOrNull(@NotNull String str) {
    try {
        return Integer.parseInt(str);
    } catch (NumberFormatException e) {
        return null;
    }
}

이제 위 자바 코드를 코틀린으로 변환해볼텐데, 코틀린에서 try-catch 문은 앞서 if 문처럼 하나의 Expression으로 간주된다. 따라서 다음과 같이 try-catch 문을 하나의 값으로 도출하여 반환할 수 있다.
즉 여러 번 return 하지 않고 한 번만 return 해도 동일하게 동작할 수 있게 된다.

fun parseIntOrNull(str: String): Int? {
    return try {
        str.toInt()
    } catch (e: NumberFormatException) {
        null
    }
}
Checked Exception과 Unchecked Exception

자바에서 File을 읽어와서 내용을 읽어올 때 주로 BufferedReader나 FileReader를 사용하는데, FileReader를 생성하는 구문이나 파일 내부의 값을 읽어오는 readLine() 함수 등을 사용할 때 IOException 이라는 예외를 던질 수 있다. 이러한 IOException은 Checked Exception 이라고 해서 이러한 메서드를 사용할 때는 Checked 예외가 발생할 수 있다는 표시를 해 줘야 한다.
위 내용에 대한 예제를 우선 자바에서 작성해보면 다음과 같다.

public void readFile() throw IOException {  // 체크 예외가 날 수 있다는 표시
	File currentFile = new File(".");
	File file = new File(currentFile.getAbsolutePath() + "/example.txt");
	BufferedReader reader = new BufferedReader(new FileReader(file));
	System.out.println(reader.readLine());
	reader.close();
}

같은 동작을 하는 코틀린 코드를 작성해보자.

fun readFile() {
    val currentFile = File(".")
    val file = File(currentFile.absolutePath + "/example.txt")
    val reader = BufferedReader(FileReader(file))
    println(reader.readLine())
    reader.close()
}

자바에서는 throw IOException 과 같이 Checked Exception을 명시해줘야 했는데 코틀린에서는 throw로 명시해주지 않아도 오류가 발생하지 않는다. 그 이유는 코틀린에서는 Checked Exception과 Unchecked Exception을 구분하지 않기 때문이다.
코틀린에서는 Checked Exception을 throw를 통해서 메서드 시그니처에 명시해줘야 하는 자바와는 다르게 모두 Unchecked Exception 으로 간주한다. 따라서 코드를 작성할 때 예외에 대해 신경써야 하는 포인트가 덜해 편하게 개발 가능하다.

try with resources

이번에는 직접 파일의 경로가 주어지면 해당 경로의 최종 내용물을 읽어오는 코드를 작성해보자. 먼저 자바로 작성하면 다음과 같이 작성할 수 있겠다.
try 앞의 괄호 안에 외부 자원을 만들어주고 try가 끝나면 자동으로 외부 자원을 닫아주는 try with resources 구문이다.

public void readFile(String path) throw IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        System.out.println(reader.readLine());
    }
}

위의 코드를 코틀린으로 변환해보자.
코틀린에서는 try with resources 구문이 없다. 위와 동일한 동작을 하는 함수를 작성하기 위해서 코틀린의 BufferedReader에 대한 확장 함수인 .use 를 사용하여 작성했다.

fun readFile(path: String) {
    BufferedReader(FileReader(path)).use { reader ->
        println(reader.readLine())
    }
}

즉 코틀린에서 try with resource 구문은 없고 비슷한 동작을 할 수 있는 확장 함수를 사용했다는 점을 인식하고 넘어가면 되겠다.

4. 함수

함수 선언 문법

두 정수가 주어지면 두 정수 중 큰 값을 반환하는 함수가 있다고 가정해보자.

fun max(a: Int, b: Int): Int {
    return if (a > b) {
        a
    } else {
        b
    }
}

위와 동일한 동작을 하는 함수를 다음 코드처럼 작성할 수도 있다.

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

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

이는 즉 중괄호 안에서 return으로 어떤 값을 반환해준다. 라고 표현하는 대신 함수의 결과물은 이거야. 라고 =로 표현할 수도 있는 것이다. 즉 코틀린에서는 함수가 하나의 결과값이면 중괄호 블록 대신 return 구문을 쓰지 않고 =을 사용하여 나타낼 수 있다.

또한 코틀린에서는 public 접근 지시어는 생략 가능하다. 따라서 따로 접근 지시어 없이 fun xxx() 라고 작성한 함수는 기본적으로 public 이라고 인지하면 된다.

한 가지 실무 팁으로는 함수 선언 시 =을 사용하는 경우에는 반환 타입을 생략하고, 중괄호 블록을 사용하여 return 해주는 값이 있는 경우에는 함수의 반환 타입을 명시해주는 것이 좋다.
=을 사용하면 해당 함수 값이 어떤 값이 오겠구나. 라는 걸 바로 인지할 수 있지만, 중괄호 블록의 경우에는 어떤 타입의 값이 반환되는지 한 번에 인지하기가 어려운 경우가 많기 때문이다.

Default Parameter

만약 주어진 문자열을 n번 출력하는 함수가 있다고 해보자. 먼저 자바로는 다음과 같이 작성할 수 있다.

public void repeat(String str, int num, boolean useNewLine) {
    for (int i = 0; i < num; i++) {
        if (useNewLine) {
            System.out.println(str);
        } else {
            System.out.print(str);
        }
    }
}

그런데 해당 함수를 사용하는 곳 대부분에서 useNewLine 인자에 true로 사용하고 있다고 해보자. 그러면 일일이 useNewLine 인자에 true를 넣어주는 건 번거로울 수 있다.
그래서 자바에서는 오버로딩(OverLoading)으로 이러한 부분을 어느정도 해결 가능하다.

public void repeat(String str, int num, boolean useNewLine) {
    for (int i = 0; i < num; i++) {
        if (useNewLine) {
            System.out.println(str);
        } else {
            System.out.print(str);
        }
    }
}

public void repeat(String str, int num) {
    repeat(str, num, true);
}

그리고 또 많은 곳에서 num 인자로 3이라는 값을 넣어주고 있다고 해보자. 그럼 또 오버로딩하여 해결하려 할 것이다.

public void repeat(String str, int num, boolean useNewLine) {
    for (int i = 0; i < num; i++) {
        if (useNewLine) {
            System.out.println(str);
        } else {
            System.out.print(str);
        }
    }
}

public void repeat(String str, int num) {
    repeat(str, num, true);
}

public void repeat(String str) {
    repeat(str, 3, true);
}

하지만 위와 같은 자바 코드에서의 단점으로는, 오버로딩을 할 수록 새로운 함수가 계속 생긴다는 점이다. 메서드를 계속 만드는 게 중복되는 느낌이 들고 거부감이 들 수도 있다.
물론 코틀린에도 오버로딩 개념은 존재하지만, 이러한 경우에는 코틀린에서는 Default Parameter 라는 것을 사용한다.
다음과 같이 함수를 작성할 때 파라미터 뒤에 = 어떤 값을 넣어주면 이 값은 파라미터의 기본 값이 된다.

fun repeat(
    str: String,
    num: Int = 3,
    useNewLine: Boolean = true
) {
    for (i in 1..num) {
        if (useNewLine) {
            println(str)
        } else {
            print(str)
        }
    }
}

위처럼 Default Parameter 값을 넣어준 인자는 함수 호출 시 해당 인자에 값을 넣지 않아도 호출이 가능하다. 즉 Default Parameter는 외부에서 파라미터를 넘겨주지 않는다면 기본값으로 사용하겠다. 라는 목적을 가지고 있다.

fun main() {
    repeat("Hello, World!")     // num, useNewLine에 해당하는 인자를 넣지 않아도 호출 가능
}
Named Argument (Parameter)

그렇다면 만약 위 repeat() 함수에서 num은 그대로, useNewLine은 false 로 사용하고 싶을 때는 어떻게 해야 할까?
첫 번째 방법으로는 함수 호출 시 num에 Default Parameter로 작성해주었더라도 3이라는 값을 같이 넘겨주는 방법이 있을 것이다.

fun main() {
    repeat("Hello, World!", 3, false)
}

두 번째 방법으로는 Default Parameter로 써준 값을 다시 한 번 넣어주고 싶지 않을 때 사용하는 방법으로, 함수 호출 시 이러한 파라미터로 이러한 인자값을 넣어줄거야. 라는 명시를 해줄 수 있는 Named Argument를 사용하는 방법이다.

fun main() {
    repeat("Hello, World!", useNewLine = false)
}

이러한 Named Argument의 장점으로는 Builder를 직접 만들지 않고도 Builder의 장점을 가지게 된다는 점이 있다.
하지만 한 가지 주의해야 할 점으로는, 코틀린에서 자바의 함수를 가져와 사용할 때는 Named Argument를 사용할 수 없다.

같은 타입의 여러 파라미터 받기 (가변인자)

만약 문자열을 n개 받아서 출력하는 함수가 있다고 가정해보자. 자바 코드로 작성해보면 다음과 같이 작성할 수 있다.

public static void printAll(String... strings) {    // 자바에서 가변인자는 타입... 으로 사용
    for (String str : strings) {
        System.out.println(str);
    }
}

public static void main(String[] args) {
    String[] strArr = new String[]{"a", "b", "c"};
    printAll(strArr);   // 함수 호출 시에는 배열을 직접 넣거나
    
    printAll("a", "b", "c");    // 콤마로 여러 파라미터를 입력
}

그러면 코틀린에서는 어떻게 가변인자를 만들고 사용하는지 알아보자.
코틀린에서 가변인자를 만들어 줄 때는 vararg 라는 키워드로 선언하여 만들게 되며, 가변인자를 파라미터로 받는 함수를 호출할 때는 콤마를 사용해서 여러 파라미터를 입력하는 건 자바와 동일하지만, 배열을 직접 입력할 때는 인자 앞에 * 표시를 해줘야 한다.
* 표시는 spread 연산자로, 배열 안에 있는 값들을 펼쳐서 콤마로 구분하여 표기한 것처럼 꺼내주는 역할을 한다.

fun printAll(vararg strings: String) {  // vararg 키워드 = 가변인자
    for (str in strings) {
        println(str)
    }
}

fun main() {
    val strArr = arrayOf("a", "b", "c")
    printAll(*strArr)   // * spread 연산자 사용
    
    printAll("a", "b", "c")
}