[Kotlin] Kotlin식 문법: 클래스, 객체, 인터페이스 -4

박준규·2022년 2월 19일
0

코틀린

목록 보기
13/19
post-custom-banner

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

java에서는 주 생성자를 1개 이상 선언할 수 있다. 물론 kotlin도 비슷하지만 java와는 좀 다르다.

코틀린은 생성자를 다음과 같이 구분한다.

1️⃣ primary 생성자
2️⃣ secondary 생성자

구분하는 법 또한 매우 간단하다.

1️⃣ primary 생성자의 경우 class A()에서 소괄호()에 있는 생성자를 이야기한다. 주 생성자는 본문 {}밖에서 정의한다.

2️⃣ secondary 생성자의 경우 본문 {} 안에서 정의한다.

물론 뿐만 아니라 초기화 블록(initializr block)을 통해 초기화 로직을 추가할 수 있다.

무언가 java보다 훨씬 더 정돈된 느낌이다.

📦 클래스 초기화: primary constructor와 initializr block

간단한 클래스를 정의해보자.

class User(val nickName: String)
  1. 이 클래스는 뭔데? 원래 중괄호가 있어야 되는거 아니야?

라고 생각할 수도 있지만, 우선은 소괄호 안에 쓰인 코드를 primary constructor(생성자)로 이야기 한다.

  1. 그러면 굳이 주생성자를 만든 목적은 무엇인가?

주 생성자는

1️⃣ 생성자 파라미터를 지정하고
2️⃣ 그 생성자가 파라미터에 의해 초기화되는 property를 정의하는

두 가지 목적에 의해 쓰인다. 다음의 코드를 봐보자.

class User constructor(_nickName:String) { // val가 빠졌음을 주의해야 한다.
    val nickName: String
    
    init {
        nickname = _nickname
    }
}

constructor와 init 키워드가 추가되었는데, 달라지는 건 init뿐이다. _nickName은 소괄호 안에 들어가 있으므로 이 역시 primary 성생자이며, 이때 class 내부의 property를 초기화할 목적으로 init을 사용한 것이다. 이때 init의 경우 class로 인한 객체가 만들어질 때(instance화 될 때) 실행될 초기화 코드가 들어간다.

물론 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다. 필요하다면 class 내부에 여러 init을 추가할 수 있다.

_nickName은 파라미터와 property를 구분해주는 역할을 하고 있으며 자바와 같이 this.nickName = nickName과 같은 식으로 모호성을 없애도 상관없다.

물론 다음과 같이 init을 사용하지 않고 코드를 작성할수도 있다.

class User(val _nickName: String) {
	val nickName = _nickName
}

지금까지 같은 class를 만드는 예제를 살펴보았다.

그러면 이제 1번 질문에 대한 해답은 바로 다음과 같다.

class User(val nickName: String)

위 코드의 val은 해당 파라미터에 상응하는 프로퍼티가 생성된다. 따라서 어떤 다른 기능을 담당할 목적이 없는 User class에서는 중괄호를{} 써줄 필요가 없었던 것이다.

다음의 코드를 살펴보자.

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

fun main() {
    val jk = User("준규")
    println(jk.isSubscribed) // true
    val junkyu = User("준규", false)
    println(junkyu.isSubscribed) // false
    val parkJun = User("박준규", isSubscribed=false)
    println("nickName: ${parkJun.nickName}, isSubscribed: ${parkJun.isSubscribed}")
    // nickName: 박준규, isSubscribed: false
}

참고
모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다. 즉 이렇게 자동으로 만들어진 파라미터 없는 생성자는 default값을 사용해 class를 initialize한다. 여기서 DI(Dependency Injection) 프레임워크 등 자바 라이브러리 중에서 파라미터가 없는 생성자를 통해 객체를 생성해야만 라이브러리 사용이 가능한 경우가 있는데, 코틀린이 제공하는 파라미터 없는 생성자는 DI 라이브러리의 통합을 쉽게 해준다.

클래스에 super class가 있다면 주 생성자에서 super class의 생성자를 호출해야 할 필요가 있다. super class를 초기화하려면 super class 이름 뒤에 괄호를 치고 생성자 인자를 넘기면 된다. 다음과 같이 말이다.

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

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

또한 클래스를 정의할 때 별도의 생성자를 정의하지 않으면, 컴파일러가 자동으로 아무 일도 하지 않는 인자가 없는 default 파라미터를 만들어준다.

open class Button

물론 아무런 파라미터가 없다고 하더라도 위 Button class를 상속한 Radiobutton class는 Button 뒤에 소괄호를 작성해야 한다.

