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

문상훈·2022년 7월 26일
1

Kotlin

목록 보기
7/7

🛴 06. 프로퍼티와 초기화

📌 06-1 프로퍼티의 접근

자바에서 필드를 사용할 때의 문제점

  • 게터와 세터에 해당하는 접근 메서드를 직접 만들어야 한다.
    • 자바의 필드가 많아지면 그와 상응하는 접근 메서드도 많아져 코드가 읽기 어려워진다.

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

  • 게터와 세터를 자동적으로 처리해주기 때문에 코드의 양이 줄어든다.
class User(val id: Int, var name: String, var age: Int)

fun main( ) {
    val user = User(1, "Sean", 30)
    val name = user.name // 게터에 의한 값 획득
    user.age = 41 // 세터에 의한 값 지정
    println("name: $name, ${user.age}")
}

출력 : name: Sean, 41


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

📍 기본 선언구조

var 프로퍼티 이름[: 프로퍼티 자료형] [= 프로퍼티 초기화]
	[get() {게터본문}]
	[set(value) {세터본문}]
val 프로퍼티 이름[: 프로퍼티 자료형] [= 프로퍼티 초기화]
	[get() {게터본문}]
// 직접 구성한 기본 게터/세터
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 user1 = User(1, "Jimuk", 20)
    // user1.id = 2  // val 프로퍼티는 값 변경 불가
    user1.age = 35 // 세터
    println("user1.age = ${user1.age}") // 게터
}

출력 : user1.age = 35

value : 세터의 매개변수로 외부로부터 값을 가져옴
field : 프로퍼티를 참조하는 변수
📍 value는 다른 이름으로 변경 가능, filed는 불가능 !!


커스텀 게터와 세터의 사용

커스텀 게터와 세터 : 사용자가 직접 게터와 세터를 정의하면서 새로운 내용을 작성하는 것
📍 입력 문자를 대문자로 바꾸는 등의 특정 연산을 수행할 경우 게터와 세터로 확장해 코드를 구성할 수 있어 편리하다.


📌 06-2 지연 초기화와 위임

  • 지연초기화가 필요한 이유
    📍 객체의 정보가 나중에 나타나는 경우에 사용
    • 특정 객체의 의존성이 있는 경우 지연초기화 사용
    • 모듈 별로 소스 코드를 테스트하는 유닛 테스트를 할 때 임시적으로 객체를 생성시켜야할 때 사용

lateinit을 사용한 지연 초기화

📫 lateinit의 제한

  • var로 선언된 프로퍼티만 가능하다.
  • 프로퍼티에 대한 게터와 세터를 사용할 수 없다.
class Person {
    lateinit var name: String

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

fun main() {
    val jimuk = Person()
    jimuk.test()
    jimuk.name = "Jimuk"
    jimuk.test()
    println("name = ${jimuk.name}")
}

출력 :

not initialized
inirialized
name = Jimuk

lazy를 사용한 지연 초기화

📫 lazy 특징

  • 호출 시점에서 by lazy {...} 정의에 의해 블록 부분의 초기화를 진행한다.
  • 불변의 변수 선언인 val에서만 사용 가능하다. (읽기 전용)
    📍 읽기 전용의 val로 선언한 객체나 프로퍼티를 나중에 초기화할 때 사용 !!
  • val이므로 값을 다시 변경할 수 없다.

📝 프로퍼티 지연 초기화

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

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

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

출력 :

init block
not initialized
lazy initialized
subject one: Kotlin Programming
subject two: Kotlin Programming
  • 프로퍼티에 최초로 접근한 시점에 해당 프로퍼티가 초기화된다. -> 그 후 이미 초기화된 내용을 재사용한다.
    • subjectval로 선언되어 있어 값을 다시 설정할 수 없다.

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


by를 이용한 위임

  • 위임 : 어떤 일을 대신하는 중간자 역할
    📍 특정 클래스를 확장하거나 이용할 수 있도록 by를 통한 위임이 가능하다.
    📍 하나의 클래스가 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 멤버를 참조 없이 호출할 수 있게 된다.
  • 프로퍼티 위임 : 프로퍼티의 게터와 세터를 특정 객체에게 위임하고 그 객체가 값을 읽거나 쓸 때 수행하도록 만든다.
    • 위임을 받을 객객체에 by 키워드를 사용하면 된다.
    • 위임자 : 프로퍼티 or 클래스를 대신할 객체

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

  • 코틀린은 default가 final 형태의 클래스이기 때문에 상속이나 직접 크래스의 기능 확장이 어렵다. 따라서 필요한 경우에만 윙ㅁ을 통해 상속과 비슷하게 해당 클래스의 모든 기능을 사용, 기능을 추가 확장 가능하게 구현한 것이다.

클래스의 위임

interface Car {
    fun go(): String
}

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

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

class CarModel(val model: String, impl: Car) : Car by impl {
    fun carInfo() {
        println("$model ${go()}")
    }
}

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

    myDamas.carInfo()
    my350z.carInfo()
}

출력 :

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

프로퍼티 위임과 by lazy

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

  • lazy는 사실 람다식이다.
  • by lazy에 의한 지연 초기화는 스레드에 좀 더 안정적으로 프로퍼티를 사용할 수 있다.
    📍 필요에 따라 객체를 접근하는 시점에서 초기화하면 시작할 때마다 프로퍼티를 생성하느라 소비되는 시간을 줄일 수 있다.

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

  • 코틀린 패키지의 Delegates를 import 해서 observable()vetoavle() 을 사용할 수 있다.
    • 두 함수는 프로퍼티를 위임한다.
  • 두 위임을 생성하기 위해서는 매개변수에 기본값을 지정해야한다.

observable() 함수의 사용 방법

