uncategorized

코틀린 기초

요약
함수와 변수
클래스와 프로퍼티
enum과 when
while과 for 루프
예외 처리

기본 요소: 함수와 변수

함수

1
2
3
fun {함수이름}({매개변수 이름}: {매개변수 타입}) : {반환값 타입} {
...
}

반환값이 없는 함수 예시

1
2
3
fun main(args: Array<String>) {
println("Hello world!")
}

반환값이 있는 함수 예시

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

문(statement)과 식(expression)의 구분
코틀린의 if는 식이지 문이 아니다. 반환값이 있는 함수 예시를 보면 반환값에 if식이 들어 간 것을 확인할 수 있다. 식은 값을 만들어내며 다른 식의 하위 요소로 계싼에 참여할 수 있다. 반면 문은 자신이 둘러싸고 있는 가장 안쪽 블록의 최상위 요소로 존재하며 아무런 값을 만들어 내지 않는다. 자바는 모든 제어 구조가 문인 반면, 코틀린은 루프를 제외한 모든 제어 구조가 식이다.

식이 본문인 함수

max 함수를 좀 더 간결하게 줄여보자.
먼저 본문이 식 하나인 블록으로 구성되어 있어서 이를 식으로 대체할 수 있다.

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

위 처럼 등호와 식으로 이뤄진 함수를 식이 본문인 함수라고 하고, 본문이 중괄호로 쌓인 함수를 블록이 본문인 함수라고 한다.

그리고 식이 본문인 함수인 경우 반환 타입을 생략할 수 있다. 식이 본문인 경우 컴파일러가 함수 본문 식을 분석해서 식의 결과 타입을 함수 반환 타입으로 정해준다. 이렇게 컴파일러가 타입을 분석해 프로그래머 대신 프로그램 구성 요소의 타입을 정해주는 기능을 타입추론 이라고 부른다.

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

변수

코틀린에서는 변수를 초기화할 때 타입을 생략할 수 있다. 타입을 생략하는 경우 타입추론을 활용한다.

1
2
3
val statement = "중요한 문장입니다."
val number = 1
val numberWithType: Int = 1

초기화 식을 사용하지 않고 변수를 선언하려면 반드시 타입을 적어줘야 한다. 변수에 저장될 값에 대한 정보가 없어서 타입추론을 할 수 없기 때문이다.

1
2
val value : Int
value = 2

변경 가능한 변수와 변경 불가능한 변수

1
2
3
4
5
// 값을 뜻하는 value - 초기화 이후 변경 불가
val value = 1

// 변수를 뜻하는 variable - 초기화 이후 변경 가능
var variable = 1

이때 재밌는 점은 자바 final은 선언과 동시에 초기화해줘야 하지만 코틀린의 val은 한번만 초기화된다. 즉 다음과 같은 제어 구조를 구성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
fun doMessage(input : String) {
val message : String
if (input.length == 0) {
message = "빈 문자열 전달받음."
// 연산...
}
else {
message = input + " 전달받음"
// 연산...
}
}

문자열 템플릿

1
2
3
4
5
fun stringTemplate(input: String) {
val name = input.ifEmpty { "Kotlin" }
println("Hello $name!")
println("HELLO ${name.uppercase()}")
}

문자열 리터럴 안에서 변수를 사용할 수 있도록 하는 방법이다. $를 앞에 붙여주고 변수명을 적어주면 상요할 수 있다.
중괄호를 써주면 변수명이 아닌 간단한 식을 넣어줄 수도 있다.

클래스와 프로퍼티

자바빈 클래스인 Person을 자바와 코틀린으로 구현해보자.
자바

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private final String name;

public Person(final String name) {
this.name = name;
}

public String getName() {
return name;
}
}

코틀린

1
class Person(val name: String)

와우! 어떻게 이렇게 된 것일까?
코틀린에서는 생성자를 통해 필드 대입 로직을 묵시적으로 생략해도 된다. 그리고 가시성 변경자가 public 인 경우 생략할 수 있다.

프로퍼티

