본문 바로가기
Program Language/Kotlin

[Do it! 코틀린 프로그래밍] 6. 프로퍼티와 초기화

by SungJe 2021. 4. 14.

6. 프로퍼티와 초기화


1. 프로퍼티의 접근

코틀린에서는 별도로 접근 메서드(Getter(), Setter())를 지정하지 않아도 자동으로 처리해주기 때문에 코드의 양을 줄일 수 있다.

/* User.kt */
class User(val id: Int, var name: String, var age: Int)
/* User.decompiled.java */
public final class User {
   private final int id;
   @NotNull
   private String name;
   private int age;

   public final int getId() {
      return this.id;
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.name = var1;
   }

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   public User(int id, @NotNull String name, int age) {
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.id = id;
      this.name = name;
      this.age = age;
   }
}

커스텀 접근 메서드

  • 보조 필드(Backing Field): 각 프로퍼티의 값을 읽는 특별한 식별자다. 프로퍼티 이름을 직접 사용하면 무한 재귀 호출되어 스택 오버플로가 발생하기 때문에 이를 방지하기 위해 보조 필드를 사용한다.
/* 구조 */
// 대괄호([ ])의 내용은 생략 가능
var 프로퍼티 이름[: 자료형] [= 초기화] // 읽기 쓰기 가능(var)
    [get() { 게터 본문 } ]
    [set(value) { 세터 본문 }]
val 프로퍼티 이름[: 자료형] [= 초기화] // 읽기 전용(val)
    [get() { 게터 본문 } ]

/* 보조 필드 역할 */
var name: String = _name
    get() = field
    set(value) {
        // ①은 자바로 변환되면 this.setName(value) 형태가 되어 무한 재귀 호출된다.
        name = value // ①
        field = value // 보조 필드 사용
    }
...

/* 커스텀 접근 메서드 사용하기 */
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 user1 = User(1, "Kim", 30)
    println("user1.name = ${user1.name}")
    user1.name = "Lee" // [User] name 프로퍼티의 세터 호출
    println("user1.name = ${user1.name}")
}

프로퍼티 오버라이딩

open class First {
    open val x: Int = 0 // open 키워드로 오버라이딩 허용
        get() {
            println("First x")
            return field
        }
    val y: Int = 0 // 기본 final 프로퍼티
}

class Second : First() {
    override var x: Int = 0 // 상위 클래스의 val 프로퍼티를 var 프로퍼티로 변경 가능
        get() {
            println("Second x")
            return field
        }
    // override val y: Int = 0 // 오류! 오버라이딩 불가
}

fun main() {
    val second = Second()
    second.x = 3
    println(second.x) // 오버라이딩된 프로퍼티
    println(second.y) // 상위 클래스로부터 상속받은 프로퍼티
}

2. 지연 초기화와 위임

코틀린에서 프로퍼티는 선언과 동시에 초기화를 해야한다. 하지만 의존성이 있는 초기화나 유닛 테스트를 위한 코드를 작성할 때 매번 초기화는 불편하기 때문에 지연 초기화를 사용할 수 있다.

lateinit을 사용한 지연 초기화

lateinit의 사용에는 두 가지 제한 사항이 존재한다.

  • var로 선언된 프로퍼티만 가능
  • 프로퍼티에 대한 게터와 세터를 사용 불가
/* lateinit을 사용한 프로퍼티 지연 초기화 */
class Person {
    lateinit var name: String // 지연 초기화를 위한 선언

    fun test() {
        if(!::name.isInitialized) { // 초기화 여부를 검사
            println("not initialized")
        } else {
            println("initialized")
        }
    }
}

fun main() {
    val kim = Person()
    kim.test()
    kim.name = "Kim" // ① 이 시점에서 초기화됨(지연 초기화)
    kim.test()
    println("name = ${kim.name}") // ②
}

①에서 name 프로퍼티를 초기화하지 않고 ②에서 접근하면 kotlin.UninitializedPropertyAccessException 예외가 발생한다.

/* lateinit을 사용한 객체 지연 초기화 */
data class Person(var name: String, var age: Int)

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

fun main() {
    person1 = Person("Kim", 30) // 생선자 호출 시점에서 초기화
    println("${person1.name} is ${person1.age}")
}

