코딩하는 개굴이

[Kotlin IN ACTION] 코틀린 기초 문법 본문

안드로이드/KOTLIN

[Kotlin IN ACTION] 코틀린 기초 문법

개굴이모자 2021. 3. 21. 16:41
반응형

본 내용은 Kotlin IN ACTION (드미트리 제메로프, 스베트라나 이사코바 지음 / 에이콘 출판사) 책을 기반으로 작성되었습니다.

Kotlin IN ACTION 2강 : 코틀린 기초

Hello World!

fun main(args: Array<String>) {
    println("Hello World!")
}

해당 코드에서 코틀린의 특징을 알아보자.

  • 함수를 선언 시에, fun 키워드를 사용한다.
  • 파라미터 이름 뒤에 파라미터의 타입을 쓴다. (변수 선언 시에도 마찬가지)
  • 함수를 최상위 수준에 정의할 수 있다. (클래스 안에 함수를 넣어야 할 필요가 없다.)
  • 배열 처리를 위한 문법이 따로 존재하지 않는다.
  • 출력 시 println을 사용한다. (표준 자바 라이브러리 함수를 간결하게 사용 할 수 있도록 감싼 wrapper 를 제공하기 때문에 가능한 것)
  • 세미 콜론을 붙이지 않아도 된다.

 

함수

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

해당 식에서 코틀린의 특징을 알아보자.

  • 함수 선언 키워드인 fun 뒤에는 함수 이름이 온다.
  • 함수 이름 뒤에는 괄호 안에 파라미터 목록이 온다.
  • 함수의 반환 타입은 파라미터 목록 다음에 콜론(:)으로 구분 후 다음에 온다.
  • 코틀린에서의 if 는 식이다.
  • 해당 코드는 본문이 중괄호로 둘러싸인 함수 즉, 블록이 본문인 함수이나, 등호화 식으로 이루어진 식이 본문인 함수로 변환할 수 있다.
    • 코틀린에서는 아래와 같은 형태로, if, when, try 등에도 사용한다.
    • 식이 본문인 함수에 한해서 반환 타입을 생략 가능하다.
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

 

식과 문

자바에서 주로 if 를 표현 시 if문이라고 배웠을 것이다. 그러나, 코틀린에서의 if는 식에 해당한다. 둘은 각각 정확히 무엇인가?

  • 식 : 값을 만들어 내며, 다른 식의 하위 요소로 계산에 참여할 수 있다.
  • 문 : 자신을 둘러써고 잇는 가장 안쪽 블록의 최상위 요소로 존재하며, 아무런 값을 만들어 내지 않는다.
  • 코틀린에서의 식 : 코틀린에서는 루프를 제외한 대부분의 제어 구조가 식에 해당한다.

 

변수

val doraemonHeight = 129.3

//타입을 명시해도 된다
val doraemonHeight: Double = 129.3
  • 타입을 지정하지 않을 시, 컴파일러가 초기화 식을 분석하여 초기화 식의 타입을 변수 타입으로 지정
  • val : 변경 불가능한 참조를 저장하는 변수로, 초기화하고나면 재대입이 불가한 final 에 해당하는 변수
    • 기본적으로 모든 변수를 val 로 선언해 사용하고 꼭 필요할 때만 var 을 사용할 것을 권장
    • 참조 자체는 변경될 수 없지만 참조가 가리키는 객체의 내부 값은 변경될 수 있다. (예시 > array 추가 등)
  • var : 변경 가능한 참조로, 일반 변수에 해당
    • 변수의 값을 변경할 수는 있지만 변수의 타입을 변경할 수는 없다

 

문자열 형식 지정

Hello World 예제를 사용자의 이름을 받아 인사를 출력하는 프로그램을 구현해 보자.

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "World" //만일 사용자가 인자를 넘기지 않았다면 Hello Wordl!을 출력한다.
    println("Hello $name!") //문자열 템플릿
}

