본문 바로가기

코틀린

코틀린(Kotlin) - 생성자와 프로퍼티를 갖는 클래스 선언

자바에서는 생성자를 하나 이상 선언할 수 있고, 코틀린도 비슷하지만 한 가지 바뀐 부분이 있습니다.

코틀린은 주생성자(primary, 클래스 본문 밖에서 정의)와 부생성자(secondary, 클래스 본문 안에서 정의)로 구분됩니다.

또한, 코틀린에서는 초기화 블록(init block)을 통해 초기화 로직을 추가할 수 있습니다.

 

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

class User(val name: String)

 

보통 클래스의 모든 선언은 중괄호{} 사이에 들어가지만, 이 클래스의 선언에는 중괄호가 없고 괄호 사이에 val 선언만 존재합니다.

이렇게 클래스 이름 뒤에 오는 괄호에 들어가는 코드를 주생성자(primary constructor) 라고 부릅니다.

주생성자는 생성자 파라미터를 지정하고, 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적으로 쓰입니다.

 

이제 이 클래스를 일반적인 코드로 풀어보도록 하겠습니다.

 

class User constructor(_name: String) {
    val name: String
    
    //초기화 블록
    init {
       name = _name //또는 this.name = name
    }
}

 

이 예제에서 "constructor" 와 "init" 이라는 새로운 키워드를 볼 수 있습니다.

constructor는 주생성자나 부생성자 정의를 할 때 사용합니다.

init은 초기화 블록으로 사용합니다.

초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어가며, 초기화 블록은 주생성자와 함께 사용 됩니다.

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

필요에 따라, 클래스 안에 여러 초기화 블록을 선언할 수도 있습니다.

 

위의 예제에서 name 프로퍼티를 주생성자의 파라미터인 _name으로 초기화 할 수 있기 때문에, 굳이 초기화 코드를 초기화 블록에 넣을 필요가 없고, 주생성자는 constructor를 생략해도 되기 때문에, 이런 변경을 적용하고 나면 코드를 다음과 같이 바꿀 수 있습니다.

 

class User(_name: String) {
    val name = _name
}

 

여기에서 중요한 것은, 프로퍼티를 초기화하는 식이나 기화 블록 안에서만 주생성자의 파라미터를 참조할 수 있다는 점에 유의해야 합니다.

그리고, 클래스 본문에서 val키워드를 통해 프로퍼티를 정의하는데, 주생성자의 파라미터로 프로퍼티를 초기화한다면 그 주생성자 파라미터명 옆에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 할 수 있습니다.

 

class User(val name: String)

 

만약, 해당 클래스에 상속 클래스가 있다면, 주생성자에서 상속 클래스의 생성자를 호출해야 합니다.

상송 클래스를 초기화하려면, 상속 클래스명 괄호안에 생성자 인자를 넘기면 됩니다.

 

 

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

 

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

 

 

Button의 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스에서는 반드시! Button 클래스의 생성자를 호출해줘야 합니다.

 

class RadioButton: Button()

 

이러한 규칙으로 인해서, 상속 클래스에는 꼭! 괄호가 들어가게 되는 것입니다. ^^ (인자가 없다면 빈괄호, 생성자 인자가 있다면 괄호 안에 인자가 들어감)

 

만약, 어떤 클래스를 외부에서 인스턴스화하지 못하게 막고 싶다면, 생성자를 private으로 선언하면 됩니다.

아래와 같이 주생성자에 private을 선언할 수 있습니다.

 

// 주 생성자가 비공개이므로, 외부에서 인스턴스화 할 수 없다. 
class User private constructor() {}

 

우리는 보통, 유틸리티 함수들을 담아두는 유틸 클래스의 경우 인스턴스화 할 필요가 없고, 싱글톤 클래스를 만드는 경우,
자바에서는 이러한 요구 사항을 명시할 방법이 없으므로 어쩔 수 없이 private 생성자를 정의해서 외부에서 인스턴스화하지 못하도록 사용하게 되는데,
반면에 코틀린은 그런 경우를 언어 자체에서 기본 지원하기 때문에, 정적(static) 유틸리티 함수 대신 최상위 함수(클래스 밖에 선언) 를 사용할 수 있고, 싱글톤을 사용하고 싶으면 싱글톤 선언(object) 을 하면 됩니다.

실제로 대부분의 경우 클래스의 생성자는 아주 단순하게 구성됩니다.

생성자에 아무 파라미터도 없는 클래스도 많고, 생성자 코드 안에서 생성자가 파라미터로 받은 값을 그대로 프로퍼티에 설정하기만 하는 생성자도 많습니다. (자바에서 mContext = context; 이런 경우들)

 

그래서 코틀린은 간단한 주 생성자 문법을 제공합니다.

대부분의 경우 이런 간단한 주 생성자 구문만으로도 충분하지만, 우리에겐 항상 다양한 요구 사항들과 어려움들이 있기 마련이므로...(ㅋ) 코틀린에서도 그런 경우를 대비해서 필요에 따라 다양한 생성자를 정의할 수 있게 해줍니다.

부 생성자

사실상 일반적으로, 코틀린에서는 생성자가 여럿 있는 경우가 자바보다 훨씬 적습니다.

예를 들면, 자바에서 오버로드 한 생성자 중 상당수는 코틀린의 디폴트 파라미터 값이름 붙인 파라미터 문법으로 해결할 수 있기 때문입니다.

 

/** 디폴트 파라미터 */
data class Car(
    ...
    val truck: Boolean = false
)

/** 이름 붙은 파라미터 */
Car(truck = true)

 