lazy를 사용한 지연 초기화

  • lazy는 val로 선언한 객체나 프로퍼티의 지연 초기화를 하기위해 사용한다. lazy는 다음과 같이 세 가지 특징을 갖는다.

    • 호출 시점에 by lay {...} 정의에 의해 블록 부분의 초기화를 진행한다.
    • 불변의 변수 선언인 val에서만 사용 가능하다.(읽기 전용)
    • val이므로 값을 다시 변경할 수 없다.
  • lazy는 세 가지 모드를 제공한다.

    1. SYNCHRONIZED: lock을 사용해 단일 스레드만이 사용하는 것을 보장(기본값)
    2. PUBLICATION: 여러 군데에서 호출될 수 있으나 처음 초기화된 후 반환값을 사용
    3. NONE: lock을 사용하지 않기 때문에 빠르지만 다중 스레드가 접근 가능(값의 일관성을 보장 못함)
/* lazy를 사용한 프로퍼티 지연 초기화 */
class LazyTest {
    init {
        println("init block") // ② 초기화 블록
    }

    val subject by lazy {
        println("lazy initialized") // ⑥ 출력문 실행
        "Kotlin Programming" // ⑦ lazy 반환값
    }

    fun flow() {
        println("not initialized") // ④ 출력문 실행
        println("subjec one: $subject") // ⑤ subject 프로퍼티 참조(최초 초기화 시점)
        println("subjec two: $subject") // ⑧ 이미 초기화된 subject 프로퍼티 참조
    }
}

fun main() {
    val test = LazyTest() // ① 객체 생성
    test.flow() // ③ // 메서드 호출
}
/* lazy를 사용한 객체 지연 초기화 */
class Person(val name: String, val age: Int)

fun main() {
    var isPersonInstantiated: Boolean = false // 초기화 확인 용도
    val person: Person by lazy { // ① lazy를 사용한 person 객체의 지연 초기화
        isPersonInstantiated = true
        Person("Kim", 32) // ② 이 부분이 Lazy 객체로 반환
    }
    val personDelegate = lazy { Person("Lee", 23) } // ③ 위임 변수를 사용한 초기화

    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()}")
}

by를 이용한 위임

  • 클래스의 위임: 무분별한 상속에 따른 복잡한 문제를 방지한다. 클래스의 모든 기능 사용과 추가 확장 구현이 용이하다.
  • 프로퍼티 위임과 by lazy: 다중 스레드 환경에서 조금 더 안정적으로 프로퍼티를 사용할 수 있다.
  • observable() 함수와 vetoable() 함수의 위임: 코틀린의 표준 위임 구현으로 Delegates 코틀린 패키지를 임포트하여 사용한다.
/* 구조 */
<val|var|class> 프로퍼티 혹은 클래스 이름: 자료형 by 위임자

/* 클래스의 위임 사용 */
interface Car {
    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 {
    fun carInfo() {
        println("$model ${go()}") // 참조 없이 각 인터페이스 구현 클래tmdml go()에 접근
    }
}

fun main() {
    val myDamas = CarModel("Damas 2010", VanImpl("100마력"))
    val my350z = CarModel("350Z 2008", SportImpl("300마력"))

    myDamas.carInfo() // carInfo에 대한 다형성을 나타냄
    my350z.carInfo()
}
/* observable() 함수 간단히 사용하기 */
import kotlin.properties.Delegates.observable

class User {
    var name: String by observable("NONAME") { // 프로퍼티 위임
        prop, old, new -> // 람다식 매개변수로 프로퍼티, 기존 값, 신규 값 지적
        println("$old -> $new") // 이벤트 발생시 실행
    }
}

fun main() {
    val user = User()
    user.name = "Kim" // 값이 변경될 때 첫 번째 이벤트 발생
    user.name = "Lee" // 값이 변경될 때 두 번째 이벤트 발생
}
/* vetoable() 함수를 사용한 최대값 구하기 */
import kotlin.properties.Delegates.vetoable

fun main() {
    var max: Int by vetoable(0) { // 초기값은 0
        prop, old, new ->
        old < new // 조건에 맞지 않으면 기존 값을 유지
    }

    println(max) // 0
    max = 10 // 기존 값(0)이 신규 값(10)보다 작으므로 ture
    println(max) // 10
    max = 5 // 기존 값(10)이 신규 값(5)보다 크므로 false
    println(max) // 10
}

3. 정적 변수와 컴패니언 객체

NOTE✏️

