Kotlin 클래스와 객체 | 클래스, 상속, 인터페이스

Kotlin 클래스와 객체 | 클래스, 상속, 인터페이스

이 글의 핵심

Kotlin 클래스와 객체에 대한 실전 가이드입니다. 클래스, 상속, 인터페이스 등을 예제와 함께 상세히 설명합니다.

들어가며

클래스는 객체의 설계도에 해당하고, data class·sealed class 등으로 용도에 맞는 뼈대를 짧게 쓸 수 있습니다. 생성자·프로퍼티 문법이 Java보다 단순한 편입니다.


1. 클래스 기본

클래스 정의

class Person {
    var name: String = ""
    var age: Int = 0
    
    fun introduce() {
        println("안녕하세요, $name입니다. ${age}세입니다.")
    }
}

val person = Person()
person.name = "홍길동"
person.age = 25
person.introduce()

주 생성자

class Person(val name: String, var age: Int) {
    fun introduce() {
        println("안녕하세요, $name입니다. ${age}세입니다.")
    }
}

val person = Person("홍길동", 25)

init 블록

class Person(val name: String, var age: Int) {
    init {
        println("Person 객체 생성: $name")
        require(age >= 0) { "나이는 0 이상이어야 합니다" }
    }
}

부 생성자

class Person(val name: String) {
    var age: Int = 0
    
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

val person1 = Person("홍길동")
val person2 = Person("김철수", 30)

2. 프로퍼티

getter/setter

class Person(val name: String) {
    var age: Int = 0
        get() = field
        set(value) {
            if (value >= 0) {
                field = value
            }
        }
    
    val isAdult: Boolean
        get() = age >= 18
}

지연 초기화

class MyClass {
    lateinit var name: String
    
    fun init() {
        name = "홍길동"
    }
    
    fun isInitialized() = ::name.isInitialized
}

3. 상속

open 클래스

Kotlin 클래스는 기본적으로 final이므로 상속하려면 open 키워드가 필요합니다:

// open: 상속 가능한 클래스
// Kotlin은 기본적으로 모든 클래스가 final (상속 불가)
// 상속을 허용하려면 명시적으로 open 선언
open class Animal(val name: String) {
    // open: 오버라이딩 가능한 메서드
    // 메서드도 기본적으로 final
    // 자식 클래스에서 재정의하려면 open 필요
    open fun makeSound() {
        println("동물 소리")
    }
    
    // open이 없는 메서드는 final (오버라이딩 불가)
    fun sleep() {
        println("$name이(가) 잠을 잡니다.")
    }
}

// Dog 클래스: Animal을 상속
class Dog(name: String) : Animal(name) {
    // : Animal(name) : 부모 생성자 호출
    // name을 부모 클래스에 전달
    
    // override: 부모 메서드 재정의
    override fun makeSound() {
        println("멍멍!")
    }
    
    // Dog만의 메서드
    fun fetch() {
        println("$name이(가) 공을 가져옵니다.")
    }
}

// Cat 클래스: Animal을 상속
class Cat(name: String) : Animal(name) {
    override fun makeSound() {
        println("야옹!")
    }
    
    fun scratch() {
        println("$name이(가) 할퀴기를 합니다.")
    }
}

// 사용 예제
fun main() {
    val dog = Dog("바둑이")
    dog.makeSound()  // 멍멍! (오버라이딩)
    dog.sleep()      // 바둑이이(가) 잠을 잡니다. (상속)
    dog.fetch()      // 바둑이이(가) 공을 가져옵니다. (Dog 고유)
    
    val cat = Cat("나비")
    cat.makeSound()  // 야옹! (오버라이딩)
    cat.sleep()      // 나비이(가) 잠을 잡니다. (상속)
    cat.scratch()    // 나비이(가) 할퀴기를 합니다. (Cat 고유)
    
    // 다형성
    val animals: List<Animal> = listOf(
        Dog("멍멍이"),
        Cat("야옹이")
    )
    
    for (animal in animals) {
        animal.makeSound()  // 각 객체의 실제 타입에 따라 호출
        // 멍멍!
        // 야옹!
    }
}

open vs final 비교:

// ❌ final 클래스 (기본)
class FinalClass {
    fun method() {}
}

// class SubClass : FinalClass()  // 컴파일 에러!
// This type is final, so it cannot be inherited from

// ✅ open 클래스
open class OpenClass {
    open fun method() {}
}

class SubClass : OpenClass() {
    override fun method() {}  // OK
}

왜 기본이 final인가:

