본문 바로가기
Android 개발/Kotlin

Kotlin을 배워보자 part3 (Class, Constructor, 상속)

by Developer88 2019. 9. 26.
반응형

벌써 kotlin에 대해 정리한 글의 part3가 되었네요.

이전 part2에서는 if, when, for, array, list, ranges에 대해서 정리해 보았는데요.
이번 글에서는 class와 생성자 그리고 상속에 대해서 정리해 보도록 하겠습니다.

혹시 이전글을 보시지 못한 분들은 아래 링크를 참조해 주세요.

>> Kotlin을 배워보자 part2(if, when, for, while, array, list, ranges)

1. Class와 생성자

코틀린에서 클래스를 만들기 위해서 class라는 키워드를 사용하구요.

클래스의 객체를 생성할 때 Java에서 쓰던 new연산자는 사용하지 않습니다.

 

아래 코드에서는 자동차 클래스를 이용해 차 객체를 만들어 준 다음,

자동차를 생성할 때 같이 생성된 Tire에 접근하고 있습니다.

Tire 클래스에는 바디블록이 없다는 것을 볼 수 있는데요.

바디블록 없이도 클래스선언이 가능합니다.

 

 

그럼 이제, 클래스의 생성자에 대해서 알아보도록 하겠습니다.

 

1-1. Primary Constructor(기본 생성자)

Kotlin에서 생성자는 하나의 Primary Constructor

Secondary Contstuctor 여러개를 가질 수 있습니다.

Primary Constuctor먼저 정리해 보도록 하겠습니다.

 

a. Primary Constructor(기본 생성자)

아래의 코드를 보도록 하겠습니다.

클래스 헤더부분에 (name: String)으로 생성자를 받고 있는데요.

이 부분이 Primary Constructor(기본 생성자)입니다.

 

실제로는 아래와 같이 constructor키워드를 붙일 수 있는데요.

 

class MyClass constructor(name: String){
	
}

 

아래에서와 같이 생략할 수 있습니다.

기본 생성자의 인자는 클래스의 프로퍼티를 선언할 때 사용하거나, init블록 안에서만 사용해야 합니다.

(아래에서와 같이 클래스의 멤버함수를 만들어서 그 안에서 인자로 넘어온 값을 사용하려고 해도 할 수 없습니다.)

 

 

그런데 init이라는 것은 무엇일까요?

initialize의 줄임말일 것이구요. 코틀린의 기본 생성자는 코드를 포함할 수 없으므로,

초기화 하는 부분을 여기서 하라고 만들어 준 것 입니다.

한가지 주의할 것은 Secondary Constructor에서 넘어온 변수는 여기서 사용할 수 없다는 것 입니다.

 

 

b. Primary Constructor 키워드 사용시 주의점

특별히 annotationion혹은 public이나 private같은 접근제어자(visibility modifier)가 없다면,

constructor 키워드는 생략해 주어도 됩니다.

아래와 같이 injection이 필요해서 쓰는 Inject와 같은 annotation을 쓰는 경우에는

생성자 앞에 construct 키워드를 꼭 아래와 같이 써 주어야 하구요.

안드로이드의 Hilt나 Dagger 같은 라이브러리를 쓸때 위치를 잘 알아두어야 하겠지요.

 

 

Hilt라이브러리를 써서 ViewModel클래스를 작성할 경우 아래와 같이 @Inject 키워드를 쓰고,

생성자 부분에 constructor 키워드를 꼭 넣어주어야 합니다.

 

@HiltViewModel
class TestViewModel
@Inject
constructor(private val apiService: Api):ViewModel() {

}

 

 

C. 기본생성자에서 넘어온 값의 사용법

Java에서 했던 것처럼 this.x = x로 생성자로 넘어온 값을 사용했던 것을 기억하시나요?

그럼 kotlin은 어떻게 할까요? 

기본생성자에서  val 혹은 var키워드를 붙여서 클래스의 프로퍼티(property)로 바로 사용해 버릴 수 있습니다.

