본문 바로가기

책 요약하기/Do it! 코틀린 프로그래밍

#06. 프로퍼티와 초기화 2021-05-02

06.1 프로퍼티의 접근

// 코틀린에서 자바 클래스의 static 메서드에 접근하기
fun main() {
    println(Customer.LEVEL)
    Customer.login()
}
/*
    결과 :
    BASIC
    Login...

// 자바의 Customer 클래스
public class Customer {
    public static final String LEVEL = "BASIC"; // static 필드
    public static void login() { // static 메서드
        System.out.println("Login...");
    }
}

코틀린에서는 변수 선언 부분과 기본적인 접근 메서드를 모두 가지고 있기 떄문에 프로퍼티라는 새로운 이름으로 부릅니다. Person 클래스에 변수에 해당하는 private 가시성 지시자로 지정된 name, age라는 필드를 가지고 있다고 가정했을 떄, 자바에서는 해당 필드에 접근하려면 게터(Getter)와 세터(Setter)를 만들어줘야 합니다.
(ex) getName(), SetName(String name)),  따라서 자바의 필드가 점점 늘어나면 그와 상응하는 접근 메서드도 아주 많아지게 되어 코드가 아주 읽기 어렵게 됩니다. 하지만 코틀린에서는 각 프로퍼티에 게터와 세터가 자동으로 만들어집니다.

public class JavaPerson {
    // 멤버 필드
    private String name;
    private int age;

    // 생성자
    public JavaPerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 게터와 세터
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
class Main {
    public static void main(String[] args) {
        JavaPerson p1 = new JavaPerson("Kildong", 35);
        // p1.name = "dooly" 1. 접근 불가
        p1.setName("dooly"); // 2. 세터에 의한 접근

        System.out.println(p1.getName()); // 3. 게터에 의한 접근
    }
}
/*
    결과 :
    dooly
 */

해당 자바코드를 코틀린 코드로 바꾸면 다음과 같습니다. :)

class KotlinPerson(var name: String, var age: Int)

 

코틀린에서 게터와 세터가 작동하는 방식

주 생성자에서 받은 매개변수 _id, _name, _age는 클래스 안에 있는 프로퍼티 id, name, age에 할당됩니다.

class User(_id: Int, _name: String, _age: Int) {
// 프로퍼티
    val id: Int = _id // 불변
    var name: String = _name // 변경 가능
    val age: Int = _age // 변경 가능
}

// 간소화
class User(val id: Int, var name: String, val age: Int)

 

게터와 세터 동작 확인하기

/*
코틀린에서는 프로퍼티의 게터와 세터가 자동으로 만들어집니다.
 */

fun main() {
    val user = User(1, "Sean", 30)

    val name = user.name // 게터에 의한 값 획득

    user.age = 41 // 세터에 의한 값 지정

    println("name: $name, age: ${user.age}")
}

class User(val id: Int, var name: String, var age: Int)

/*
    결과 :
    name: Sean, age: 41
 */

해당 코드를 decompile해보면 실제로 val로 생성된 id는 getId() 게터밖에 생성되지 않습니다. :)

import kotlin.Metadata;

@Metadata(
   mv = {1, 4, 2},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
   d2 = {"main", "", "Kotlinstudy2"}
)
public final class _06_1_1__프로퍼티의_접근_게터와_세터_Kt {
   public static final void main() {
      User user = new User(1, "Sean", 30);
      String name = user.getName();
      user.setAge(41);
      String var2 = "name: " + name + ", age: " + user.getAge();
      boolean var3 = false;
      System.out.println(var2);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}
// User.java
package chapter_06;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 4, 2},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\f\u0018\u00002\u00020\u0001B\u001d\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005\u0012\u0006\u0010\u0006\u001a\u00020\u0003¢\u0006\u0002\u0010\u0007R\u001a\u0010\u0006\u001a\u00020\u0003X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\b\u0010\t\"\u0004\b\n\u0010\u000bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\f\u0010\tR\u001a\u0010\u0004\u001a\u00020\u0005X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\r\u0010\u000e\"\u0004\b\u000f\u0010\u0010¨\u0006\u0011"},
   d2 = {"Lchapter_06/User;", "", "id", "", "name", "", "age", "(ILjava/lang/String;I)V", "getAge", "()I", "setAge", "(I)V", "getId", "getName", "()Ljava/lang/String;", "setName", "(Ljava/lang/String;)V", "Kotlinstudy2"}
)
public final class User {
   private final int id;
   @NotNull
   private String name;
   private int age;

   public final int getId() {   <- 이부분, name, age의 경우 게터와 세터가 있지만 id는 게터밖에 없음.
      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;
   }
}

 

기본 게터와 세터 직접 지정하기

var 프로퍼티 이름 [: 프로퍼티 자료형] [= 프로퍼티 초기화]
    [get() { 게터 본문 }]
    [set(value) { 세터 본문 }]
val 프로퍼티 이름[: 프로퍼티 자료형] [=프로퍼티 초기화]
    [get() { 게터 본문 }]
package chapter_06