//중괄호를 이용해 문자열 템플릿에 식을 넣어 더 간략하게 사용 가능
fun main(args: Array<String>) {
    println("Hello ${if (args.size > 0) args[0] else "World"}!") //문자열 템플릿
}
  • 문자열 템플릿 : $을 앞에 붙여 변수를 문자열 안에 사용할 수 있다
    • 컴파일러는 각 식을 정적으로 컴파일 시점에 검사하며, 이때 존재하지 않는 변수가 발견될 경우 컴파일 오류를 발생시킨다.
    • 참고 : $을 문자열에 넣고 싶을 경우 \을 앞에 붙인다.
    • 중괄호로 둘러 쌓아 식의 형식을 넣을 수 있다.코틀린도 자바처럼 한글을 변수처럼 사용할 수 있기 때문에 문자열 템플릿 사용 시 '$name님이 접속하였습니다.' 와 같은 형태 사용 시 영문자와 한글을 동시에 인식하여 'name님' 을 변수로 인식할 수 있다. 따라서, 식이 아닌 일반 변수이 더라도 '${name}'의 형태로 쓰는 습관을 가지자.

 

클래스

자바에서의 클래스와 코틀린의 클래스 선언 형태를 비교해보자.

class Doraemon {
    private double height;
    public Doraemon(double height) {
        this.height = height;
    }
    public double getHeight() {
        return height;
    }
}

자바에서는 클래스의 필드가 늘어날수록 생성자의 파라미터와 getter/setter가 점점 늘어나게 된다.

그에 비해 Kotlin은 간단하게 해결이 가능하다.

class Doraemon(val height: Double) //정말 이게 끝이다. 어메이징 :D
  • java와 달리 클래스의 접근자 public이 생략되었는데, 코틀린은 기본 가시성이 java와 달리 public이므로, 생략해도 된다.

 

프로퍼티

클래스의 개념은 데이터를 캡슐화 하는 것에 있다. 자바에서는 필드와 접근자(getter/setter)를 한데 묶어 프로퍼티라고 부르나, 코틀린은 프로퍼티를 언어의 기본으로 제공하고 선언 시에 변수와 마찬가지로 val(읽기 전용) 이나 var(변경도 가능)을 사용한다.

class Doraemon(
    val height: Double,
    var weight: Double,
)

...
//사용
val miniDora = Doraemon(129.3, 129.3)
println(miniDora.height)

miniDora.weight = 132.9 //변경 가능한 프로퍼티의 경우 자바의 세터 대신 위와 같이 사용한다.
println(miniDora.weight)
  • 커스텀 접근자 : 별도의 필드에 저장할 필요가 없이 구현을 제공하는 getter만 존재하며, 프로퍼티에 접근할 때마다 getter가 프로퍼티 값을 매번 다시 계산한다.
class Doraemon(
    val height: Double,
    var weight: Double,
) {
    val doreamonBMI : Double 
        get(){
            return weight/(height * height)
    }
}

...
//사용
val miniDora = Doraemon(129.3, 129.3)
println(miniDora.doraemonBMI)

코틀린 소스 코드 구조

기본적인 import는 자바와 유사하다. 하위 정의된 함수와 프로퍼티를 구체적으로 import 가능하나, 상위를 import 하여 사용해도 컴파일에 문제가 없다. 코틀린에서는 클래스 뿐 아니라 함수와 프로퍼티까지 단위로 임포트 가능하다.

위의 코드를 예로 들어, package comics.doreamonfun getTools 라는 함수가 존재한다고 하면, import comics.doraemon.getTools를 하면 해당 함수만 import 가능하다.

  • 자바에서는 디렉터리 구조가 패키지 구조를 그대로 따라야 하여, comics package 아래 doraemon 패키지가 존재해야만 하나, 코틀린에서는 여러 클래스를 한 파일에 넣을 수 있고 이름도 자유롭게 정할 수 있기 때문에 패키지 구조와 디렉터리 구조가 일치해야 할 필요가 없다.
    • 여러 클래스를 한 파일에 넣는 것을 주저하지 않으면서, 일반적으로 자바의 방식대로 패키지 별로 디렉터리를 구성할 것을 권장한다.

enum 클래스 정의

enum class Tools { //enum class 를 사용해야 한다.
    POCKET, DOOR, COPTER, LIGHT, TIMEMACHINE
}
//위와 같이 단순히 열거할 수도 있으나, 클래스 안에 프로퍼티나 메소드를 정의할 수 있다.

enum class Tool(val name: String, val id: Int) {
    POCKET("주머니", 0), 
    DOOR("어디로든 문", 1), 
    COPTER("대나무 헬리콥터",2), 
    LIGHT("스몰라이트", 3), 
    TIMEMACHINE("타임머신", 4); //끝나는 부분에 반드시 세미콜론을 사용해야 한다.

