뻔하지 않는 생성자나 프로퍼티를 갖는 클래스 선언

유우선·2026년 2월 1일

Kotlin Study📚

목록 보기
5/32

코틀린은 주 생성자부 생성자를 구분한다.

또한 초기화 블록을 통해 초기화 로직을 추가할 수 있다.


클래스 초기화: 주 생성자와 초기화 블록

class User constructor (_nickname: String) { // 주 생성자
	val nickname: String
	
	init { // 초기화 블록
		nickname = _nickname
	}
}

이 예제에 사용된 constructor와 init 키워드의 뜻을 알아보자.

constructor는 주 생성자난 부 생성자 정의를 시작할 때 사용한다.

init 키워드는 초기화 블록을 시작한다.
초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어간다.
초기화 블록은 주 생성자와 함께 사용된다.

주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다.

필요하다면 클래스 안에 여러 초기화 블록을 선언할 수 있다.

이 예제에서는 nickname 프로퍼티를 초기화 하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있어 초기화 코드를 초기화 블록에 넣을 필요가 없다.

또 주 생성자 앞에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.

class User (_nickname: String) {
	val nickname = _nickname
}
//프로퍼티를 초기화하는 식이나 초기화 블록 안에서만 주 생성자 파라미터를 참조할 수 있다.

주 생성자의 파라미터에 val 키워드를 사용하면 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.

class User (val nickname: String) // 파라미터에 상응하는 프로퍼티가 생성됨

함수 파라미터와 마찬가지로 생성자 파라미터에도 기본값을 정의할 수 있다.

class User (val nickname: String, val isSubscribed: Boolean = true)

클래스 인스턴스를 만들려면 new와 같은 키워드를 사용할 필요 없이 생성자를 직접 호출하면 된다.

fun main() {
	val alice = User("Alice") // 기본값 사용
	println(alice.isSubscribed) // true
	
	val bob = User("Bob", false) // 모든 인자를 순서대로 지정할 수 있음
	println(bob.isSubscribed) // false
	
	val carol = User("Calor", isSubscribed = false) // 생성자 인자 중 일부에 이름을 지정할 수 있디.
	println(carol.isSubscribed) // false
	
	val dave = User(nickname = "Dave", isSubscribed = true) // 모든 생성자 인자에 이름을 지정할 수 있다.
	println(dave.isSubscribed) // true
	
}

기반 클래스의 생성자가 인자를 받아야 한다면 클래스의 주 생성자에서 기반 생성자를 호출해야 한다.

open class User(val nickname: String) { /*...*/ }

class SocialUser(nickname: String): User(nickname) { /*...*/ }

클래스를 정의할 때 생성자를 정의하지 않으면 컴파일러가 자동으로 아무 일도 하지 않는 인자가 없는 디폴트 생성자를 만들어준다.

open class Button // 인자가 없는 디폴트 생성자가 만들어진다.

Button 클래스를 상속하는 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.

class RadioButton: Button()

인터페이스와 클래스 상속의 차이는 괄호의 유무다.

기반 클래스의 경우 생성자를 호출해야하기 때문에 괄호가 붙지만

인터페이스는 생성자가 없기 때문에 괄호를 붙이지 않는다.

어떤 클래스를 외부에서 인스턴스화 하지 못하게 막고 싶다면 생성자를 private로 만들어야 한다.

class Secretive private constructor(private val agentName: String) {}

부 생성자: 상위 클래스를 다른 방식으로 초기화

생성자가 여럿 필요한 경우가 있다.
가장 일반적인 상황은 프레임워크 클래스를 확장해야 하는데,
여러 가지 방법으로 인스턴스를 초기화할 수 있도록 다양한 생성자를 지원해야 하는 경우다.

자바의 경우를 한번 봐보자

import java.net.URI;

public class Downloader {
	public Downloader(String Url) {
		// code
	}
	
	public Downloader(URI uri) {
		// code
	}
}

위의 자바 코드를 코틀린에서 구현해보자

open class Downloader {
	constructor(url: String?){
		//code
	}
	
	constructor(uri: URI?){
		//code
	}
}

이 클래스는 주 생성자를 선언하지 않고 부 생성자만 2가지 선언한다.

부 생성자는 constructor 키워드로 시작한다.
필요에 따라 얼마든지 선언해도 된다.

이 클래스를 확장하면 똑같이 부 생성자를 정의할 수 있다.

class MyDownloader: Downloader {
	constructor(url: String?): super(url) {
		//code
	}
	
	constructor(uri: URI): super(uri) {
		//code
	]
}

여기서 부 생성자는 super() 키워드를 통해 자신에 대응하는 상위 클래스의 생성자를 호출한다.

자바와 마찬가지로 생성자에서 this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.

class MyDownloader: Downloader {
	constructor(url: String?): this(URI(url)) // 같은 클래스의 다른 생성자에 위임
	constructor(uri: URI): super(uri)
}

클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화 하거나 다른 생성자에게 위임해야 한다.

부 생성자가 필요한 주된 이유는 자바 상호 운용성이다.

