클래스
클래스 (Classes)
다른 객체 지향 언어처럼, Kotlin도 클래스(class)를 써서 데이터(속성, property)와 동작(함수, function)을 하나로 묶어 재사용 가능하고 구조화된 코드를 만듭니다. 클래스는 객체의 설계도(blueprint)나 템플릿이라고 할 수 있어요. 클래스의 생성자로 인스턴스(instance)를 만들면, 그 설계도를 바탕으로 한 구체적인 객체가 탄생합니다.
클래스를 만들기 전에 먼저 이렇게 물어보면 좋아요. 저장하고자 하는 게 데이터라면 data class를 쓸 수 있는지, 처음부터 새로 만드는 대신 기존 클래스를 확장(extension)해도 되는지. 이 두 가지를 먼저 고려해 보는 거죠.
출처: Kotlin 공식 문서
본문
Kotlin은 클래스 선언을 위한 간결한 문법을 제공해요. 클래스를 선언하려면 class 키워드 뒤에 클래스 이름을 붙입니다.
class Person { /*...*/ }
클래스 선언은 크게 두 부분으로 구성됩니다.
- 클래스 헤더(class header) — 여기에는 다음이 들어가요.
class키워드- 클래스 이름
- 타입 파라미터(있을 때)
- 기본 생성자(primary constructor)(선택)
- 클래스 본문(class body)(선택) — 중괄호
{}로 감싸며, 클래스 멤버를 담아요.
클래스 헤더와 본문 모두 최소한으로 유지할 수 있어요. 본문이 없는 클래스라면 중괄호 {}를 생략해도 됩니다.
// 기본 생성자는 있지만 본문이 없는 클래스
class Person(val name: String, var age: Int)
헤더와 본문을 모두 가진 클래스를 선언하고, 거기서 인스턴스를 만드는 예시를 볼게요.
// name 속성을 초기화하는 기본 생성자를 가진 Person 클래스
class Person(val name: String) {
// age 속성을 가진 클래스 본문
var age: Int = 0
}
fun main() {
// 생성자를 호출해 Person 클래스의 인스턴스를 만든다
val person = Person("Alice")
// 인스턴스의 속성에 접근한다
println(person.name)
// Alice
println(person.age)
// 0
}
인스턴스 만들기
인스턴스는 클래스를 설계도 삼아, 프로그램에서 실제로 다루는 객체를 만드는 순간 생성됩니다. 클래스의 인스턴스를 만들려면 클래스 이름 뒤에 괄호 ()를 붙이는데, 함수를 호출하는 모양과 비슷해요.
// Person 클래스의 인스턴스를 만든다
val anonymousUser = Person()
Kotlin에서는 두 가지 방식으로 인스턴스를 만들 수 있어요.
- 인자 없이(
Person()): 클래스에 기본값이 선언되어 있다면 그 기본값을 사용해 인스턴스를 만들어요. - 인자와 함께(
Person(value)): 특정 값을 넘겨 인스턴스를 만들어요.
만든 인스턴스는 변경 가능한(var) 또는 읽기 전용(val) 변수에 할당할 수 있습니다.
// 기본값을 사용해 인스턴스를 만들고
// 변경 가능한 변수에 할당한다
var anonymousUser = Person()
// 특정 값을 넘겨 인스턴스를 만들고
// 읽기 전용 변수에 할당한다
val namedUser = Person("Joe")
인스턴스는 필요한 곳이라면 어디서든 만들 수 있어요. main() 함수 안에서, 다른 함수 안에서, 다른 클래스 안에서요. 다른 함수 안에서 인스턴스를 만든 뒤 그 함수를 main()에서 호출하는 방식도 가능합니다.
다음 코드는 이름을 저장할 속성을 가진 Person 클래스를 선언하면서, 기본 생성자의 기본값과 특정 값을 각각 사용해 인스턴스를 만드는 방법을 보여줘요.
// name을 기본값으로 초기화하는 기본 생성자를 가진 클래스 헤더
class Person(val name: String = "Sebastian")
fun main() {
// 기본 생성자의 기본값을 사용해 인스턴스를 만든다
val anonymousUser = Person()
// 특정 값을 넘겨 인스턴스를 만든다
val namedUser = Person("Joe")
// 인스턴스의 name 속성에 접근한다
println(anonymousUser.name)
// Sebastian
println(namedUser.name)
// Joe
}
다른 객체 지향 언어와 달리, Kotlin에서는 클래스 인스턴스를 만들 때
new키워드가 필요 없어요.
중첩·내부·익명 내부 클래스의 인스턴스를 만드는 방법은 Nested classes 섹션을 참고하세요.
생성자와 초기화 블록
클래스 인스턴스를 만들 때는 그 생성자 중 하나를 호출합니다. Kotlin 클래스는 기본 생성자 하나와 보조 생성자 하나 이상을 가질 수 있어요. 기본 생성자는 클래스를 초기화하는 주된 경로로, 클래스 헤더에 선언합니다. 보조 생성자는 추가적인 초기화 로직을 제공하며 클래스 본문에 선언하죠. 둘 다 선택이지만, 클래스에는 최소한 하나의 생성자가 있어야 합니다.
기본 생성자
기본 생성자는 인스턴스가 만들어질 때 그 초기 상태를 설정해요. 기본 생성자를 선언하려면 클래스 이름 뒤, 클래스 헤더에 배치합니다.
class Person constructor(name: String) { /*...*/ }
기본 생성자에 애노테이션이나 가시성 수정자가 없다면 constructor 키워드를 생략할 수 있어요.
class Person(name: String) { /*...*/ }
기본 생성자는 파라미터를 속성으로 선언할 수 있어요. 인자 이름 앞에 val 키워드를 쓰면 읽기 전용 속성이 되고, var를 쓰면 변경 가능한 속성이 됩니다.
class Person(val name: String, var age: Int) { /*...*/ }
이런 생성자 파라미터 속성은 인스턴스의 일부로 저장되어, 클래스 밖에서도 접근할 수 있어요. 반대로 속성이 되지 않는 기본 생성자 파라미터도 선언할 수 있는데, 앞에 val이나 var가 없어서 인스턴스에 저장되지 않고 클래스 본문 안에서만 사용할 수 있습니다.
// 속성이기도 한 기본 생성자 파라미터
class PersonWithProperty(val name: String) {
fun greet() {
println("Hello, $name")
}
}
// 파라미터만 있을 뿐(속성으로 저장되지 않음)
class PersonWithAssignment(name: String) {
// 나중에 쓰려면 반드시 속성에 할당해야 한다
val displayName: String = name
fun greet() {
println("Hello, $displayName")
}
}
기본 생성자에 선언된 속성은 클래스의 멤버 함수에서 접근할 수 있어요.
// 속성을 선언하는 기본 생성자를 가진 클래스
class Person(val name: String, var age: Int) {
// 클래스 속성에 접근하는 멤버 함수
fun introduce(): String {
return "Hi, I'm $name and I'm $age years old."
}
}
기본 생성자의 속성에 기본값도 지정할 수 있습니다.
class Person(val name: String = "John", var age: Int = 30) { /*...*/ }
인스턴스 생성 시 생성자에 값이 넘어가지 않으면 속성은 자신의 기본값을 사용해요.
// name과 age의 기본값을 포함하는 기본 생성자를 가진 클래스
class Person(val name: String = "John", var age: Int = 30)
fun main() {
// 기본값을 사용해 인스턴스를 만든다
val person = Person()
println("Name: ${person.name}, Age: ${person.age}")
// Name: John, Age: 30
}
기본 생성자 파라미터를 클래스 본문에서 직접 사용해 다른 클래스 속성을 초기화할 수도 있어요.
// name과 age의 기본값을 포함하는 기본 생성자를 가진 클래스
class Person(
val name: String = "John",
var age: Int = 30
) {
// 기본 생성자 파라미터로부터 description 속성을 초기화한다
val description: String = "Name: $name, Age: $age"
}
fun main() {
// Person 클래스의 인스턴스를 만든다
val person = Person()
// description 속성에 접근한다
println(person.description)
// Name: John, Age: 30
}
함수와 마찬가지로 생성자 선언에도 후행 쉼표(trailing commas)를 쓸 수 있어요.
class Person(
val name: String,
val lastName: String,
var age: Int,
) { /*...*/ }
초기화 블록
기본 생성자는 클래스를 초기화하고 속성을 설정합니다. 대부분의 경우 단순한 코드로 처리할 수 있어요. 인스턴스 생성 중 더 복잡한 동작이 필요하다면, 그 로직을 클래스 본문의 초기화 블록에 넣습니다. 이 블록은 기본 생성자가 실행될 때 함께 실행되죠.
초기화 블록은 init 키워드 뒤에 중괄호 {}를 붙여 선언하고, 초기화 동안 실행하고 싶은 코드를 중괄호 안에 작성해요.
// name과 age를 초기화하는 기본 생성자를 가진 클래스
class Person(val name: String, var age: Int) {
init {
// 인스턴스가 생성될 때 실행되는 초기화 블록
println("Person created: $name, age $age.")
}
}
fun main() {
// Person 클래스의 인스턴스를 만든다
Person("John", 30)
// Person created: John, age 30.
}
초기화 블록(init {})은 필요한 만큼 추가할 수 있어요. 이 블록들은 클래스 본문에 나타난 순서대로, 속성 초기화와 함께 실행됩니다.
// sampleStart
// name과 age를 초기화하는 기본 생성자를 가진 클래스
class Person(val name: String, var age: Int) {
// 첫 번째 초기화 블록
init {
// 인스턴스가 생성될 때 가장 먼저 실행된다
println("Person created: $name, age $age.")
}
// 두 번째 초기화 블록
init {
// 첫 번째 초기화 블록 다음에 실행된다
if (age < 18) {
println("$name is a minor.")
} else {
println("$name is an adult.")
}
}
}
fun main() {
// Person 클래스의 인스턴스를 만든다
Person("John", 30)
// Person created: John, age 30.
// John is an adult.
}
// sampleEnd
초기화 블록에서 기본 생성자 파라미터를 사용할 수 있어요. 위 코드에서도 첫 번째·두 번째 초기화 블록이 기본 생성자의 name과 age 파라미터를 쓰고 있죠.
init 블록의 흔한 용도는 데이터 검증입니다. 예를 들어 require 함수를 호출하는 방식이에요.
class Person(val age: Int) {
init {
require(age > 0) { "age must be positive" }
}
}
보조 생성자
Kotlin에서 보조 생성자는 클래스가 기본 생성자 외에 가질 수 있는 추가 생성자예요. 클래스를 초기화하는 여러 경로가 필요하거나, Java 상호 운용을 위해 유용합니다.
보조 생성자를 선언하려면 클래스 본문 안에서 constructor 키워드를 쓰고 괄호 () 안에 파라미터를 두며, 중괄호 {} 안에 생성자 로직을 넣어요.
// name과 age를 초기화하는 기본 생성자를 가진 클래스 헤더
class Person(val name: String, var age: Int) {
// age를 String으로 받아 Int로 변환하는 보조 생성자
constructor(name: String, age: String) : this(name, age.toIntOrNull() ?: 0) {
println("$name created with converted age: ${this.age}")
}
}
fun main() {
// age를 String으로 받는 보조 생성자를 사용한다
Person("Bob", "8")
// Bob created with converted age: 8
}
age.toIntOrNull() ?: 0표현식에 쓰인 연산자는 엘비스 연산자(Elvis operator)예요. 자세한 내용은 Null safety를 참고하세요.
위 코드에서 보조 생성자는 this 키워드로 기본 생성자에 위임하며, name과 정수로 변환된 age 값을 넘깁니다. Kotlin에서 보조 생성자는 반드시 기본 생성자에 위임해야 해요. 이 위임 덕분에 기본 생성자의 모든 초기화 로직이 보조 생성자 로직보다 먼저 실행됩니다.
생성자 위임은 두 가지 방식이 있어요.
- 직접 위임(direct) — 보조 생성자가 기본 생성자를 바로 호출하는 경우.
- 간접 위임(indirect) — 보조 생성자가 다른 보조 생성자를 호출하고, 그것이 다시 기본 생성자로 위임하는 경우.
직접·간접 위임이 어떻게 동작하는지 보여주는 예시를 볼게요.
// name과 age를 초기화하는 기본 생성자를 가진 클래스 헤더
class Person(
val name: String,
var age: Int
) {
// 기본 생성자로 직접 위임하는 보조 생성자
constructor(name: String) : this(name, 0) {
println("Person created with default age: $age and name: $name.")
}
// 간접 위임하는 보조 생성자:
// this("Bob") -> constructor(name: String) -> 기본 생성자
constructor() : this("Bob") {
println("New person created with default age: $age and name: $name.")
}
}
fun main() {
// 직접 위임을 기반으로 인스턴스를 만든다
Person("Alice")
// Person created with default age: 0 and name: Alice.
// 간접 위임을 기반으로 인스턴스를 만든다
Person()
// Person created with default age: 0 and name: Bob.
// New person created with default age: 0 and name: Bob.
}
초기화 블록(init {})이 있는 클래스에서는 그 블록 안의 코드가 기본 생성자의 일부가 됩니다. 보조 생성자는 항상 먼저 기본 생성자에 위임하므로, 모든 초기화 블록과 속성 초기화가 보조 생성자 본문보다 먼저 실행돼요. 클래스에 기본 생성자가 없더라도 그 위임은 암시적으로 일어납니다.
// 기본 생성자가 없는 클래스 헤더
class Person {
// 인스턴스가 생성될 때 실행되는 초기화 블록
init {
// 보조 생성자보다 먼저 실행된다
println("1. First initializer block runs")
}
// 정수 파라미터를 받는 보조 생성자
constructor(i: Int) {
// 초기화 블록 다음에 실행된다
println("2. Person $i is created")
}
}
fun main() {
// Person 클래스의 인스턴스를 만든다
Person(1)
// 1. First initializer block runs
// 2. Person 1 created
}
생성자가 없는 클래스
생성자(기본·보조)를 전혀 선언하지 않은 클래스는 파라미터가 없는 암시적 기본 생성자를 가집니다.
// 명시적 생성자가 없는 클래스
class Person {
// 기본·보조 생성자 선언 없음
}
fun main() {
// 암시적 기본 생성자를 사용해
// Person 클래스의 인스턴스를 만든다
val person = Person()
}
이 암시적 기본 생성자의 가시성은 public이라 어디서든 접근할 수 있어요. 클래스에 public 생성자를 두고 싶지 않다면, 기본 가시성이 아닌 빈 기본 생성자를 명시적으로 선언하면 됩니다.
class Person private constructor() { /*...*/ }
JVM에서는 기본 생성자 파라미터 전부에 기본값이 있다면, 컴파일러가 그 기본값을 사용하는 파라미터 없는 생성자를 암시적으로 제공해요. 덕분에 Jackson이나 Spring Data JPA처럼 파라미터 없는 생성자로 클래스 인스턴스를 만드는 라이브러리와 함께 Kotlin을 쓰기 쉬워집니다.
다음 예시에서 Kotlin은 기본값 ""을 사용하는 파라미터 없는 생성자 Person()을 암시적으로 제공해요.
class Person(val personName: String = "")
상속
Kotlin의 클래스 상속은 기존 클래스(기반 클래스)로부터 새 클래스(파생 클래스)를 만들어, 그 속성과 함수를 물려받으면서 동작을 추가하거나 변경할 수 있게 해줍니다. 상속 계층과 open 키워드를 쓰는 방법에 대한 자세한 내용은 Inheritance 섹션을 참고하세요.
추상 클래스
Kotlin에서 추상 클래스는 직접 인스턴스화할 수 없는 클래스예요. 실제 동작을 정의하는 다른 클래스에 상속되도록 설계되지요. 이 동작을 구현(implementation)이라고 부릅니다. 추상 클래스는 서브클래스가 반드시 구현해야 할 추상 속성과 추상 함수를 선언할 수 있어요. 추상 클래스는 생성자도 가질 수 있는데, 이 생성자는 클래스 속성을 초기화하고 서브클래스가 필요한 파라미터를 강제합니다.
추상 클래스는 abstract 키워드로 선언해요.
abstract class Person(val name: String, val age: Int)
추상 클래스는 추상 멤버와 비추상 멤버(속성·함수)를 모두 가질 수 있어요. 멤버를 추상으로 선언하려면 abstract 키워드를 명시적으로 써야 합니다. 추상 클래스나 추상 함수에 open 키워드를 붙일 필요는 없어요. 기본적으로 상속 가능하기 때문이죠. open 키워드에 대한 자세한 내용은 Inheritance를 참고하세요.
추상 멤버는 추상 클래스에 구현이 없습니다. 구현은 서브클래스나 상속하는 클래스에서 override 함수나 속성으로 정의해요.
// name과 age를 선언하는 기본 생성자를 가진 추상 클래스
abstract class Person(
val name: String,
val age: Int
) {
// 추상 멤버
// 구현을 제공하지 않으며
// 서브클래스가 반드시 구현해야 한다
abstract fun introduce()
// 비추상 멤버 (구현이 있음)
fun greet() {
println("Hello, my name is $name.")
}
}
// 추상 멤버의 구현을 제공하는 서브클래스
class Student(
name: String,
age: Int,
val school: String
) : Person(name, age) {
override fun introduce() {
println("I am $name, $age years old, and I study at $school.")
}
}
fun main() {
// Student 클래스의 인스턴스를 만든다
val student = Student("Alice", 20, "Engineering University")
// 비추상 멤버를 호출한다
student.greet()
// Hello, my name is Alice.
// 오버라이드된 추상 멤버를 호출한다
student.introduce()
// I am Alice, 20 years old, and I study at Engineering University.
}
컴패니언 객체
Kotlin에서 각 클래스는 컴패니언 객체(companion object)를 가질 수 있어요. 컴패니언 객체는 객체 선언의 한 종류로, 클래스 인스턴스를 만들지 않고도 클래스 이름으로 그 멤버에 접근할 수 있게 해줍니다.
클래스 인스턴스를 만들지 않고 호출할 수 있으면서도 그 클래스와 논리적으로 연결된 함수(예: 팩토리 함수)가 필요하다고 가정해 볼게요. 그럴 때는 클래스 안의 컴패니언 객체 선언 안에 그 함수를 선언하면 됩니다.
// name 속성을 선언하는 기본 생성자를 가진 클래스
class Person(
val name: String
) {
// 컴패니언 객체를 가진 클래스 본문
companion object {
fun createAnonymous() = Person("Anonymous")
}
}
fun main() {
// 클래스 인스턴스를 만들지 않고 함수를 호출한다
val anonymous = Person.createAnonymous()
println(anonymous.name)
// Anonymous
}
클래스 안에 컴패니언 객체를 선언하면, 클래스 이름만으로 그 멤버에 접근할 수 있어요. 자세한 내용은 Companion objects를 참고하세요.