[Kotlin] Ch2. 프로퍼티와 초기화

leeeha·2022년 8월 4일
0

코틀린

목록 보기
18/28
post-thumbnail

출처: https://www.boostcourse.org/mo234/lecture/154228?isDesc=false

프로퍼티의 접근

자바와 코틀린의 차이점

  • 자바의 필드: 단순한 변수 선언만 가지기 때문에 접근을 위한 메서드를 따로 만들어야 한다.
  • 코틀린의 프로퍼티: 변수 선언과 기본적인 접근 메서드를 모두 가지고 있으므로, 접근 메서드를 따로 생성하지 않아도 내부적으로 생성하게 된다.

getter, setter

getter, setter를 접근 메서드라고 부르는데, 자바에서는 아래 예시처럼 모든 필드에 대한 접근 메서드를 만들어야 해서 매우 번거롭다.

코틀린에서는 접근 메서드를 생략할 수 있다. (내부적으로 생성됨.)

package chap02.section1

class User(_id: Int, _name: String, _age: Int){
    val id: Int = _id
    var name: String = _name
    var age: Int = _age
}

fun main() {
    val user = User(1, "Sean", 30)
    val name = user.name // getter
    user.age = 41 // setter 
    println("name: $name, ${user.age}")
}

위의 코드를 Tools > Kotlin > Show Kotlin Bytecode > Decompile 을 해보면,

이렇게 val 변수는 getter만 생성되고, var 변수는 getter와 setter가 모두 생성된다는 걸 확인할 수 있다.

getter, setter 커스텀

package chap02.section1

class User(_id: Int, _name: String, _age: Int){
    val id: Int = _id
        get() = field // 프로퍼티를 대신할 임시 필드 (프로퍼티 이름을 직접 사용하면 무한 재귀 호출에 빠짐)

    var name: String = _name
        get() = field
        set(value){ // 외부로부터 값을 가져오는 매개변수
            field = value
        }

    var age: Int = _age
        get() = field
        set(value){
            field = value
        }
}

fun main() {
    val user = User(1, "Sean", 30)
    //user.id = 2
    user.age = 41
    println("user.age = ${user.age}")
}

value, field와 같은 특수 변수를 사용하여 기존의 getter, setter를 커스텀 할 수 있다.

주의해야 할 점은 field 대신 프로퍼티 자체의 이름을 사용하면, 프로퍼티는 내부적으로 게터와 세터를 생성하기 때문에 무한 재귀 호출에 빠질 수 있다는 것이다.

따라서 field라는 이름을 사용해야 하고, value는 외부의 값을 받는 역할을 하므로 어떤 이름을 사용하든지 상관 없다.

package chap02.section1

class User(_id: Int, _name: String, _age: Int){
    val id: Int = _id

    var name: String = _name
        set(value){
            println("The name was changed")
            field = value.toUpperCase() // 인자를 대문자로 변경하여 프로퍼티에 할당
        }

    var age: Int = _age
}

fun main() {
    val user = User(1, "Sean", 30)
    user.name = "coco"
    println("user.name = ${user.name}")
}

The name was changed
user.name = COCO

package chap02.section1

open class First {
    open val x: Int = 0 // 오버라이딩 가능
        get() {
            println("First x")
            return field
        }

    val y: Int = 0 // open 키워드가 없으면 final 프로퍼티
}

class Second: First() {
    override val x: Int = 0
        get(){
            println("Second x")
            return field + 3
        }
    //override val y: Int = 4
    // 'y' in 'First' is final and cannot be overridden
}

fun main() {
    val second = Second()
    println(second.x) // 오버라이딩 된 값
    println(second.y) // 상속 받은 값
}

Second x
3
0


지연 초기화와 위임

클래스에서 기본적으로 선언하는 프로퍼티들은 null 값을 가질 수 없다. 하지만, 객체의 정보가 나중에 나타나는 경우에는 나중에 초기화 할 수 있는 방법이 필요한데 이때 사용하는 게 lateinit과 lazy 키워드이다.

lateinit

  • 의존성이 있는 초기화, 유닛 테스트 코드 작성 시 사용
    • Car 클래스의 초기화 부분에 Engine 클래스와 의존성을 가지는 경우, Engine 객체가 생성되지 않으면 완전하게 초기화 할 수 없다. 이때 lateinit 사용!
    • Unit 테스트를 위해 임시적으로 객체를 생성해야 하는 경우
  • 프로퍼티 지연 초기화
    • 클래스에서 기본적으로 선언하는 프로퍼티는 null을 허용하지 않는다.
    • 하지만, 지연 초기화를 위한 lateinit 키워드를 사용하면 프로퍼티에 값을 바로 할당하지 않아도 된다.
  • lateinit의 제한
    • var로 선언된 프로퍼티에만 사용 가능
    • 프로퍼티에 대한 게터와 세터를 사용할 수 없음.