fun main() {
    val user2 = User2(1, "Kildong", 30)
    user2.age = 35
    println("user1.age = ${user2.age}")
}
/*
    결과 :
    user1.age = 25
 */


class User2(_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
        }
}
value: 세터의 매개변수로 외부로부터 값을 가져옴
field: 프로퍼티를 참조하는 변수

user2.age = 35 형태로 사용하면 정숫값(45)가 set(value)의 매개변수(value) 인자로 들어와 value에 할당되고, value는 정해진 이름이 아닙니다(바꿀 수 있음). 하지만 field는 정해져 있는 이름입니다.

 

보조 필드의 역할

field는 프로퍼티를 참조하는 변수로 보조 필드(Backing Field)라고도 합니다. get() = field는 결국 각 프로퍼티의 값을 읽는 특별한 식별자입니다. 만일 게터와 세터 안에서 field 대신에 get() = age와 같이 사용하면 프로퍼티의 get()이 다시 호출되는 것과 같으므로 무한 재귀 호출에 빠져 스택 오버플로우 오류가 발생할 수 있습니다. set()에도 값을 할당하기 위해 프로퍼티 이름을 직접 사용하지 않도록 주의해야 합니다.

var name: String = _name
    get() = field
    set(value) {
        name = value // 결국 자바로 변환되어 this.setName(value)형태가 됨.
    }
...

name = value는 this.setName(value)형태로 변환되기 떄문에 지속적으로 setName()이 계속 호출되다가 어느 순간 스택이 꽉 차게 되어 스택 오버플로우가 발생합니다.
따라서 field를 적절히 사용하여 게터와 세터를 작성해야 합니다.

 

커스텀 게터와 세터의 사용

package chapter_06

fun main() {
    val user3 = User3(1, "kildong", 35)
    user3.name = "COCO"
    println("user3.name = ${user3.name}")
}
/*
    결과 :
    The name was changed
    user3.name = coco
 */

class User3(_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
        private set
    // private set(가시성 지시자 넣어 주면 외부에서 프로퍼티에 값 할당 x)
}

(세터에 private 가시성 지시자를 붙여주면 외부에서 값 할당이 불가하고, field = value.toUpperCasse()에 의해 받은 인자를 대문자로 변경에 프로퍼티에 할당합니다. :))

 

보조 프로퍼티 사용

package chapter_06

import java.lang.AssertionError

fun main() {
    val user4 = User4(1, "kildong", 35)
    user4.name = ""
    println("user4.name = ${user4.name}")
}

/*
    결과 :
    user4.name = NONAME
 */

class User4(_id: Int, _name: String, _age: Int) {
    val id: Int = _id
    private var tempName: String? = null
    var name: String = _name
    get() {
        if (tempName == null) tempName = "NONAME"
        return tempName ?: throw AssertionError("Asserted by others")
    }

    var age: Int = _age
}

(이런 경우 보조 필드인 field를 사용하지 않고 추가로 내부의 프로퍼티를 임시로 선언해 사용할 수 있습니다.)

 

프로퍼티의 오버라이딩

package chapter_06

fun main() {
    val second = Second()
    println(second.x)   // 5. 오버라이딩된 두 번쨰 클래스 객체의 x
    println(second.y)   // 상위 클래스로부터 상속받은 값
}
/*
    결과 :
    Second x
    3
    0
 */

open class First {
    open val x: Int = 0 // 1. 오버라이딩 가능
        get() {
            println("First x")
            return field
        }
    val y: Int = 0 // 2. open 키워드가 없으면 final 프로퍼티
}

class Second : First() {
    // 상위 클래스 val -> 하위 클래스 var 바꿀 수 있음(반대는 불가)
    override val x: Int = 0 // 3. 상위 클래스와 구현부가 다르게 오버라이딩됨
        get() {
            println("Second x")
            return field + 3
        }
    // override val y: Int = 0 // 4. 오류! 오버라이딩 불가
}

(x의 프로퍼티에 open키워드로 수직하여 정의하면 오버라이딩 할 수 있습니다. 오버라이딩 시 상위 클래스에 프로퍼티를 val로 정의한 경우 하위 클래스에서 var로 변경할 수 있습니다. 반대로 var를 val로 변경할 수는 없습니다.)

 

프로퍼티를 이용한 나이속이기 예제

package chapter_06

fun main() {
    val kim = FakeAge()
    kim.age = 15
    println("Kim`s real age = 15, pretended age = ${kim.age}")

    val hong = FakeAge()
    hong.age = 35
    println("Hong`s real age = 35, pretended age = ${hong.age}")
}

/*
    결과 :
    Kim`s real age = 15, pretended age = 18
    Hong`s real age = 35, pretended age = 32
 */


class FakeAge {
    var age: Int = 0
    set(value) { // 나이에 따라 판별하는 세터
        field = when {
            value < 18 -> 18
            value in 18..30 -> value
            else -> value - 3
        }
     }
}

 

06.2 지연 초기화와 위임

