kotlin (코틀린) 시작해보기 (5)

오랜만에 코틀린 포스팅을 해보자!
이번시간에는 코틀린의 data class 와 object에 대해서 알아볼 예정이다.

data class

우리는 가끔 데이터만 저장하고 다른건 하지 않을 때가 많다. 예를들어 java의 getter, setter, hashcode/equals, toString 등 기계적인 작업을 할 때가 많다. 그래서 우리는 java 라이브러리인 lombok을 사용하긴 한다. java는 서드파트에서 지원해주지만 코틀린 경우에는 언어 자체에서 지원해 준다.

기본 문법은 아래와 같다.

data class User(val id: Long, val name: String)

class 앞에 data 키워드를 넣어 주면 된다. data class는 컴파일러가 자동으로 아래와 같은 메서드를 알아서 만들어 준다
– constructor
– getter/setter
– hashcode/equals
– toString
– componentN() functions
– copy() function

val user = User(1, "wonwoo")
println(user.id)
println(user.name)
val hashCode = user.hashCode()
val equals = user.equals(User(2, "test"))
val id = user.component1()
val name = user.component2()
val copy = user.copy(2, "wonwoo1")

위와 같이 생성자와 getter, hashCode, equals, componentN, copy 등이 만들어 졌다.
좀 더 자세히 살펴보자.

생성자

생성자는 기본적으로 파라미터에 따라서 만들어 진다. 예를들어 위와 같은 경우에는 id와 name의 생성자만 존재한다. 예를들어 모든 생성자를 만들고 싶다면 아래와 같이 하면 된다.

data class User(var id: Long? = null, var name: String? = null)

해당 변수에 초기화를 해주면 그에 맞게 생성자를 만들어 준다.

getter, setter

data class의 생성자에는 var혹은 val를 사용해야 된다. 동일하게 var는 읽기 쓰기가 가능하므로 getter, setter모두 생성 되지만 val 경우에는 읽기 전용이므로 getter만 생성 된다.

data class User(val id: Long, var name: String)

val user = User(1, "wonwoo")
user.id = 1 //컴파일 에러
user.name = "wonwoo1"

위의 name은 var로 선언하여 변경이 가능하지만 id 경우에는 val로 선언되어서 변경이 불가능하다.

hashcode/equals 와 toString

자바와 동일하게 hashcode와 equals는 쌍으로 만들어 진다. 물론 구현 또한 비슷하게 구현되어 지지 않나 싶다.
toString의 경우에는 "User(id=1, name=wonwoo)" 와 같은 형태로 구현되어 진다.
딱히 설명할게 없기에 넘어가자.

componentN

다른 언어(스칼라)에서는 Destructuring라고도 불린다.

val id = user.component1()
val name = user.component2()

위와 같이 N번째에 값을 자유롭게 꺼낼수 있다. component1 경우에는 첫 번째 파라미터인 id component2 경우에는 두 번째 파라미터인 name을 꺼내서 사용 할 수 있다. 이 것도 귀찮다 좀 더 깔끔하고 짧게 사용할 수 있다.

val (id, name) = user
println("id : $id name : $name")

위와 같이 각각의 원소들을 순서대로 꺼내서 사용할 수 있다. 좀 더 간편하게 object의 프로퍼티에 접근할 수 있다.

copy

말 그대로 복사를 하는 개념이다. java의 clone과 비슷하다. 조금 다른점은 일부 프로퍼티만 변경하고 나머지는 그대로 유지 할 수 있다는 점이다.

val user = User(1, "wonwoo")
val copy = user.copy(name = "wonwoo1")

위와 같이 id는 기존과 동일하게 하고 name만 wonwoo1로 변경하였다. 여기서 주의할 점은 새로운 user가 다시 생기는거지 기존 user를 변경하는 것은 아니다. 여기서 생소한 것이 보인다. 바로 (name = "wonwoo1") 이부분이다. 요즘 나오는 언어에는 거의 대부분 지원하는 듯하다. named parameters 혹은 Named Arguments 라고 불리는데 필자는 named parameters 라고 알고 있었는데 코틀린 문서에는 Named Arguments라고 있어 추가해서 넣었다.