observable() : 프로퍼티를 감시하고 있다가 특정 코드의 로직에서 변경이 일어날 때 호출되어 처리된다.

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("NONAME") { prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "Jimuk"
    user.name = "Dooly"
}

출력 :

NONAME -> Jimuk
Jimuk -> Dooly
  • 프로퍼티의 값이 변경되는지 감시한다.

vetoable() 함수의 사용 방법

vetoable() : observable()과 비슷하지만 반환값에 따라 프로퍼티 변경을 허용하거나 취소할 수 있다.

import kotlin.properties.Delegates

fun main() {
    var max: Int by Delegates.vetoable(0) { prop, old, new ->
        new > old // 기존값 보다 커야 프로퍼티 교체
    }
    println(max)
    max = 10
    println(max)

    max = 5 // 조건에 맞지 않아 거부
    println(max)
}

출력 :

0
10
10
  • vetoable()은 컬렉션과 같이 큰 데이터를 다룰 때 유용하다.

📌 06-3 정적 변수와 컴패니언 객체

  • 지역변수 : 특정 코드 블록 안에 사용되는 변수, 원래 있던 코드가 코드 블록을 벗어나면 해당 변수는 프로그램 메모리에서 더 이상 사용되지 않고 삭제된다.
  • 전역변수 : 프로그램 특정 코드 블록 외부에 있는 변수로서 프로그램이 실행되는 동안 메모리에서 유지될 수 있다.
    • 코드가 길어질 경우 잘못된 동작을 유발할 수 있으니 많이 사용하지 않는 것이 좋음

✏️ 동적인 초기화 없이 사용할 수 있는 변수 : 정적변수 캠패니언 객체

  • 동적인 메모리에 할당 해제되는 것이 아닌 프로그램을 실행할 때 고정적으로 가지는 메모리로 객체 생성 없이 사용할 수 있다.
  • 인스턴스화할 필요 없이 사용할 수 있다.
  • 독립적으로 값을 가지고 있어 어떠한 객체라도 동일한 참조값을 가지고 있어 해당 클래스의 상태에 상관없이 접근할 수 있다.
    📍 모든 객체에 의해 공유되는 효과를 가진다.

정적 변수와 컴패니언 객체

컴패니언 객체 사용하기

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

    companion object {
        var language: String = "Korean"
        fun work() {
            println("working ...")
        }
    }
}

fun main() {
    println(Person.language)
    Person.language = "English"
    println(Person.language)
    Person.work()
}

출력 :

Korean
English
working ...
  • Person 클래스의 language는 객체의 생성 없이도 접근할 수 있게 되었다.
  • 컴패니언 객체는 실제 객체의 싱글톤으로 정의된다.

📫 싱글톤이란?
전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 디자인 패턴의 하나
📍 객체가 서로 동일한 정보를 가질 때 하나의 메모리만 유지해 자원의 낭비를 줄일 수 있다.

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

📝 java

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

📝 kotlin

// 코틀린에서 자바의 static 접근
fun main() {
    println(Customer.LEVEL)
    Customer.login()
}

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

  • 자바에서 코틀린의 캠패니언 객체에 접근하려면 @JvmStatic 애노테이션 표기법을 사용해야 한다.

📫 애노테이션이란?

  • @ 기호로 시작
  • 사전적으로 주석이라는 뜻이다.
  • 코드에서는 특수한 의미를 부여해 컴파일러가 목적에 맞추어 해석하도록 하거나 실행할 때 특정 기능을 수행하게 할 수도 있다.

📝 kotlin

class KCustomer {
    companion object {
        const val LEVEL = "INTERMEDIATE"
        @JvmStatic fun login() = println("Login...") // 어노테이션 표기 사용
        @JvmStatic val score = 3
        @JvmField val JOB = KJob()
    }
}

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

📝 java

public class KCustomerAccess {

    public static void main(String[] args) {

        // 코틀린 코드의 KotlinFoo의 멤버를 접근
        System.out.println(KCustomer.LEVEL);
        KCustomer.login(); // 어노테이션을 사용할 때 접근 방법
        KCustomer.Companion.login(); // 위와 동일한 결과로 어노테이션을 사용하지 않을 때 접근 방법

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

        // KCostomer를 통한 접근
        KCustomer.JOB.setTitle("Accountant");
        System.out.println(KCustomer.JOB.getTitle());
    }
}
  • const : 컴파일 시간의 상수, 컴파일 시간에 이미 할당되어 있다.

최상위 함수 사용하기

최상위함수 (=패키지 레벨 함수) : 클래스 없이 만든 함수는 객체 생성 없이도 main() 함수 어디에서든 실행할 수 있다.

  • JVM에서 실행되기 위해 static으로 선언되어 있다.
fun packageLevelFunc() {
    println("Package-Level Function")
}

fun main() {
    packageLevelFunc()
}

object와 싱글톤

object 선언

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

    init {
        println("Init!")
    }
}

// 2. companion object를 사용한 방식
class CCustomer {
    companion object {
        const val HELLO = "hello"  // 상수 표현
        var name = "KIM"

        @JvmField
        val HOBBY = Hobby("Programming")

        @JvmStatic
        fun greeting() = println("Hello World!")
    }
}

class Hobby(val name: String)

fun main() {

    OCustomer.greeting()
    OCustomer.name = "Baby"
    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 = Baby
Drinking
Hello World!
name = KIM, HELLO = hello
Programming
  • 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()
}

출력 :

Taking photos
Talking with people.
I'm not a real superman. I can't fly!
  • object 표현식은 object 선언과 달리 이름이 없고 싱글톤이 아니다.
  • object 표현식이 사용될 때마다 새로운 인스턴스가 생성된다.
profile
내가 왜 개발잔거지

0개의 댓글