2023. 3. 26. 20:51ㆍProgramming Languages/Kotlin
Java에서는 하나 이상의 생성자를 선언할 수 있다. Kotlin은 약간 다른 점이 있다. Kotlin은 주(primary) 생성자와 부(secondary) 생성자를 구분한다. 또한 초기화 블록을 통해 초기화 로직을 추가할 수 있다.
주 생성자는 보통 클래스를 초기화할 때 주로 사용하는 간략한 생성자로 클래스 본문 밖에서 정의한다. 부 생성자는 클래스 본문 안에서 정의한다. 주 생성자부터 살펴보자.
1. 클래스 초기화: 주 생성자와 초기화 블록
Java에서 클래스를 선언하면 다음과 같을 것이다.
class User {
private final String nickname;
public User(String nickname) {
this.nickname = nickname;
}
}
하지만 위 코드를 Kotlin에서 간단히 작성하면 다음과 같다.
class User(val nickname: String)
이렇게 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 부른다. 주 생성자는 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다. 이를 명시적으로 표현하면 다음과 같다.
class User constructor(_nickname: String) {
val nickname: String
init { // 초기화 블록
nickname = _nickname
}
}
위 예제에서 constructor
와 init
이라는 새로운 키워드를 볼 수 있다. init
키워드는 초기화 블록을 시작한다. 초기화 블록은 Java와 마찬가지로 클래스의 인스턴스가 만들어질 때 실행될 초기화 코드가 들어간다. 초기화 블록은 주 생성자와 함께 사용된다. 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다. 필요하면 클래스 안에 여러 초기화 블록을 사용할 수 있다.
위 예제에서는 밑줄(_)을 통해 파라미터와 프로퍼티를 구분했다. Java처럼 this
키워드로 구분해도 된다.
class User constructor(nickname: String) {
val nickname: String
init {
this.nickname = nickname
}
}
또한 nickname 프로퍼티를 초기화하는 코드를 nickname 프로퍼티 선언에 포함시킬 수 있다면 초기화 코드를 초기화 블럭에 넣을 필요가 없다. 그리고 주 생성자에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor
를 생략해도 된다. 따라서 위 예제는 다음과 같이 축약된다.
// val은 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다.
class User(val nickname: String)
생성자 파라미터에도 디폴트 파라미터를 넣을 수 있다.
class User(val nickname: String = "anonymous")
💡모든 생성자 파라미터에 디폴트 파라미터 값을 지정하면 컴파일러는 자동으로 파라미터가 없는 생성자를 만들어준다. 그렇게 자동으로 만들어진 파라미터 없는 생성자는 디폴트 값을 사용해 클래스를 초기화한다.
만약 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다. 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 파라미터를 넘긴다.
open class User(val nickname: String) {...}
class TwitterUser(nickname: String): User(nickname) {...}
클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 아무 일도 하지 않는 파라미터가 없는 디폴트 생성자를 만들어준다.
open class Button
Button의 생성자는 아무 파라미터도 받지 않지만 이 클래스를 상속하는 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.
이 규칙으로 인해 기반 클래스의 이름 뒤에는 반드시 괄호가 들어간다. 반면 인터페이스는 생성자가 없기 때문에 어떤 클래스가 인터페이스를 구현하는 경우에는 괄호가 없다. 이를 통해 클래스 상속과 인터페이스 구현을 구별할 수 있다.
어떤 클래스를 클래스 외부에서 인스턴스화하지 못하게 하려면 모든 생성자를 private
으로 만들면 된다.
class Secretive private constructor() {}
대부분의 경우 주 생성자를 사용하면 되지만 복잡한 문제도 있기 마련이다. 이를 위해 부 생성자가 존재한다.
2. 부 생성자: 상위 클래스를 다른 방식으로 초기화
일반적으로 코틀린에서는 생성자가 많이 있는 경우가 자바보다 적다. 자바에서 오버로드한 생성자가 필요한 경우 중 대부분은 코틀린의 디폴트 파라미터 값과 이름 붙인 파라미터 문법으로 해결할 수 있다.
💡파라미터에 대한 디폴트 값을 제공하기 위해 부 생성자를 여러 개 만들지 마라. 대신 파라미터의 디폴트 값을 생성자 시그니처에 직접 명시해라.
그래도 생성자가 여러 개 필요한 경우가 있다. 일반적인 상황은 프레임워크 클래스를 확장해야 하는데 여러 가지 방법으로 인스턴스를 초기화할 수 있게 다양한 생성자를 지원해야 하는 경우이다.
open class View {
// 부 생성자들
constructor(ctx: Context) {...}
constructor(ctx: Context, attr: AttributeSet) {...}
}
위 클래스는 주 생성자를 선언하지 않고 부 생성자만 2개를 선언했다. 부 생성자는 constructor
키워드로 시작한다. 위 클래스를 확장하면서 똑같이 부 생성자를 정의할 수 있다.
class MyButton: View {
constructor(ctx: Context): super(ctx) {...}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {...}
}
이 2가지 부 생성자는 상위 클래스에 생성을 위임한다.
Java와 마찬가지로 생성자에서 this()
를 사용해 다른 생성자에게 생성을 위임할 수 있다.
class MyButton: View {
constructor(ctx: Context): this(ctx, MY_STYLE) {...}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {...}
}
클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 위임해야 한다. 이처럼 부 생성자가 필요한 주요 이유는 Java와의 상호운용성이다.
하지만 부 생성자가 필요한 다른 경우도 있다. 클래스 인스턴스를 생성할 때 파라미터 목록이 다른 생성 방법이 여러 개 존재하는 경우 부 생성자를 여러 개를 둘 수 밖에 없다.
3. 인터페이스에 선언된 프로퍼티 구현
Kotlin에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
interface User {
val nickname: String
}
이는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 수단을 제공해야 한다는 뜻이다. 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게터 등의 정보가 들어있지 않다. 사실 인터페이스는 아무 상태도 포함할 수 없다.
이 인터페이스를 구현하는 방법을 몇 가지 살펴보자.
// 주 생성자에서 구현
class PrivateUser(override val nickname: String): User
// 커스텀 게터를 구현. 필드에 접근할 때마다 게터가 실행됨.
class SubscribingUser(val email: String): User {
override val nickname: String
get() = email.substringBefore('@')
}
// 프로퍼티 초기화 식으로 구현.
// Facebook에 접속해 정보를 가져오는 비용은 비싸기 때문에
// 객체를 초기화할 때 한 번만 호출하도록 설계함.
class FacebookUser(val accountId: Int): User {
override val nickname = getFacebookName(accountId)
}
인터페이스에는 추상 프로퍼티뿐 아니라 getter와 setter가 있는 프로퍼티를 선언할 수도 있다. 물론 그런 getter와 setter는 뒷받침하는 필드를 참조할 수 없다(뒷받침하는 필드가 있다면 인터페이스에 상태가 추가되는 꼴이므로).
interface User {
val email: String
val nickname: String
// nickname의 게터. 매번 결과를 계산해 리턴한다.
get() = email.substringBefore('@')
}
위 인터페이스를 구현하는 클래스는 email 프로퍼티를 반드시 오버라이드해야 한다. 하지만 nickname은 오버라이드하지 않고 상속할 수 있다.
4. getter와 setter에서 뒷받침하는 필드에 접근
지금까지 프로퍼티의 두 가지 유형(값을 저장하는 프로퍼티와 커스텀 접근자에서 매번 값을 계산하는 프로퍼티)에 대해 살펴봤다. 이제 이 두 유형을 조합해서 어떤 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만들어보자. 값을 저장하는 동시에 로직을 실행하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다.
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent()) // 뒷받침하는 필드값 읽기
field = value // 뒷받침하는 필드값 변경
}
}
접근자의 본문에서는 field
라는 특별한 식별자로 뒷받침하는 필드에 접근할 수 있다. getter에서는 field
에 접근할 수만 있고 setter에서는 field
의 값을 읽거나 변경할 수 있다.
val user = User("Bob")
user.address = "Seoul, South Korea"
// Address was changed for Bob:
// "unspecified" -> "Seoul, South Korea".
5. 접근자의 가시성 변경
접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다. 하지만 원한다면 get
이나 set
앞에 가시성 변경자를 추가하여 접근자의 가시성을 변경할 수 있다.
// set의 가시성을 private으로 제한하여 해당 클래스에서만 사용하도록 한다.
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
'Programming Languages > Kotlin' 카테고리의 다른 글
[Kotlin] 11. object 키워드: 클래스 선언과 인스턴스 생성 (0) | 2023.03.28 |
---|---|
[Kotlin] 10. 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임 (0) | 2023.03.28 |
[Kotlin] 8. 클래스 계층 정의 (0) | 2023.03.26 |
[Kotlin] 7. 메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티 (0) | 2023.03.26 |
[Kotlin] 6. 함수 정의와 호출 (0) | 2023.03.24 |