object

코틀린에서는 static한 함수가 존재 하지 않는다. 하지만 그에 맞게 object라는 것을 지원해주는데 싱글톤 개념이다. 스칼라에서도 object가 있는데 스칼라와 동일한 것으로 보인다.
문법을 살짝 보자.

object Utils {
    fun sum(a: Int, b :Int) = a + b
}

위와 같이 object라는 키워드를 선언하면 된다. class와 문법은 비슷하다. object 뒤에 object명을 기술 해주고 바디를 작성해주면 된다. 사용법은 아래와 같다.

Utils.sum(1,10)

자바의 static 메서드를 호출하듯이 호출 하면 된다. 더 다양하게 사용하는 법은 있지만 아직 필자도 잘 모르기에 나중에 기회가 된다면 설명하도록 하자.

이렇게 간단하게 data class와 object에 대해서 알아봤다. object는 좀 더 봐야 할 듯하다. data class의 경우에는 아주 유용하게 쓰일 것 같다.
다음 시간에는 function에 대해서 살펴 보도록 하자.

kotlin (코틀린) 시작해보기 (4)

이번시간에는 코틀린의 프로퍼티와 인터페이스에 대해서 알아보자. 프로퍼티 경우에는 조금 다른점이 있는데 인터페이스같은 경우에는 자바와 거의 비슷하므로 간단하게만 설명해보겠다.

프로퍼티

프로퍼티 선언

class Product {
    val id :Long? = null
    val name : String? = null
    val price : BigDecimal? = null
}

null은 컴파일 에러만 피하기 위해 작성하였다. 여기서는 중요한게 아니므로 주의깊게 보지 않아도 된다. 실제 저런 코드는 거의 사용할 일이 없을 듯하다.
코틀린의 경우에는 위와 같은 형식으로 프로퍼티를 작성할 수 있다. val 로 선언된 경우에는 읽기만 가능하고 var 로 선언한 것은 쓰기 읽기 모두 가능하다.

var product = Product()
product.name = "iphone 7" //컴파일 에러

위와 같이 Product를 생성 후에 name에 접근해서 어사인을 할 경우에는 컴파일 에러가 발생한다. 위의 코드를 쓰기도 가능하게 하려면 프로퍼티 선언을 다음과 같이 해야한다.

class Product {
    var id :Long? = null
    var name : String? = null
    var price : BigDecimal? = null
}

한마디로 getter만 사용하고 싶을 경우에는 val getter setter 모두 사용하고 싶을 때는 var로 선언 하면 된다.
위의 경우에는 일반적인 방법이다. 위와 같이 일반적으로 할당한 값을 넣고 빼고 할 수도 있겠지만 그렇지 않은 경우도 있다.
예를들어 Product 클래스의 name 프로퍼티에 무조건 # 해쉬 값을 넣고 싶다고 가정해보자. 그럼 우리는 조금 커스텀하게 getter나 setter를 만들어야 된다.

getter와 setter

class Product {
    var id :Long? = null
    var name : String? = null
        set(value) {
            field = "#" + value
        }
    var price : BigDecimal? = null
}

위와 같이 name 프로퍼티 아래에 set이라는 키워드를 사용하면 된다. set이라는 키워드에 파라미터는 굳이 value라는 변수로 하지 않아도 된다. 코틀린의 권장사항일 뿐이지 다른 변수명으로 사용해도 상관은 없다. 그리고 뜬금 없이 field 라는 필드가 보인다. 저 변수는 getter와 setter 안에서만 사용할 수 있는 Backing 필드라고 부른다. 저 필드가 실제 위에서는 name이라는 필드에 어사인을 하는 그런 필드 인 듯 싶다.
위에서는 setter만 이용해서 값을 넣어지만 이번에는 getter로 가져올 때 # 해쉬를 붙어서 가져와 보자.

class Product1 {
    var id: Long? = null
    var name: String? = null
        get() {
            return "#" + field
        }
    var price: BigDecimal? = null
}

setter와 마찬가지로 get이라는 키워드를 사용해서 getter를 커스텀하게 만들 수 있다. getter의 경우에는 expression 으로도 만들 수 있다.