이제 위의 Button 클래스와 같은 코드를 보았을 때 아무런 일도 하지 않은 default 파라미터를 kotlin compiler가 자동으로 만들어 주었구나 라는 생각을 하면 된다.

따라서 어떤 생성자도 작성하지 않은 class를 상속 받을 때는 파라미터가 없는 default 생성자도 함께 상속해야하기 때문에 다음과 같이 작성한다.

class Radiobutton: Button()

이러한 규칙으로 인해 class를 상속하는 것과 interface를 상속하는 것에 조금의 차이가 존재한다. interface의 경우 생성자가 없기 때문에 별도의 소괄호가 필요하지 않지만, class의 경우 어떤 상황에서도 항상 생성자가 존재하기 때문에(실제로 일을 하지 않는 생성자라도) ()를 붙여야 한다.

추가적으로 어떤 클래스를 외부에서 인스터스화하지 못하게 막고 싶다면, 모든 생성자를 private으로 만들면 된다.

class Secretive private constructor() {}
// 이 클래스의 주 생성자는 비공개이다.

Secretive class 안에는 주 생성자 밖에 없고 그 주 생성자는 비공개이므로 외부에서는 Secretive를 인스턴스할 수 없다.

참고
이렇게 private 생성자로만 이루어진 class가 필요할까? 생각해보면 단순하다. 예를 들어서 utility 함수만을 담아두는 역할만 하는 class의 경우 인스턴스화할 필요가 없다. 우리가 알고리즘 문제를 때 많이 사용하는 import java.util.*의 경우가 이에 해당한다. 따라서 java에서는 이렇게 instance화를 막기 위해 일부러 생성자를 private으로 만들기도 한다. 물론 매우 번거로운 작업에 해당된다.

그렇다면 kotlin의 경우 어떨까?

kotlin의 경우 이러한 상황을 언어에서 지원한다. 최상위 함수나 싱글턴 객체에서 이러한 기능을 지원하고 있으니, java보다 다채로운 코드를 짤 수 있다.

📦 Secondary constructor: Initialize the super class to use another method

일반적으로 kotlin에서는 생성자가 여럿 있는 경우가 java보다 훨씬 적다. java에서 overload한 생성자가 필요한 상황 중 상당수는 kotlin의 default parameter 값과 이름 붙인 argument sentence를 사용해 해결할 수 있다.

주의
argument에 대한 default값을 주기 위해 부 생성자를 여러 개 만들면 좋지 않다. 대신 파라미터의 default값을 생성자 signiture에 직접 명시하는게 좋다.

그래도 생성자가 여러 개 필요한 경우가 있다. 예를 들어서 java에서 선언된 생성자가 2개인 View class가 있다고 하자. 그 클래스를 코틀린으로는 다음과 비슷하게 정의할 수 있다.

open class View {
    constructor(ctx: Context) {
        // code
    }
    
    constructor(ctx: Context, attr: AttributeSet) {
        // code
    }
}

일단 View class에는 주 생성자는 없다. 그리고 부 생성자만 2가지를 선언했다. 부 생성자는 constructor 키워드로 시작한다. 그리고 이 class를 확장하고 싶으면 다음과 같이 코드를 작성하면 된다.

class MyButton: View() {
    constructor(ctx: Context): super(ctx) { // 1번 부생성자
        // code
    }
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) { // 2번 부생성자
        // code
    }
}

위 코드를 통햐ㅐ 상위 클래스의 생성자를 super를 통해서 호출한다. super를 통해서 sub class는 super class에게 생성자의 호출을 delegation한다. 위에서는 1번과 2번 부생성자 모두를 super class인 View class에 위임한 것이다.

또한 java와 마찬가지로 생성자에서 this를 통해 클래스 자신의 다른 생성자를 호출할 수 있다. 다음과 같이 작성하면 된다.

open class View {
    constructor(ctx: Context) {
        // code
    }
    
    constructor(ctx: Context, attr: AttributeSet) {
        // code
    }
}

class MyButton: View() {
    constructor(ctx: Context): this(ctx, MY_STYLE) {
        // code
    }
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
        // code
    }
}

여기서 MyButton class의 constructor 중 하나가 parameter의 default 값을 넘겨서 같은 클래스의 다른 생성자에게 생성을 위임한다. 그리고 다른 생성자의 경우 여전히 super를 호출한다.

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

다른 생성자에게 생성을 위임해도 그 끝에는 항상 super를 통해 super class에게 생성을 위임해야 한다. 부 생성자가 필요한 주된 이유는 java의 상호운용성 때문이다. 다른 이유도 존재하지만 그것은 나중에 설명하기로 한다.

profile
'개발'은 '예술'이고 '서비스'는 '작품'이다
post-custom-banner

0개의 댓글