보통 클래스에서는 기본적으로 선언하는 프로퍼티 자료형들은 null을 가질 수 없기 때문에 생성자에서 초기화하거나 매개변수로부터 값을 초기화해야 하는 것이 규칙입니다. 그러면 초기화를 미루려면 어떻게 할까요? 지연 초기화 lateinit과 lazy 키워드를 통해 이를 사용할 수 있습니다.

lateinit을 사용한 지연 초기화

예를 들어 Car클래스의 초기화 부분이 Engine클래스와 의존성을 가질 떄 Engine 객체가 생성되지 않으면 완전하게 초기화 할 수 없습니다. 특정 객체의 의존성이 있는 경우에는 지연 초기화를 해야 합니다. 또 해당 자료형의 프로퍼티를 즉시 사용하지 않는데도 미리 생성해서 초기화한다면 메모리가 사용되어 낭비될 수 있습니다. 

프로퍼티 지연 초기화

클래스를 선언할 떄 프로퍼티 선언은 null을 허용하지 않습니다. 하지만 지연 초기화를 위한 lateinit 키워드를 사용하면 프로퍼티에 값이 바로 할당되지 않아도 컴파일러에서 허용하게 됩니다.

lateinit의 제한
- var로 선언된 프로퍼티만 가능하다.
- 프로퍼티에 대한 게터와 세터를 사용할 수 없다.
package chapter_06

/*
lateinit의 제한
- var로 선언된 프로퍼티만 가능하다.
- 프로퍼티에 대한 게터와 세터를 사용할 수 없다.
 */

fun main() {
    val kildong = Person()
    kildong.test()
    kildong.name = "Kildong" // 3. 이 시점에서 초기화됨(지연 초기화)
    kildong.test()
    println("name = ${kildong.name}")
}
/*
    결과 :
    not initialized
    initialized
    name = kildong

 */


class Person {
    lateinit var name: String // 1. 지연 초기화를 위한 선언

    fun test() { // 2. 프로퍼티의 초기화 여부 판단
        if (!::name.isInitialized) {
            println("not initialized")
        } else {
            println("initialized")
        }
    }
}

1번에서 Person 클래스의 name 프로퍼티를 lateinit으로 선언했습니다. 그러면 name프로퍼티는 초기화하지 않은 채 선언할 수 있습니다. 즉, main() 함수의 블록에서 val kindong = Person()으로 객체를 생성해도 name은 초기화되지 않습니다. lateinit은 지연된 초기화를 허용해, 프로퍼티에 값을 할당하지 않아도 오류가 발생하지 않습니다.
2번의 isInitialized는 프로퍼티가 초기화되었는지 검사하는 코틀리니 표준 함수의 API입니다. 프로퍼티 참조를 위해 (::)를 사용했습니다. 만약 lateinit을 사용한 프로퍼티를 그냥 사용하면 오류가 발생합니다.

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

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

fun main() {
    person1 = Person("Kildong", 30) // 생성자 호출 시점에서 초기화됨.
    print(person1.name + " is " + person1.age.toString())
}

(이렇게 객체를 생성하는 것도 지연하여 생성 할 수 있습니다.)

 

lazy를 사용한 지연 초기화

lateinit을 통해서 프로퍼티나 객체를 선언할 떄는 val은 허용하지 않고 var로 선언해야 했습니다. 하지만 var로 선언하면 객체나 프로퍼티의 경우 언제든 값이 변경될 수 있는 단점이 있습니다. 

lazy 특징
- 호출 시점에 by lazy{...}정의에 의해 블록 부분의 초기화를 진행합니다.
- 불변의 변수 선언인 val에서만 사용 가능합니다.(읽기 전용)
- val이므로 값을 다시 변경할 수 없습니다.

 

by lazy로 선언된 프로퍼티 지연 초기화

package chapter_06

fun main() {
    val test = LazyTest() // 1
    test.flow() // 3
}
/*
    결과 :
    init block
    not initialized
    lazy initialized
    subject one: Kotlin Programming
    subject two: Kotlin Programming
 */

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

    val subject by lazy {
        println("lazy initialized") // 6
        "Kotlin Programming"
    }
    fun flow() {
        println("not initialized") // 4
        println("subject one: $subject") // 5. 최초 초기화 시점
        println("subject two: $subject") // 8. 이미 초기화된 값 사용
    }
}

1번에 의해 test객체를 생성하고 2번이 실행됩니다. 아직 subject프로퍼티는 by lazy에 의해 초기화되지 않았습니다.
여기서 by는 프로퍼티를 위임할 때 사용하는 키워드입니다. 그다음 main()함수로 돌아와 3번과 같이 test객체의 flos()메서드를 실행합니다. 4번, 5번이 실행되고 5번에서 사용된 $subject에 의해 subject 프로퍼티가 최초로 접근됩니다. 이때 subject값이 lazy블록에 있는 6번을 실행한 후 람다식의 맨 마지막 문장이 반환값이 되어 7번의 내용으로 초기화됩니다.

 

객체 지연 초기화하기

package chapter_06

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