(참고로 class의 property와 field에 대한 구분이 애매하신 분들은 아래 링크를 참조해 주세요)

>> https://en.wikipedia.org/wiki/Property_(programming)

 

아래의 Student와 같이 val을 선언하면,

java에서 name이라는  변수를 선언하고 getter와 setter가 설정된 것과 비슷한 효과를 가집니다.

 

프로퍼티를 선언하는 방법은 아래 이미지와 같은데요.

property의 Type이나 초기값 그리고 getter와 setter는 생략할 수 있습니다.

그리고 var대신 val을 쓰면 read-only타입이 됩니다.

(read-only일 경우는 당연히 setter가 없는 것어야 겠지요.)

 

 

아래의 경우, Address클래스의 property들이 초기화 된 상태로 객체 생성시 접근되어 지고 있습니다.

물론 아래와 같이 하지 않고, 위에서 써 본대로 기본생성자에서 바로 property를 생성하고 초기화 할 수도 있습니다.

 

 

D. Getter와 Setter 재정의와 Backing Field

Kotlin에서 getter와 setter는 어떻게 재정의 할 수 있을까요?

아래와 같이 간단하게 할 수 있는데요.

field라는 키워드로 접근해주면 됩니다.

 

 

아래와 같이 print해보면 "Hello Daniel" 이 출력되는 것을 볼 수 있습니다.

 

 

좀 더 디테일하게 보면,

프로퍼티에 값을 저장해야 할 때 코틀린이 자동으로 만들어준 필드를 Backing Field라고 하는 것 인데요.

위에서 사용하였던 field라는 키워드를 통해서 Backing Field에 접근해 값을 받아오거나 수정하는 것 입니다.

 

아래에서는 name property의 getter와 setter를 재정의 하였는데요.

field를 이용해서 기존값을 사용하거나 아니면 다른 값을 넣어주고 있습니다.

setter는 private으로 설정하여 직접 set하지 못하게 하였습니다.

대신 addName이라는 메소드를 통해 접근하도록 하였구요.

 

 

 

그런데 아래와 같은 경우는 backing field가 생성되지 않습니다.

val이므로 read-only여서 setter가 없는상태에서 field라는 식별자를 사용하지도 않고 있기 때문에,

이런 경우는 backing field가 생성되지 않습니다.

 

 

E. Backing Property

Backing Property라는 것도 있는데요.

public으로 된 property를 하나 생성하고,

property내부에서만 사용할 private property를 "_+프로퍼티 이름"으로 생성해 줍니다.

이것이 바로 Backing Property인데요.

아래의 경우, 만약 null이면 HashMap객체를 만들어서 넣어주도록 하고 있습니다.

 

 

이러한 방식의 접근패턴은 은근히 여기저기 쓰이기 때문에, 복잡하지만 알아둘 필요가 있습니다.

다시 정리하면, 외부에서 접근할 수 있는 public property와,

앞에 언더바(_)를 붙인 private한 property로 나누어서 캡슐화 해서 사용하는 것 입니다.

 

이상으로 Primary Construtor 그리고 Property에 대해서 정리해 보았는데요.

아래에서는 Secondary Constructor에 대해서 정리해 보도록 하겠습니다.

 

1-2. Secondary Constructor

primary constuctor는 클래스의 헤더부분에 바로 쓸 수 있었는데요.
secondary Constructor는 여러개를 클래스의 body에 사용할 수 있습니다.

 

a. 위임(Delegate)

한가지 신경써야 할 것은 Secondary 생성자를 만들면 Primary 생성자가 있을 경우 위임(Delegate)을 해 주어야 한다는 것입니다.

this키워드를 이용해서 위임해줄 Primary생성자나

이미 Primary생성자에 위임하고 있는 다른 Secondary생성자를 호출해 주어야 하는데요.

복잡해 보이므로, 코드를 보면서 정리해 보겠습니다.

 

 

위의 코드에서는 Primary생성자 한개에 Secondary생성자가 2개가 있습니다.

Primary생성자에 위임하는 첫번째 Secondary생성자에게

