Kotlin in Action 2/e 서적을 읽으면서 정리하고자 쓴 글입니다.

함수와 변수(Variables)
Java, Kotlin 각각 `Hello, world` 출력 함수를 출력하면 다음과 같습니다.
// Java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, world");
}
}
// Kotlin
fun main(args: Array<String>) {
println("Hello, world")
}
- 함수 선언을 할 때 `fun` (파라미터 이름: 파라미터 타입) 키워드를 사용합니다.
- Java의 경우 메서드를 호출하기 위해 클래스를 선언 후 작성해야 하지만 Kotlin의 함수는 클래스 생성 없이 파일 내 최상위 수준에 위치할 수 있습니다.
- Java와 달리 배열 처리를 위한 문법이 따로 존재하지 않습니다. Kotlin에서 배열은 일반적인 클래스와 동일하게 사용됩니다.
- `System.out.println ` 대신 `println`을 사용합니다. Java 표준 라이브러리 함수를 간결하게 사용할 수 있도록 ` wrapper `를 제공합니다.
- 세미콜론(`;`)을 붙이지 않아도 됩니다.
- Kotlin은 Java와 다르게 `변수: 타입`순으로 선언합니다.
1) 함수
Kotlin의 함수는 `fun` 키워드를 사용하며, 기본적인 형태는 다음과 같습니다.
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
함수 형태는 `fun` (파라미터 이름: 파라미터 타입): 반환 타입 { 함수 본문 }과 같습니다.
위 예시에서 주목해야 할 점은 `if`가 Statement(문)가 아니고 Expression(식)으로 사용되었다는 점입니다. 즉, 예제의 `if` 식은 Java의 삼항 연산자 ` (a < b) ? a : b `와 유사하게 동작하여 결과 값을 즉시 반환합니다.
`if` 식처럼 함수 본문 전체가 하나의 식으로만 구성되어 값을 반환하는 경우, Kotlin에서는 식이 본문인 함수 형태로 더욱 간결하게 표현할 수 있습니다. (Kotlin에서 자주 사용되는 형태)
fun max(a: Int, b: Int): Int = if (a > b) a else b
더 나아가 Kotlin은 타입 추론 기능이 있어 반환 타입을 생략할 수 있습니다. 단, 본문인 함수에서는 컴파일러가 타입을 추론할 수 없기 때문에 반드시 반환 타입을 명시하고 `return` 문을 이용해 반환 값을 명시해야 합니다.
fun max(a: Int, b: Int) = if (a > b) else b
// : 해당 함수처럼 Int 반환 타입이 없으면 컴파일 오류!!
fun max(a: Int, b: Int) {
return if (a > b) else b
}
Statement(문): 값을 만들어내지 않습니다. 자신을 둘러싼 블록의 최상위 요소만 존재합니다. (ex. `var`, `fun`, `return`, `while`)
Expression(식): 값을 만들어 냅니다. 다른 식의 하위 요소로 계산에 참여할 수 있습니다. (ex. `if`, `when`, `a+b`, 10)
2) 변수
Kotlin에서는 변수 이름 뒤에 타입을 명시하는 방식을 사용하며, 종종 타입 지정을 생략할 수 있습니다.
val age = 10 // 타입이 생략되었지만 값 10을 보고 Int 타입을 추론한다.
만약 초기화 식을 사용하지 않고 변수를 선언하려면 컴파일러가 타입을 추론할 수 없기 때문에 변수 타입을 반드시 명시해야 합니다.
val age: Int // 값 초기화 X
age = 10 // 이후 값 초기화 O
Kotlin은 변경 가능한 `var`과 불변성을 가진 `val` 2가지 키워드를 제공합니다.
| 키워드 | 유래 | 특징 | Java 대응 |
| `val` | Value (값) | 변경 불가능한 참조(Immutable Reference) 초기화 후 재대입이 불가능합니다. |
`final` 변수 |
| `var` | Variable (변수) | 변경 가능한 참조(Mutable Reference) 초기화 후 재대입이 가능합니다. |
일반 변수 |
🔑 불변성 사용 권장 원칙
기본적으로 모든 변수를 `val` 키워드를 사용하여 불변 변수로 선언하고, 꼭 필요할 때에만 `var`로 변경하는 것을 권장합니다. 이는 불변성 기반 설계의 핵심이며 코드의 안정성을 높입니다.
🔑 `val` 변수의 초기화 규칙
`val`은 참조 자체는 불변이지만, 반드시 블록이 실행될 때 정확히 한 번만 초기화 되어야 합니다. 컴파일러가 특정 실행 경로에서 오직 하나의 초기화 문장만 실행됨을 확인할 수 있다면, 조건에 따라 `val` 값을 여러 다른 값으로 초기화할 수 있습니다.
val message: String
if (canPerformOp()) { // 특정 조건 별 분기
message = "success"
} else {
message = "failed"
}
// 컴파일러는 이 시점에서 message가 반드시 한 번 초기화되었음을 확인
🔑 참조의 불변성과 객체의 가변성
`val` 참조 자체는 불변이지만, 그 참조가 가리키는 객체의 내부 값은 변경될 수 없음을 유의해야 합니다.
val numbers = mutableListOf(1, 2, 3) // numbers 참조 자체는 불변
numbers.add(4) // numbers가 가리키는 객체의 내부 상태는 변경 가능하다.
// numbers = mutableListOf(5) 재대입 시도 시 컴파일 오류 발생!!
클래스와 프로퍼티
Kotlin은 데이터를 저장하는 클래스, 특히 값 객체(Value Object)를 간결하게 정의할 수 있도록 지원합니다.
// Java
public class Person {
private final String name;
public Person(String name) { this.name = name; }
public getName() { return name; }
}
// Kotlin
class Person(val name: String)
위처럼 데이터만을 포함하는 클래스는 Kotlin의 간결성을 극대화하는 대표적인 예시입니다.
1) 프로퍼티
클래스의 주된 목적은 데이터를 캡슐화하고, 데이터를 다루는 코드를 하나의 주체 아래에 통합하는 것입니다.
Java 방식: 데이터를 필드(Field)에 저장하고, 이 필드에 접근할 수 있는 통로로 접근 메서드(Accessor Method)인 getter/setter를 제공합니다. Java에서는 필드와 해당 접근자 메서드를 묶어 프로퍼티라고 관례적으로 부릅니다.
Kotlin 방식: 프로퍼티 개념을 언어의 기본 기능으로 제공하며, Java의 필드와 접근자 메서드를 완전히 대신합니다.
선언: Kotlin에서 프로퍼티를 선언할 때는 변수와 마찬가지로 `val`과 `var` 키워드를 선언합니다.
| 키워드 | 프로퍼티 유형 | 자동 생성 접근자 |
| `val` | 읽기 전용 프로퍼티 | 비공개 필드와 getter만 선언합니다. |
| `var` | 변경 가능한 프로퍼티 | 비공개 필드와 getter/setter 모두 선언합니다. |
2) 커스텀 접근자
프로퍼티는 단순히 값을 저장하는 필드 역할뿐만이 아닌 접근할 때도 특정 로직을 실행하도록 정의할 수 있습니다.
커스텀 `getter` 정의
직사각형을 표현하는 `Rectangle` 클래스에 해당 사각형이 정사각형인지 여부를 판단하는 기능을 커스텀 `getter`로 구현할 수 있습니다. 이 경우 별도의 필드를 만들어 상태를 저장할 필요가 없습니다.
class Rectangle(
val height: Int,
val width: Int
) {
val isSquare: Boolean
get() = height == width
}
fun run() {
val r = Rectangle(10, 10)
println("${r.isSquare}") // 출력: true
}
위처럼 `getter`를 호출할 때마다 값을 새로 계산합니다. 즉, `Rectangle`의 `height`나 `width`가 변경된다면, `isSquare` 프로퍼티를 호출할 때마다 변경된 값을 반영한 최신 결과를 얻을 수 있습니다.
enum & when
1) `enum` 클래스
Kotlin에서 `enum`은 Soft keyword입니다. 즉, 예약어로 동작하는 것이 아니라 `enum class` 형태로 `class` 앞에 사용될 때만 특별한 의미를 가지게 됩니다. 단순히 상수를 열거하는 것 이상으로, 프로퍼티와 메서드를 가질 수 있는 완전한 클래스입니다.
enum class Color(val r: Int, val g: Int, val b: Int) {
RED(255, 0, 0), ORANGE(255, 165, 0), YELLOW(255, 255, 0),
GREEN(0, 255, 0), BLUE(0, 0, 255); // 상수 목록 끝에 세미콜론 필수
fun rgb() = (r * 256 + g) * 256 + b // 메서드 정의
}
2) `when` 식
Kotlin은 Java의 `switch`문을 대체하면서 더 강력한 기능을 제공하는 `when`이 있습니다. `when`은 문(Statement)이 아닌 식(Expression)이므로 결괏값을 반환할 수 있습니다.
fun getWarmth(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
Color.GREEN -> "neutral"
Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
Java와 달리 각 분기 끝에 `break`를 넣지 않아도 되며, 한 분기 안에서 여러 값을 매칭하려면 콤마(`,`)로 연결합니다. 또한 Java와 달리 `when`의 분기 조건에 상수 뿐만이 아닌 임의의 객체를 사용할 수 있습니다.
fun mix(c1: Color, c2: Color) = when (setOf(c1, c2)) {
setOf(Color.RED, Color.YELLOW) -> Color.ORANGE
setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
else -> throw Exception("Dirty color")
}
위 처럼 `setOf`를 통해 두 색상의 순서와 상관없이 조합을 검사할 수 있습니다.
다만 위 방식은 setOf를 호출할 때마다 새로운 Set 객체가 생성되므로, 함수가 자주 호출되는 경우 불필요한 임시 객체와 가비지 컬렉션 부담이 발생할 수 있습니다.
Kotlin은 이러한 상황을 위해 인자 없는 `when`을 사용할 수 있습니다.
fun mixOptimized(c1: Color, c2: Color) = when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == YELLOW && c2 == BLUE) ||
(c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) ||
(c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}
3) 스마트 캐스트
Kotlin 컴파일러는 형 변환을 자동으로 수행해 주는 스마트 캐스트 기능을 제공합니다.
- `is` 연산자: Java의 `instanceof`와 비슷하게 타입을 검사합니다.
- 스마트 캐스팅: `is`로 타입을 검사하고 나면, 컴파일러가 해당 블록 내에 변수를 해당 타입으로 간주하므로 명시적인 캐스팅(`as`) 없이 프로퍼티를 사용할 수 있습니다.
fun eval(e: Expr): Int = when (e) {
is Num -> e.value // 스마트 캐스트 적용
is Sum -> eval(e.left) + eval(e.right) // 스마트 캐스트 적용
else -> throw IllegalArgumentException("Unknown expression")
}
4) `if`와 `when`의 리팩토링 및 블록 사용
Kotlin은 `if` 역시 식(Exprssion)이므로 값으로 반환할 수 있습니다. 따라서 Java와 같은 별도의 3항 연산자가 없습니다.
val result = when (e) {
is Num -> {
println("value: ${e.value}")
e.value // 이 블록의 결과값
}
// ...
}
while과 for loop
Kotlin의 `while` & `do-while`의 경우 Java와 완전히 동일한 구조를 가지고 있습니다.
`for loop`의 경우 Java의 고전적인 `int i = 0; i < 10; i++` 방식이 존재하지 않으며, Kotlin은 범위 연산자를 사용합니다.
1) 수에 대한 이터레이션: 범위와 순열
| 표현식 | 설명 | Java 대응 코드 |
| 1..10 | 1부터 10까지 (10포함) | i = 0; i <= 10; i++ |
| 1 until 10 | 1부터 9까지 | i = 0; i < 10; i++ |
| 10 downTo 1 | 10부터 1까지 역순 | i = 10; i >= 1; i-- |
| step 2 | 2씩 증가 (간격 설정) | i += 2 |
// 1. 기본 범위 반복 (10 포함)
for (i in 1..10) { // a..z 로 사용해도 a b c d e f g... 출력
print("$i ") // 1 2 3 ... 10
}
// 2. 마지막 숫자 제외 (until) - 배열 인덱스 순회 시 유용
val list = listOf("Java", "Kotlin", "Spring")
for (i in 0 until list.size) {
println("Index $i: ${list[i]}")
}
// 3. 역순 및 간격 설정 (downTo, step)
for (i in 10 downTo 1 step 2) {
print("$i ") // 10 8 6 4 2
}
// 4. 컬렉션 직접 순회 (for-each 스타일)
for (lang in list) {
println("Learning $lang")
}
// 5. 인덱스와 값을 동시에 추출 (withIndex)
for ((index, value) in list.withIndex()) {
println("[$index]: $value")
}
2) `in`으로 컬렉션이나 범위의 원소 검사
`in` 연산자를 통해 어떤 값이 범위에 속하는지, 반대로 `!in`을 사용한다면 범위에 속하지 않는지 검사할 수 있습니다.
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun main() {
println(isLetter('h')) // true
println(isLetter('11')) // false
}
fun recognize(c: Char) = when (c) {
in '0'..'9' -> "It's a digit!"
in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
else -> "I don't know…"
}
fun main() {
println(recognize('8')) // It's a digit!
}
Kotlin의 예외처리
Kotlin의 예외처리는 Java나 다른 언어의 예외처리와 비슷합니다.
1) try, catch, finally를 사용한 예외처리와 오류 복구
Java 코드와 가장 큰 차이는 `throws` 절이 Kotlin에 없다는 점입니다.
fun readNumber (reader: BufferedReader): Int {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return 0
} finally {
reader.close()
}
}
Kotlin은 모든 예외를 언체크 예외(Unchecked Exception)로 취급합니다. 따라서 Java와 달리 `throws`를 명시하거나 예외 처리를 강제할 필요가 없어 코드가 간결해집니다.
또한 `try-with-resources` 대신 표준 라이브러리의 `.use` 확장 함수를 사용해 리소스를 안전하게 해제할 수 있습니다.
// Kotlin: .use() 확장 함수 사용
val line = BufferedReader(FileReader(path)).use { br ->
br.readLine()
}
// 블록을 나가는 순간 자동으로 br.close() 호출
2) try를 식으로 사용
`try`문 또한 식(Expression)으로 사용할 수 있습니다.
fun readNumber (reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine()) // 이 식의 값이 try 식의 값이 된다.
} catch (e: NumberFormatException) {
return
}
println(number)
}
`if`와 달리 `try`의 본문을 중괄호 {}로 둘러싸야합니다.
요약
- 함수를 정의할 때 `fun` 키워드를 사용하며, val는 읽기 전용 변수, var는 변경 가능한 변수를 선언할 때 쓰인다.
- Kotlin의 `if`는 식(Expression)이며, 값을 만들 수 있다.
- Java의 `switch`를 대체하며, 더 강력한 기능을 제공하는 `when`이 있다.
- 스마트 캐스팅: 어떤 변수의 타입을 검사하면 직접 캐스팅하지 않아도 형 변환을 한 변수처럼 사용할 수 있다.
- `1..5`와 같은 식으로 범위를 나타낼 수 있고, 어떤 값이 범위 안에 들어있거나 들어있지 않은지 검사하기 위해 `in`이나 `!in`을 사용할 수 있다.
- Kotlin의 예외처리는 Java와 비슷하다. 다만 함수가 던질 수 있는 예외를 선언하지 않아도 된다. (언체크 예외)
'Kotlin' 카테고리의 다른 글
| [Kotlin in action 2/e] 코틀린 타입 시스템 (0) | 2026.01.07 |
|---|---|
| [Kotlin in action 2/e] 람다로 프로그래밍 (0) | 2025.12.31 |
| [Kotlin in Action 2/e] 클래스, 객체, 인터페이스 (0) | 2025.12.30 |
| [Kotlin in Action 2/e] 함수 정의와 호출 (1) | 2025.12.30 |
| [Kotlin in Action 2/e] 코틀린이란 무엇이며, 왜 필요한가? (1) | 2025.12.12 |