package chap02.section2

class Person {
    lateinit var name: String // var만 가능 

    fun test() {
        if(!::name.isInitialized){
            println("uninitialized")
        }else{
            println("initialized")
        }
    }
}

fun main() {
    val kildong = Person()
    kildong.test() // uninitialized
    kildong.name = "Kildong" // 여기서 초기화
    kildong.test() // initialized
    println("name = ${kildong.name}")
}
package chap02.section2

data class Person(var name: String, var age: Int)

lateinit var person: Person // 객체 생성의 지연 초기화

fun main() {
    person = Person("Kildong", 30) // 여기서 객체 초기화
    println("${person.name} is ${person.age} years old.")
}

lazy

  • 호출 시점에 by lazy {...} 정의에 의해 블록 부분의 초기화를 진행한다.
  • 불변의 변수 선언인 val에서만 사용 가능하다. (read-only)
  • val이므로 값을 변경할 수 없다.
package chap02.section2

class LazyTest {
    init {
        println("init block") // 2 
    }

    private val subject by lazy {
        println("lazy initialized") // 6 
        "Kotlin Programming"// 7. 마지막 표현식 반환 (람다식) 
    }

    fun flow(){ 
        println("uninitialized") // 4 
        println("subject one: $subject") // 5. 최초 초기화 시점
        println("subject two: $subject") // 8. 이미 초기화 된 값 사용
    }
}

fun main() {
    val test = LazyTest() // 1 
    test.flow() // 3 
}

init block
uninitialized
lazy initialized
subject one: Kotlin Programming
subject two: Kotlin Programming

package chap02.section2

class Person(val name: String, val age: Int)

fun main() {
    var isPersonInstantiated: Boolean = false

    val person: Person by lazy { // person 객체의 지연 초기화
        isPersonInstantiated = true
        Person("Kim", 23) // 이 부분이 lazy 객체로 반환됨.
    }

    // 위임 변수를 사용한 초기화
    val personDelegate = lazy { Person("Hong", 30) }

    println("person Init: $isPersonInstantiated")
    println("personDelegate Init: ${personDelegate.isInitialized()}")

    // 여기서 객체 초기화
    println("person.name = ${person.name}")
    println("personDelegate.value.name = ${personDelegate.value.name}")

    println("person Init: $isPersonInstantiated")
    println("personDelegate Init: ${personDelegate.isInitialized()}")
}

person Init: false
personDelegate Init: false
person.name = Kim
personDelegate.value.name = Hong
person Init: true
personDelegate Init: true

by lazy의 모드

  • SYNCHRONIZED: 락을 사용해 단일 스레드만 사용하도록 보장 (디폴트)
  • PUBLICATION: 여러 곳에서 호출될 수 있으나, 처음 초기화 되었을 때의 반환값만 사용함.
  • NONE: 락을 사용하지 않기 때문에 빠르지만, 다중 스레드가 접근할 수 있음. (값의 일관성을 보장할 수 없음.)

lazy에 Ctrl + B를 눌러 함수의 실체를 확인해보자.

그러면, mode를 인자로 받지 않는 lazy 람다식은 기본적으로 SYNCHRONIZED 모드라는 걸 알 수 있다. 그리고 SynchronizedLazyImpl의 내부를 보면 lock으로 동기화를 하고 있다는 것도 확인할 수 있다.

by를 이용한 위임 (delegation)

위임(delegation)이란 어떤 특정 일을 대신하는 중간자 역할을 말한다. 예를 들어 상속 재산을 위해 합의서에 모든 상속자가 서명해야 하지만, 특정 상속자가 다른 상속자를 대신하여 서명할 수 있게 위임장이라는 것을 쓰면 대신 서명할 수 있게 된다.

프로그래밍 세계에서도 클래스 간의 위임이 가능하다. 하나의 클래스가 다른 클래스에 위임하도록 선언되면, 위임된 클래스가 가지는 멤버는 참조없이 호출할 수 있다.

<val|var|class> 프로퍼티 혹은 클래스 이름: 자료형 by 위임자

클래스의 위임

interface Animal {
	fun eat() { ... }
    ... 
}

class Cat: Animal {}
val cat = Cat()
class Robot: Animal by cat // Animal에 정의된 Cat의 모든 멤버를 Robot에 위임한다. 
  • cat은 Animal 자료형의 private 멤버로 Robot 클래스 내에 저장된다.
  • Cat에 구현된 모든 Animal 메서드는 정적 메서드로 생성된다.
  • 따라서, Animal에 대한 명시적인 참조를 사용하지 않고도 eat()을 바로 호출할 수 있다.

