Skip to content

프로퍼티

코틀린에서 프로퍼티를 사용하면 데이터를 액세스하거나 변경하기 위한 함수를 직접 작성하지 않고도 데이터를 저장하고 관리할 수 있습니다. 프로퍼티는 클래스, 인터페이스, 객체, 컴패니언 객체에서 사용할 수 있으며, 심지어 이러한 구조 외부에서 최상위 프로퍼티로도 사용할 수 있습니다.

모든 프로퍼티는 이름, 타입, 그리고 게터(getter)라고 불리는 자동으로 생성된 get() 함수를 가집니다. 게터를 사용하여 프로퍼티의 값을 읽을 수 있습니다. 프로퍼티가 가변(mutable)인 경우, 프로퍼티의 값을 변경할 수 있게 해주는 세터(setter)라고 불리는 set() 함수도 가집니다.

게터와 세터는 _접근자(accessors)_라고 불립니다.

프로퍼티 선언하기

프로퍼티는 가변(var)이거나 읽기 전용(val)일 수 있습니다. .kt 파일에서 최상위 프로퍼티로 선언할 수 있습니다. 최상위 프로퍼티는 특정 패키지에 속하는 전역 변수라고 생각하면 됩니다.

kotlin
// File: Constants.kt
package my.app

val pi = 3.14159
var counter = 0

클래스, 인터페이스 또는 객체 내부에서도 프로퍼티를 선언할 수 있습니다.

kotlin
// 프로퍼티를 가진 클래스
class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
}

// 프로퍼티를 가진 인터페이스
interface ContactInfo {
    val email: String
}

// 프로퍼티를 가진 객체
object Company {
    var name: String = "Detective Inc."
    val country: String = "UK"
}

// 인터페이스를 구현하는 클래스
class PersonContact : ContactInfo {
    override val email: String = "[email protected]"
}

프로퍼티를 사용하려면 그 이름을 참조하면 됩니다.

kotlin
class Address {
    var name: String = "Holmes, Sherlock"
    var street: String = "Baker"
    var city: String = "London"
}

interface ContactInfo {
    val email: String
}

object Company {
    var name: String = "Detective Inc."
    val country: String = "UK"
}

class PersonContact : ContactInfo {
    override val email: String = "[email protected]"
}

fun copyAddress(address: Address): Address {
    val result = Address()
    // result 인스턴스의 프로퍼티에 액세스
    result.name = address.name
    result.street = address.street
    result.city = address.city
    return result
}

fun main() {
    val sherlockAddress = Address()
    val copy = copyAddress(sherlockAddress)
    // copy 인스턴스의 프로퍼티에 액세스
    println("Copied address: ${copy.name}, ${copy.street}, ${copy.city}")
    // Copied address: Holmes, Sherlock, Baker, London

    // Company 객체의 프로퍼티에 액세스
    println("Company: ${Company.name} in ${Company.country}")
    // Company: Detective Inc. in UK
    
    val contact = PersonContact()
    // contact 인스턴스의 프로퍼티에 액세스
    println("Email: ${contact.email}")
    // Email: [email protected]
}

코틀린에서는 코드를 안전하고 읽기 쉽게 유지하기 위해 프로퍼티를 선언할 때 초기화하는 것을 권장합니다. 하지만 특수한 경우에는 나중에 초기화할 수도 있습니다.

컴파일러가 초기화 식이나 게터의 반환 타입으로부터 타입을 추론할 수 있는 경우 프로퍼티 타입 선언은 선택 사항입니다.

kotlin
var initialized = 1 // 추론된 타입은 Int입니다.
var allByDefault    // 오류: 프로퍼티는 반드시 초기화되어야 합니다.

커스텀 게터와 세터

기본적으로 코틀린은 게터와 세터를 자동으로 생성합니다. 유효성 검사, 포맷팅 또는 다른 프로퍼티를 기반으로 한 계산과 같이 추가적인 로직이 필요한 경우 자신만의 커스텀 접근자를 정의할 수 있습니다.

커스텀 게터는 프로퍼티에 액세스할 때마다 실행됩니다.

kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int
        get() = this.width * this.height
}
fun main() {
    val rectangle = Rectangle(3, 4)
    println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}

컴파일러가 게터로부터 타입을 추론할 수 있다면 타입을 생략할 수 있습니다.

kotlin
val area get() = this.width * this.height

커스텀 세터는 초기화할 때를 제외하고 프로퍼티에 값을 할당할 때마다 실행됩니다. 관례적으로 세터 파라미터의 이름은 value이지만, 다른 이름을 선택할 수도 있습니다.

class
    var coordinates: String
        get() = "$x,$y"
        set(value) {
            val parts = value.split(",")
            x = parts[0].toInt()
            y = parts[1].toInt()
        }
}

fun main() {
    val location = Point(1, 2)
    println(location.coordinates) 
    // 1,2

    location.coordinates = "10,20"
    println("${location.x}, ${location.y}") 
    // 10, 20
}

가시성 변경 또는 어노테이션 추가

코틀린에서는 기본 구현을 대체하지 않고도 접근자의 가시성을 변경하거나 어노테이션을 추가할 수 있습니다. 이러한 변경을 위해 본문 {}을 만들 필요는 없습니다.

접근자의 가시성을 변경하려면 get 또는 set 키워드 앞에 수정자(modifier)를 사용하세요.

kotlin
class BankAccount(initialBalance: Int) {
    var balance: Int = initialBalance
        // 클래스 내부에서만 balance를 수정할 수 있음
        private set 

    fun deposit(amount: Int) {
        if (amount > 0) balance += amount
    }

    fun withdraw(amount: Int) {
        if (amount > 0 && amount <= balance) balance -= amount
    }
}

fun main() {
    val account = BankAccount(100)
    println("Initial balance: ${account.balance}") 
    // 100

    account.deposit(50)
    println("After deposit: ${account.balance}") 
    // 150

    account.withdraw(70)
    println("After withdrawal: ${account.balance}") 
    // 80

    // account.balance = 1000  
    // 오류: 세터가 private이므로 할당할 수 없음
}

접근자에 어노테이션을 달려면 get 또는 set 키워드 앞에 어노테이션을 사용하세요.

kotlin
// 게터에 적용할 수 있는 어노테이션 정의
@Target(AnnotationTarget.PROPERTY_GETTER)
annotation class Inject

class Service {
    var dependency: String = "Default Service"
        // 게터에 어노테이션 추가
        @Inject get 
}

fun main() {
    val service = Service()
    println(service.dependency)
    // Default service
    println(service::dependency.getter.annotations)
    // [@Inject()]
    println(service::dependency.setter.annotations)
    // []
}

이 예제는 리플렉션을 사용하여 게터와 세터에 어떤 어노테이션이 있는지 보여줍니다.

보조 필드 (Backing fields)

코틀린에서 접근자는 메모리에 프로퍼티 값을 저장하기 위해 보조 필드(backing field)를 사용합니다. 보조 필드는 게터나 세터에 추가 로직을 더하고 싶을 때나, 프로퍼티가 변경될 때마다 추가적인 동작을 트리거하고 싶을 때 유용합니다.

보조 필드를 직접 선언할 수는 없습니다. 코틀린은 필요할 때만 이를 생성합니다. 접근자 내에서 field 키워드를 사용하여 보조 필드를 참조할 수 있습니다.

코틀린은 기본 게터나 세터를 사용하거나, 하나 이상의 커스텀 접근자에서 field를 사용하는 경우에만 보조 필드를 생성합니다.

예를 들어, isEmpty 프로퍼티는 field 키워드 없이 커스텀 게터를 사용하므로 보조 필드가 없습니다.

kotlin
val isEmpty: Boolean
    get() = this.size == 0

반면 다음 예제에서 score 프로퍼티는 세터가 field 키워드를 사용하므로 보조 필드를 가집니다.

kotlin
class Scoreboard {
    var score: Int = 0
        set(value) {
            field = value
            // 값을 업데이트할 때 로깅 추가
            println("Score updated to $field")
        }
}

fun main() {
    val board = Scoreboard()
    board.score = 10  
    // Score updated to 10
    board.score = 20  
    // Score updated to 20
}