  • 정적 변수(Static Variable): 프로그램을 실행하기 앞서 컴파일 시간에 메모리를 할당하는 것으로 프로그램 실행 전반에 걸쳐 변수의 수명이 유지된다. Link
  • 컴패니언 객체(Companion Object): 코틀린은 static 키워드를 제공하지 않고 대 싱글톤(Singletion)으로 정의되는 컴패니언 객체를 제공한다. Link
  • 싱글톤 패턴(Singletion pattern): 싱글톤 패턴을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 이후에 호출된 생성자는 생성한 객체를 리턴한다. Link

코틀린 컴패니언 객체 사용

class Person {
    var id: Int = 0
    var name: String = "Sungje"
    companion object {
        var language: String = "Korean"
        fun work() = println("working...")
    }
}

fun main() {
    println(Person.language) // 인스턴스를 생성하지 않고 사용
    Person.language = "English" // 기본값 변경 가능
    println(Person.language)
    Person.work() //매서드 실행
    // Person.name // name은 컴패니언 객체가 아니므로 오류
}

자바에서 코틀린 컴패니언 객체 사용

  • Companion 키워드를 생략하기 위해 @JvmStatic 애노테이션을 사용
  • 프로퍼티를 자바에서 사용하기 위해 @JvmField 애노테이션을 사용
/* KCustomer.kt */
class KCustomer {
    companion object {
        const val LEVEL = "INTERMEDIATE"
        @JvmField
        val JOB = KJob()

        @JvmStatic
        fun login() = println("Longin...")
    }
}

class KJob {
    var title: String = "Programmer"
}
/* KCustomerAccess.java */
public class KCustomerAccess {
    public static void main(String[] args) {
        // 코틀린 클래스의 컴패니언 객체에 접근
        System.out.println(KCustomer.LEVEL);
        KCustomer.login(); // 애노테이션 사용시 Companion 키워드 생략 가능
        KCustomer.Companion.login();

        // KJob에 대한 객체 생성 후 접급
        KJob kjob = KCustomer.JOB;
        System.out.println(kjob.getTitle());

        // KCustomer를 통한 접근
        KCustomer.JOB.setTitle("Accountant");
        System.out.println(KCustomer.JOB.getTitle());
    }
}

최상위 함수 사용

/* PackageLevelFunction.kt */
@file:JvmName("PKLevel") // 접근할 클래스 이름을 변경
package chap06.section3

// 최상위 함수는 Java로 역컴파일되면 static final로 선언됨
fun packageLevelFunc() = println("Package-Level Function")

fun main() {
    packageLevelFunc()
}
/* PackageLevelAccess.java */
package chap06.section3;

public class PackageLevelAccess {
    public static void main(String[] args) {
        // PackageLevelFunctionKt.packageLevelFunc();
        PKLevel.packageLevelFunc(); // 변경된 이름으로 접근 가능
    }
}

object와 싱글톤: 내용이 조금 변경된 클래스를 만들 때 익명 내부 클래스를 사용할 수 있다. 코틀린에서도 사용 가능하지만 object 표현식이나 object 선언으로 조금 더 쉽게 처리 가능하다.

  • object 선언: 접근 시점에 객체가 생성되기 때문에 주 생성자와 부 생성자를 사용 불가능하나 초기화 블록은 사용 가능하다. object 선언에서도 클래스나 인터페이스를 상속할 수 있다.
// object 키워드를 사용한 방식
object OCustomer {
    var name = "Kim"
    val HOBBY = Hobby("Basketball")
    fun greeting() = println("Hello Kotlin!")
    init {
        // 최초 접근 시점에 실행된다.
        println("Init!")
    }
}

// 컴패니언 객체를 사용한 방식
class CCustomer {
    companion object {
        const val HELLO = "hello" // 상수 표현
        var name = "Lee"
        @JvmField
        val HOBBY = Hobby("Football")
        @JvmStatic
        fun greeting() = println("Hello Kotlin!")
    }
}

class Hobby(val name: String)

fun main() {
    OCustomer.greeting() // 객체의 접근 시점
    OCustomer.name = "Choi"
    println("name = ${OCustomer.name}")
    println(OCustomer.HOBBY.name)

    CCustomer.greeting()
    println("name = ${CCustomer.name}, HELLO = ${CCustomer.HELLO}")
    println(CCustomer.HOBBY.name)
}
  • object 표현식: object 선언과 달리 이름이 없으며 싱글톤이 아니다. 따라서 사용될 때마다 새로운 인스턴스가 생성된다. 결과적으로 익명 내부 클래스의 형태를 object 표현식으로 구현할 수 있다.
/* object 표현식 사용하기 */
open class Superman() {
    fun work() = println("Taking photos")
    fun talk() = println("Talking with people.")
    open fun fly() = println("Flying in the air.")
}

fun main() {
    val pretendedMan = object: Superman() { // object 표현식으로 fly() 구현의 재정의
        override fun fly() = println("I'm not a real superman. I can't fly!")
    }
    pretendedMan.work()
    pretendedMan.talk()
    pretendedMan.fly()
}