클래스라는 개념의 목적은 데이터를 캡슐화 하고 캡슐화한 데이터를 다루는 코드를 한 주체 아래 가두는 것이다.
자바에서는 데이터를 필드에 저장하고 해당 데이터를 접근할 수 있는 접근자 메서드를 제공한다. (보통 게터, 세터)
자바에서는 이런 필드와 접근자 메서드를 묶어서 프로퍼티라고 정의한다.
코틀린은 프로퍼티를 언어 기본 기능으로 제공한다. 코틀린 프로퍼티는 자바의 필드 + 접근자 메서드를 완전히 대체한다.

1
2
3
4
class Person(
val name: String,
var isMarried: Boolean
)

변수 선언과 비슷하게 valvar 키워드로 선언할 수 있다. 이때 두 키워드에 따라 프로퍼티 유형이 달라진다.

  • val : 읽기 전용 프로퍼티, 비공개 필드 + 게터
  • var : 쓸 수 있는 프로퍼티, 비공개 필드 + 게터 + 세터

is가 붙은 변수명의 게터 세터
isMarried와 같이 변수명이 is로 시작하는 경우 게터가 get이 붙지 않고 원래 이름 그대로 사용한다.
그리고 세터는 is 부분을 set으로 바꿔 사용한다. isMarriedsetMarried가 될 것이다.

그렇다면 자바와 코틀린의 클래스 활용법을 비교해보자.
자바

1
2
3
4
Person person = new Person("yang", false);
System.out.println(person.getName());
person.setMarried(true);
System.out.println(person.isMarried());

코틀린

1
2
3
4
val person = Person("yang", false)
println(person.name)
person.isMarried = true
println(person.isMarried)

커스텀 접근자

프로퍼티는 그 값을 저장하기 위해 프로퍼티를 뒷받침하는 필드가 존재한다. 그런데 필요하면 프로퍼티 값을 그때그때 계산할 수도 있다. 커스텀 게터를 사용하면 그런 프로퍼티를 구현할 수 있다.

1
2
3
4
5
6
7
8
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}

val isSquare2: Boolean get() = height == width
}

직사각형이 정사각형인지를 굳이 필드로 저장하지 않고 커스텀 접근자로 구현한 예시이다. 본문이 식인 경우와 블록인 경우 모두 가능하다. 식이 복잡하면 블록으로 구현할 수 있다.

선택 표현과 처리: enum과 when

enum 클래스 정의

1
2
3
enum class Color {
RED, ORANGE, YELLOW
}

코틀린에서 enum은 소프트 키워드이다. 소포트 키워드는 특정 조건에서만 특정한 의미를 갖는 키워드이다. 소프트 키워드는 변수 명 같이 다른 이름으로 사용할 수 있다. 하지만 class는 키워드이고 다른 곳에서 이름으로 사용할 수 없다.

코틀린 enum도 프로퍼티와 메서드를 추가해줄 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
enum class Color(
val r: Int, val g: Int, val b: Int
) {
RED(255, 0, 0),
ORANGE(255, 165, 0),
YELLOW(255, 255, 0); // 반드시 끝에 세미콜론

fun rgb() = (r * 256 + g) * 256 + b
}

fun main(args: Array<String>) {
println(Color.YELLOW.rgb())
}

일반적인 클래스와 비슷하게 생성자를 통해 프로퍼티를 선언할 수 있다. 각 상수마다 프로퍼티 값을 정의해준다.
그리고 중요한 점은 enum에 메서드가 존재하는 경우 마지막 enum 상수 끝에 세미콜론을 넣어줘야 한다!

when으로 enum 클래스 다루기

자바의 switch가 있다면 코틀린에는 when이 있다. 각 색깔에 맞는 과일 이름을 반환하는 메서드를 when을 활용해서 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
fun getFruitNameOf(color: Color) =
when (color) {
Color.RED -> "Apple"
Color.ORANGE -> "Orange"
Color.YELLOW -> "Banana"
}

fun getTemperatureOf(color: Color) =
when (color) {
Color.RED, Color.ORANGE -> "Hot"
Color.YELLOW -> "warm"
}

코틀린의 whenif와 마찬가지로 값을 반환하는 식이다. 그래서 식이 본문인 함수로 구현할 수 있다. 자바 switch와는 다르게 매번 break을 넣어주지 않아도 된다.

when과 임의의 객체를 함께 사용

when은 분기 조건에 임의의 객체도 지원해서 상수(enum 상수나 숫자 리터럴)만 허용하는 switch보다 강력하다.