보조 프로퍼티 (Backing properties)

때로는 보조 필드가 제공하는 것보다 더 많은 유연성이 필요할 수 있습니다. 예를 들어, 프로퍼티를 내부적으로는 수정할 수 있지만 외부에서는 수정할 수 없게 하고 싶은 API가 있는 경우입니다. 이러한 경우 _보조 프로퍼티(backing property)_라고 불리는 코딩 패턴을 사용할 수 있습니다.

다음 예제에서 ShoppingCart 클래스는 쇼핑카트의 모든 항목을 나타내는 items 프로퍼티를 가집니다. items 프로퍼티가 클래스 외부에서는 읽기 전용이기를 원하지만, 여전히 사용자가 items 프로퍼티를 직접 수정할 수 있는 "승인된" 방법을 하나 허용하고 싶을 수 있습니다. 이를 위해 _items라는 비공개(private) 보조 프로퍼티를 정의하고, 보조 프로퍼티의 값을 위임하는 items라는 공개(public) 프로퍼티를 정의할 수 있습니다.

kotlin
class ShoppingCart {
    // 보조 프로퍼티
    private val _items = mutableListOf<String>()

    // 공개 읽기 전용 뷰
    val items: List<String>
        get() = _items

    fun addItem(item: String) {
        _items.add(item)
    }

    fun removeItem(item: String) {
        _items.remove(item)
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.addItem("Apple")
    cart.addItem("Banana")

    println(cart.items) 
    // [Apple, Banana]
    
    cart.removeItem("Apple")
    println(cart.items) 
    // [Banana]
}

이 예제에서 사용자는 addItem() 함수를 통해서만 카트에 항목을 추가할 수 있지만, 여전히 items 프로퍼티에 액세스하여 내용물을 확인할 수 있습니다.

보조 프로퍼티의 이름을 지을 때는 코틀린 코딩 컨벤션에 따라 이름 앞에 언더스코어(_)를 사용하세요.

JVM에서 컴파일러는 함수 호출 오버헤드를 피하기 위해 기본 접근자를 가진 비공개 프로퍼티에 대한 액세스를 최적화합니다.

보조 프로퍼티는 하나 이상의 공개 프로퍼티가 상태를 공유하게 하고 싶을 때도 유용합니다. 예를 들어:

kotlin
class Temperature {
    // 섭씨 온도를 저장하는 보조 프로퍼티
    private var _celsius: Double = 0.0

    var celsius: Double
        get() = _celsius
        set(value) { _celsius = value }

    var fahrenheit: Double
        get() = _celsius * 9 / 5 + 32
        set(value) { _celsius = (value - 32) * 5 / 9 }
}

fun main() {
    val temp = Temperature()
    temp.celsius = 25.0
    println("${temp.celsius}°C = ${temp.fahrenheit}°F") 
    // 25.0°C = 77.0°F

    temp.fahrenheit = 212.0
    println("${temp.celsius}°C = ${temp.fahrenheit}°F") 
    // 100.0°C = 212.0°F
}

이 예제에서 _celsius 보조 프로퍼티는 celsiusfahrenheit 프로퍼티 모두에서 액세스됩니다. 이 구성은 두 개의 공개 뷰를 가진 단일 진실 공급원(single source of truth)을 제공합니다.

컴파일 시간 상수

읽기 전용 프로퍼티의 값을 컴파일 시간에 알 수 있다면, const 수정자를 사용하여 _컴파일 시간 상수(compile-time constant)_로 표시하세요. 컴파일 시간 상수는 컴파일 시점에 인라인(inline)화되므로, 각 참조가 실제 값으로 대체됩니다. 게터가 호출되지 않기 때문에 더 효율적으로 액세스할 수 있습니다.

kotlin
// File: AppConfig.kt
package com.example

// 컴파일 시간 상수
const val MAX_LOGIN_ATTEMPTS = 3

컴파일 시간 상수는 다음 요구 사항을 충족해야 합니다:

컴파일 시간 상수는 여전히 보조 필드를 가지므로, 리플렉션을 사용하여 상호작용할 수 있습니다.

이러한 프로퍼티는 어노테이션에서도 사용할 수 있습니다.

kotlin
const val SUBSYSTEM_DEPRECATED: String = "이 서브시스템은 사용 중단되었습니다"

@Deprecated(SUBSYSTEM_DEPRECATED) fun processLegacyOrders() { ... }

지연 초기화 프로퍼티 및 변수

일반적으로 프로퍼티는 생성자에서 초기화해야 합니다. 하지만 이것이 항상 편리한 것은 아닙니다. 예를 들어, 의존성 주입을 통해 프로퍼티를 초기화하거나 유닛 테스트의 설정 메서드 내에서 초기화할 수도 있습니다.

이러한 상황을 처리하려면 프로퍼티를 lateinit 수정자로 표시하세요.

kotlin
public class OrderServiceTest {
    lateinit var orderService: OrderService