팁! 파라미터에 대한 디폴트 값을 제공하기 위해서, 부 생성자를 여러개 만들지 말고, 대신 파라미터의 디폴트 값을 활용하여
생성자에 직접 명시하세요!

그럼에도 생성자가 여럿 필요한 경우가 가끔 있는데, 가장 흔하고 일반적인 상황은 프레임워크 클래스를 확장해야 할 때, 여러 방법으로 인스턴스를 초기화할 수 있게 다양한 생성자를 명시해야 하는 경우입니다.

예를 들어, 자바에서 선언된 생성자가 두개인 View 클래스가 있다고 가정하면, 그 클래스를 코틀린으로는 다음과 같이 비슷하게 정의할 수 있습니다.

 

open class View { //클래스명 뒤에 괄호가 없기 때문에 주생성자가 없음을 알 수 있다.
   constructor(context: Context) { //부생성자
   //TODO
   }
   
   constructor(context: Context, attr: AttributeSet) { //부생성자
   //TODO
   }
}

 

이 클래스를 상속받아 확장하면 아래와 같이 부 생성자를 정의할 수 있습니다.

 

class MyButton : View {
   constructor(context: Context) : super(context) {...} //상위 클래스 생성자 호출
   constructor(context: Context, attr: AttributeSet) : super(context, attr) {...}
}

/** this()를 통해 자신의 다른 생성자 호출 */
class MyButton : View {
   constructor(context: Context) : this(context, MY_STYLE_ATTR) {...} //다른 생성자 호출 연계
   constructor(context: Context, attr: AttributeSet) : super(context, attr) {...}
}

 

MyButton 클래스의 생성자 중 하나가 this()로 자신의 다른 생성자에게 생성을 위임하고, 두번째 생성자는 여전히 super()로 상위 클래스 생성자를 호출합니다.

 

이렇듯, 클래스에 주 생성자가 없다면, 모든 부 생성자는 반드시 상위 클래스를 초기화 하거나, 다른 생성자에게 생성을 위임해서 최종적으로는 상위 클래스 생성자를 호출해야만 합니다.

이렇게 부 생성자가 필요한 주된 이유는 자바와의 상호운용성때문의 이유가 가장 큽니다. 

하지만, 반드시 이 이유때문만은 아니며 부 생성자가 필요한 다른 경우도 있기 마련인데,

예를 들면, 클래스 인스턴스를 생성할 때 파라미터가 서로 다르게 생성해야하는 경우에는 부 생성자를 여럿 둘 수 밖에 없겠죠?ㅎㅎ

 

지금까지 생성자 정의하는 방법에 대해 살펴보았고, 다음은 뻔하지 않은 프로퍼티 정의를 살펴보도록 하겠습니다.

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

코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있습니다.

 

interface User {
   val name: String
}

 

이는 User 인터페이스를 구현하는 클래스가 name의 값을 얻을 수 있는 방법을 구현해야 한다는 뜻입니다.

인터페이스에 있는 프로퍼티 선언에는 setter/getter가 없습니다. 
사실 인터페이스는 아무 상태도 포함할 수 없으므로, 상태를 저장할 필요가 있다면 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 로직을 만들어야 합니다.

 

이제 이 인터페이스를 구현하는 방법을 몇 가지 살펴보도록 하겠습니다.

 

/** 인터페이스의 프로퍼티 구현하기 */

class PrivateUser(override val name: String) : User //주 생성자에 있는 프로퍼티

class EmailUser(val email: String) : User {
   override val name: String
     get() = email.substringBefore('@') //커스텀 getter
}

class FacebookUser(val accountId: Int) : User {
   override val name = getFacebookName(accountId) //페이스북 ID 가져오기
}

>>> println(PrivateUser("jykim").name)
jykim

>> println(EmailUser("jykim@gmail.com").name)
jykim

 

PrivateUser 클래스는, 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문을 사용해 보았습니다.

이 프로퍼티는 User의 추상 프로퍼티를 구현하고 있으므로 override를 표시해야 합니다.

 

EmailUser 클래스는, 커스텀 getter로 name 프로퍼티를 설정합니다. 

 

FacebookUser 클래스는, 초기화 식으로 name값을 초기화 합니다. 

 

여기에서, EmailUser 와 FacebookUser의 name 구현 차이에 대해서 혹시 눈치 채셨나요?

 

EmailUser는 name을 호출할 때 마다 계산하는 커스텀 getter를 활용하고, FacebookUser는 초기화 시 name 값을 초기화하여 저장했다가 불러오는 방식으로 활용합니다.

 

인터페이스에는 추상 프로퍼티뿐만 아니라, "setter"와 "getter"가 있는 프로퍼티를 선언할 수도 있습니다.

 

interface User {
   val email: String
   val name: String
     get() = email.substringBefore('@') //커스텀 getter
}

 

이렇게 되면, 이 인터페이스는 추상 프로퍼티인 email과 커스텀 getter인 name프로퍼티가 함께 들어있는 형태가 되고,

하위 클래스에서는 추상 프로퍼티인 email은 반드시 오버라이드 해야 하며, 반면에 name은 오버라이드 하지 않고 상속할 수가 있습니다.

 

setter / getter 의 가시성 바꾸기

접근자(setter/getter)의 가시성은 기본적으로 프로퍼티의 가시성과 동일합니다.

하지만 원한다면 set이나 get 앞에 접근제어자를 추가해서, 접근자의 가시성을 변경할 수 있습니다.

 

class User {
   var name: String = "코틀린"
     private set
}

User.name = "자바" //Error! 값을 바꿀 수 없음!