fun main() {
    var isPersonInstantiated: Boolean = false // 초기화 확인 용도

    val person: Person1 by lazy { // 1. lazy를 사용한 person1 객체의 지연 초기화
        isPersonInstantiated = true
        Person1("Kim", 23) // 2. 이 부분이 Lazy 객체로 반환됨
    }

    val personDelegate = lazy { Person1("Hong", 40) } // 3. 위임 변수를 사용한 초기화

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

    println("person.name = ${person.name}") // 4. 이 시점에서 초기화
    println("personDelegate.value.name = ${personDelegate.value.name}") // 5. 이 시점에서 초기화

    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

 */

여기서는 1번의 by lazy를 사용해 person 객체를 지연 초기화하고 있고, 3번의 lazy만 사용해 위임 변수를 받아서 지연 초기화에 사용하고 있습니다. 2가지 방법 모두 지연 초기화를 lazy 블록 구문에서 수행합니다. lazy 블록의 마지막 표현식이 초기화된 후 Lazy 객체로 반환되므로 2번과 같이 객체 생성자를 반환합니다. 
해당 객체의 초기화는 프로퍼티나 메서드가 접근되는 시점에서 초기 시점인 4 5번에서 초기화됩니다.
by lazty나 lazy 할당의 차이점은 by lazy는 객체의 위임을 나타내며 lazy는 변수에 위임된 Lazy 객체를 자체로 나타내므로 이 변수의 value를 한 단계 더 거쳐 객체의 멤버인 value.name과 같은 형태로 접근해야 합니다.

 

lazy 모드 확인하기

lazy의 모드를 이해하기 위해 lazy의 선언부를 살펴보고 각 모드를 살표보면 다음과 같습니다.

// 코틀린 표준 라이브러리 파일 LazyJVM.kt의 선언부
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

lazy()는 매개변수 없는 람다식을 받을 수 있으며 Lazy<T>를 반환합니다. lazy()의 실행은 구현부 SynchronizedLazyImpl()에 보내 처리합니다. 그리고 mode에 대한 매개변수를 지정할 경우 3가지 모드인 SYNCHRONIZED, PUBLICATION, NONE을 지정할 수 있습니다. 여기서는 mode를 지정하지 않으면 기본저긍로 SYNCHRONIZED가 사용되므로 SynchonizedLazyImpl() 구현부를 다음과 같이 호출합니다.

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE // <- 초기화
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) { // <- return value
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

(이전에 학습한 내용만 집중해서 보면 _value에 의해 게터에서 읽을 떄 UNINITIALIZED_VALUE로 먼저 초기화하고 synchronized 블록을 반환합니다. 이 블록은 스레드에 안전한(Thread safe)형태로 동작시키기 위해 lock 기법을 사용합니다.
아직 초기화되지 않았다면 _v2 as T를 통해 값은 해당 자료형으로 형 변환되어 반환합니다.
또다시 호출이 일어나면 이미 생성한 값 typedValue를 통해 불러옵니다.
기본적인 lazy의 실행 모드가 SYNCHRONIZED로 설정되어 있기 떄문에 위의 구현부가 호출되었습니다.

lazy의 모드
- SYNCHRONIZED: lock을 사용해 단일 스레드만이 사용하는 것을 보장(기본값)
- PUBLICATION: 여러 군데에서 호출될 수 있으나 처음 초기화된 후 반환값을 사용
- NONE: lock을 사용하지 않기 때문에 빠르지만 다중 스레드가 접근할 수 있습니다.(값의 일관성을 보장할 수 없음.)

만일 다른 모드를 사용하고 싶다면 by lazy(모드 이름) { ... } 형태로 사용할 수 있습니다. 만일 항상 단일 스레드에서 사용하고 있다는 것이 보장되면 LazyThreadSafetyMode.NONE을 사용해도 됩니다. 하지만 따로 동기화 기법을 사용하지 않는다면 다른 모드는 사용하는 것을 권장하지 않습니다.

private val model by lazy(mode = LazyThreadSafetyMode.NONE) {
    Injector.app().transactionModel() // 이 코드는 단일 스레드의 사용이 보장될 떄
}
단일 스레드 사용의 보장

단일 스레드의 사용을 보장한다는 말은, 단 하나의 코드 흐름에서 해당 데이터를 접근하기 떄문에 다른 코드에 의해 변경되지 않을 것임을 보장한다는 말입니다. 보통 프로그램은 다양한 루틴에 의해 수행되고 여러 개의 스레드가 동시에 수행되는 경우가 많기 때문에 특정 자원 사용할 때 다른 스레드에 의해 값이 변경될 수 있습니다. 따라서 이것을 보호하기 위해 동기화 기법인 look을 사용하는 synchronized() {...} 블록을 사용합니다. 

 

by를 이용한 위임

코틀린에서는 특정 클래스를 확장하거나 이용할 수 있도록 by를 통한 위임이 가능합니다.
by를 사용하면 하나의 클래스가 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 멤버를 참조 없이 호출할 수 있습니다. 이 중 프로퍼티 위임은 프로퍼티의 게터와 세터를 특정 객체에게 위임하고 그 객체가 값을 읽거나 쓸 떄 수행하도록 만드는 것을 말합니다. 프로퍼티 위임을 하려면 위임을 받을 객체에 by 키워드를 사용하면 됩니다. 

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

프로퍼티 위임을 위해서는 val, var와 같은 프로퍼티 선언을 위한 키워드, 프로퍼티의 이름, 자료형 그리고 by와 위임자가 필요합니다. 이때 위임자란 프로퍼티나 클래스를 대신할 객체입니다.

클래스의 위임

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

class Cat: Animal {
override fun eat() { ... }
...
}
val cat = Cat()
class Robot(impl: Animal): Animal by cat // Animal의 정의된 Cat의 모든 멤버를 Robot에 위임.

만약 Animal 인터페이스를 구현하고 있는 Cat 클래스가 있다면 Animal에서 정의하고 있는 Cat의 모든 멤버를 Robot 클래스로 위임할 수 있습니다. 즉, Robot은 Cat이 가지는 모든 Animal의 메서드를 가지는데 이것을 클래스 위임(Class Delegation)이라고 합니다. 사실 Cat은 Animal 자료형의 private 멤버로 Robot 클래스 안에 저장되며 Cat에서 구현된 모든 Animal의 메서드는 정적 메서드로 생성됩니다. 따라서 우리가 Robot 클래스를 사용할 떄 Animal클래스를 명시적으로 참조하지 않다러도 eat()을 바로 호출하는 것이 가능합니다. 

[쉬운 설명]

interface A { ... }
class B : A { }
val b = B()
// C를 생성하고, A에서 정의하는 B의 모든 메서드를 C에 위임합니다.
class C : A by b

만약 interface A를 구현하고 있는 class B가 있다면, A에서 정의하고 있는 B의 모든 메소드를 클래스 C로 위임할 수 있습니다. 즉, C는 B가 가지는 모든 A의 메소드를 가집니다.

위임을 사용하는 이유?
기본적으로 코틀린이 가지고 있는 표준 라이브러리는 open으로 정의되지 않은 클래스를 사용하고 있는데 이것은 final형태의 클래스이므로 상속이나 직접 클래스의 기능 확장이 어렵습니다. 이렇게 어렵게 만들어 둠으로써 표준 라이브러리의 무분별한 상속에 따른 복잡한 문제를 방지할 수 있습니다. 따라서 필요한 경우에만 위임을 통해 상속과 비슷하게 해당 클래스의 모든 기능을 사용하면서 동시에 기능을 추가 확장 구현할 수 있습니다.

package chapter_06

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

    myDamas.carInfo() //2. carInfo에 대한 다형성
    my350z.carInfo()
}
/*
    결과 :
    Damas 2010 은 짐을 적재하며 100마력 을 가집니다.
    350Z 2008 은 경주용에 사용되며 350마력 을 가집니다.
 */
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()}") // 1. 참조 없이 각 인터페이스 구현 클래스의 go()에 접근
    }
}