    fun getInfo() = "[ID : ${id}] ${name}"
}
  • 코틀린에서 유일하게 세미콜론을 필수로 넣어야 한다.
    • enum클래스 안에 메소드를 정의하는 경우는 반드시 상수목록과 메소드 정의 사이에 구분을 위해 세미콜론을 넣어야 한다.

When

  • 자바에서의 switch 에 해당한다.
  • if 와 마찬가지로 식이며, 식이 본문인 함수에 바로 사용할 수 있다.
  • 자바와 같이 한 분기에 여러 식을 사용할 수도 있다.
  • 임의의 객체 형태 또한 허용하여 switch 보다 강력하다.
  • 각 분기의 조건이 boolean 결과를 계산하는 식이라면 when 에 인자가 없어도 된다.
import example.colors.Color
fun checkWarmColor(c: Color) = 
    when (c) {
        RED, ORANGE, YELLOW -> true
        BLUE, BLACK -> false
    }

fun mixColor(c1: Color, c2: Color) = 
    when (setOf(c1, c2)) {
        setOf(RED, YELLOW) -> ORANGE
        setOf(YELLOW, BLUE) -> GREEN
        setOf(RED, BLUE) -> PURPLE
        else -> throw Exception("Color Not Supported")
    }

//동일한 코드를 아래와 같이도 구현 가능하다.
fun mixColor(c1: Color, c2: Color) = 
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) -> ORANGE

        (c1 == YELLOW && c2 == BLUE) ||
        (c1 == BLUE && c2 == YELLOW) -> GREEN

        (c1 == RED && c2 == BLUE) ||
        (c1 == BLUE && c2 == RED) -> PURPLE

        else -> throw Exception("Color Not Supported")
    }

타입 캐스팅

  • 원하는 타입으로 명시적 타입 캐스팅 시 as 키워드를 사용한다.
    val value = v as Num
  • 원하는 타입이 맞는지 확인 시 is 키워드를 사용한다.
fun eval (e: Expr) : Int = 
    if(e is Num) {
        e.value
    } else if (e is Sum) {
        eval(e.right)+ eval(e.left)
    }

//when 으로 변경
fun eval (e: Expr) : Int = 
    when (e) {
        is Num -> e is Num
            e.value
        is Sum -> e is Sum
            eval(e.right)+ eval(e.left)
}   

이터레이션

while

while(조건) {
    //해당 조건이 참인 동안 반복 실행한다.
}

do {
    //맨 처음에 무조건 한번 실행 한 후, 조건이 참인 동안 반복 실행한다.
} while(조건)

for

for (i in 1..100) {
    print("count : ${i}")
}

for (i in 100 downTo 1 step 2) { 
    //downTo는 역방향 수열을 만든다
    //step은 증가 값의 절댓값이 2로 바뀐다.
    print("even count down : ${i}")
}

val list = arrayListOf("사이다", "콜라", "주스")
println("[메뉴]")
for((index, element) in list.withIndex()) {
    //인덱스와 함께 컬렉션을 이터레이션 한다.
    println("${index} : ${element}")
}

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

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "digit"
    in 'a'..'z', in 'A'..'Z' -> "letter"
    else -> "ERROR"
}

print("OrangeJuice" in "Milk".."Coffee") //"OrangeJuice" <= "Milk" && "OrangeJuice" <= "Coffee"와 동일하다

print("OrangeJuice" in setOf("Milk","Coffee")) //이 집합에 OrangeJuice가 들어있는지 확인한다.

예외 처리

if(number !in 0..100) {
    throw IllegalArgumentException("This is not between 0~100 : ${number}") //throw 또한 식으로, 다른 식에 포함될 수 있다.
}

val checkNumber = if(number !in 0..100) {
    number
    } else {
     throw IllegalArgumentException("This is not between 0~100 : ${number}") //false 이면 변수가 초기화 되지 않는다. 출력시 null 이 반환된다.
    }

//예외 처리 시 try, catch, finally를 함께 사용하며, 자바와 동일하다.
val readNumber(reader: bufferedReader) : Int? {
    try{
        val line = reader.readLine()
        return Integer.parseInt(line)
        //try 또한 if, when 과 같이 식이므므로 변수에 대입할 수 있다. 이때 catch 는 return 만 사용하면 그 이상 실행되지 않으나, 해당 대입 이후 기본 값으로 진행을 원할 때는 null 등을 이용하면 된다.
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}
반응형
Comments