- Published on
Kotlin IN ACTION | 코틀린 기초
- Authors
- Name
- 이건창
Introduction
오랜만에 스터디에 참여 하면서 다른 사람들과 생각과 지식을 공유 해보려 해! 함께하는건 즐거운 일이지. 새로운 사람들을 알아가는 것도 즐겁지~!
2장 : 코틀린 기초
함수와 변수
함수
함수는 다음처럼 fun
키워드를 활용해 구성 할 수 있어.

코틀린에서는 expression
을 쉽게 표현 할 수 있어. (이런 상황을 Convert block body to expression body
로 변환 한다고 해.)
AS-IS
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
TO-BE
fun max(a: Int, b: Int): Int = if (a > b) a else b
또한 반환 타입도 생략이 가능하지.
AS-IS
fun max(a: Int, b: Int): Int = if (a > b) a else b
TO-BE
fun max(a: Int, b: Int) = if (a > b) a else b
위처럼 변환할 수 있는 이유는 타입 추론 덕분이야. 반환 값을 추론 할 수 있다면 생략이 가능하다는 이야기지!
변수
변수는 타입을 표기 하거나 생략 할 수 있어.
var question = "안녕"
var question: String = "안녕"
생략하는게 더 깔끔해보이지? 그러나 표기해야 하는 경우는 초기화하지 않았을 때야.
var question: String
question = "안녕"
변수를 표현하는 방법은 val
, var
두 가지야.
val
:immutable
변수야.value
약자야.var
:mutable
변수야.variable
약자야.
변경 여부에 따라 결정하면 돼.
클래스와 프로퍼티
클래스
클래스는 우리가 아는 그 클래스가 맞아. 자바에서는 클래스를 만들 때 많은 요소들이 필요하지만 코틀린은 그렇지 않아.
AS-IS
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
public String getName(){
return name;
}
}
TO-BE
class Person(val name: String)
여기에서 없어진 부분을 찾아보자.
Getter
를 명시하지 않아도 된다.public
키워드를 명시하지 않아도 된다.final
키워드를 명시하지 않아도 된다.- 본문이 없는 경우 본문을 명시하지 않아도 된다.
생략 할 수 있는 부분이 정말 많아.
프로퍼티
프로퍼티는 상태를 의미하게 돼. 결국 객체 안에 저장된 정보이지. 자바는 객체 내부에 저장된 프로퍼티를 관리하려면 많은 관리가 필요하지만 코틀린은 간단하게 관리가 가능해!
val
:Getter
만 제공한다.var
:Getter
와Setter
를 제공한다.
다음처럼 변경하고 싶은 여지를 두고 싶으면 다음처럼 구성하면 돼.
class Person (
val personNumber: Int,
var name: String,
var isMarried: Boolean
)
사용방법은 쉬워 다음처럼 프로퍼티를 호출하면 돼.
val 건창 = Person(1, "건창", false)
println(건창.personNumber)
println(건창.name)
println(건창.isMarried)
var
로 선언된 프로퍼티는 다음처럼 변경하게 되면 객체 프로퍼티가 변경 돼.
val 건창 = Person(1, "건창", false)
건창.name = "건창아님" // 가능
건창.isMarried = true // 가능
만약 val
로 선언된 personNumber
를 변경하려해도 변경되지 않으니 주의하자.
val 건창 = Person(1, "건창", false)
건창.personNumber = 3 // 불가능!!!
커스텀 접근자
대부분 프로퍼티는 값을 저장하는 필드(backing field
)가 있지만 없는 경우 커스텀 접근자를 활용해 구성할 수 있어.
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() {
return height == width
}
}
만약 이런 방법이 가독성이 떨어진다고 생각하면 다음처럼 사용해보자!
class Rectangle(val height: Int, val width: Int) {
val isSquare = height == width
}
선택 표현과 처리
enum
enum
은 class
가 없으면 존재하지 못해서 soft keyword
라고 불려. 자바와 다른점은 단순히 값만 나열하는 게 아니라 프로퍼티나 메서드를 정의 할 수 있어.
enum class Color(
val red: Int, val green: Int, val blue: Int // 프로퍼티 선언
) {
RED(255, 0, 0), ORANGE(255, 165, 0), // 상수 선언
GREEN(0, 255, 0), BLUE(0, 0, 255); // 세미콜론 필수!!
fun rgb() = (red * 256 + green) * 256 + blue // 함수 정의
}
when
when
은 자바의 switch
문을 대체 할 수 있어. when
을 사용하는 케이스는 다음과 같아.
enum
다루기- 임의 객체 다루기
- 인자 없이 사용
각각에 맞게 예시를 들어볼게
enum 다루기
enum을 활용할 때는 모든 프로퍼티를 사용해야 하나봐! 더욱 안정적이게 사용 할 수 있어.
fun getLooseName(color: Color) = when(color) {
Color.RED -> "REEEED"
Color.ORANGE -> "Bluuuue"
Color.GREEN -> "Greeen"
Color.BLUE -> "blllue"
}
동일한 값을 제공해야 한다면 다음처럼 표현 할 수 있어.
fun getLooseName(color: Color) = when(color) {
Color.RED, Color.ORANGE, Color.GREEN, Color.BLUE -> "COOOOOLOR"
}
임의 객체 다루기
임의 객체, 즉 여러 개의 객체를 다룰 떄는 다음처럼 구성 할 수 있어.
fun mix(c1: Color, c2: Color) = when(setOf(c1, c2)) {
setOf(Color.BLUE, Color.RED) -> Color.ORANGE
setOf(Color.BLUE, Color.GREEN) -> Color.BLUE
else -> throw Exception("Ambiguous color")
}
모든 조건을 표현 할 수 없을 때는 else 키워드를 붙일 수 있어!
인자 없이 사용
위와 같은 방법은 비교하기 위해 많은 Set 인스턴스
를 사용하고 있음을 알 수 있어. 이런 경우는 인자를 받지 않고도 비교할 수 있는 방법을 사용할 수 있지.
fun mix(c1: Color, c2: Color) = when {
(c1 == Color.BLUE && c2 == Color.RED) || (c1 == Color.RED && c2 == Color.BLUE) -> Color.ORANGE
(c1 == Color.BLUE && c2 == Color.GREEN) || (c1 == Color.GREEN && c2 == Color.BLUE) -> Color.BLUE
else -> throw Exception("Ambiguous color")
}
위처럼 하면 성능은 좋아지겠지만 가독성이 떨어지게 돼. 어떤 방법을 사용할 지는 고민해보고 결정하자.
스마트 캐스트
코틀린에서는 어떤 블록에 접근할 때 캐스팅이 되는지 판별했다면 블록 내부는 자동으로 변환된 객체를 사용 할 수 있어.
fun getString(str: Any): String {
if(str is String) {
return str
}
return "No String"
}
스마트 캐스트를 사용할 때 주의할 점은
val
인 경우만 사용 할 수 있어.
명시적으로 타입 캐스팅을 하려면 as
키워드를 사용하면 돼. 잘.. 동작은 안하겠지만 말이야!
fun getString(str: Any): String {
return str as String
}
이터레이션
수열 이터레이션
코틀린에서는 수열을 간단하게 생성 할 수 있도록 지원하고 있어. 시작과 끝과 함께 ..
연산자를 사용하면 돼.
val oneToTen = 1..10
Map 이터레이션
Map 과 같은 이터레이션은 다음처럼 사용 할 수 있어.
import java.util.TreeMap
val map = TreeMap<String, String>()
for ((key, value) in map) {
println("$key and $value")
}
in 키워드
in
키워를 활용해 범위에 속하는 지 판단 할 수 있어.
fun isZeroToTen(i: Int) = i in 0..10
예외 처리
예외
코틀린 예외는 비슷하지만 expression
임을 명심하자. expression
은 다른 expression
에 포함될 수 있어.
다음처럼 말이지!
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException()
try, catch, finally
try-catch-finally 는 자바와 유사하지만 expression
임을 명심해야 해.
import java.io.BufferedReader
fun readInt(reader: BufferedReader) {
val int = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
}
또한 catch 절 입력 값에 따라 예외 전파 방법이 변경돼. 위와 같이 작성되면 예외가 전파되고 다른 값을 전달하면 다른 값이 전달되겠지.
여담으로 코틀린은 자바와 달리 체크 예외인 경우 throws XXXException 이 붙지 않아. 자바에서 사용하는 오류 발생을 막기 위한 예외 잡는 방식이 오류 발생을 방지하지 못할 뿐더러 불필요하기 때문이래.
3장 : 함수 정의와 호출
컬렉션
코틀린은 자바와의 호환성을 위해 자바 컬렉션을 활용하고 있어. 덕분에 자바와 코틀린 간 연결에서 컬렉션이 변경되지 않지.
val list = listOf(1, 2, 3)
val set = setOf(1, 2, 3)
val map = mapOf(1 to "one", 2 to "two", 3 to "three")
특정한 자료구조를 직접 선택 할 수도 있어.
val list = arrayListOf(1, 2, 3)
val set = hashSetOf(1, 2, 3)
var map = hashMapOf(1 to "one", 2 to "two", 3 to "three")
함수 호출
편리한 함수 호출
자바와는 달리 함수 호출을 유연하게 구성할 수 있어. 코틀린은 컬렉션을 String
으로 반환해야 할 때 toString()
함수를 호출하기도 하지.
val list = listOf(1, 2, 3)
println(list) // list.toString() 호출!!
그리고 파라미터 힌트를 직접 제공한다거나 기본 값으로 설정해 값을 전달하지 않아도 되거나 다양한 방법이 있지. 다음과 같은 함수가 있다고 생각해볼게.
fun <T> joinToString(
collection: Collection<T>,
separator: String,
prefix: String,
suffix: String
): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(suffix)
return result.toString()
}
아래처럼 호출도 가능하지만 이름에 인자를 붙여 전달 할 수 있어.
val list = listOf(1, 2, 3)
println(joinToString(list, ",", "[", "]"))
다음처럼 인자 일부를 명시해서 전달 할 수 있어.
val list = listOf(1, 2, 3)
println(joinToString(list, separator = ",", prefix = "[", suffix = "]"))
자바에서는 동일한 자료구조에 잘못된 값을 전달하는 경우가 존재했는데, 코틀린에서는 이런 인지 오류를 일부 해소할 수 있어보여.
디폴트 파라미터
코틀린에서는 오버로딩으로 인한 일부 중복을 막기 위해서 디폴트 파라미터를 지정할 수 있게 구현되어 있어.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix: String = "[",
suffix: String = "]"
){
//...
}
그럼 다음처럼 사용 할 수 있지.
println(joinToString(list, prefix = "[", suffix = "]"))
println(joinToString(list, separator = ",", suffix = "]"))
println(joinToString(list, ",")) // separator 로 설정됨
코틀린에서 일반 호출 문법을 사용하게 되면 함수를 선언할 때와 같은 순서로 인자를 지정하게 돼. 그런 경우 일부 생략하면 뒷부분의 인자가 생략되니 유의해야 해.
자바에서 디폴트 파라미터 함수를 사용하는 경우
자바에서 디폴트 파라미터가 포함된 함수를 호출 할 때는 코틀린 함수에 @JvmOverloads
를 추가해야 자바도 동일하게 사용 할 수 있어.
@JvmOverloads
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix: String = "[",
suffix: String = "]"
) {
//...
}
@JvmOverloads
를 추가하지 않으면 기본 함수 형식만 사용 가능해.
확장
확장 함수
코틀린을 기존 자바 프로젝트에 통합하는 경우 코틀린으로 변환할 수 없는 경우 다음처럼 확장 함수로 기능을 확장해 사용 할 수 있어.
fun String.lashChar(): Char = this.get(this.length - 1)
println("kotlin".lashChar())
확상 함수를 만드려면 확장할 클래스 이름만 붙이면 돼. 이 때 클래스 이름을 receiver type
이라 하고 호출 되는 값을 receiver object
라고 해.
fun /*receiver type is string*/String.lashChar(): Char
= /*receiver object*/this.get(/*receiver object*/this.length - 1)
그리고 일반 메서드처럼 확상 함수 본문에 this
를 생략할 수 있어.
fun String.lashChar(): Char = get(length - 1)
확장 함수를 사용한다고 해도 캡슐화가 깨지지 않으니 안심해도 돼. 즉, 비공개(private) 되거나 보호된(protected) 멤버에 접근 할 수 없어.
자바에서 확장함수를 사용할 때는 확장 함수가 추가된 코틀린 파일을 호출 해 사용하면 돼.
char c = StringUtilKt.lastChar("Java");
확장 함수 주의 사항
확장 함수를 통해 쉽게 기능을 확장 할 수 있지만 꽤나 제약사항이 많아.
- 확장 함수와 멤버 함수 이름이 동일한 경우 멤버 함수가 호출된다.
- 확장 함수는 정적으로 결정되기 때문에 오버라이드 할 수 없다.
이런 제약이 있다는 것을 인지해야 해.
확장 프로퍼티
프로퍼티도 확장 할 수 있는데, getter 와 setter 를 명시해야 해. 그리고 확장 프로퍼티는 뒷바침하는 필드가 없기 때문에 getter는 꼭 정의해줘야 해!
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value) {
setCharAt(length - 1, value)
}
println(StringBuilder("Kotlin").lastChar)
컬렉션 처리
코틀린은 자바 컬렉션 API를 확장해서 관리하고 있어. 꽤나 많은 확장 함수를 제공하고 있으니 필요할 때마다 찾아봐야겠어.
가변 인자 함수
varargs
키워드를 사용하면 리스트 생성할 때 원소를 나열해야하는 번거로움을 코틀린에서는 쉽게 해결 할 수 있어.
바로 스프레드(*) 연산자가 이를 도와주고 있어.
fun main(args: Array<String>) {
val list = listOf("args: ", *args)
println(list)
}
중위 호출과 구조 분해 선언
코틀린에서는 Map 자료구조를 간단하게 구성할 수 있지. 그건 바로 중위 호출(infix call)을 통해 to
라는 일반 메서드를 호출 할 수 있기 때문이다.
val map = mapOf(1 to "one")
중위 호출은 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는 방법을 말해. to
메서드 말고 kotest
의 shouldBe
등에서도 활용 할 수 있어.
구조 분해 선언은 다음처럼 두 변수를 즉시 초기화하는 방법을 의미해.
val (number, name) = 1 to "one"
문자열과 정규식
코틀린 문자열은 자바 문자열과 동일해. 그래서 자바 메서드로 넘겨도 문제가 생기지 않지. 코틀린은 확장 함수로 다양한 기능을 제공하고 있어.
문자열 나누기
코틀린은 여러 개의 문자로 문자열을 나눌 수 있도록 확장 함수를 제공하고 있어.
println("12.345-6.A".split("\\.|-".toRegex()))
코틀린에서는 toRegex 확장 함수를 활용해 정규식으로 쉽게 변경 가능하지.
아니면 split 확장 함수에는 여러 개의 문자열을 받는 경우도 있어.
println("12.345-6.A".split(".", "-"))
text block
코틀린은 text block
을 제공하고 있어. 원래 이스케이핑하려면 //.
라고 써야하지만 \.
로 사용해도 되지.
val regex = """(.+)/(.+)/.(.+)""".toRegex()
text block
은 이스케이핑 말고도 줄 바꿈 등 편한 기능을 제공하고 있어.
val text = """
hello
hi
"""
로컬 함수 확장
코틀린은 함수에서 추출한 함수를 중첩시킬 수 있어. 덕분에 DRY 원칙을 어느정도 지킬 수 있지.
fun save(from: String, to: String, chat: String) {
fun validate(name: String, chat: String) {
if (chat.isEmpty()) {
throw IllegalArgumentException("${name}은 아무 말도 하지 않았어요")
}
}
validate(from, chat)
validate(to, chat)
}
확장 함수를 통해 로컬 함수를 추가 할 수도 있지만 관리가 어려워질 수 있으니 주의하자.
통찰
statement 와 expression
위 예제 중 다음과 같은 함수 본문에서 특이한 점은 코틀린에서 if 문은 expression
이라는 점이야.
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
자바에서는 사용할 수 없는 코드지.
public class A{
public int max(int a, int b) {
// 사용할 수 없는 코드
// return if (a > b) a else b
if (a > b) return a;
return b;
}
}
여기서 확인 할 수 있는 점은 자바의 if
는 statement
이고 코틀린의 if
는 expression
이라는 점이야.
statement
는 자식을 둘러싼 상위 블록 요소로 존재하며 아무 값을 만들어내지 않지. 반면expression
은 값을 만들어 다른 식의 하위 요소로 계산에 참여할 수 있어! 이러한 특징을 잘 활용하면 간결하게 코드를 구성 할 수 있어.
그리고 코틀린은 대입문을 statement
로 사용하고 있으니 주의하자고. 즉, 제어 구조는 expression
, 대입문은 statement
야.
enum 으로 할 수 있는 멋진 일
enum 으로 어떤 일을 할 수 있을지 고민해봤는데, 요즘 코틀린으로 로또 게임기를 구현하는데 요긴하게 쓰일 수 있어 보였어! 로또 게임기의 결과로 등수와 상품을 함께 전달해야 하는 데, 여기에서 쉽게 관리 할 수 있지 않을까 싶어.
import java.math.BigDecimal
enum class Prize(
val rank: Int,
val money: BigDecimal
) {
FIRST_PRIZE(1, BigDecimal(1_000_000_000)),
SECOND_PRIZE(2, BigDecimal(50_000_000)),
THIRD_PRIZE(3, BigDecimal(1_000_000)),
FOURTH_PRIZE(4, BigDecimal(50_000)),
FIFTH_PRIZE(5, BigDecimal(5_000)),
QQUANG(6, BigDecimal(0));
fun getPrize() = money
}
약간 아쉬운 건 값만 열거하는 친구가 된 것 같아. 다음에 더 멋지게 사용 할 수 있는 일을 고민해보자!
@JvmOverloads
아래와 같은 함수가 존재 할 때 @JvmOverloads
붙인 경우와 붙이지 않는 경우 바이트 코드가 어떻게 생성되는지 확인해보자.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix: String = "[",
suffix: String = "]"
) {
//...
}
붙이지 않는 경우는 다음처럼 하나의 메서드만 존재하게 돼.
public static synthetic joinToString$default(Ljava/util/Collection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
그러나 붙이게 된다면 4 개의 메서드가 생성되게 되지.
public static synthetic joinToString$default(Ljava/util/Collection;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
public final static joinToString(Ljava/util/Collection;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public final static joinToString(Ljava/util/Collection;Ljava/lang/String;)Ljava/lang/String;
public final static joinToString(Ljava/util/Collection;)Ljava/lang/String;
아쉽게도 인자 이름을 함께 전달 할 수는 없지만 오버로딩을 할 필요가 없어져서 코드가 매우 간결해져.