본문 바로가기
Kotlin

[깡샘의 코틀린 프로그래밍] 정리 7 - primary constructor

by 들풀민들레 2018. 3. 2.

본 글은 [깡샘의 코틀린 프로그래밍 - 루비페이퍼] 의 내용을 발췌한 것입니다.

좀더 자세한 내용은 책을 통해 확인해 주세요.

 

 

 

7장. 클래스

 

7.2.1. primary constructor

 

클래스에는 여러 생성자를 정의할 수 있는데 이 중 대표 생성자가 주 생성자(Primary Constructor)입니다. 주 생성자는 하나의 클래스에 하나만 정의할 수 있습니다. 나중에 설명하겠지만 보조 생성자는 하나의 클래스에 여러 개 정의할 수 있습니다.

 

  • 클래스 선언 부분에 작성
  • 하나의 클래스에 하나의 주 생성자만 정의 가능
  • 꼭 작성해야 하는 건 아니며 보조 생성자가 있다면 작성하지 않을 수 있음

 

주 생성자는 클래스 몸체가 아닌 헤더에서 클래스 이름 뒤에 선언합니다

 

01 package seven_two_one

02

03 class MyClass1 { }

04

05 class MyClass2() { }

06

07 class MyClass3 constructor() { }

08

09 fun main(args: Array<String>) {

10 val obj1 = MyClass1()

11 val obj2 = MyClass2()

12 val obj3 = MyClass3()

13 }

 

03번 줄의 MyClass1 클래스에는 생성자를 정의하지 않았습니다. 클래스 헤더에 주 생성자도, 클래스 몸체에 보조 생성자도 없습니다. 하지만 객체지향 문법상 객체가 생성될 때 반드시 하나 이상의 생성자가 호출되어야 합니다. 따라서 MyClass1처럼 개발자가 별도로 선언한 생성자가 없다면 컴파일러가 자동으로 매개변수가 없는 주 생성자를 추가합니다.

 

반면에 05번과 07번 줄에 선언한 클래스는 개발자가 명시적으로 매개변수 없는 주 생성자를 정의한 예 입니다. 원래 생성자는 constructor 예약어를 이용해 정의하지만, 만약 주 생성자에 별도의 수식 구문(어노테이션, 접근 제한자 등)이 없다면 05번 줄처럼 constructor 예약어는 생략할 수 있습니다. 위의 소스에서 3개의 클래스를 선언했는데 모두 매개변수가 없는 주 생성자를 선언한 예이며, 10~12번 줄처럼 주 생성자를 호출하면서 각 클래스의 객체를 생성할 수 있습니다.

 

매개변수가 있는 주 생성자

 

주 생성자를 정의할 때 매개변수를 선언하면 해당 생성자로 객체를 생성할 때 매개변수에 맞는 인수를 전달해야 합니다.

 

01 class User1 constructor(name: String, age: Int){ }

02 class User2(name: String, age: Int){ }

03 //…

04 val user1 = User1() //에러

05 val user2 = User1("kkang", 33)

06 val user3 = User2("kim", 28)

 

위의 소스에서 두 개의 클래스를 선언하고 각 클래스를 선언할 때 주 생성자에 매개변수를 지정했습니다. 이처럼 매개변수가 있는 생성자를 호출해 객체를 생성하려면 05~06번 줄처럼 생성자의 매개변수개수와 타입에 맞추어 인수를 전달해야 합니다. 그렇지 않고 04번 줄처럼 인수를 전달하지 않은 채 호출하면 컴파일 에러가 발생합니다.

 

생성자 매개변수 기본값 명시

 

일반 함수처럼 생성자의 매개변수에도 기본값을 명시할 수 있습니다. 만약 생성자의 매개변수에 기본값을 명시하면 객체를 생성할 때 해당 매개변수 값은 대입하지 않아도 되며, 이때는 자동으로 기본값이 적용됩니다.

 

01 class User3(name: String, age: Int = 0) { }

02 //…

03 val user4 = User3("kkang", 33)