  • 안전성: 의도하지 않은 상속 방지
  • 성능: final 메서드는 최적화 가능
  • 명확성: 상속 가능 여부가 명시적

abstract 클래스

abstract class Shape {
    abstract fun area(): Double
    abstract fun perimeter(): Double
    
    fun describe() {
        println("넓이: ${area()}, 둘레: ${perimeter()}")
    }
}

class Circle(val radius: Double) : Shape() {
    override fun area() = Math.PI * radius * radius
    override fun perimeter() = 2 * Math.PI * radius
}

class Rectangle(val width: Double, val height: Double) : Shape() {
    override fun area() = width * height
    override fun perimeter() = 2 * (width + height)
}

4. 인터페이스

기본 인터페이스

interface Drawable {
    fun draw()
    fun erase() {
        println("지우기")  // 기본 구현
    }
}

class Circle : Drawable {
    override fun draw() {
        println("원 그리기")
    }
}

다중 인터페이스

interface Clickable {
    fun click()
}

interface Focusable {
    fun focus()
}

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

5. 데이터 클래스

기본 사용

data class User(
    val name: String,
    val age: Int,
    val email: String
)

val user1 = User("홍길동", 25, "[email protected]")
val user2 = User("홍길동", 25, "[email protected]")

println(user1 == user2)  // true (equals 자동 생성)
println(user1)  // User(name=홍길동, age=25, [email protected])

copy 메서드

val user1 = User("홍길동", 25, "[email protected]")
val user2 = user1.copy(age = 26)

println(user1)  // age=25
println(user2)  // age=26

구조 분해

val user = User("홍길동", 25, "[email protected]")
val (name, age, email) = user

println("이름: $name, 나이: $age")

6. Sealed 클래스

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
    object Loading : Result()
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("성공: ${result.data}")
        is Result.Error -> println("에러: ${result.message}")
        is Result.Loading -> println("로딩 중...")
    }
}

7. Object 선언

싱글톤

object Database {
    private var connection: String? = null
    
    fun connect() {
        connection = "Connected"
        println("데이터베이스 연결됨")
    }
    
    fun disconnect() {
        connection = null
        println("데이터베이스 연결 해제됨")
    }
}

Database.connect()

Companion Object

class User(val name: String) {
    companion object {
        const val MAX_AGE = 150
        
        fun create(name: String): User {
            return User(name)
        }
    }
}

val user = User.create("홍길동")
println(User.MAX_AGE)

8. 실전 예제

예제: 쇼핑 시스템

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

data class CartItem(
    val product: Product,
    var quantity: Int
)

class ShoppingCart {
    private val items = mutableListOf<CartItem>()
    
    fun addItem(product: Product, quantity: Int = 1) {
        val existing = items.find { it.product.id == product.id }
        if (existing != null) {
            existing.quantity += quantity
        } else {
            items.add(CartItem(product, quantity))
        }
    }
    
    fun removeItem(productId: String) {
        items.removeIf { it.product.id == productId }
    }
    
    fun getTotalPrice(): Int {
        return items.sumOf { it.product.price * it.quantity }
    }
    
    fun printCart() {
        println("=== 장바구니 ===")
        items.forEach {
            println("${it.product.name} x${it.quantity} = ${it.product.price * it.quantity}원")
        }
        println("총액: ${getTotalPrice()}원")
    }
}

fun main() {
    val cart = ShoppingCart()
    
    cart.addItem(Product("P001", "노트북", 1000000))
    cart.addItem(Product("P002", "마우스", 30000), 2)
    
    cart.printCart()
}

정리

핵심 요약

  1. 클래스: 주 생성자, init 블록
  2. 상속: open, override
  3. 인터페이스: 다중 구현 가능
  4. 데이터 클래스: equals, hashCode, copy 자동
  5. Sealed 클래스: 제한된 상속
  6. Object: 싱글톤, Companion Object

다음 단계

  • Kotlin 컬렉션
  • Kotlin 코루틴
  • Kotlin Android 개발

관련 글

  • Java 클래스와 객체 | OOP, 상속, 인터페이스
  • JavaScript 클래스 | ES6 Class 문법 완벽 정리
  • Python 클래스 | 객체지향 프로그래밍(OOP) 완벽 정리
  • C++ 클래스와 객체 |
  • C++ 상속과 다형성 |