impl은 CarModel의 위임되어 각 구현 클래스인 VanImpl과 SportImpl의 go()메서드를 생성된 위임자에 맞춰 호출할 수 있습니다. 이때는 1번과 같이 특정 참조 없이 go()를 사용할 수 있습니다. 

 

프로퍼티 위임과 by lazy

프로퍼티의 lazy도 by lazy { ... } 처럼 by가 사용되어 위임된 프로퍼티가 사용되었다는 것을 알 수 있습니다. lazy는 람다식인데 사용된 프로퍼티는 람다식에 전달되어(위임되어) 사용됩니다.

1. lazy 람다식은 람다식을 전달받아 저장한 Lazy<T> 인스턴스를 반환한다.
2. 최초 프로퍼티의 게터 실행은 lazy에 넘겨진 람다식을 실행하고 결과를 기록합니다.
3. 이후 프로퍼티의 게터 실행은 이미 초기화되어 기록된 값을 반환합니다.

by lazy에 의한 지연 초기화는 스레드에 좀 더 안정적으로 프로퍼티를 사용할 수 있습니다. 예를 들어 프로그램 시작 시 큰 객체가 있다면 초기화할 떄 모든 내용을 시작 시간에 할당해야 하므로 느려질 수밖에 없습니다. 이것을 필요에 따라 해당 객체를 접근하는 시점에서 초기화하면 시작할 때마다 프로퍼티를 생성하느라 소비되는 시간을 줄일 수 있습니다.

 

observable() 함수와 vetoable() 함수의 위임

코틀린의 표준 위임 구현 중에 하나인 observable() 함수와 vetoable()함수입니다.
프로퍼티를 위임하는 object인 Delegates로부터 사용할 수 있는 위임자인 observable() 함수는 프로퍼티를 감시하고 있다가 특정 코드의 로직에서 변경이 일어날 때 호출되어 처리됩니다. 특정 변경 이벤트에 따라 호출되므로 콜백이라고도 부릅니다. vetoable()함수는 observable()함수와 비슷하지만 반환값에 따라 프로퍼티 변경을 허용하거나 취소할 수 있습니다.

package chapter_06