하지만 부 생성자가 필요한 경우도 있다.

클래스 인스턴스를 생성할 때 파라미터 목록이 다른 생성 방법이 여럿 존재하는 경우에는 부 생성자를 여럿 둘 수밖에 없다.


인터페이스에 선언된 프로퍼티 구현

인터페이스에 추상 프로퍼티가 정의되어 있다면 인터페이스를 구현하는 클래스는 추상 프로퍼티의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻이다.

인터페이스에 있는 프로퍼티 선언에는 뒷바침하는 필드나 게터등의 정보가 들어있지 않다.

상태를 저장행야 한다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다.

interface User{
    val nickname:String
}

class PrivateUser(override val nickname:String): User

class SubscribeingUser(val email: String): User{
    override val nickname: String
        get() = email.substringBefore('@')
}

class SocialUser(val accountId: Int): User{
    override val nickname = getNameFromSocialNetwork(accountId)
}

fun getNameFromSocialNetwork(accountId: Int) = "kodee$accountId"

fun main() {
    println(PrivateUser("kodee").nickname)
    // kodee
    println(SubscribeingUser("test@Kotilnlang.org").nickname)
    // test
    println(SocialUser(123).nickname)
    // kodee123
}

인터페이스에 추상 프로퍼티 뿐만 아니라 게터와 세터가 있는 프로퍼티를 선언할 수 있다.

interface EmailUser {
	val email: String
	val nickname: String
		get() = email.subStringBefore('@')
}

하위 클래스에선 추상 프로퍼티인 email을 반드시 오버라이드 해야한다. 반면 nickname은 상속할 수 있다.


함수 대신 프로퍼티를 사용하는 경우

  • 예외를 던지지 않는 경우
  • 계산 비용이 적게 드는 경우
  • 객체 상태가 바뀌지 않으면 여러 번 호출해도 항상 같은 결과를 돌려주는 경우

게터와 세터에서 뒷바침하는 필드에 접근

지금까지 값을 저장하는 프로퍼티와 커스텀 접근자에서 매번 값을 계산하는 프로퍼티를 살펴봤다.

이제는 두 유형을 조합해서 어떤 값을 저장하되 그 값을 변경하거나 읽을 때마다 정해진 로직을 수행하는 프로퍼티를 만들어보자.

값을 저장하는 동시에 로직을 실행하려면 접근자 안에서 프로퍼티를 뒷바침하는 필드에 접근할 수 있어야 한다.

class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
                Address was changed for $name: "$field" -> "$value".
            """.trimIndent())

            field = value
        }
        // field 키워드를 통해 프로퍼티에 접근
}

fun main() {
    val user = User("Alice")
    user.address = "Christoph-Rapparini-Bogen 23"
}

user.address = "Christoph-Rapparini-Bogen 23" 를 사용해 user의 프로퍼티의 값을 변경한다.
이때 구문의 내부적으로는 address의 세터를 호출한다.

접근자의 본문에서는 field라는 특별한 식별자를 통해 뒷바침 하는 프로퍼티에 접근할 수 있다.
게터에서는 field의 값을 읽을 수만 있고 세터에서는 field의 값을 읽거나 쓸 수 있다.

클래스의 프로퍼티를 사용하는 쪽에서 프로퍼티를 읽는 방법이나 쓰는 방법은 뒷바침하는 필드의 유무와는 관계가 없다.

컴파일러는 디폴트 접근자 구현을 사용하건 커스텀 접근자를 정의하건 관계없이
게터나 세터에서 field를 사용하는 프로퍼티에 대해 뒷바침하는 필드를 생성해준다.

다만 field를 사용하지 않는 커스텀 접근자를 정의한다면 컴파일러는 프로퍼티가 아무 정보도 저장하지 않는 것으로 이해하고 뒷바침 필드를 생성하지 않는다.

class Person(var birthYear: Int){
    var ageIn2050
        get() = 2050 - birthYear // 게터 안에 필드 참조가 전혀 없고
        set(value) {
            birthYear = 2050 - value // 세터 안에도 아무 필드 참조가 없기 때문에 뒷바침하는 필드가 생성되지 않는다.
        }
}

접근자의 가시성 변경

접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다.

하지만 원한다면 게터와 세터 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.

class LengthCounter {
    var counter: Int = 0
        private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다.
    
    fun addWord(word: String) {
        counter += word.length
    }
}

fun main() {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter)
}

counter 프로퍼티는 클라이언트에게 제공하는 API의 일부분이므로 public으로 외부에 공개된다.

하지만 외부 코드에서 단어 길이의 합을 마음대로 바꾸지 못하도록 이 클래스 내부에서만 길이를 변경할 수 있다.

프로퍼티에 대해 나중에 다룰 점

  • lateinit 변경자를 널이 될 수 없는 프로퍼티에 지정해서 프로퍼티 초기화를 미룰 수 있다.
  • 요청이 들어오면 바로 초기화되는 지연 초기화(lazy initialzed) 프로퍼티는 더 일반적인 위임 프로퍼티의 일종이다.

0개의 댓글