var name: String? = null
    get() = "#" + field

expression으로 좀 더 간단하고 깔끔해 진 듯하다. 하지만 setter의 경우에는 expression으로 만들 수 없는 듯하다. 해보니까 컴파일 에러가 발생한다. 가능하게 만들 수 있을 지는 모르겠으나 일단 현재는 잘 모르겠다.

접근제한자 및 어노테이션

setter나 getter에 접근제한자 및 어노테이션을 선언 할 수 있다.

var name: String? = null
    private set(value) {
        field = "#" + value
    }

위와 같이 set 키워드 앞에 접근제한자 private를 선언 하고 있다. 그러면 자바와 동일하게 현재 클래스 외에 다른 클래스들은 name에 접근하여 값을 어사인 할 수 없다.

var name: String? = null
    private get() = "#" + field

위와 같이 getter에도 할 수 있을 거라고 생각했지만 위의 코드는 컴파일 에러가 발생한다. 그 이유는 필드와 getter의 접근제한자가 다르기 때문이다. 그래서 필드에도 private를 선언해줬다.

private var name: String? = null
    private get() = "#" + field
    set(value) {
        field = "#" + value
    }

하지만 필드에 접근제한자를 사용하면 그 아래에 있는 getter setter 모두가 필드의 접근제한자를 따라간다.

var product = Product1()
product.name = "iphone 7" //컴파일 에러
println(product.name)     //컴파일 에러

위의 코드는 어사인할 때 도 컴파일 에러가 나며 가져올 때도 컴파일 에러가 발생한다.

@Inject set(value) {
    field = "#" + value
}

위와 같이 어노테이션도 사용할 수 있다.

상수

컴파일 타임에 값을 알 수 있는 프로퍼티는 const라는 키워드를 사용하면 된다.

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"
@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() {  }

위와 같이 어노테이션에서도 사용할 수 있다.

초기화 지연

보통 non-null 타입을 갖는 프로퍼티를 선언하면 생성자에서 초기화해야 한다. 물론 개발을 할 때는 초기화 해주는 것이 좋지만 테스트할 경우에는 늘 좋은 것만은 아니다. 예를 들어 의존성 주입이나 단위 테스트를 통해 setup 메서드에서 프로퍼티를 초기화 할 수 도 있다. 그때 사용할 수 있는 키워드가 lateinit이라는 키워드이다.

class MyTest {
    lateinit var subject: TestSubject
    @SetUp 
    fun setup() {
        subject = TestSubject()
    }

    @Test 
    fun test() {
        subject.method() // null 검사 없이 직접 접근
    }
}

위와 같이 초기화를 하지 않아도 사용 할 수 있다. 하지만 이 경우에는 val 키워드를 사용할 수 없고 var 키워드만 사용 가능하다. 만약 초기화되기 전에 lateinit 키워드가 사용된 프로퍼티에 접근하면 초기화되지 않은 프로퍼티에 접근했음을 정확하게 알려주기 위해 특수한 익셉션 을 발생한다.

인터페이스

코틀린의 인터페이스는 java8과 아주 비슷하다. 그래서 java8을 사용하는 개발자라면 쉽게 배울 수 있을 듯하다. 자바와 마찬가지로 추상클래스와 인터페이스의 차이는 상태를 가질 수 없다.

interface MyInterface {
    fun bar()
    fun foo() {
      //함수 바디
    }
}

자바와 동일하게 interface라는 키워드를 사용하면 된다. java8과 동일하게 함수의 바디를 가질 수 있다.

인터페이스 구현

class MyClass : MyInterface{
    override fun bar() {

    }
}

class 명 뒤에 :를 사용해서 구현할 수 있다. 자바와 마찬가지로 한개 이상의 인터페이스를 구현할 수 있다.

인터페이스의 프로퍼티

interface MyInterface {
    val price :Int
    fun bar()
    fun foo() {
        println(price)
    }
}

위와 같이 인터페이스에 프로퍼티를 선언 할 수 있다. 인터페이스의 선언된 프로퍼티는 추상프로퍼티이므로 구현체에서 구현을 해주어야 한다.

class MyClass : MyInterface{
    override val price: Int = 100
    override fun bar() {

    }
}