import kotlin.properties.Delegates

fun main() {
    // observable() example
    val user = ObservableUser()
    user.name = "Kildong" // [4]. 값이 변경되는 시점에서 첫 이벤트 발생
    user.name = "Dooly"   // [5]. 값이 변경되는 시점에서 두 번쨰 이벤트 발생

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

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

    // 여기서는 기존값이 새 값보다 크므로 false, 따라서 5를 재할당하지 않음
    max = 5
    println(max) // 10
}
/*
    결과 :
    NONAME -> Kildong
    Kildong -> Dooly
    0
    10
    10
 */

class ObservableUser{
    var name: String by Delegates.observable("NONAME") { // [1]. 프로퍼티 위임
        prop, old, new -> // [2]. 람다식 매개변수로 프로퍼티, 기존 값, 새로운 값 지정
        println("$old -> $new") // [3]. 이 부분은 이벤트가 발생할 때만 실행
    }
}

[observable()}

먼저 ObservableUser 클래스의 name프로퍼티를 [1]번과 같이 observable()함수로 위임합니다. 이떄 초깃값 initialValue는 "NONAME"입니다. [4], [5]번과 같이 값의 변경이 일어나면 [3]번의 println("$old -> $new")를 실행합니다. 값의 변경이 일어나는 시점은 main() 블록의 name에 새로운 값을 설정할 때입니다. 이때 감시 역할을 하는 observable()함수의 코드가 수행됩니다.

 

[vetoable()]

1번의 Delegates.vetoable(0)에 의해 초깃값은 0이고 프로퍼티 max를 다루고 있습니다. 기존 값보다 새 값이 커야만 true가 되면서 프로퍼티의 교체 작업이 진행됩니다. 따라서 기존 값이 작은 max = 5는 실행되지 않고 2번의 조건대로 거부됩니다. 

var data: List<Any> by Delegates.vetoable(listOf()) { p, old, nes ->
    notifyDataSetChanged()
    old != new
}

...

// 코드 어딘가에서 data 프로퍼티를 설정함

여기서는 초깃값을 빈 목록인 listOf()로 받고 있습니다. 만일 프로퍼티의 변경이 일어나면 notifyDataSetChanged()를 실행하고 old 값이 new 값과 다르다면 true를 반환하는 것입니다. 반환값이 true가 되면 드디어 기존 값이 새 값으로 교체됩니다. false가 반환되면 data에 변화가 없다는 것이기 떄문에 할당할 필요가 없으므로 할당 작업을 거부됩니다.

 

06.3 정적 변수와 컴패니언 객체

정적 변수와 컴패니언 객체

코틀린에서는 정적 변수를 사용할 때 static 키워드가 없는 대신 컴패니언 객체를 제공합니다

컴패니언 객체 사용

package chapter_06

fun main() {
    println(Person3.language) // 인스턴스를 생성하지 않고 기본값 사용
    Person3.language = "English" // 기본값 변경 가능
    println(Person3.language) // 변경된 나용 출력
    Person3.work() // 메서드 실행
    // println(Person3.name) // name은 컴패니언 객체가 아니므로 오류
}
/*
    결과 :
    Korean
    English
    working...
 */


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

Person3 클래스의 language는 객체의 생성 없이도 접근할 수 있게 되었습니다. 물론 work()멤버 메서드도 객체 생성 없이 실행할 수 있습니다. 컴패니언 객체는 실제 객체의 싱글톤(Singleton)으로 정의됩니다.

// 코틀린에서 자바 클래스의 static 메서드에 접근하기
fun main() {
    println(Customer.LEVEL)
    Customer.login()
}
/*
    결과 :
    BASIC
    Login...
*/


// 자바의 Customer 클래스
public class Customer {
    public static final String LEVEL = "BASIC"; // static 필드
    public static void login() { // static 메서드
        System.out.println("Login...");
    }
}

 

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

자바에서는 코틀린의 컴패니언 객체에 접근하려면 @JvmStatic 애노테이션(Annotation)표기법을 사용해야 합니다.

애노테이션
@JvmStatic, @Override와 같이 @ 기호로 시작하는 애노테이션 표기는 사전적으로 '주석'이라는 뜻입니다. 하지만 코드에서는 특수한 의미를 부여해 컴파일러가 목적에 맞추어 해석하도록 하거나 실행(런타임)할 때 특정 기능을 수행하게 할 수도 있습니다.
// 컴패니언 객체를 가진 코틀린의 클래스
class KCustomer {
    companion object {
        const val LEVEL = "INTERMEDIATE"

        @JvmStatic // 애노테이션 표기
        fun login() = println("Login...")
    }
}


// KCustomerAccess.java
public class KCustomerAccess {
    public static void main(String[] args) {
        // 코틀린 클래스의 컴패니언 객체에 접근
        System.out.println(KCustomer.LEVEL);
        KCustomer.login(); // 애노테이션을 사용할 떄 접근 방법
        KCustomer.Companion.login(); // 애노테이션을 사용하지 않을 떄 접근 방법
    }
}
/*
    결과 :
    INTERMEDIATE
    Login...
    Login...
 */

