Kotlin을 공식 문서를 통해 배우는 글 연재를 시작합니다. 공식 문서의 Basics와 Concepts 부분을 다루려고 합니다. 기본적으로 공식 문서의 내용과 순서대로 1대 1 번역 수준으로 진행되며, 필요에 따라 내용이 첨삭될 예정입니다. Basics와 Concepts 부분은 Kotlin의 기본 내용이기도 하고, JVM하에서 Kotlin을 사용하는 경우라면 필수로 읽어 봐야 하는 내용이기도 합니다.
Kotlin 기본 문서는 묵시적이지만 어느 정도 Java와 프로그래밍에 익숙한 사람이 보는 것을 전제 하는 듯 합니다. 그래서, 처음 프로그래밍을 시작하는 분들에게는 적합하지 않습니다. 이 연재도 공식 문서를 1대 1 번역하는 식으로 진행되기 때문에 프로그래밍을 시작하시는 분들에게는 적합하지 않을 수 있습니다. 이 점 참고 부탁 드립니다.
Basics 부분은 Kotlin 문법을 빠르게 살펴보게 하려는 게 목적인 듯합니다. 그래서, 예제 코드로 아주 간결하게 보여주고 설명이 많지 않습니다. 그래서, Basic이지만 프로그래밍 경험이 좀 있는 분들에게 오히려 적합니다. 개인적 생각으로는 Java 프로그래머가 Kotlin을 시작하려고 할 때 빠르게 살피기에 최적의 문서가 아닌가 싶습니다.
그럼 첫번째로 Basic Systax를시작합니다.
패키지 정의 및 import
package my.demo
import kotlin.text.*
// ...
Kotlin에서는 (Java 처럼)패키지와 디렉토리가 일치하지 않아도 괜찮습니다. 즉, 위의 예제라면 Java의 경우 해당 파일은 my/demo라는 디렉토리에 위치해야 하지만 Kotlin은 그렇지 않아도 괜찮습니다.소스 파일은 임의의 다른 디렉토리에 위치해도 상단에 선언된 패키지를 따르게 됩니다. 다만, Java 코드와 함께 사용할 때는 자바의 구조를 따라줘야 합니다. 이 부분은 추후 코딩 규칙에서 다시 다루게 됩니다.
프로그램 시작 지점
Kotlin 애플리케이션의 시작 지점은 main 함수입니다.
fun main() {
println("Hello world!")
}
main 함수는 다음과 같이 여러개의 문자열 인수를 받는 형태도 가능합니다.
fun main(args: Array<String>) {
println(args.contentToString())
}
표준출력(standard output)으로 출력
print 함수는 표준 출력으로 전달 받은 인수를 출력합니다.
print("Hello ")
print("world!")
// 결과: Hello world!
println은 전달 받은 인수를 출력하고 줄 바꿈을 추가적으로 출력합니다.
println("Hello world!")
println(42)
// 결과
// Hello world!
// 42
함수
다음은 두 개의 Int 형 매개변수를 갖고 반환형이 Int인 함수입니다.
// fun 함수이름(매개변수정의): 반환형
fun sum(a: Int, b: Int): Int {
return a + b
}
Java에서는 함수가 객체에 종속된 형태인 메소드로만 쓰이지만, Kotlin에서는 메소드 형태뿐만 아니라 단일형의 함수로도 사용되고 최상위 수준(top level)의 함수도 정의해서 사용할 수 있습니다. Kotlin에서 함수는 메소드든 최상위 수준이든 기본적으로 fun 키워드를 사용해서 정의한다는 것을 기억하시면 됩니다. Basics 부분에서는 이후에도 그렇겠지만 빠르게 기본만을 설명합니다. 상세한 내용이 궁금한 분들은 함수를 참고하시면 됩니다.
함수의 몸체는 표현식도 가능합니다. 이런 경우 반환형은 추론됩니다.
fun sum(a: Int, b: Int) = a + b
함수는 명시적인 반환값이 없을 수도 있습니다.
// 반환형 Unit
fun printSum(a: Int, b: Int): Unit {
println("sum of $a and $b is ${a + b}")
}
Unit 반환형은 다음과 같이 생략될 수 있습니다.
fun printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}
변수
읽기 전용 지역변수는 val 키워드를 사용하여 정의합니다. 이런 변수는 값을 한 번만 할당할 수 있습니다. Kotlin에서 변수의 타입은 변수 이름 뒤에 지정합니다.
val a: Int = 1 // 바로 할당
val b = 2 // 타입을 지정하지 않아도 `Int` 타입이 추론됩니다.
val c: Int // 초기화 값이 할당되지 않은 경우 (당연한 얘기지만) 타입을 추론할 수 없으므로 타입 지정이 필요합니다.
c = 3 // 변수 선언시 값을 할당하지 않고 지연된 할당
본 연재에서는 type에 대해서 타입과 형을 혼용해서 사용할 예정입니다. '변수의 타입을 추론한다', '반환형이 무엇이다' 처럼 경우에 따라 문장이 매끄러운 쪽으로 혼용할 예정입니다.
var 키워드로 정의한 변수는 값을 재할당 할 수 있습니다.
var x = 5 // `Int` 타입은 추론됩니다.
x += 1 // 6이 재할당 됨
(비교적) 최근에 만들어지거나 기능이 추가되고 있는 언어들(ES6, TypeScript, Kotlin, Rust, Go 등등)에서는 불변성을 중시하여 변수 선언을 불변성 여부로 구분하여 선언하게 하고 있습니다. 이에 대해 언제 어떤 걸 써야 하나 헷갈리는 경우도 있고, 그래서 이에 대해서 상세히 설명하는 글들도 많은 거 같은데요, 기억하는 방법은 간단합니다. 재할당(reassignment)이라는 단어만 기억하시면 됩니다. 즉, 값을 다시 할당할지 여부에 따라서 구분해서 사용하면 됩니다.
변수는 최상위 수준(top level)에 선언할 수 있습니다.
val PI = 3.14
var x = 0
fun incrementX() {
x += 1
}
클래스 정의와 인스턴스화
class 키워드를 사용하여 클래스를 정의합니다.
class Shape
프로퍼티는 클래스 정의 부분이나 몸체에 정의할 수 있습니다.
// height, length, perimeter 모두 프로퍼티입니다.
class Rectangle(val height: Double, val length: Double) {
val perimeter = (height + length) * 2
}
Kotlin에서 프로퍼티(property)는 필드 + 접근자(getter, setter)라고 볼 수 있습니다. 지금은 (Java에 익숙하시다면) Java의 필드와 같다고 생각하셔도 무방합니다.
클래스 정의 부분에 열거된 매개변수들을 가진 생성자는 자동으로 생성되기 때문에 기본적으로 사용할 수 있습니다.
// 바로 위 소스 코드 같이 클래스를 정의한 경우 다음과 같이 (아무런 정의 없이) 생성자를 사용할 수 있습니다.
val rectangle = Rectangle(5.0, 2.0)
// ${rectangle.perimeter} 이 부분은 문자열 템플릿에서 변수를 표현하는 방법입니다.
// 조금 아래 바로 설명이 나옵니다.
println("The perimeter is ${rectangle.perimeter}")
클래스 간의 상속은 : 으로 선언됩니다. 클래스는 기본적으로 final입니다. 즉, 상속이 불가합니다. 클래스를 상속되게 하려면 open 키워드를 추가해야 합니다.
// Shape 클래스가 상속 가능하게 open 지정
open class Shape
// : Shape() 상속
class Rectangle(val height: Double, val length: Double): Shape() {
val perimeter = (height + length) * 2
}
추후에도 이런 부분들이 나오겠지만, 개인적으로 Kotlin을 매우 좋아하는 이유가 이런 실용적이고 진보적(?)인 부분입니다. 좋은 OOP 설계 방법론 등에서는 상속은 확실한 "is-a" 관계가 아닌 경우에는 사용을 자제할 것을 권하고 있습니다. 상속보다는 조립(composition)을 권고하고 있으며, 기능의 확장이라는 측면에서 확실한 경우에만 상속을 사용하라고 합니다. 이런 내용은 권고안 같은 것이기 때문에 선택의 문제여서 언어에 기본 규칙으로 바로 적용하기는 쉽지 않습니다. 하지만, 코틀린은 그렇게 했습니다.
주석
대부분의 현대 언어들처럼, Kotlin은 한 줄과 여러 줄(블록) 주석을 지원합니다
// This is an end-of-line comment
/* This is a block comment
on multiple lines. */
블록 주석은 중첩될 수 있습니다.
/* The comment starts here
/* contains a nested comment */
and ends here. */
문자열 템플릿
스트링 템플릿, 템플릿 스트링 등으로 불릴 수 있습니다. 현업에서는 스트링 템플릿으로 더 많이 말하지 않을까 생각됩니다.
var a = 1
// 단순한 변수명 같은 경우 중괄호 없이 $사용
val s1 = "a is $a"
// s1은 "a is 1"
a = 2
// 임의의 표현식인 경우 ${}
val s2 = "${s1.replace("is", "was")}, but now is $a"
// s2는 "a was 1, but now is 2"
조건 표현식
fun maxOf(a: Int, b: Int): Int {
if (a > b) {
return a
} else {
return b
}
}
Kotlin에서는 if 를 표현식(expression)으로 다음과 같이 사용할 수 있습니다.
fun maxOf(a: Int, b: Int) = if (a > b) a else b
for 반복문
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
println(item)
}
while 반복문
val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
println("item at $index is ${items[index]}")
index++
}
when 표현식
fun describe(obj: Any): String =
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}
when은 문(statement)으로 사용 가능합니다. 추후 Concepts의 when 표현식에서 상세히 설명됩니다.
범위
다음은 숫자가 특정 범위 안에 포함되는지 in 연산자를 사용하여 확인하는 코드입니다.
val x = 10
val y = 9
if (x in 1..y+1) {
println("fits in range")
}
다음은 숫자가 범위 밖인지 확인하는 코드입니다.
val list = listOf("a", "b", "c")
if (-1 !in 0..list.lastIndex) {
println("-1 is out of range")
}
if (list.size !in list.indices) {
println("list size is out of valid list indices range, too")
}
범위만큼 반복은 다음과 같이 할 수 있습니다.
for (x in 1..5) {
print(x)
}
진행 방향이나 단위도 지정할 수 있습니다.
// 13579
for (x in 1..10 step 2) {
print(x)
}
println()
// 9630
for (x in 9 downTo 0 step 3) {
print(x)
}
컬렉션
다음은 컬렉션의 각 항목을 열거하는 코드입니다.
fun main() {
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
println(item)
}
}
특정 객체가 컬렉션에 포함돼 있는지 in 연산자로 확인할 수 있습니다.
fun main() {
val items = setOf("apple", "banana", "kiwifruit")
when {
"orange" in items -> println("juicy")
"apple" in items -> println("apple is fine too")
}
}
람다 표현식을 사용하여 필터링하거나 map 연산을 할 수 있습니다.
val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
.filter { it.startsWith("a") }
.sortedBy { it }
.map { it.uppercase() }
.forEach { println(it) }
널 가능(nullable) 값과 널 확인
협업에서는 그냥 널어블이라고 얘기하는 경우가 더 많을 겁니다. 본 연재에서는 경우에 따라 적절히 사용하겠습니다.
null이 될 수 있는 참조(reference)에는 명시적으로 널 가능성(nullable)이 있다고 표기해 줘야 합니다. 널 가능한 타입명에는 끝에 ?를 붙입니다.
예를 들어 str이 숫자 문자열을 가지지 않은 경우 null 반환하는 함수를 정의하는 경우 다음과 같이 합니다.
fun parseInt(str: String): Int? {
// ...
}
위에 정의한 null 값을 반환하는 함수는 다음과 같이 사용합니다.
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// Using `x * y` yields error because they may hold nulls.
if (x != null && y != null) {
// x and y are automatically cast to non-nullable after null check
println(x * y)
}
else {
println("'$arg1' or '$arg2' is not a number")
}
}
fun main() {
printProduct("6", "7")
printProduct("a", "7")
printProduct("a", "b")
}
또는 다음과 같이도 가능합니다.
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}
fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)
// ...
if (x == null) {
println("Wrong number format in arg1: '$arg1'")
return
}
if (y == null) {
println("Wrong number format in arg2: '$arg2'")
return
}
// x and y are automatically cast to non-nullable after null check
println(x * y)
}
fun main() {
printProduct("6", "7")
printProduct("a", "7")
printProduct("99", "b")
}
많은 프로그래머를 괴롭혀 왔고, 심지어는 만든 사람인 토니 호어도 십억 달러의 실수라고 인정한 null에대해서 kotlin은 언어 차원에서 어느정도 강제를 통해 완화할 수 있는 방법을 제시하고 있습니다. Java를 쓰다가 Kotlin을 처음 접해서 쓰다보면 Java때는 별로 신경쓰지 않던 널 가능성과 널 확인 때문에 번거롭고 곤욕스러울 때가 있습니다. 하지만, 쓰다보면 null 예외 발생에 대해서 굉장히 잘 방어할 수 있는 좋은 규칙이라는 것을 느끼게 됩니다. 상세한 내용은 추후 널 안정성 부분에 다뤄지게 됩니다.
타입 확인(type checks)과 자동 캐스트(cast)
보통 cast를 동사로 보고 캐스팅이라는 용어나 형변환이라는 말도 많이 쓰입니다만, 공식 문서에서도 그렇고 cast가 명사이기도 하기 때문에 연재에서는 일단 형변환에 대해서 캐스트라는 용어를 사용합니다.
is 연산자는 특정 표현식이 특정 타입의 인스턴스인지 확인합니다. 불변 지역 변수나 프로퍼티가 특정 타입으로 확인되면, 명시적으로 캐스트를 할 필요가 없습니다.
fun getStringLength(obj: Any): Int? {
if (obj is String) {
// 이 분기에서 `obj`는 자동으로 `String`으로 캐스트됩니다.
return obj.length
}
// 타입 확인 분기 바깥 영여에서는 `obj` 여전히 `Any` 타입입니다.
return null
}
fun main() {
fun printLength(obj: Any) {
println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ")
}
printLength("Incomprehensibilities")
printLength(1000)
printLength(listOf(Any()))
}
또는,
fun getStringLength(obj: Any): Int? {
if (obj !is String) return null
// `obj` 자동으로 `String`으로 캐스트됩니다.
return obj.length
}
fun main() {
fun printLength(obj: Any) {
println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ")
}
printLength("Incomprehensibilities")
printLength(1000)
printLength(listOf(Any()))
}
또 다른 방법으로는 이렇게도 할 수 있습니다.
fun getStringLength(obj: Any): Int? {
// `&&`의 오른쪽 부분에서 `obj`는 `String`으로 자동 캐스트 됩니다.
if (obj is String && obj.length > 0) {
return obj.length
}
return null
}
fun main() {
fun printLength(obj: Any) {
println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ")
}
printLength("Incomprehensibilities")
printLength("")
printLength(1000)
}
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 Kotlin - 6. Basic Types - Booleans (0) | 2024.03.02 |
---|---|
공식 문서로 배우는 Kotlin - 5. Basic Types - Unsigned integer types (0) | 2024.03.01 |
공식 문서로 배우는 Kotlin - 4. Basic Types - Numbers (0) | 2024.03.01 |
공식 문서로 배우는 Kotlin - 3. Basics - Coding conventions (0) | 2024.02.29 |
공식 문서로 배우는 Kotlin - 2. Basics - Idioms (0) | 2024.02.27 |