위와 같이 구현클래스에서 price라는 프로퍼티를 구현?(선언)해야 한다.
인터페이스의 프로퍼티를 제외 하곤 자바의 인터페이스가 거의 동일하므로 나머지는 생략하겠다.

이렇게 오늘은 코틀린의 프로퍼티와 인터페이스에 대해 살펴 봤다. 다음 시간에는 data class와 object에 대해서 살펴보기로 하겠다.

kotlin (코틀린) 시작해보기 (3)

이번 시간에는 코틀린의 클래스에 대해 살펴보자. 가장 많이 사용할 것 같은 문법들만 소개하겠다. 나머지는 실제로 레퍼런스를 보면 되겠다. 레퍼런스에 있는 것을 다 하면 좋겠지만 그럴 여력이 없다. 기본적인 것과 자주 사용하는 것 혹은 조금 특이한 것들만 소개할 예정이다. 이전 중간에도 return, break,continue, 라벨 등은 건너 띄었다.

클래스

class Product {
}

기본적인 형태는 위와 같다. 자바와 비슷한 형태이지만 class 앞에 접근제한자가 없다. 대부분의 클래스는 public 이므로 기본값은 public 접근제한자 이다.

class Product

만약 클래스 본체가 없는 경우에는 컬리브레이스를 생략해도 된다.

생성자

코틀린의 생성자는 자바와 형태가 조금다르다.

class Product constructor(name: String) {

}

constructor 키워드를 사용해 생성자를 만들 수 있다. 자바와 동일하게 기본 생성자는 컴파일러가 만들어 주지만 생성자가 한개 이상 있을 때에는 기본생성자를 만들어 주지 않는다.

class Product(name: String) {

}

생성자 필드에 어노테이션이나 접근제한자가 존재 하지 않는다면 위와 같이 constructor 키워드를 생략할 수도 있다.

class Product constructor(name: String) {
    init {
        println(name)
    }
}

init 키워드를 이용해서 name에 접근 할 수 있다. 하지만 조작할 수는 없어 읽기 전용인 듯하다.

class Product constructor(name: String) {
    val length = name.length
    init {
        println(name)
        println(length)
    }
}

위와 같이 클래스 본체에서도 name에는 접근할 수 있다. 위와 동일하게 조작할 수는 없다. 그렇다면 다른점은 무엇일까? 흠

class Product private constructor(name: String) {
    val length = name.length
    init {
        println(name)
        println(length)
    }
}

생성자에 접근제한자 혹은 어노테이션을 선언하고 싶다면 위와 같이 constructor 키워드 앞에 작성해주면 된다.

class Product constructor(name: String) {
    constructor(name: String, price: Int) : this(name) {
        println(price)
    }
}

위와 같이 보조 생성자를 만들 수 있다. 조금 희한한건 생성자 필드 앞에 valvar 를 넣지 않으면 생성자 혹은 init 블럭, 클래스 바디에서만 접근 가능하다.

class Product constructor(name: String) {
    constructor(name: String, price: Int) : this(name) {
        println(price)
    }

    fun nameLength() : Int {
        return name.length  //컴파일 에러
    }
}

만약 위와 같은 코드가 있다면 nameLength() 함수 안에 name은 컴파일 에러가 발생한다.

class Product constructor(val name: String) {
}

생성자 필드에 valvar를 선언 할 수 있다 이 때에는 클래스 안에 있는 함수도 접근 가능하다. 마찬가지로 valfinal이 내포 되어 있으므로 읽기 전용이고 var는 읽기 쓰기 둘다 가능하다.

인스턴스 생성

val product = Product("iphone 7")

위와 같이 인스턴스를 생성 할 수 있다. 자바와 다른점은 new 키워드가 필요 없다.

상속

코틀린은 자바와 마찬가지로 최상위 클래스를 갖고 있다. Any라는 클래스가 코틀린의 최상위 클래스이다. 슈퍼타입을 지정하지 않으면 자동으로 모든 클래스는 Any를 상속하고 있다.

class NewProduct(name: String, price: Int) : Product(name, price) {

}