위임을 사용하는 이유는?

코틀린의 기본 라이브러리는 open 되지 않은 최종 클래스이다. 이는 표준 라이브러리의 무분별한 상속에 의한 복잡한 문제들을 방지하기 위한 것이지만, 상속을 통한 클래스의 기능 확장이 어렵긴 하다. 이럴 때 위임을 사용하면, 상속과 비슷하게 최종 클래스의 모든 기능을 사용하면서 동시에 기능을 추가 및 확장할 수 있다.

예제

package chap02.section2

interface Car { // 기본적으로 open 되어 있음.
    fun go(): String // 오버라이딩 필수
}

class VanImpl(val power: String): Car {
    override fun go() = "는 짐을 적재하며 $power 마력을 가집니다."
}

class SportImpl(val power: String): Car {
    override fun go() = "는 경주용에 사용되며 $power 마력을 가집니다."
}

class CarModel(val model: String, impl: Car): Car by impl  {
    // by impl로 위임 받지 않으면, CarModel 역시 go() 메서드를 오버라이딩 해줘야 함.
    //override fun go(): String = "CarModel go()"

    // 참조 없이 각 인터페이스 구현 클래스의 go에 접근 가능
    fun carInfo(){
        println("$model ${go()}")
    }
}

fun main() {
    val myDamas = CarModel("Damas 2010", VanImpl("100"))
    val my350z = CarModel("350z 2008", SportImpl("350"))

    // CarModel 두번째 인자의 객체 타입에 따라
    // carInfo()가 실행하는 go()가 달라짐. (다형성)
    myDamas.carInfo()
    my350z.carInfo()
}

Damas 2010 는 짐을 적재하며 100 마력을 가집니다.
350z 2008 는 경주용에 사용되며 350 마력을 가집니다.

observable, vetoable의 위임

  • observable: 프로퍼티를 감시하고 있다가 특정 코드의 로직에서 변경이 일어날 때 호출된다.
  • vetoable: 감시보다는 수여한다는 의미로, 반환값에 따라 프로퍼티 변경을 허용하거나 취소한다.
package chap02.section2

import kotlin.properties.Delegates

class User {
    // observable은 값의 변화를 감시하는 일종의 콜백 루틴
    var name: String by Delegates.observable("NONAME") { // 프로퍼티를 위임
        prop, old, new -> // 람다식 매개변수
        println("$old -> $new") // 이 부분은 이벤트가 발생할 때만 실행됨.
    }
}

fun main() {
    val user = User()
    user.name = "Kildong" // 값이 변경되는 시점에서 첫번째 이벤트 발생
    user.name = "Dooly" // 값이 변경되는 시점에서 두번째 이벤트 발생
}

NONAME -> Kildong
Kildong -> Dooly

name은 Delegates.observable의 위임자가 되어서 값의 변화가 생길 때마다 콜백 함수가 실행된다.

package chap02.section2

import kotlin.properties.Delegates

fun main() {
    var max: Int by Delegates.vetoable(0){ // 초기값은 0
        prop, old, new ->
        new > old // 조건에 맞지 않으면 거부권 행사
    }

    println(max) // 0
    max = 10
    println(max) // 10

    max = 5 // 새로운 값이 기존 값보다 작으므로 false (5를 재할당 하지 않음)
    println(max) // 10
}

0
10
10


정적 변수와 메서드

정적 변수와 Companion Object

보통 클래스는 프로그램 실행 중에 동적으로 객체를 생성하는데, 정적으로 메모리에 고정시키는 방법도 있다. 코틀린에서는 이것을 Companion Object를 사용해 구현한다. 컴페니언 객체는 동반 객체라고도 불리는데, 기본적인 클래스에 무언가 미리 고정된 것을 함께 동반한다고 이해하면 쉽다. 실제로 컴페니언 객체는 클래스 내부에 정적으로 고정된 메모리를 가지며, 객체를 생성하지 않고도 사용할 수 있다. 단, 자주 사용하지 않는 변수나 객체를 정적으로 만들면 메모리를 낭비할 수도 있다는 걸 유의해야 한다.

package chap02.section3

class Person {
    var id: Int = 0
    var name: String = "haeun"

    // Person 객체를 여러 개 생성해도 내부 companion object는 하나로 유지
    companion object {
        var language: String = "Korean"
        fun work(){
            println("working...")
        }
    }
}

fun main() {
    println(Person.language) // 객체 생성하지 않고 클래스명으로 바로 접근
    Person.language = "English" // 프로퍼티 변경 가능 
    println(Person.language)
    Person.work()
   //println(Person.name) 
}

