6 minute read

클래스와 프로퍼티

코틀린을 활용하면 자바보다 더 적은 양의 코드로 클래스와 관련있는 대부분의 작업을 수행할 수 있다.
간단한 자바빈 Person 클래스를 정의하자.
public class Person {
    private final String name;
   
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return this.name;
    }
}
위의 자바 Person 클래스를 코틀린으로 변환해보자.
class Person(val name: String)
이처럼 코틀린에서는 필드 대입 로직등을 훨씬 더 적은 코드로 작성할 수 있다.
코틀린의 기본 가시성은 public 이므로 자바에서 작성한 public 접근 제어자가 사라졌다.

프로퍼티

자바에서는 보통 데이터를 필드에 저장하며, 멤버 필드의 가시성은 private으로 지정하고, 접근자 메소드(getter/setter)를 제공한다.
필드와 접근자를 묶어 프로퍼티라고 하며, 코틀린은 프로퍼티를 언어 기본으로 제공한다.
class Person(
    val name: String, // 비공개 필드, getter만 제공, 생성자로 필드 초기화
    var isMarried: Boolean // 비공개 필드, getter/setter 제공, 생성자로 필드 초기화     
)
코틀린은 값을 저장하기 위한 비공개 필드와 그 필드에 값을 저장하기 위한 setter,
필드의 값을 읽기 위한 getter로 이뤄진 간단한 디폴트 접근자 구현을 제공한다.
Person person = new Person("홍제", true)
System.out.println(person.getName())
System.out.println(person.isMarried())
System.out.println(person.setMarried())
val person = Person("홍제", true) // new 키워드를 사용하지 않고 생성자를 호출한다(파이썬과 같다)
println(person.name) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
println(person.isMarried) // 프로퍼티 이름을 직접 사용해도 코틀린이 자동으로 게터를 호출해준다.
코틀린에서는 게터를 호출하는 대신 프로퍼티를 직접 사용했다.
세터도 마찬가지로 작동한다. 자바에서는 person.setMarried(true) 로 값을 변경하지만,
코틀린에서는 person.isMarried = false로 값을 변경한다.

커스텀 접근자

직사각형 클래스인 Rectangle을 정의하면서 자신이 정사각형인지 알려주는 프로퍼티 접근자를 직접 작성해보자.
별도의 필드에 저장하지 않고 너비와 높이가 같은지 검사하면 그때그때 알 수 있다.
class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() { // 프로퍼티 게터 선언
            return height == width
        }
    
    val isSquare2: Boolean 
        get() = height == width // 식을 본문으로 하는 구문
}

val rectangle = Rectangle(3, 5)
println(rectangle.isSquare)
println(rectangle.isSquare2)
isSquare 프로퍼티에는 값을 저장하는 필드가 필요 없고 자체 구현을 제공하는 getter만 존재한다.
이러한 프로퍼티 게터는 아래의 파라미터가 없는 함수와 같은 기능을 한다.
class Rectangle(val height: Int, val width: Int) {
    fun isSquare() = height == width
}

val rectangle = Rectangle(3, 5)
println(rectangle.isSquare)
파라미터가 없는 함수를 정의하는 방식과 커스텀 게터를 정의하는 방식 모두 비슷하다.
구현이나 성능상 차이는 없고 차이가 나는 부분은 가독성 뿐이다.

일각에서는 클래스의 속성을 의미한다면 property 정의가 낫고 그렇지 않다면 파라미터가 없는 함수를 정의하는 것이
낫다고 하는 사람들도 있다.

디렉터리와 패키지

모든 클래스를 패키지 단위로 관리, 구분하는 자바와 같이 코틀린에서도 파일(.kt)를 패키지 단위로 관리한다.
코틀린 파일의 맨 위에 package 문을 작성하면 되고, 그러면 파이썬과 같이 그 파일 안에 있는 모든 선언(클래스, 함수, 프로퍼티 등)이
해당 패키지에 들어간다.

다른 패키지에 정의한 선언을 사용하려면 임포트를 사용해야 한다.
아래의 예시를 보자.
package geomotry.shapes

import java.util.Random
class Rectangle(val height: Int, val width: Int) {}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}
이제 만약 다른 패키지에서 Rectangle 클래스나 createRandomRectangle 함수를 사용하고 싶으면 import를 통해 가져오면 된다.
만약 import 문에 "*"를 사용하면 해당 패키지의 모든 선언을 다 가져올 수 있다.
package geomotry.example
import geometry.shapes.createRandomRectangle
import geometry.shapes.Rectangle

fun main(args: Array<String>) {
    createRandomRectangle()
    val rectangle = Rectangle(2,4)
}

enum과 when

enum 클래스

  • 색을 표현하는 enum을 코틀린으로 정의해보자.
enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
자바와 마찬가지로 enum은 단순히 값만 열거하는 존재가 아니라, 프로퍼티나 메소드를 정의할 수 있다.
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); // 코틀린에서 유일하게 세미콜론을 사용해야 한다.
    
    fun rgb() = (r*256 + g) * 256 + b // 메소드를 정의할 수 있다.
}

println(Color.RED.rgb())

when으로 enum 클래스 다루기

  • 코틀린의 when은 자바의 switch를 대치하되 훨씬더 강력하며 자주 사용할 프로그래밍 요소이다.
if 와 마찬가지로 when도 값을 만들어내는 식이다. 따라서 식이 본문인 함수에 when을 바로 사용할 수 있다.
fun getMnemonic(color: Color) = 
    when(color) {
    Color.RED, Color.ORANGE -> "warm"
    Color.YELLOW, Color.GREEN -> "neutral"
    else -> throw Exception("nothing") // 매칭되는 분기 조건이 없으면 이 문장을 실행한다. 자바의 default
}