생성자는 자바와 동일하다. 상위 클래스의 생성자는 하위 클래스에도 존재 해야 된다. 하지만 위의 코드는 컴파일 에러가 발생한다. 기본적으로 코틀린의 클래스는 final이 내포 되어 있다. 다들 알겠지만 class의 final이 선언 되어 있으면 상속을 하지 못한다.

open class Product constructor(name: String) {
  //..
}

위와 같이 open 키워드를 사용하여 상속을 허용하도록 할 수 있다. 이것은 이펙티브 자바 item 17 계승을 위한 설계와 문서를 갖추거나 그럴 수 없다면 계승을 금지하라 를 채용하였다.

오버라이딩

open class Super {
    fun print(): String {
        return "super.print()"
    }
}

class Sub : Super() {
    override fun print(): String {
        return "sub.print()"
    }
}

자바와 동일하게 override 어노테이션을 이용해서 오버라이딩을 할 수 있다. 자바에서는 override 어노테이션을 생략해도 되지만 코틀린에서는 생략하면 컴파일 에러가 발생한다. 명시적이라 더 좋은듯 하다. 하지만 위의 코드는 컴파일 에러가 발생한 코드이다. 함수 역시 기본이 final이 내포 되어있다. 마찬가지로 상위 메서드에 open 키워드를 선언하면 오버라이딩 할 수 있다.

open class Super {
    open fun print(): String {
        return "super.print()"
    }

    final override fun toString(): String {
        return "super.toString()"
    }
}

만약 오버라이딩 한 메서드에 하위 클래스에게 오버라이딩을 허용하고 싶지 않다면 final 을 선언해주면 하위 클래스는 오버라이딩을 할 수 없다.

추상 클래스

abstract class Super {

    abstract fun foo()
}
class Sub : Super() {
    override fun foo() {

    }
}

자바와 마찬가지로 abstract 키워드를 사용해서 추상 클래스를 만들 수 있다. 또한 abstract 사용해서 추상함수를 만들 수 있다. 여기서는 open이라는 키워드가 별도로 필요 없다.

Companion Objects

조금 낯선 단어일 지 모른다. 하지만 스칼라를 했다면 익숙한 단어이다. 스칼라의 컴페니언 오브젝트와 비슷한 개념이다. 코틀린에서도 마찬가지로 static한 메서드 필드가 개념이 존재 하지 않는다. 하지만 비슷한 개념으로 컴페니언 오브젝트라는 것이 있는데 싱글톤 객체라고 생각하면 되겠다. 언어에서 싱글톤을 지원한다. 이런말이 있지 않는가? 어제의 패턴이 오늘의 언어가 되었다.

class Customer {
    companion object Factory {
        fun create() = Customer()
    }
}

위와 같이 companion object 를 만들 수 있다. 클래스 안에 companion object 키워드를 넣어 만들어 준다.

val create = Customer.create()

그리고 위와 같이 생성할 필요 없이 자바의 static 메서드 처럼 사용하면 된다. 다른 방법으로 companion object 명을 지정해주지 않아도 된다.

class Customer1 {
    companion object {
        fun create() = Customer1()
    }
}

val companion = Customer1.Companion
companion.create()

만약 object명을 지정해 주지 않으면 Companion라는 명을 사용해도 된다.

Sealed Classes

실드 클래스는 열거형 클래스의 확장이다. 실드 클래스의 하위클래스는 상태를 포함할 수 있는 여러 인스턴스를 가질 수 있다.
실드 클래스를 선언하려면 클래스 앞에 sealed를 선언하면 된다. 실드 클래스는 하위 클래스를 가질 수 있지만 모든 하위 클래스는 실드 클래스 선언 자체에 중첩해야 한다. 아래와 같이 말이다.

sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
}

실드 클래스의 최대 장점은 when식과 함께 사용할 수 있다. 모든 경우를 확실하게 다루고 있다면 else 절을 추가할 필요가 없다.

print(eval(Expr.Sum(Expr.Const(99.1232), Expr.Const(1821.299))))

위의 형태로 사용할 수 있다.

이렇게 오늘은 클래스에 대해서 알아봤다. 다음 시간에는 프로퍼티와 필드 그리고 인터페이스에 대해서 알아보자.