컴페니언 객체는 실제 객체의 싱글톤 (singleton)으로 정의된다. 싱글톤은 오직 하나의 값만 허용하는 객체를 말한다. 따라서, Person 객체를 여러 개 생성해도 내부의 컴페니언 객체는 오직 한 개로 유지된다.

코틀린에서 자바의 static 멤버 사용하기

자바에서 코틀린의 companion object 사용하기

@JvmStatic

자바에서는 코틀린의 컴페니언 객체에 접근하기 위해 @JvmStatic 표기법을 사용한다.

@JvmStatic 어노테이션을 사용하면 컴페니언 객체의 메서드를 바로 참조할 수 있는데, 어노테이션을 사용하지 않으면 메서드 앞에 Companion 키워드를 붙여줘야 한다.

cf) 보통 val만 사용하면, 실행 시간에 값이 할당되고 그 값은 변경 불가능하다. 근데 const와 val을 함께 사용한 상수 표현은 컴파일 시간에 값이 결정되며, const는 오직 String이나 원시 자료형에만 사용할 수 있다.

최상위 함수 정리

클래스 없이 만든 최상위 (top-level) 함수들은 객체 생성 없이도 어디서든지 실행 가능하다. 패키지 레벨 함수라고도 불린다. 최상위 함수는 결국 자바에서 static final로 선언된 함수이다.

자바에서 코틀린의 최상위 함수에 접근하기

자바에서는 무조건 클래스를 통해서 함수에 접근해야 한다. 코틀린의 최상위 함수는 클래스가 없으나, 자바와 연동 시 내부적으로 파일명에 Kt 접미사가 붙은 클래스를 자동으로 생성하게 된다. 자동 변환되는 클래스명을 명시적으로 지정하려면, @file:JvmName("ClassName")을 코드 상단에 명시하면 된다.

object와 싱글톤

cf) 싱글톤 패턴: 객체의 인스턴스가 오직 하나만 생성되는 패턴

상속할 수 없는 클래스에서 내용이 변경된 객체를 생성하고 싶을 때, 자바에서는 익명 내부 클래스를 사용해 새로운 클래스를 선언한다. 코틀린에서는 object 표현식이나 object 선언으로 이 경우를 좀 더 쉽게 처리한다.

object 선언과 컴페니언 객체의 비교

package chap02.section3

object OCustomer {
    var name = "Kildong"
    fun greeting() = println("Hello World!")
    val HOBBY = Hobby("Basketball")
    init {
        println("Init!")
    }
}

class Hobby(val name: String)

fun main(){
    // 접근 시점에 객체가 생성되는 object
    OCustomer.greeting()
    OCustomer.name = "Dooly"
    println("name = ${OCustomer.name}")
    println(OCustomer.HOBBY.name)
}

Init!
Hello World!
name = Dooly
Basketball

위의 예시를 보면 객체를 별도로 생성하지 않고, OCustomer 자체를 사용한다는 걸 알 수 있다. object 선언 방식은 접근 시점에 객체가 생성된다는 중요한 특징이 있다. 따라서, 생성자를 호출하지 않으므로 object 선언에서는 주/부 생성자를 사용할 수 없다. 자바에서는 OCustomer.INSTANCE.getName();과 같이 접근해야 한다.

object 표현식

object 표현식은, object 선언과 달리 이름이 없으며 싱글톤이 아니다. 따라서, object 표현식이 사용될 때마다 새로운 인스턴스가 생성된다. 자바에서 이름이 없는 익명 내부 클래스로 불리는 형태를 object 표현식으로 더 쉽게 만들 수 있다.

package chap02.section3

open class Superman {
    fun work() = println("Taking photos")
    fun talk() = println("Talking with people.")
    open fun fly() = println("Flying in the air.")
}

fun main() {
    // Superman 접근 시점에 인스턴스가 생성됨.
    // 하위 클래스를 만들지 않고도 object 표현식을 이용하여
    // fly() 메서드를 재정의 할 수 있음. (오버라이딩)
    val pretendedMan = object: Superman() {
        override fun fly() = println("I'm not a real superman. I can't fly!")
    }
    pretendedMan.work()
    pretendedMan.talk()
    pretendedMan.fly()
}

어떤 클래스의 메서드를 오버라이딩 하려면, 하위 클래스를 정의하고 해당 메서드를 재정의 한 뒤에 객체를 생성하는 과정을 거쳐야 했다. 하지만, object 표현식은 이러한 과정 없이 간단하게 객체의 내용을 변경할 수 있다.

profile
꾸준히!

0개의 댓글