println(getMnemonic(Color.RED)) // Richard
color로 전달된 값과 같은 분기를 찾는다.
자바와 달리 각 분기의 끝에 break를 넣지 않아도 되고, 한 분기 안에서 여러 값을 매치 패턴으로 사용할 수 있다.
(자바 14부터는 -> 표현으로 break가 필요없다.)

인자 없는 when 사용

분기 조건에 숫자, 문자열, enum만을 사용할 수 있는 자바의 switch와 달리 코틀린 when의 분기 조건은 임의의 객체를 허용한다.
fun mix(color1: Color, color2: Color) = 
    when (setOf(c1, c2)) {
    setOf(RED, YELLOW) -> ORANGE
    setOf(YELLOW, BLUE) -> GREEN
    else -> throw Exception("nothing") // 매칭되는 분기 조건이 없으면 이 문장을 실행한다. 자바의 default
}

println(mix(BLUE, YELLOW)) // Color.GREEN
코틀린 표준 라이브러리에 있는 인자로 전달받은 여러 객체를 집합 Set 객체로 만드는 setOf 라는 함수가 있다.
이처럼 when의 분기 조건 부분에 식을 넣을 수 있기 때문에 코드를 더 간결하게 작성할 수 있다.

하지만 위에서 작성한 함수는 호출될 때마다 분기 조건을 만족하지 못할수록 여러 Set 인스턴스를 생성한다.
이럴경우 불필요한 가비지 객체가 늘어나게 된다.
인자가 없는 when식을 사용하면 가독성이 안좋아지지만 불필요한 객체 생성을 막을 수 있다. 
fun mix(color1: Color, color2: Color) = 
    when {
    (c1 == RED && c2 == YELLOW || c1 == YELLOW && c2 == RED) -> ORANGE
    (c1 == YELLOW && c2 == BLUE || c1 == BLUE && c2 == YELLOW) -> GREEN 
    else -> throw Exception("nothing") // 매칭되는 분기 조건이 없으면 이 문장을 실행한다. 자바의 default
}

println(mix(BLUE, YELLOW)) // Color.GREEN

when에 아무 인자도 없으려면 각 분기의 조건이 불리언 결과를 계산하는 식이어야 한다.

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

타입 검사와 타입 캐스트를 설명하기 위해 간단한 함수를 자바와 코틀린 스타일을 함께 사용해서 작성해보자.
fun eval(e: Expr): Int {
    if (e is Num) { // 타입 검사
        val n = e as Num  // as를 사용한 자바식 명시적 타입 캐스팅
        return n.value
    } 
    if (e is Sum) { // 타입 검사
        return eval(e.right) + eval(e.left) // 코틀린식 컴파일러의 스마트 캐스팅  
    }
    
    throw IllegalArgumentException("exception")
}
자바의 instanceof와 비슷하게 코틀린에서는 is를 사용해 변수 타입을 검사한다.
is를 통해 원하는 타입인지 검사하고 나면, 굳이 변수를 원하는 타입으로 캐스팅하지 않고 사용해도
컴파일러가 캐스팅을 수행해주고 이를 스마트 캐스트라고 부른다.
**스마트 캐스트는 is로 변수에 든 값의 타입을 검사한 다음에 그 값이 바뀔 수 없는 val로 선언한 경우에만 작동한다.
val이 아니거나 val이지만 커스텀 접근자를 사용하는 경우에는 해당 프로퍼티에 대한 접근이 항상 같은 값을 내놓는다고 확신할 수 없기 때문에
as 키워드를 사용해 명시적으로 타입 캐스팅해야 한다.(p.86)
val n = e as Num

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

앞서 말했듯이 if는 식이고 if를 사용해서 3항 연산자처럼 작동하는 코드를 작성할 수 있다.
정확히 말하자면 if가 값을 만들어내므로 3항 연산자처럼 작동하는 것이지 3항 연산자인 것은 아니다.

여기에 코틀린 기초 1장에서 배운 식이 본문인 함수 형태로 위에서 작성한 코드를 더욱 간결하게 만들 수 있다.
아래의 예시를 보자.
fun eval(e: Expr): Int = if (e is Num) e.value else if (e is Sum) eval(e.right) + eval(e.left) else throw IllegalArgumentException("exception")
위와 같이 if의 분기에 블록이 있다면 마지막 식이 결과 값이 되고, 식이 하나밖에 없다면 중괄호를 생략하고 식이 결과 값이 된다.
위의 코드를 when을 사용해서 더욱 다듬어보자.
fun eval(e: Expr): Int = 
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.right) + eval(e.left)
        else -> throw IllegalArgumentException("exception")
    }

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

앞서 말했듯이 if, when은 식이고 if, when 안에서는 블록을 사용할 수 있다.
그런 경우 블록의 "마지막 문장이 결과 값"이 되고 앞선 문장들은 로깅, 계산등으로 활용할 수 있다.
fun eval(e: Expr): Int = 
    when (e) {
        is Num -> {
            println("Hello world")
            e.value
        }
        is Sum -> {
            val left = eval(e.left)
            val right = eval(e.right)
            println("HIIII")
            left + right
        }
        else -> throw IllegalArgumentException("exception")
    }
즉, 블록의 마지막 식이 블록의 결과라는 규칙은 블록이 값을 만들어내야 하는 경우 항상 성립한다.
하지만 이 규칙은 함수에 대해서는 성립하지 않는다.
식이 본문인 함수는 블록을 본문으로 가질 수 없고, 블록이 본문인 함수는 내부에 return 문이 반드시 있어야 한다.

Categories:

Updated:

Comments