프로퍼티와 초기화 (1/2)

장똑대·2022년 4월 3일
0

Do it! 코틀린 프로그래밍 [둘째마당, 객체 지향 프로그래밍] 학습

✏️1. 프로퍼티의 접근✏️

📌 자바의 필드

  • 코틀린에서는 클래스 내에 선언한 변수를 '프로퍼티' 라고 부르고, 자바에서는 '필드' 라고 부름
  • 자바의 필드는 단순한 변수 선언 부분만을 가짐
  • 코틀린은 변수 선언 부분과 기본적인 접근 메서드를 가짐

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

  • 보통 변수에 접근하기 위해서는 접근 메서드인 게터(Getter)와 세터(Setter)를 사용
  • 게터와 세터가 있으면 각 필드는 외부에서 직접 접근할 수 없게되고 필요한 경우에만 게터와 세터를 통해서만 값을 읽거나 지정할 수 있음
  • 다양한 개체가 필드에 직접 접근하게 하면 데이터의 무결성이 깨질 수 있고 모안상 문제도 생기기때문에 이러한 접근 메서드를 사용
  • 자바의 필드가 점점 늘어나면 그와 상응하는 접근 메서드도 많아지게 되어 코드가 아주 읽기 어렵게 됨 -> 자바의 최대 단점


1-1. 코틀린에서 게터와 세터의 작동

📌 게터와 세터 동작 확인하기

class User(_id: Int, _name: String){
	val id: Int = _id // 읽기전용 (변경 불가능, 세터 없음)
    var name: String = _name
}

// 위의 코드를 아래처럼 간소화 할 수 있음
class User(val id: Int, var name: String){ }

fun main() {
	val user = User(1, "Sean", 30)
    
    val user = user.name // user.getName() 형태와 같음
    user.name = "Ddockdae" // user.setName("Ddockdae") 형태와 같음
    user.id = 2 // id는 val이므로 세터로 값 지정 불가능
}

-> user.name은 프로퍼티에 직접 접근하는 것처럼 보이나 코틀린 내부적으로 접근 메서드(getName())가 내장되어 있음
-> 코틀린 코드도 JVM에서 동작하기 때문에 역컴파일된 코드는 자바 코드와 거의 동일. 변환된 코드를 보면 프로퍼티에 대한 게터와 세터가 자동으로 생성되어 있음을 확인할 수 있음


1-2. 게터와 세터 직접 지정하기

📌 프로퍼티 선언 구조

var 프로퍼티 이름: 프로퍼티 자료형 = 프로퍼티 초기화
	get() { /*게터 본문*/ }
    set(value) { /*게터 본문*/ }
    
val 프로퍼티 이름: 프로퍼티 자료형 = 프로퍼티 초기화
	get() { /*게터 본문*/ }
    // 세터 없음

📌 기본 게터와 세터 지정하기

  • value : 세터의 매개변수로 외부로부터 값을 가져옴
  • field : 프로퍼티를 참조하는 변수
    • 보조 필드(Backing Field)라고도 함
    • 만일 게터와 세터 안에서 field대신 get() = age 와 같이 사용하면 프로퍼티의 get()이 다시 호출되는 것과 같으므로 무한 재귀호출에 빠져 스택 오버플로 오류가 발생할 수 있음
class User(_id: Int, _name: String){
	val id: Int = _id
    	get() = field
    
    var name: String = _name
    	get() = field
        set(value) {
        	field = value
            
            // this.setName(value) 형태로 변환
            // 무한 재귀호출 (!스택 오버플로 오류 발생!)
            name = value 
        }
}

📌 커스텀(Custom) 게터와 세터의 사용

  • 사용자가 직접 게터와 세터를 정의하면서 새로운 내용을 작성하는 것을 커스텀 게터와 세터라고 함

⬇️ 받은 인자를 대문자로 변경해주는 커스텀 세터

class User(_name: String){
	var name: String = _name
    	set(value) {
        	field = value.toUpperCase()
        }
}

📌 보조 프로퍼티의 사용

  • 보조 필드를 사용하지 않는 경우 임시적으로 사용할 프로퍼티를 선언해 놓고 게터나 세터에서 사용할 수 있음
class User(_name: String) {
	// 보조 프로퍼티 정의
	private var tempName: String? = null
    
    var name: String = _name
    	get() {
        	if(tempName == null) tempName = "Noname"
            return tempName ?: throw AssertionError("Asserted by others")
        }
}

📌 프로퍼티의 오버라이딩

  • 프로퍼티는 기본적으로 오버라이딩할 수 없는 final 형태로 선언
  • 오버라이딩 가능하게 하려면 open키워드를 사용해 프로퍼티를 선언
  • 상위 클래스에 프로퍼티를 val로 정의한 경우 하위 클래스에서 var로 변경 가능 (var -> val은 불가)
open class First {
	// 오버라이딩 가능하게 open 키워드 사용
	open val x: Int = 0
    	get() {
        	return field
        }
        
	val y: Int = 0
}

class Second: First() {
	// override와 함께 게터가 재정의
	override val x: Int = 0
    	get() {
        	return field + 3
        }
	override val y: Int = 0 // !오류! 오버라이딩 불가
}

✏️2. 지연 초기화와 위임✏️

2-1. lateinit을 사용한 지연 초기화

  • 보통 클래스에서 기본적으로 프로퍼티 자료형들은 null을 가질 수 없기 때문에 생성자에서 초기화하거나 매개변수로터 값을 초기화 해야함
  • 특정 객체의 의존성이 있는 경우에는 지연 초기화 필요
  • 해당 자료형의 프로퍼티를 즉시 사용하지 않는데도 미리 생성해서 초기화한다면 메모리가 사용되어 낭비될 수 있음

