객체

객체 (Objects)

이 장에서는 객체 선언을 살펴보면서 클래스에 대한 이해를 더 넓혀 볼게요. 이 지식은 프로젝트 전반에서 동작을 효율적으로 관리하는 데 도움이 돼요.

출처: Kotlin 공식문서

본문

객체 선언 (Object declarations)

Kotlin에서는 **객체 선언(object declaration)**을 사용해 인스턴스가 하나뿐인 클래스를 선언할 수 있어요. 어떤 의미에서, 클래스를 선언하면서 동시에 그 단일 인스턴스도 만드는 거예요. 객체 선언은 프로그램의 단일 참조 지점으로 쓰거나, 시스템 전반의 동작을 조율하기 위한 클래스를 만들고 싶을 때 유용해요.

TIP: 쉽게 접근할 수 있는 인스턴스가 하나뿐인 클래스를 **싱글턴(singleton)**이라고 불러요.

Kotlin의 객체는 lazy해요. 즉 접근할 때에만 생성돼요. Kotlin은 또한 모든 객체가 스레드 안전(thread-safe)한 방식으로 생성되도록 보장해서, 이걸 직접 확인하지 않아도 돼요.

객체 선언을 만들려면 object 키워드를 써요:

object DoAuth {}

object 이름 뒤에, 중괄호 {}로 정의된 객체 본문 안에서 프로퍼티나 멤버 함수를 추가하면 돼요.

NOTE: 객체는 생성자를 가질 수 없어서, 클래스 같은 헤더가 없어요.

예를 들어 인증을 담당하는 DoAuth라는 객체를 만들고 싶다고 해 볼게요:

object DoAuth {
    fun takeParams(username: String, password: String) {
        println("input Auth parameters = $username:$password")
    }
}

fun main(){
    // The object is created when the takeParams() function is called
    DoAuth.takeParams("coding_ninja", "N1njaC0ding!")
    // input Auth parameters = coding_ninja:N1njaC0ding!
}

이 객체는 usernamepassword 변수를 파라미터로 받아 콘솔에 문자열을 출력하는 takeParams 멤버 함수를 가져요. DoAuth 객체는 이 함수가 처음 호출될 때에만 생성돼요.

NOTE: 객체는 클래스와 인터페이스에서 상속받을 수 있어요. 예를 들어:

interface Auth {
    fun takeParams(username: String, password: String)
}

object DoAuth : Auth {
    override fun takeParams(username: String, password: String) {
        println("input Auth parameters = $username:$password")
    }
}

데이터 객체 (Data objects)

객체 선언의 내용을 쉽게 출력하려면 Kotlin의 **데이터 객체(data object)**를 쓰면 돼요. 초급 투어에서 배운 데이터 클래스와 비슷하게, 데이터 객체는 toString()equals()라는 추가 멤버 함수를 자동으로 가져와요.

TIP: 데이터 클래스와 달리 데이터 객체는 copy() 멤버 함수를 자동으로 제공하지 않아요. 복제할 수 있는 단일 인스턴스가 하나뿐이기 때문이에요.

데이터 객체를 만들려면 객체 선언과 같은 문법을 쓰되 앞에 data 키워드를 붙이면 돼요:

data object AppConfig {}

예를 들어:

data object AppConfig {
    var appName: String = "My Application"
    var version: String = "1.0.0"
}

fun main() {
    println(AppConfig)
    // AppConfig
    
    println(AppConfig.appName)
    // My Application
}

데이터 객체에 대한 자세한 내용은 데이터 객체(Data objects) 문서를 참고하세요.

동반 객체 (Companion objects)

Kotlin에서 클래스는 객체 하나, 즉 **동반 객체(companion object)**를 가질 수 있어요. 클래스당 동반 객체는 하나만 가질 수 있어요. 동반 객체는 해당 클래스가 처음 참조될 때에만 생성돼요.

동반 객체 안에 선언된 프로퍼티나 함수는 모든 클래스 인스턴스에서 공유돼요.

클래스 안에서 동반 객체를 만들려면 객체 선언과 같은 문법을 쓰되 앞에 companion 키워드를 붙이면 돼요:

companion object Bonger {}

NOTE: 동반 객체는 이름이 없어도 돼요. 이름을 정의하지 않으면 기본값은 Companion이에요.

