자바에서는 생성자를 하나 이상 선언할 수 있고, 코틀린도 비슷하지만 한 가지 바뀐 부분이 있습니다.
코틀린은 주생성자(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! 값을 바꿀 수 없음!
'코틀린' 카테고리의 다른 글
코틀린(Kotlin) - object 키워드 : 싱글톤, static 멤버, 객체 식 선언 (1) | 2020.03.05 |
---|---|
코틀린(Kotlin) - 데이터 클래스와 클래스 위임(by) (0) | 2020.03.03 |
코틀린(Kotlin)의 Scope Function(let, with, run, apply, also) 정리 (5) | 2020.02.28 |
코틀린(Kotlin) - 클래스 계층 정의 (0) | 2020.02.26 |
코틀린(Kotlin) - 리팩토링 : 로컬 함수와 확장 함수로 코드 중복 없애기 (0) | 2020.02.19 |