const : 컴파일 시간의 상수 (val과 다르게 컴파일 시간에 이미 값이 할당되는 것으로 자바에 접근하기 위해서 필요합니다. val은 실행 시간에 할당합니다.) const는 기본형으로 사용할 자료형과 String에만 적용됩니다.
@JvmStatic애노테이션은 자바 소스에서 코드를 해석할 때 Companion을 생략할 수 있게 해줍니다. 

@JvmStatic
fun login() = println("Login...")

애노테이션이 있는 login()메서드는 자바에서 접근할 때 Companion을 생략해 다음과 같이 접근할 수 있게 해줍니다.
KCustomer.login();

애노테이션을 사용하지 않는 경우 Companion을 포함해 다음과 같이 접근해야 합니다.
KCustomer.Companion.login();

 

@JvmField 애노테이션 사용

public class KCustomerAccess2 {
    public static void main(String[] args) {
        // KJob에 대한 객체 생성 후 접근
        KJob kjob = KCustomer2.JOB;
        System.out.println(kjob.getTitle());

        // KCustomer를 통한 접근
        KCustomer2.JOB.setTitle("Accountant");
        System.out.println(KCustomer2.JOB.getTitle());
    }
}
/*
    결과 :
    Programmer
    Accountant
 */
 
 
 class KCustomer2 {
    companion object {
        const val LEVEL = "INTERMEDIATE"
        @JvmStatic fun login() = println("Login...") // 애노테이션 표기 사용
        @JvmField val JOB = KJob() // 특정 자료형을 사용하기 위한 에노테이션
    }
}

class KJob {
    var title: String = "Programmer"
}

@JvmField애노테이션으로 정의되었기 떄문에 KCustomer.JOB.getTitle()과 같은 방법으로 접근하거나 KJob에 대한 객체를 만들고 접근할 수 있습니다.

 

최상위 함수 사용하기

클래스 없이 만든 함수는 객체 생성 없이도 main() 함수 어디에서든 실행할 수 있었습니다. 이것을 최상위 함수(Top-level Function)혹은 패키지 레벨 함수(Package-level Function)라고 합니다. 

package chapter_06

fun main() {
    packageLevelFUn()
}

fun packageLevelFUn() {
    println("Package-Level Function")
}
/*
    결과 :
    Package-Level Function
 */
public final class _06_3_4__정적_변수와_컴패니언객체_최상위함수_Kt {
   public static final void main() {
      packageLevelFUn();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void packageLevelFUn() {
      String var0 = "Package-Level Function";
      boolean var1 = false;
      System.out.println(var0);
   }
}

해당 최상위함수 예제를 디컴파일 해보면 _06_3_4__정적_변수와_컴패니언객체_최상위함수_Kt 클래스가 자동 생성되었고(파일 이름과 확장자 이름이 합쳐져 만들어진 상태)입니다. 그리고 자바코드에서는 다음과 같이 접근 할 수 있습니다.

package chapter_06;

public class PackageLevelAccess {
    public static void main(String[] args) {
        _06_3_4__정적_변수와_컴패니언객체_최상위함수_Kt.packageLevelFUn();
    }
}
/*
    결과 :
    Package-Level Function
 */

만약 접근할 클래스 이름을 바꾸려면 @file:JvmName("ClassName")을 코드 상단에 입력하면 됩니다.

// 코틀린 파일
@file:JvmName("PKLevel")
package chapter_06

fun main() {
    packageLevelFUn()
}
/*
    결과 :
    Package-Level Function
 */


fun packageLevelFUn() {
    println("Package-Level Function")
}
// 자바 파일
package chapter_06;

public class PackageLevelAccess2 {
    public static void main(String[] args) {
        PKLevel.packageLevelFUn();
    }
}
/*
    결과 :
    Package-Level Function
 */

 

object와 싱글톤

package chapter_06

import javax.swing.plaf.ColorChooserUI

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

    CCustomer.greeting()
    println("name = ${CCustomer.name}, HELLO = ${CCustomer.HELLO}")
    println(CCustomer.HOBBY.name)
}
/*
    결과 :
    Init
    Hello World!
    name = Dooly
    Basketball
    Hello World!
    name = Joosol, HELLO = hello

 */


// 1. object 키워드를 사용한 방식
object OCustomer {
    var name = "Kildong"
    fun greeting() = println("Hello World!")
    val HOBBY = Hobby("Basketball")
    init {
        println("Init!")
    }
}

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

class Hobby(val name: String)

object로 선언된 OCustomer는 멤버 프로퍼티와 메서드를 객체 생성 없이 이름의 점(.) 표기법으로 바로 사용할 수 있습니다. 이것 역시 단일 인스턴스를 생성해 처리하기 떄문에 싱글톤 패턴에 이용됩니다.
object 선언 방식을 사용하면 접근 시점에 객체가 생성됩니다. 그렇기 때문에 생성자 호출을 하지 않으므로 object 선언에는 주 생성자와 부 생성자를 사용할 수 없습니다. 하지만 초기화 블록인 init{}이 들어갈 수 있는데 최초 접근에서 실행됩니다. 

