본문 바로가기

코틀린

코틀린: 생성자와 속성 초기화 - 주 생성자

클래스 생성자에 관한 보충 설명입니다(책에서 충분히 설명하지 못했던 내용을 책에 소개한 예제를 사용해 보충 설명합니다). 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 4.1 클래스 선언을 참고하기 바랍니다.

생성자와 속성 초기화는 객체 지향 프로그래밍 언어를 이해하기 위한 첫 번째 관문입니다.  생성자 호출은 어떤 객체를 생성할 것인가를 결정합니다. 생성자가 속성 초깃값을 할당하기 때문입니다. 생성자는 일반 함수와 기능이 거의(?) 같습니다. 차이점은 함수 이름이 constructor()로 정해져 있으며, 반환 값이 없어 return 문을 사용하지 않습니다(보조 생성자에서는 return 문을 사용할 수 있습니다). 

■ 생성자와 객체 
생성자는 클래스의 속성(property)을 초기화하는 특별한 함수입니다. 클래스 속성에 어떤 값을 할당하는가에 따라 다양한 객체를 생성할 수 있습니다. Person("Hong", 23, true)에서 클래스 이름 Person( )이 생성자를 호출합니다. 

생성자를 호출해 객체(object = 클래스 인스턴스)를 생성하는 것은 객체에 대한 메모리 공간(heap, 힙)을 확보한 다음, 객체의 속성에 초깃값을 저장하는 과정입니다. 객체는 참조 타입(reference type)입니다. 객체는 자신이 저장된 메모리 주소를 가리킵니다.

class Person (_name: String, _age: Int, _isMarried: Boolean) {
    val name = _name
    var age = _age
    var isMarried = _isMarried
    fun getName() = println("The name is $name")
}

fun main() {
    val hong = Person("Hong", 23, true)
    val kim = Person("Kim", 25, false)
    
    println(hong.age)
    println(kim.isMarried)
}

 

객체를 생성하고 나면, kim.isMarried처럼 점-표기법(dot-notation)을 사용해 객체의 속성을 참조할 수 있습니다. 점('.') 왼쪽의 객체를 수신 객체(receiver)라 부릅니다. 수신 객체란 속성을 참조할 때 (어느 객체에 속한 속성인지) 지정하는 객체입니다. 클래스 블록 안에서는 키워드 this를 사용해  수신 객체를 참조할 수 있습니다. 

class Person ( ... ) {
    val name = _name
    var age = _age
    var isMarried = _isMarried
    
    fun getName() = println("The name is ${this.name}")
}

 

■ 주 생성자(primary constructor)
생성자는 주 생성자와 보조 생성자 2가지가 있습니다. 주 생성자는 클래스 블록 안에 포함되지 않지만, 보조 생성자는 클래스 블록 안에 정의해야 합니다. 주 생성자이든 보조 생성자이든 이름은 모두 constructor( ... )를 사용합니다. 주 생성자는 키워드 constructor를 생략할 수 있습니다. 클래스 이름 바로 뒤에 3개의 형식 인자를 전달받는 주 생성자를 정의하였습니다. 전달받은 형식 인자를 속성 초깃값으로 할당하였습니다.

class Person constructor(_name: String, _age: Int, _isMarried: Boolean) {
    val name = _name
    var age = _age
    var isMarried = _isMarried
    . . . 
}

 

주 생성자는 복잡한 초기화 작업이 필요할 때 init 블록을 정의할 수 있습니다. init 블록은 클래스 블록 안에 정의합니다.  init 블록에서도 속성을 초기화할 수 있습니다. init 블록에서만 주 생성자에 전달된 파라미터(_age)를 참조할 수 있으며, 다른 멤버 메소드에서는 이 파라미터를 참조할 수 없습니다. 또, 초깃값을 할당하지 않는 속성 age는 반드시 타입을 지정해야 합니다. 

class Person (_name: String, _age: Int, _isMarried: Boolean) {
    val name = _name
    var isMarried = _isMarried
    var age: Int
    init {
        if (_age < 0) {
            throw IllegalArgumentException("Age cannot be negative: $_age")
        }
        age = _age
    }
    fun getName() = println("The name is $name")     // 속성 name을 참조할 수 있습니다.
    // fun getName() = println("The name is $_name") // 파라미터 _name을 참조할 수 없습니다.
}

init 블록은 객체를 생성할 때만 호출됩니다. 아래 코드는 예외가 발생합니다.

fun main() {
    val hong = Person("Hong", -1, true)
}

 

객체가 생성되고 나서 해당 객체의 속성 값을 참조할 때는 init 블록이 실행되지 않습니다. 아래 코드는 예외가 발생하지 않습니다.

fun main() {
    val hong = Person("Hong", 20, true)
    hong.age = -1
}

 

주 생성자는 위의 예처럼 사용하기보다는 주 생성자에서 직접 속성을 선언하고 초기화하는 방식을 더 많이 사용합니다. 위와 같은 코딩 방식은 에러(syntax error - 컴파일 타임 에러)에 취약하기 때문입니다. 주  생성자의 형식 인자 이름 앞에 키워드 val 이나 var 를 붙이면 속성을 정의할 수 있습니다. 물론 isMarried 처럼 속성의 디폴트 초깃값 설정도 가능합니다.

class Person (val name: String, var age: Int, var isMarried: Boolean=false){
    init {
        if (age < 0) {
            throw IllegalArgumentException("Invalid age: $age")
        }
    }
    fun getName() = println("The name is $name")
}