두번째 Seconday생성자가 위임(delegation)을 하고 있습니다.

인자가 없는 생성자를 호출할 경우  생성자에서 정의한 값이 디폴트로 들어가게 됩니다.

 

b. Seconday 생성자의 인자

그런데, 첫번째 Secondary 생성자로 부터 받은 age는 어떻게 쓸 수 있는 것일까요?

class에 property인 age를 만들어서 초기화 해 주고 그 값에 인자로 들어온 age를 넣어주면 됩니다.

주의해야 할 것은, age를 init블록에서 실행하려고 하면 원하는 값을 얻을 수 없다는 것인데요.

왜냐하면, Secondary생성자보다 init블록이 더 먼저 실행되기 때문입니다.

아래 코드의 결과 값에서 볼 수 있듯이 init블록에서 프린트 한 값은 초기화한 값 입니다.

Secondary생성자로부터 들어온 인자의 값이 아닌 것이지요.

출력되는 순서도 init블록에서 찍은 값이 먼저 나온다는 것을 알 수 있구요.

 

 

위와 같은 경우를 머리속에 생각해 두고 생성자를 만드는 것이 좋은데요.

인자가 더 많이 받는 경우를 Primary 생성자로 하는 방법(Java에서 생성자 쓸 때와는 반대)이,

보다 더 간결하게 코드를 사용할 수 있는 방법인 것 같네요..

 

 

 

C. Seconday생성자 없이 Primary생성자에 초기값 주기

Primary생성자에서는 val혹은 var를 붙여 Property를 만들고 초기 값을 주어서,

Secondary생성자가 있는 것 같은 효과를 줄 수 있는데요.

아래의 경우 인자가 없는 생성자는 만든적이 없는데, 정상적으로 실행되는 것을 볼 수 있습니다.

Secondary생성자에서 특별한 처리가 필요한 것이 아니라면, 굳이 Secondary생성자를 만들 필요가 없어 보이네요.

(이러한 사용법은 Java에서는 없던 방법이지요)

 

 

D. 기본 생성자가 없는 Secondary 생성자

기본 생성자가 없이 Secondary 생성자만 여러개 만들수도 있습니다.

위임해야할 대상이 없으므로 조금더 자유롭게 사용할 수 있을 텐데요.

대신 인자로 들어온 값을 바로 property로 사용할 수 있는 장점이 사라지게 되는데요.

따라서 property를 한 줄씩 넣어주면서 코드가 더 길어지게 되겠지요.

 

 

2. Class의 상속

Kotlin에서 상속을 표현하는 방법은 Java와는 조금 다른데요.

상속하면서 부모의 생성자를 호출해 주어야 합니다.

부모의 생성자가 인자를 받는다면,

자식도 부모의 생성자를 호출하므로

자식의 생성자에서 인자를 받아서 부모에게 넘기거나, 디폴트 값을 가지고 부모를 호출해야 합니다.

 

상속해주는 부모는 open이라는 키워드를 써서,

이 클래스가 open되어 있다고 명시해 주어야 하는데요.

(참고로 open은 final과는 반대의 의미이며, 기본적으로 kotlin의 모든 class는 final 상태 입니다.

Java에서는 반대로 final이 아니였었지요.)

 

상속이나 구현은 아래와 같이 ":"을 이용해서 표현하구요. 다중상속의 경우는 "," 를 붙여서 이용합니다.

또한 부모의 생성자를 호출하므로, 부모의 클래스이름뒤에 ()를 붙여서 명시적으로 생성자를 호출해 줍니다.

 

 

아래는 부모와 자식 모두가 Primary생성자가 존재하지 않는데요.

만약 자식이 Primary생성자가 존재하지 않는경우,

Secondary 생성자에서 super키워드를 이용해서 부모의 생성자를 호출해 초기화 해 주어야 합니다.

 

 

3. Class의 메소드와 프로퍼티 오버라이드

메소드 오버라이드 시에 오버라이드 되는 대상은 open 키워드를 사용해서 final이 아님을 명시해 주어야 하구요.