📌 '프로퍼티' 지연 초기화 하기

  • lateinit 키워드를 사용하면 프로퍼티에 값이 바로 할당되지 않아도 컴파일러에서 허용 -> 컴파일러에게 나중에 할당한다고 알려주는 것
  • lateinit의 제한
    • var로 선언된 프로퍼티만 가능
    • 프로퍼티에 대한 게터와 세터를 사용할 수 없음
class Person {
	lateinit var name: String
}

fun main() {
	val kildong = Person() // name 초기화 X
    kildong.name = "kildong" // 이 시점에서 name 초기화
}

📌 '객체' 지연 초기화하기

  • 생성자를 통해 객체를 생성할 때도 lateinit을 사용해 필요한 시점에 객체를 지연 초기화 할 수 있음
data class Person(var name:String, var age: Int)

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

fun main() {
	person1 = Person("Kildong", 30) // 생성자 호출 시점에 초기화
}

2-2. lazy를 사용한 지연 초기화

  • 읽기 전용의 val로 선언한 객체나 프로퍼티를 나중에 초기화할 때 lazy를 적용
  • lazy의 특징
    • 호출 시점에 by lazy {...} 정의에 의해 블록 부분의 초기화를 진행
    • 불변의 변수 선언인 val에서만 사용 가능(읽기 전용)
    • val이므로 값을 다시 변경할 수 없음
    • 람다식으로 구성되어 lazy인스턴스 반환값을 가지는 함수

📌 '프로퍼티' 지연 초기화 하기

class LazyTest {
	val subject by lazy {
    	println("lazy initialized")
        "Kotlin Programming" // lazy 반환값
    }
    
    fun flow() {
    	println("not initialized")
        println("subject one: $subject") // 최초 초기화 시점
        println("subject two: $subject") //이미 초기화된 값을 재사용
    }
}

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

-> 프로퍼티에 최초로 접근한 시점에 해당 프로퍼티가 초기화
-> by는 프로퍼티를 위임할 때 사용하는 키워드

📌 '객체' 지연 초기화 하기

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

fun main() {
	val person: Person by lazy { // lazy를 사용한 person 객체의 지연 초기화
    	Person("Kim", 23) // 이 부분이 Lazy 객체로 반환
    }
    
	// 위임 변수를 사용한 초기화
    val personDelegate = lazy { Person("Hong", 40) } 
    
    // 이 시점에서 초기화
    println("person.name = ${person.name}") 
    // 이 시점에서 초기화
    println("personDelegate.value.name = ${personDelegate.value.name}") 
}

-> by lazy는 객체의 위임(기능을 떠넘기는것)을 나타냄
-> lazy는 변수에 위임된 Lazy 객체 자체를 나타내므로 이 변수의 value를 한 단계 더 거쳐 객체의 멤버인 value.name과 같은 형태로 접근해야함

📌 lazy의 모드

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

⬇️ lazy 모드 사용 예시

private val model by lazy(mode = LazyThreadSafetyMode.NONE) {
	Injector.app().transactionModel() // 
}

2-3. by를 이용한 위임(Delegation)

  • by를 사용하면 하나의 클래스가 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 멤버를 참조 없이 호출할 수 있게 됨
  • 위임을 받을 객체에 by 키워드를 사용
    • <val|var|class> 프로퍼티 혹은 클래스 이름: 자료형 by 위임자
    • 위임자는 프로퍼티나 클래스를 대신할 객체


📌 클래스의 위임(Class Delegation)

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

-> Robot은 Cat이 가지는 모든 Animal의 메소드를 가지게 되는데 이를 클래스의 위임이라고 함
-> Cat은 Animal 자료형의 private멤버로 Robot 클래스 안에 저장
-> Cat에서 구현된 모든 Animal의 메서드는 정적 메서드로 생성
-> Robot 클래스를 사용할 때 Animal을 명시적으로 참조하지 않고도 eat()을 바로 호출 할 수 있음

📌 위임을 사용하는 이유

  • 표준 라이브러리는 open으로 저의되지 않은 클래스를 사용하므로 상속이나 직접 클래스의 기능 확장이 어려움 (무분별한 상속에 따른 복잡한 문제를 방지 가능)
  • 필요한 경우에만 위임을 통해 상속과 비슷하게 해당 클래스의 모든 기능을 사용하면서 동시에 기능을 추가 확장 구현할 수 있게 함

📌 프로퍼티 위임과 by lazy

  • lazy의 동작
    1. lazy 람다식은 람다식을 전달받아 저장한 Lazy 인스턴스를 반환
    2. 최초 프로퍼티의 게터 실행은 lazy에 넘겨진 람다식을 실행하고 결과를 기록
    3. 이후 프로퍼티의 게터 실행은 이미 초기화되어 기록된 값을 반환
  • by lazy에 의한 지연 초기화는 스레드에 좀 더 안정적으로 프로퍼티를 사용할 수 있음
    • 프로그램 시작 시 큰 객체가 있따면 초기화할 때 모든 내용을 시작 시간에 할당해야 하므로 느려질 수밖에 없음. 이것을 필요에 따라 해당 객체를 접근하는 시점에서 초기화하면 시작할 때마다 프로퍼티를 생성하느라 소비되는 시간을 줄일 수 있음
profile
장똑대와 안드로이드

0개의 댓글