동반 객체의 프로퍼티나 함수에 접근하려면 클래스 이름을 참조하면 돼요. 예를 들어:

class BigBen {
    companion object Bonger {
        fun getBongs(nTimes: Int) {
            repeat(nTimes) { print("BONG ") }
            }
        }
    }

fun main() {
    // Companion object is created when the class is referenced for the
    // first time.
    BigBen.getBongs(12)
    // BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG 
}

이 예시는 Bonger라는 동반 객체를 가진 BigBen 클래스를 만들어요. 동반 객체는 정수를 받아 그 정수만큼 "BONG"을 콘솔에 출력하는 getBongs() 멤버 함수를 가져요.

main() 함수에서 getBongs() 함수는 클래스 이름을 참조해 호출돼요. 이 시점에 동반 객체가 생성돼요. getBongs() 함수는 파라미터 12와 함께 호출돼요.

자세한 내용은 동반 객체(Companion objects) 문서를 참고하세요.

연습 (Practice)

연습 1

커피숍을 운영하면서 고객 주문을 추적하는 시스템이 있어요. 아래 코드를 보고 main() 함수의 다음 코드가 성공적으로 실행되도록 두 번째 데이터 객체의 선언을 완성해 보세요:

interface Order {
    val orderId: String
    val customerName: String
    val orderTotal: Double
}

data object OrderOne: Order {
    override val orderId = "001"
    override val customerName = "Alice"
    override val orderTotal = 15.50
}

data object // Write your code here

fun main() {
    // Print the name of each data object
    println("Order name: $OrderOne")
    // Order name: OrderOne
    println("Order name: $OrderTwo")
    // Order name: OrderTwo

    // Check if the orders are identical
    println("Are the two orders identical? ${OrderOne == OrderTwo}")
    // Are the two orders identical? false

    if (OrderOne == OrderTwo) {
        println("The orders are identical.")
    } else {
        println("The orders are unique.")
        // The orders are unique.
    }

    println("Do the orders have the same customer name? ${OrderOne.customerName == OrderTwo.customerName}")
    // Do the orders have the same customer name? false
}

연습 2

Vehicle 인터페이스에서 상속받는 객체 선언을 만들어 독특한 차량 유형 FlyingSkateboard를 만들어 보세요. main() 함수의 다음 코드가 성공적으로 실행되도록 객체에 name 프로퍼티와 move() 함수를 구현하세요:

interface Vehicle {
    val name: String
    fun move(): String
}

object // Write your code here

fun main() {
    println("${FlyingSkateboard.name}: ${FlyingSkateboard.move()}")
    // Flying Skateboard: Glides through the air with a hover engine
    println("${FlyingSkateboard.name}: ${FlyingSkateboard.fly()}")
    // Flying Skateboard: Woooooooo
}

연습 3

앱용 사용자 등록 모듈을 만들고 있어요. 이메일 검증을 User 클래스와 연관지어 두고 싶지만, 이메일 주소가 유효하지 않은데 불필요한 User 인스턴스를 만드는 건 피하고 싶어요.

이 연습에서는 이메일 주소가 @.을 모두 포함하면 유효하다고 간주해요. main() 함수의 다음 코드가 성공적으로 실행되도록 데이터 클래스를 완성하세요:

User 클래스의 동반 객체에 이메일 검증 함수를 추가해서, 그 함수를 User에서 직접 호출할 수 있게 해 보세요.

data class User(val name: String, val email: String) {
    // Write your code here
}

fun main() {
    val candidates = listOf(
        Pair("Alice", "[email protected]"),
        Pair("Bob", "bob2example-com")
    )

    for ((name, email) in candidates) {
        if (User.isValidEmail(email)) {
            val user = User(name, email)
            println("Registered: ${user.name}, ${user.email}")
            // Registered: Alice, [email protected]
        } else {
            println("Error: '${email}' is not valid. The email should contain '@' and '.'")
            // Error: 'bob2example-com' is not valid. The email should contain '@' and '.'
        }
    }
}

TIP: 이 연습의 확장으로, 동반 객체의 함수를 팩토리 메서드로 사용해 클래스의 인스턴스를 구성해 보세요. 예시와 이 패턴에 대한 자세한 내용은 동반 객체(Companion objects) 문서를 참고하세요.

더 알아보기