오버라이드 하는 메소드에는 override라는 키워드를 붙여 주어야 합니다.

 

상속에서도 보았지만, kotlin에서는 상속이든 오버라이드의 대상이 되는 클래스들은

open이라는 키워드를 사용해서 final상태가 아님을 명시해 주어야 합니다.

 

 

 

메소드의 프로퍼티도 오버라이드 될 수 있는데요.

방식은 메소드의 그것과 비슷합니다.

부모 프로퍼티의 get메소드를 오버라이드 해주는 방식인데요.

 

 

오버라이드를 하는데 있어서 알고 있어야 할 규칙이 있습니다.

만약 class를 상속받으면서 동시에 interface도 구현한다고 했을 때,

같은 멤버메소드를 가지고 있을 경우는 어떻게 해야할까요?

무슨 말인지 어려울 수 있으니 코드를 보도록 하겠습니다.

 

 

kotlin에서는 interface에서 구현을 할 수 있기 때문에,

그래서 아예 부모클래스와 Inteface에서 상속및 구현을 받고나서,

구현하는 자식쪽에서 override해서 명확하게 하라는 것인데요.

 

위의 코드를 보면,

f()함수의 경우 parentClassF로 프린트 될지 interfaceF로 프린트될지 알 수가 없기 때문에,

Child클래스가 f()함수를 override해서 구현하였는데요.

그냥 부모들을 순차적으로 호출했습니다.

참고로 상위 클래스나 인터페이스를 호출할 때는 위 코드와 같이 super+<클래스 명>을 호출하면 됩니다.

 

4. Compile-time 상수

Kotlin에서 const라는 키워드를 통해서,

compile time에 사용할 수 있는 상수로 표현할 수 있습니다.

 

 

공식문서에 보면 다음과 같은 조건을 만족시켜야 한다고 나옵니다.

  • top-level(최상단) 프로퍼티 이거나 또는, object나 companion object의 멤버여야 함
  • String이나 primitive type으로 초기화 되야 함
  • Custom한 getter를 사용하면 안됨

 

5. Late-Initialized 프로퍼티(Property)

보통 프로퍼티는 non-null 타입으로 선언되는데요.

dependency injection을 통해서 초기화 되거나,

unit test의 setup method 로 사용되는 경우에

non-null타입으로 초기화 할 수 없게 됩니다.(나중에 초기화 될테니까요)

 

비록 현재 생성자로 초기화 할 수는 없지만, 나중에(늦게) 초기화 될테니 현재 null체크를 할 필요는 없다라고

표현할 수 있는 방법이 있습니다.

문자 그대로 lateinit이라는 키워드를 사용합니다.

lazy하게 나중에 필요할 때 값이 들어오니까 걱정하지 말라는 것입니다.

 

 

위 코드는 원래 TestSubject가 아니라, TestSubject?로 선언되었어야 하는데요.

그렇게 하지않고, 나중에 setup()함수를 통해서 late하게 값이 특어올테니 걱정말라는 의미로 lateinit을 사용하였습니다.

 

안드로이드에서 DataBinding에 의해서 값이 들어오는 biding도 선언할 때, lateinit으로 선언해서,

나중에 binding을 통해 값이 들어올테니 "?"을 붙이지 않습니다.

 

 

이러한 lateInit은 클래스의 바디안에서만 선언이 가능합니다.

Primary constructor에서는 lateinit을 사용할 수 없구요.

custom getter나 setter를 사용할 때도 lateInit은 적용할 수 없습니다.

나중에 값이 들어올 것이므로 var를 통해서만 사용할 수 있으며,

실제로도 non-null이어야 하겟지요.

런타임에서는 Kotlin의 도움을 받지 못하므로 개발자가 nullException이 나지않게 잘 챙겨야 합니다.

 

6. 정리

이번 part3에서는 class와 생성자, 프로퍼티 그리고 상속 등에 대해서 정리해 보았는데요.

다음 글에서는 Data Class, Nest and Inner Class 등에 대해서 정리해 보도록 하겠습니다

 

728x90

댓글