1
2
3
4
5
6
fun mix(c1: Color, c2: Color) =
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(RED, ORANGE) -> RED
else -> throw Exception("Dirty Color")
}

분기 조건에 두 색을 가진 집합 객체를 받아서 사용할 수 있다. 이때 임의의 객체를 분기 조건으로 사용하면 해당 분기가 맞는지는 동등성을 통해 확인한다.
하지만 위 코드는 매번 메서드를 실행할 때마다 여러 Set 객체를 만들어서 비교한다. 메서드 호출이 굉장히 많을 경우 불필요한 가비지 객체가 많아짐을 의미한다.

인자 없는 when 사용

인자가 없는 when 식 사용하면 불필요한 객체 생성을 막을 수 있다. 코드는 장황해지만 성능 상의 이점을 얻을 수 있다.

1
2
3
4
5
6
7
8
fun mixOptimized(c1: Color, c2: Color) =
when {
(c1 == RED && c2 == YELLOW) ||
(c1 == YELLOW && c2 == RED) -> ORANGE
(c1 == RED && c2 == ORANGE) ||
(c1 == ORANGE && c2 == RED) -> RED
else -> throw Exception("Dirty Color")
}

when 식에 인자가 없으려면 매 분기가 참 거짓을 판별하는 식이어야 한다.

스마트 캐스트: 타입 검사와 타입 캐스트를 조합

(1 + 2) + 4 와 같이 덧셈을 계산하는 함수를 만들어보자.
우선 식을 인코딩하는 방법을 생각해본다. 우리는 식을 트리 구조로 저장한다. 노드는 Num(값)과 Sum(합) 두가지 형식을 가지고, 최하단 노드는 항상 Num이고 Sum은 자식을 가진 중간 노드다.
트리로 표현한 계산

Expr 인터페이스를 선언하고 SumNum 모두 구현하도록 하자.

1
2
3
interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

그렇다면 (1 + 2) + 4라는 식은 Sum(Sum(Num(1), Num(2)), Num(4)) 이런 객체로 표현할 수 있다.

그렇다면 해당 Expr 객체의 값을 반환하는 eval 메서드를 통해 값을 구한다고 하면 다음과 같은 코드가 된다.
println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))

Expr 객체가 SumNum에 따라 eval이 다르게 작동해야 한다.

  • Num : 그 값을 반환
  • Sum : 좌항과 우항을 계산한 다음 두 값을 합한 값을 반환

이런 분기를 처리하는 코드를 짜보는데, Java 방식으로 Kotlin 코드를 짜보자.

1
2
3
4
5
6
7
8
9
10
11
fun eval(e: Expr): Int {
if (e is Num) {
val n = e as Num
return n.value
}
if (e is Sum) {
val n = e as Sum
return eval(n.left) + eval(n.right)
}
throw IllegalArgumentException("unknown expression")
}

is는 Java의 instancof와 비슷하고, as는 캐스팅하는 역할을 한다.
Kotlin에서는 is로 검사하고 나면 해당 변수는 컴파일러가 검사했던 타입으로 캐스팅해준다. 이를 스마트 캐스트라고 부른다.

스마트 캐스트를 적용하면 캐스팅하던 코드가 사라진다.

1
2
3
4
5
6
7
8
9
fun eval(e: Expr): Int {
if (e is Num) {
return e.value
}
if (e is Sum) {
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("unknown expression")
}

리팩토링: if를 when으로 변경

Kotlin의 if는 값을 만들어내는 식임을 더 활용해보자.

1
2
3
4
fun eval(e: Expr): Int =  
if (e is Num) e.value
else if (e is Sum) eval(e.left) + eval(e.right)
else throw IllegalArgumentException("unknown expression")

식이 본문인 함수로 변경됐다. 하지만 ifwhen으로 변경해서 더 다듬을 수 있다.

1
2
3
4
5
6
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegalArgumentException("unknown expression")
}

분기 조건에 값 동등성 조건 대신 다른 기능을 활용하였다. 이 경우에도 스마트 캐스트가 작동한다.

if와 when의 분기에서 블록 사용