04 val user5 = User3("kkang")

 

위의 소스 01번 줄에 클래스를 선언하고 2개의 매개변수가 있는 생성자를 선언했습니다. 그런데 age 매개변수에는 기본값을 0으로 명시했습니다. 이렇게 하면 객체를 생성할 때 03번 줄처럼 인수 2개를 모두 전달할 수도 있고, 04번 줄처럼 기본값이 있는 두 번째 인수는 전달하지 않아도 됩니다.

 

생성자 초기화 블록 init

 

클래스의 생성자에 매개변수를 지정하는 것 이외에 실행문을 작성할 수도 있습니다. 객체 생성과 동시에 생성자가 호출되므로 클래스의 멤버 변수를 초기화하는 등 보통 초기에 수행해야 할 작업을 생성자에 작성합니다. 그런데 주 생성자는 클래스 헤더에 명시하다 보니 실행문을 포함할 수 없다는 문제가 있습니다. 즉, 주 생성자는 { }를 가질 수 없습니다.

 

01 class User4(name: String, age: Int) { }{ //에러

02

03 }

 

위의 코드는 컴파일 에러입니다. 그냥 봐도 이치에 맞지 않는 구문입니다. 주 생성자를 선언하면서 바로 옆에 주 생성자를 위한 실행 영역을 { }로 명시할 수는 없습니다. 그렇다면 주 생성자는 실행 영역을 가질 수 없는 걸까요? 그렇지는 않습니다. 주 생성자는 클래스 선언 영역에 작성하다 보니 실행 영역을 클래스 몸체에 init 예약어로 따로 명시하는 기법을 사용합니다.

 

01 class User4(name: String, age: Int) {

02   init {

03     println("i am init...")

04   }

05 }

06 //…

07 val user6 = User4("kkang", 33)

 

07번 줄에서 클래스의 객체를 생성했습니다. 이때 01번 줄에 선언한 주 생성자가 호출됩니다. 그리고 객체가 생성될 때 클래스 내에 init 예약어로 선언한 02번 줄의 초기화 블록도 함께 실행됩니다. 이처럼 주생성자의 실행 구문은 클래스 내부에 init 예약어를 이용해 작성하면 됩니다.

 

생성자 매개변수 값 이용

 

객체를 생성할 때 전달한 값은 생성자의 매개변수로 받아서 대부분 클래스 내의 멤버(프로퍼티, 함수등)에서 이용합니다. 그런데 생성자의 매개변수를 이용할 때 제약이 있습니다. 클래스의 초기화 블록이나 프로퍼티에서는 접근할 수 있지만, 클래스에 정의된 멤버 함수에서는 사용할 수 없습니다

 

01 class User5(name: String, age: Int){

02   init {

03     println("i am init... constructor argument : $name .. $age")

04   }

05   val upperName = name.toUpperCase()

06

07   fun sayHello(){

08     println("hello $name") 에러

09   }

10 }

 

01번 줄에 선언한 주 생성자의 매개변수는 클래스 내부에서 이용할 수 있습니다. 그런데 02번 줄의 초기화 블록과 05번 줄의 클래스 프로퍼티에서는 이용하는 데 별문제가 없는데, 08번 줄의 클래스 내에 선언한 함수에서는 컴파일 에러가 발생합니다.

 

그렇다면 생성자의 매개변수는 함수에서 사용할 수 없는 걸까요? 그렇지는 않습니다. 두 가지 방법이 있는데, 첫 번째는 생성자의 매개변수를 클래스 프로퍼티에 대입하고 함수에서는 프로퍼티로 이용하는 방법입니다.

 

01 class User5(name: String, age: Int){

02   val name = name

03   init {

04     println("i am init... constructor argument : $name .. $age")

05   }

06   fun sayHello(){

07     println("hello $name")

08   }

09 }

10 //…

11 val user7 = User5("kkang", 33)

12 user7.sayHello()

 

【 실행결과 】

i am init... constructor argument : kkang .. 33

