Skip to content
ioob.dev
Go back

Kotlin 입문 4편 — 클래스와 객체

· 3분 읽기

Table of contents

클래스 선언

Kotlin의 클래스 선언은 Java에 비해 훨씬 간결하다. 생성자를 클래스 헤더에 바로 쓸 수 있기 때문이다.

class User(val name: String, var age: Int)

이 한 줄이 Java의 다음 코드와 같은 역할을 한다.

public class User {
    private final String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

val로 선언하면 getter만, var로 선언하면 getter와 setter가 자동 생성된다. 프로퍼티라고 부르는데, Java의 필드 + getter/setter를 합친 개념이다.

val user = User("Kim", 25)
println(user.name)   // getter 호출
user.age = 26        // setter 호출

data class

실무에서 가장 많이 쓰게 될 기능 중 하나다. DTO나 값 객체를 만들 때 Java에서는 equals(), hashCode(), toString(), copy() 같은 보일러플레이트를 직접 작성하거나 Lombok을 써야 했다. Kotlin은 data 한 단어로 해결된다.

data class Product(
    val id: Long,
    val name: String,
    val price: Int
)

이렇게 선언하면 다음이 자동으로 생성된다.

val p1 = Product(1, "키보드", 50000)
val p2 = Product(1, "키보드", 50000)

println(p1)           // Product(id=1, name=키보드, price=50000)
println(p1 == p2)     // true (내용 비교)
println(p1.hashCode() == p2.hashCode())  // true

// copy — 일부 필드만 바꿔서 복사
val p3 = p1.copy(price = 45000)
println(p3)           // Product(id=1, name=키보드, price=45000)

// 구조 분해
val (id, name, price) = p1
println("$name: ${price}원")  // 키보드: 50000원

copy()는 불변 객체를 다룰 때 특히 유용하다. 객체 전체를 새로 만들지 않고 바꾸고 싶은 필드만 지정할 수 있기 때문이다.

object — 싱글톤

Java에서 싱글톤을 만들려면 private 생성자, static instance, 스레드 안전성까지 신경 써야 한다. Kotlin은 object를 쓰면 된다.

object Database {
    val url = "jdbc:mysql://localhost:3306/mydb"

    fun connect() {
        println("Connected to $url")
    }
}

fun main() {
    Database.connect()  // 인스턴스 생성 없이 바로 사용
}

object로 선언하면 언어 차원에서 싱글톤이 보장된다. 별도의 인스턴스 생성 없이 바로 접근할 수 있다.

companion object

Java의 static 메서드에 해당하는 기능이다. Kotlin에는 static 키워드가 없는 대신 companion object를 쓴다.

class User(val name: String) {
    companion object {
        fun create(name: String): User {
            println("Creating user: $name")
            return User(name)
        }
    }
}

fun main() {
    val user = User.create("Kim")  // 클래스명으로 직접 호출
}

팩토리 메서드 패턴을 구현할 때 자주 쓰인다.

상속

Kotlin의 클래스는 기본적으로 final이다. 상속을 허용하려면 open을 명시해야 한다. 이건 의도적인 설계 결정인데, “Effective Java”에서 “상속을 위해 설계하지 않은 클래스는 상속을 금지하라”고 권장하는 것과 같은 맥락이다.

open class Animal(val name: String) {
    open fun sound(): String = "..."
}

class Dog(name: String) : Animal(name) {
    override fun sound(): String = "멍멍"
}

class Cat(name: String) : Animal(name) {
    override fun sound(): String = "야옹"
}

fun main() {
    val animals = listOf(Dog("바둑이"), Cat("나비"))
    for (animal in animals) {
        println("${animal.name}: ${animal.sound()}")
    }
}

상속할 클래스와 오버라이드할 메서드 모두 open이 필요하다. override는 Java의 @Override와 같지만, Kotlin에서는 필수 키워드다.

인터페이스

Kotlin의 인터페이스는 Java 8 이후의 인터페이스와 비슷하다. 추상 메서드와 기본 구현을 모두 가질 수 있다.

interface Drawable {
    fun draw()  // 추상 메서드

    fun description(): String {  // 기본 구현
        return "도형"
    }
}

class Circle(val radius: Double) : Drawable {
    override fun draw() {
        println("반지름 ${radius}인 원을 그린다")
    }
}

클래스는 하나만 상속 가능하지만, 인터페이스는 여러 개를 구현할 수 있다. 이 원칙은 Java와 동일하다.

interface Clickable {
    fun click()
}

interface Focusable {
    fun focus()
}

class Button : Clickable, Focusable {
    override fun click() = println("클릭!")
    override fun focus() = println("포커스!")
}

다음 편에서는 Kotlin의 컬렉션과 람다를 다룬다. filter, map, forEach 같은 함수형 API와 scope function(let, apply, run)을 살펴보자.

5편: 컬렉션과 람다


Share this post on:

Comments

Loading comments...


Previous Post
Kotlin 입문 5편 — 컬렉션과 람다
Next Post
Kotlin 입문 3편 — 함수