    @SetUp fun setup() {
        orderService = OrderService()
    }

    @Test fun processesOrderSuccessfully() {
        // null이나 초기화 여부를 확인하지 않고 orderService를 직접 호출
        orderService.processOrder()  
    }
}

lateinit 수정자는 다음과 같이 선언된 var 프로퍼티에 사용할 수 있습니다:

  • 최상위 프로퍼티.
  • 지역 변수.
  • 클래스 본문 내부의 프로퍼티.

클래스 프로퍼티의 경우:

  • 기본 생성자에서 선언할 수 없습니다.
  • 커스텀 게터나 세터를 가질 수 없습니다.

모든 경우에 프로퍼티나 변수는 null을 허용하지 않는 타입이어야 하며, 기본 타입(primitive type)이 아니어야 합니다.

초기화하기 전에 lateinit 프로퍼티에 액세스하면, 코틀린은 액세스 중인 초기화되지 않은 프로퍼티를 식별하는 특정 예외를 던집니다.

kotlin
class ReportGenerator {
    lateinit var report: String

    fun printReport() {
        // 초기화 전에 액세스되므로 예외 발생
        println(report)
    }
}

fun main() {
    val generator = ReportGenerator()
    generator.printReport()
    // Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property report has not been initialized
}

lateinit var가 이미 초기화되었는지 확인하려면 해당 프로퍼티에 대한 참조에서 isInitialized 프로퍼티를 사용하세요.

kotlin
class WeatherStation {
    lateinit var latestReading: String

    fun printReading() {
        // 프로퍼티가 초기화되었는지 확인
        if (this::latestReading.isInitialized) {
            println("Latest reading: $latestReading")
        } else {
            println("No reading available")
        }
    }
}

fun main() {
    val station = WeatherStation()

    station.printReading()
    // No reading available
    station.latestReading = "22°C, sunny"
    station.printReading()
    // Latest reading: 22°C, sunny
}

코드에서 해당 프로퍼티에 이미 액세스할 수 있는 경우에만 isInitialized를 사용할 수 있습니다. 프로퍼티는 동일한 클래스, 외부 클래스에 선언되어 있거나 동일한 파일의 최상위 프로퍼티로 선언되어 있어야 합니다.

프로퍼티 오버라이딩

프로퍼티 오버라이딩을 참조하세요.

위임 프로퍼티 (Delegated properties)

로직을 재사용하고 코드 중복을 줄이기 위해, 프로퍼티의 게터와 세터 책임을 별개의 객체에 위임할 수 있습니다.

접근자 동작을 위임하면 프로퍼티의 접근자 로직을 중앙 집중화하여 재사용하기 쉽게 유지할 수 있습니다. 이 접근 방식은 다음과 같은 동작을 구현할 때 유용합니다:

  • 값을 지연 계산(lazy computing)하는 경우.
  • 주어진 키로 맵(map)에서 읽어오는 경우.
  • 데이터베이스에 액세스하는 경우.
  • 프로퍼티에 액세스할 때 리스너에게 알리는 경우.

라이브러리에서 이러한 공통 동작을 직접 구현하거나 외부 라이브러리에서 제공하는 기존 위임자를 사용할 수 있습니다. 자세한 내용은 위임 프로퍼티를 참조하세요.