만일 자바에서 object 선언으로 생성된 인스턴스에 접근하려면 INSTANCE를 사용합니다.

public class OCustomerAccess {
    public static void main(String[] args) {
        String name = OCustomer.INSTANCE.getName(); // 코틀린의 object 선언 객체의 메서드 접근
        System.out.println(name);
    }
}
/*
    결과 :
    Init!
    Kildong
 */

 

object 선언 방식의 디컴파일을 해보면 다음과 같습니다.

public final class OCustomer {
   @NotNull
   private static String name;
   @NotNull
   private static final Hobby HOBBY;
   @NotNull
   public static final OCustomer INSTANCE;

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

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

   public final void greeting() {
      String var1 = "Hello World!";
      boolean var2 = false;
      System.out.println(var1);
   }

   @NotNull
   public final Hobby getHOBBY() {
      return HOBBY;
   }

   private OCustomer() {
   }

   static {
      OCustomer var0 = new OCustomer();
      INSTANCE = var0;
      name = "Kildong";
      HOBBY = new Hobby("Basketball");
      String var1 = "Init!";
      boolean var2 = false;
      System.out.println(var1);
   }
}

 변환된 코드에서 OCustomer 객체를 INSTANCE라는 이름으로 static 블록에서 생성되고 있음을 알 수 있습니다.

 

object 표현식

object 표현식은 object 선언과 달리 이름이 없으며 싱글통이 아닙니다. 따라서 object 표현식이 사용될 때마다 새로운 인스턴스가 생성됩니다. 결과적으로 이름이 없는 익명 내부 클래스로 불리는 형태를 object 표현식으로 만들 수 있습니다.

package chapter_06

import java.awt.SystemColor.window
import java.awt.event.MouseAdapter

interface Shape {
    fun onDraw()
}
fun main() {
    val pretendedMan = object: Superman() { // 1. object 표현식으로 fly() 구현의 재정의
        override fun fly() = println("I`m not a real superman. I can`t fly")
    }
    pretendedMan.work()
    pretendedMan.talk()
    pretendedMan.fly()
}
/*
    결과 :
    Taking photos
    Talking with people
    I`m not a real superman. I can`t fly
 */


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

1번에 익명 객체가 object 표현식으로 만들어졌습니다. 여기서 익명 객체는 Superman 클래스를 상속해 fly() 메서드를 오버라이딩하고 있습니다. 결국 하위 클래스를 만들지 않고도 Superman 클래스의 fly() 메서드를 오버라이딩 해 변경했습니다.

// 안드로이드 활용 사례
window.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
    	...
    }
    
    override fun mouseEntered(e: MouseEvent) {
    	...
    }
})

addMouseListener()의 매개변수로 object 표현식이 사용되었는데 이떄 2개의 메서드가 MouseAdapter()를 통해서 오버라이딩하고 클래스의 이름 없이 사용했습니다.

 

interface Shape {
	fun onDraw() // 구현해야 할 메서드
}

val triangle = object: Shape {
	override fun onDraw() { //여기서 딱 한 번 구현됨.
    ...
    }
}
fun foo() {
    val adHoc = object {
    	var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

상위 인터페이스나 클래스가 없는 경우 다음과 같이 활용됩니다. :)

 

class C {
    // Private function -> 반환 자료형은 익명 객체 자료형이 됨.
    private fun foo() = object {
    	val x: String = "x"
    }
    
    // Public function -> 반환 자료형은 Any형이 됨.
    fun publicFoo() = object {
    	val x: String = "x"
    }
    
    fun bar() {
    	val x1 = foo().x // 문제 x
        val x2 = publicFoo().x // 오류
    }
}

익명 객체를 public 함수의 반환 자료형이나 public 속성의 자료형으로 쓴다면, 이러한 함수나 속성의 실제 자료형은 익명 객체로 선언된 상위 자료형이 되거나 혹은 상위 자료형을 선언하지 않으면 Any형이 됩니다.

 

자바의 익명 내부 클래스와 같이 object 표현식 안의 코드는 둘러싸여 있는 범위 내부의 변수에 접근할 수 있습니다.

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0
    

    window.addMouseListener(object: MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
    	clickCount++
    }
    
    override fun mouseEntered(e: MouseEvent) {
    	enterCount++
    }
})

 

마무리 문제

package chapter_06

import java.lang.reflect.Field


// Q1. name 프로퍼티에 할당하는 세터 작성
class UserFinalProblem06(_name: String, _age: Int) {
    var name: String = _name
    set(value) {
        println("The name was changed")
        field = value.toUpperCase()
    }
    var age: Int = _age
}

// Q2. 지연 초기화를 위한 Person 클래스의 name 선언
class PersonFinalProblem06 {
    lateinit var name: String
}

// Q3. 클래스를 이용하지 않아도 (object)를 사용하면
// 특정 객체를 사용하거나 클래스 선언 없이 객체를 변경하고 생성할 수 있습니다.

 

이상 마치겠습니다. :)