클래스 정의하기
JAVA와 마찬가지로 Kotlin에서는 클래스 정의를 통해 커스텀으로 정의된 연산들이 포함된 새로운 타입을 만들어줍니다. 기본적으로 클래스 선언은 참조 타입(referential type)을 정의합니다.
- Kotlin 1.3에 도입된 인라인 클래스(inline class)를 통해 참조 타입이 아닌 타입 역시 정의가 가능합니다.
클래스 내부 구조
자바와 마찬가지로, 코틀린 클래스도 class 키워드 다음에 클래스 이름이 오고 그 다음에 클래스 본문이 오는 형태입니다.
- 코틀린 클래스는 기본적으로 공개(public) 가시성입니다. 최상위 함수와 마찬가지로 최상위 클래스를 internal이나 private으로 설정할 수 있습니다.
- 코틀린 클래스 명은 자바와 달리, 소스 파일명과 동일하게 작성하지 않아도 됩니다. 코틀린에서는 클래스명과 소스 파일명을 갖게 만드는 건 요구사항이 아닌 취향의 문제입니다.
class Person {
var firstName: String = ""
var familyName: String = ""
var age: Int = 0
fun fullName() = "$firstName $familyName"
fun showMe() {
println("${fullName()}: $age")
println("${this.fullName()}: $this.age") // 9번째줄과 동일 결과
}
fun setName(firstName: String, familyName: String) {
this.firstName = firstName // this 붙여야 구분 가능
this.familyName = familyName // this 붙여야 구분 가능
}
}
- 위와 같이 Person 클래스를 정의하면, 각 인스턴스마다 firstName, familyName, age라는 프로퍼티와 fullName(), showMe()라는 함수가 들어있게 됩니다.
- 프로퍼티는 어떤 클래스의 구체적인 인스턴스와 엮여 있으므로, 호출하고자 할 때에는 해당 인스턴스(수신 객체: receiver)를 식으로 지정해야 합니다. 수신 객체는 프로퍼티에 접근할 때 사용해야 할 객체를 의미합니다.
- 멤버 함수 역시 수신객체를 가지고 있고, 이런 멤버 함수를 메소드(method)라고 부릅니다.
- 클래스 내부에서는 this 식으로 수신 객체를 참조할 수 있습니다.
- this는 생략 가능하나, 어떤 클래스의 프로퍼티와 메소드 파라미터 이름이 같은 경우에는 이 둘을 구분하기 위해 프로퍼티 앞에는 this를 써야 합니다.
fun showAge(p: Person) = println(p.age) // p: 수신객체
자바와 달리, 코틀린에서는 클라이언트 코드를 바꾸지 않아도 프로퍼티의 구현을 바꿀 수 있습니다.
- 예를 들어, 커스텀 게터나 세터를 추가해도 클라이언트 소스코드를 바꾸지 않아도 되며, 코틀린 프로퍼티는 캡슐화(encapsulation)에 위배되지 않습니다.
생성자
class Person {
val firstName = "John" // 이렇게만 작성하면, 생성자로 초기화할 방법이 없어 Person 클래스의 모든 인스턴스의 firstName은 John이 됨
}
위 코드를 보면, Person이라는 클래스에 firstName이라는 프로퍼티가 있음을 알 수 있습니다.
firstName은 val 타입으로 불변값이며, "John"으로 초기화되는 것을 확인할 수 있습니다. 위와 같은 클래스의 인스턴스는 생성할 때마다 항상 firstName은 John이 되게 됩니다. 이럴 때 커스텀 생성자를 사용하면, firstName 프로퍼티를 다른 값으로 초기화할 수 있습니다.
생성자는 자바에서와 마찬가지로, 인스턴스 생성 시 호출되는 특별한 함수입니다.
class Person(fullName: String) {
val firstName: String
val familyName: String
init {
val names = fullName.split(" ")
if(names.size != 2) {
throw IllegalArgumentException("Invalid name: $fullName")
}
firstName = names[0]
familyName = names[1]
}
}
fun main() {
val person = Person("js h") // 코틀린은 자바와 달리, 생성자 호출 시 new를 사용하지 않음
println(person.firstName)
}
위와 같이 class 키워드 이름 뒤에 덧붙인 파라미터 목록은 클래스의 인스턴스를 생성할 때 클래스에 전달되는 파라미터들입니다. 이 파라미터들을 사용하여 프로퍼티를 초기화할 수 있습니다.
이렇게 class 키워드 이름 뒤에 덧붙인 파라미터 목록을 주생성자 선언이라고 합니다. 위 main 함수에서 생성한 person 객체는 주생성자를 통해 호출한 객체라고 볼 수 있습니다.
- 주생성자를 통해 객체를 생성하게 되면, 먼저 클래스 내 프로퍼티 초기화와 함께 초기화 블록(init 블록)을 수행합니다.
- init 블록에서는 클래스 초기화 시 필요한 초기화 로직을 수행할 수 있습니다.
- 클래스 내부에 init 블록이 여러개 들어갈 수 있으며, 각 블록은 프로퍼티 초기화와 함께 순서대로 실행됩니다.
- init 블록 내부에는 return문이 들어갈 수 없습니다.
- 주생성자 파라미터는 프로퍼티 초기화나 init 블록 밖에서 사용할 수 없습니다. 예를 들어, 위와 같이 멤버 함수 내부에서는 사용할 수 없습니다.
- 하지만, 위와 같이 생성자 파라미터의 값을 저장할 멤버 프로퍼티가 존재한다면, 멤버 함수 내부에서 사용 가능합니다.
- 위 왼쪽 예시처럼, 멤버 프로퍼티를 생성하고, 해당 프로퍼티에 값을 넣을 수도 있지만, 코틀린은 오른쪽 예시처럼 생성자 파라미터 앞에 val 또는 var 키워드를 붙여 해당 생성자 파라미터로 초기화되는 (생성자 파라미터와 이름이 같은) 프로퍼티를 정의해줍니다.
- 이러한 코틀린 특성으로 본문이 생략된 클래스 역시 생성 가능합니다.
- 함수와 마찬가지로 클래스 역시, default 값을 생성자 파라미터로 넣을 수 있습니다.
- default 값을 생성자 파라미터에 넣을 수 있음에 따라 default 값으로만 채워진 멤버 프로퍼티를 가진 객체를 생성할 수도 있고, 모든 프로퍼티를 하나하나 넣어준 객체를 생성할 수도 있습니다.
경우에 따라, 주생성자만으로 원하는 객체를 생성하기 어렵다면 아래와 같이 부생성자(secondary constructor)를 사용해 객체를 생성할 수 있습니다. 부생성자는 함수 이름 대신에 constructor 키워드를 사용하면 됩니다.
class Person {
val firstName: String
val familyName: String
constructor(fullName: String) { // 부생성자는 반환 타입 지정 불가(Unit 타입 반환)
val names = fullName.split(" ")
if(names.size != 2) {
throw IllegalArgumentException("Invalid name: $fullName")
}
firstName = names[0]
familyName = names[1]
}
constructor(firstName: String, familyName: String): this("$firstName $familyName") // this를 사용해 생성자 위임 호출
}
- 위처럼, 주생성자 없이 부생성자만 있다면 자신의 본문을 실행하기 전 프로퍼티 초기화와 init 블록을 실행합니다.
- 부생성자는 위 두번째 예시처럼, 다른 부생성자에 객체 생성을 위임할 수 있습니다.
- 부생성자는 일반 함수와 마찬가지로 val이나 var 키워드를 사용할 수 없습니다.
멤버 가시성
자바와 마찬가지로 코틀린도 클래스 멤버마다 다르게 지정할 수 있습니다. 다만, 코틀린에는 자바의 패키지 전용에 해당하는 가시성이 없습니다.
- public(공개): 멤버를 어디에서나 볼 수 있음(default 가시성)
- internal(모듈 내부): 멤버를 멤버가 속한 클래스가 포함된 컴파일 모듈 내부에서만 볼 수 있음
- protected(보호): 멤버를 멤버가 속한 클래스와 멤버가 속한 클래스의 모든 하위 클래스 안에서 볼 수 있음
- private(비공개): 멤버를 멤버가 속한 클래스 내부에서만 볼 수 있음
프로퍼티 뿐 아니라, 주생성자와 부생성자 역시 가시성 변경자를 사용할 수 있습니다.
class Empty private constructor() { // 주생성자의 가시성을 지정하려면, constructor 키워드 사용 필요
fun showMe() = println("empty")
}
내포된 클래스(nested class)
코틀린 클래스는 함수, 프로퍼티, 생성자 외에 다른 클래스 역시 멤버로 가질 수 있습니다. 이러한 클래스를 내포된 클래스(nested class)라고 부릅니다.
class Person(val id: Id, val age: Int) {
class Id(val firstName: String, val familyName: String)
fun showMe() = println("${id.firstName} ${id.familyName}")
}
fun main() {
val id = Person.Id("js", "h")
val person = Person(id, 28)
person.showMe()
}
내포된 클래스를 둘러싸고 있는 클래스의 본문 밖에서는 내포된 클래스 이름 앞에 바깥쪽 클래스 이름을 덧붙여야만 내포된 클래스를 참조할 수 있습니다.
자바 역시, 내포된 클래스를 가지고 있지만 코틀린과 조금 차이가 있습니다. 아래 사진에서와 같이 코틀린의 클래스는 자바와 달리 자신에게 내포된 클래스의 비공개 멤버에 접근할 수 없습니다.
자바의 경우, 디폴트로 내포된 클래스가 내부 클래스입니다. 이에 기본적으로 내포된 클래스는 외부 클래스의 프로퍼티에 접근이 가능하고, 내부 클래스가 외부 클래스 인스턴스와 연관되지 않길 바라면 명시적으로 static을 붙여야 합니다.
하지만, 코틀린은 내포된 클래스에 inner 키워드를 붙여야만 내부 클래스가 되기 때문에 내부 클래스로 사용하고자 한다면 앞에 inner를 반드시 붙여주어야 합니다.
class Outer {
inner class Inner
class Nested
}
// 위 코틀린 코드를 자바로 바꾸면 아래와 같음
class outer {
public class Inner
public static class Nested
}
- 내부(inner) 클래스의 생성자를 호출할 때는 외부 클래스 인스턴스를 지정한 채 생성자를 호출해야 합니다(위 오른쪽 사진).
참고 자료
'PROGRAMMING LANGUAGE > KOTLIN' 카테고리의 다른 글
[Kotlin] 객체 (0) | 2022.08.25 |
---|---|
[Kotlin] 널 가능성 (0) | 2022.08.12 |
[Kotlin] 코틀린 함수 (0) | 2022.08.04 |
[Kotlin] 코틀린 언어 기초 (0) | 2022.08.04 |
[Kotlin] 코틀린이란? (0) | 2022.08.04 |