hello kkang

 

01번 줄에서 선언한 생성자의 매개변수를 02번 줄에서 프로퍼티에 대입하고, 이 프로퍼티를 07번 줄의 함수 내부에서 이용했습니다. 생성자의 매개변수를 함수에서 이용하는 두 번째 방법은 생성자 내에서 val, var를 이용해 매개변수를 선언하는 방법입니다.

 

01 class User6(val name: String, val age: Int){

02   val myName = name

03   init {

04     println("i am init... constructor argument : $name .. ${age}")

05   }

06   fun sayHello() {

07     println("hello $name")

08   }

09 }

 

 

위의 소스를 보면 01번 줄의 생성자 매개변수를 val로 선언했습니다. 이처럼 생성자의 매개변수를 선언 할 때 val 또는 var를 추가하면 해당 매개변수는 클래스의 프로퍼티가 됩니다. 따라서 07번 줄처럼 클래스에 정의된 함수에 이용할 수 있습니다.

 

생성자의 매개변수와 클래스의 프로퍼티

 

생성자에는 매개변수를 선언하고 클래스 영역에는 프로퍼티를 선언할 수 있습니다. 그런데 여기서 발생하는 문제가 하나 있습니다. 만약 생성자의 매개변수명과 클래스의 프로퍼티명이 같으면 어떻게 될까요? 아무 문제 없이 컴파일되고 잘 실행됩니다. 잘 실행되는데 무엇이 문제일까요?

 

01 class User7(name: String, age: Int){

02   val name: String = "kim"

03   init {

04     println("i am init... constructor argument : $name")

05   }

06   fun sayHello() {

07     println("hello $name")

08   }

09 }

10 //…

11 val user9 = User7("kkang", 33)

12 user9.sayHello()

 

【 실행결과 】

i am init... constructor argument : kkang

hello kim

 

위의 소스에서 01번 줄에 선언한 생성자의 매개변수명과 02번 줄에 선언한 프로퍼티명이 name으로 같습니다. 컴파일 후 실행하는 데는 문제가 없습니다. 그런데 04번 줄의 초기화 블록에서 사용하는 name 변수는 어떤 name을 참조하는 걸까요? 또한, 07번 줄의 함수에서 이용하는 name 변수는 어떤 name 을 참조하는 걸까요?

 

초기화 블록에서 이용하는 변수는 생성자의 매개변수입니다. 실행결과를 보면 객체를 생성할 때 전달한 "kkang"이라는 문자열이 출력됐습니다. 그런데 07번 줄의 함수에서 이용하는 변수는 클래스의 프로퍼티입니다. 따라서 실행결과를 보면 02번 줄에서 선언한 프로퍼티의 변숫값 "kim"이라는 문자열이 출력 됐습니다.

 

정리하면 생성자의 매개변수명과 프로퍼티명을 같게 선언할 수 있지만, 적용되는 곳이 다르다는 것에 주의해야 합니다. 따지고 보면 초기화 블록은 주 생성자의 실행 영역이므로 여기서는 생성자의 매개변수를 이용하고, 함수는 클래스의 프로퍼티를 제어하는 목적이므로 프로퍼티를 이용하는 게 당연합니다.

 

그렇다면 생성자의 매개변수를 선언할 때 var, val을 추가하면 어떻게 될까요? 앞에서 설명한 대로 생성자의 매개변수를 var, val로 선언하면 클래스의 프로퍼티로 이용됩니다. 이때 클래스 영역에 매개변수와 같은 이름의 프로퍼티를 선언하면 어떻게 될까요? 예상했겠지만 당연히 에러가 발생합니다.

 

01 class User8(val name: String, age: Int){

02   val name: String = "kkang" //에러

03   val age = 10

04 }

 

위의 소스에서 생성자의 매개변수인 name을 val로 선언했습니다. 그리고 02번 줄에서 같은 이름으로 다시 클래스의 프로퍼티를 선언했습니다. 이렇게 하면 컴파일 에러가 발생합니다.