ifwhen의 분기에서 복잡한 로직을 실행하려면 블록을 사용한다. 그리고 각 블록은 반환하려는 값을 맨 마지막에 작성하면 된다. 즉 블록의 마지막 식이 블록의 결과이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun evalWithLogging(e: Expr): Int =
when (e) {
is Num -> {
println("num: ${e.value}")
e.value
}
is Sum -> {
val right = evalWithLogging(e.right)
val left = evalWithLogging(e.left)
println("num: ${left + right}")
left + right
}
else -> throw IllegalArgumentException("unknown expression")
}

대상을 이터레이션: while과 for 루프

while

코틀린의 while은 자바와 별반 다르지 않다.

1
2
3
4
5
6
7
while (조건) {
...
}

do {
...
} while (조건)

수에 대한 이터레이션: 범위와 수열

전통적인 for문을 코틀린에서 제공하지 않는다. 이를 대신하기 위해 코틀린에서는 범위를 사용한다.
범위는 val oneToTen = 1..10 이런 방식으로 만든다. 코틀린의 범위는 폐구간 혹은 양 끝을 포함하는 구간이다.

이때 역방향으로 수열을 만들고 싶으면 100 downTo 1 이런 식으로 구현할 수 있다. 이때 감소폭을 다루고 싶으면 step 키워드를 붙이면 된다. 즉 100 downTo 1 step 2 이런 식으로 구현할 수 있다.

코틀린은 기본적으로 개발자가 작성한 마지막 끝 점을 포함한 수열을 만든다. 만약 닫힌 구간을 구현하고 싶으면 until 키워드를 사용하자. 0 until 1010..100과 같다.

맵에 대한 이터레이션

1
2
3
for ((key, value) in someMap) {
println("$key = $value")
}

이런 형태로 맵 객체를 구조 분해해서 사용할 수 있다.
물론 굳이 맵이 아니더라도 구조 분해할 수 있다.

1
2
3
for ((index, element) in someList.withIndex()) {
println("$index: $element")
}

in으로 컬렉션이나 범위의 원소 검사

in 연산자로 순회 뿐만아니라 원소가 포함됐는지 검사할 수 있다.

1
2
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

when 절에서도 분기를 구분할 때 사용할 수 있다.

1
2
3
4
5
6
fun recognize(c: Char) = when(c) {
in 'a'..'z' -> "is lower case"
in 'A'..'Z' -> "is upper case"
in '0'..'9' -> "is numeric value"
else -> "don't know"
}

in 절로 원소 겁사할 수 있는 범위는 비교가 가능한 클래스(Comparable을 구현한 클래스)면 무엇이든 된다.

예외 처리

코틀린의 예외 발생은 자바와 거의 비슷하다. 다만 코틀린에서는 throw 키워드가 식을 만든다는 점만 알고 있자. 그래서 다른 식 내부에서 사용될 수 있다.

1
2
3
4
5
fun main(args: Array<String>) {
val percentage =
if (number in 0..100) number
else throw Exception()
}

try, catch, finally

자바와 마찬가지로 try, catch, finally를 사용한다. 다만 throws IOException이 없다는 점을 주목하자. 자바는 체크 예외를 반드시 어떻게든 처리해줘야 하는데 코틀린은 체크 예외와 언체크드 예외를 구분하지 않는다. 체크 예외가 발생한다고 해서 클라이언트 프로그램이 취할 수 있는 의미있는 동작이 마땅하지 않은 경우가 많기 때문에 의미 없는 예외를 다시 던지거나 예외를 잡고 처리하지 않도록 구현하는 경우가 많기 때문이다.

1
2
3
4
5
6
7
8
9
10
fun readNumber (reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
} catch (e: NumberFormatException) {
return null
} finally {
reader.close()
}
}

try를 식으로도 사용할 수 있다.

1
2
3
4
5
6
7
8
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}

이 경우 catch 블록을 보면 return 문을 통해 메서드를 종료시키고 있다. 이런 방식은 catch 됐을 때 메서드를 종료시키고 싶은 경우 적절하다. 하지만 다른 값을 반환해야 하는 경우는 다르게 작성할 수 있다.

1
2
3
4
5
6
7
8
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
null
}
println(number)
}

이렇게 마지막줄에 블록의 결과를 넣는 규칙에 따라 null을 결과값으로 반환되도록 구현했다.

Share