2025년 1월 2일 작성
Kotlin Object와 Companion Object
object는 singleton을 선언하고, companion object는 class 내부에서 Java의 static member를 대체하며, object expression은 익명 객체를 생성합니다.
Object : 진정한 Singleton 객체
object는 class 선언과 instance 생성을 동시에 수행합니다.- 일반 class는
classkeyword로 선언하고, 별도로 instance를 생성합니다. object는 선언과 동시에 단 하나의 instance가 만들어집니다.- 해당 instance는 program 전체에서 공유됩니다.
- 일반 class는
// 일반 class : 선언과 생성이 분리됨
class RegularClass {
fun doSomething() = println("Regular")
}
val instance = RegularClass() // instance 생성 필요
// object : 선언과 동시에 instance 생성
object SingletonObject {
fun doSomething() = println("Singleton")
}
SingletonObject.doSomething() // 바로 사용 가능
- Kotlin에는
statickeyword가 없습니다.- Java의
staticmember는 class에 속하지만, instance와 무관하게 존재합니다. - Kotlin은
static대신object와companion object로 이 역할을 대체합니다. object는 진정한 singleton 객체이므로, 상속과 interface 구현이 가능합니다.
- Java의
Object를 사용하는 이유
- singleton pattern을 언어 차원에서 지원합니다.
- Java에서 thread-safe singleton을 구현하려면 복잡한 boilerplate가 필요합니다.
- Kotlin
object는 자동으로 thread-safe하고 lazy initialization됩니다.
// Java : thread-safe singleton 구현
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
// Kotlin : 한 줄로 동일한 기능
object Singleton
- 전역적으로 하나만 존재해야 하는 것을 표현합니다.
- 설정(configuration), logger, cache, database connection pool 등에 적합합니다.
- application 전체에서 동일한 상태를 공유해야 할 때 사용합니다.
Object Keyword의 세 가지 용도
objectkeyword는 Kotlin에서 세 가지 형태로 사용됩니다.- Object Declaration : singleton instance를 선언합니다.
- Companion Object : class 내부에 static-like member를 정의합니다.
- Object Expression : 익명 객체를 생성합니다.
flowchart TD
object_keyword[object Keyword]
object_keyword --> declaration[Object Declaration]
object_keyword --> companion[Companion Object]
object_keyword --> expression[Object Expression]
declaration --> singleton[Singleton Pattern]
companion --> static_like[Static Member 대체]
companion --> factory[Factory Method]
expression --> anonymous[익명 객체]
Object Declaration
- object declaration은 singleton pattern을 간결하게 구현합니다.
- class 선언과 단일 instance 생성을 동시에 처리합니다.
- 처음 접근할 때 초기화(lazy initialization)됩니다.
- thread-safe합니다.
object DatabaseConfig {
val url = "jdbc:mysql://localhost:3306/db"
val maxConnections = 10
fun connect() {
println("Connecting to $url")
}
}
// 사용 : class 이름으로 직접 접근
val url = DatabaseConfig.url
DatabaseConfig.connect()
Property와 Method
- object는 일반 class처럼 property와 method를 가질 수 있습니다.
initblock으로 초기화 logic을 실행할 수 있습니다.- 다른 class를 상속하거나 interface를 구현할 수 있습니다.
object Logger {
private val logs = mutableListOf<String>()
init {
println("Logger initialized")
}
fun log(message: String) {
logs.add("[${System.currentTimeMillis()}] $message")
}
fun printLogs() {
logs.forEach { println(it) }
}
}
Interface 구현
- object가 interface를 구현할 수 있습니다.
- singleton으로 동작하는 구현체를 정의합니다.
interface Serializer {
fun serialize(data: Any): String
fun deserialize(json: String): Any
}
object JsonSerializer : Serializer {
override fun serialize(data: Any): String {
return """{"data": "$data"}"""
}
override fun deserialize(json: String): Any {
return json
}
}
val json = JsonSerializer.serialize("Hello")
Class 상속
- object가 class를 상속할 수 있습니다.
open class Vehicle(val name: String) {
open fun start() = println("$name starting")
}
object ServiceCar : Vehicle("Service Car") {
override fun start() {
println("Service car is ready")
super.start()
}
}
ServiceCar.start()
// Service car is ready
// Service Car starting
초기화 시점
- object는 처음 접근할 때 초기화됩니다.
- application 시작 시점이 아닌 실제 사용 시점에 초기화됩니다.
- lazy initialization이 자동으로 적용됩니다.
- 무거운 초기화 작업이 있어도 사용하지 않으면 비용이 발생하지 않습니다.
- application 시작 시점이 아닌 실제 사용 시점에 초기화됩니다.
object HeavyResource {
init {
println("HeavyResource initialized")
// 무거운 초기화 작업
}
val data = "Important data"
}
fun main() {
println("Before accessing HeavyResource")
println(HeavyResource.data) // 이 시점에 초기화
println("After accessing HeavyResource")
}
// 출력:
// Before accessing HeavyResource
// HeavyResource initialized
// Important data
// After accessing HeavyResource
Companion Object
- companion object는 class 내부에 정의된 singleton입니다.
- Java의
staticmember를 대체합니다. - class 이름으로 직접 접근할 수 있습니다.
- class당 하나의 companion object만 가질 수 있습니다.
- Java의
class User(val name: String) {
companion object {
const val MIN_AGE = 0
const val MAX_AGE = 150
fun create(name: String): User {
return User(name.trim())
}
}
}
// class 이름으로 접근
val minAge = User.MIN_AGE
val user = User.create(" Kim ")
Factory Method Pattern
- companion object로 factory method를 구현합니다.
- private constructor와 함께 사용하여 객체 생성을 제어합니다.
- 다양한 생성 방식을 명확한 이름으로 제공합니다.
class User private constructor(
val id: Long,
val name: String,
val email: String
) {
companion object {
private var nextId = 1L
fun fromName(name: String): User {
return User(nextId++, name, "")
}
fun fromEmail(email: String): User {
val name = email.substringBefore("@")
return User(nextId++, name, email)
}
fun fromJson(json: String): User {
// JSON parsing logic
return User(nextId++, "parsed", "parsed@example.com")
}
}
}
val user1 = User.fromName("Kim")
val user2 = User.fromEmail("lee@example.com")
val user3 = User.fromJson("""{"name": "Park"}""")
Companion Object 이름
- companion object에 이름을 지정할 수 있습니다.
- 이름을 지정하지 않으면
Companion이 기본 이름입니다. - Java에서 접근할 때 이름이 사용됩니다.
- 이름을 지정하지 않으면
class User(val name: String) {
companion object Factory {
fun create(name: String) = User(name)
}
}
// 이름 없이 접근 (일반적인 방식)
val user1 = User.create("Kim")
// 이름으로 접근
val user2 = User.Factory.create("Lee")
// 기본 이름 Companion
class Product(val name: String) {
companion object {
fun create(name: String) = Product(name)
}
}
val product = Product.Companion.create("Phone")
Interface 구현
- companion object가 interface를 구현할 수 있습니다.
- class 자체를 interface type으로 전달할 수 있습니다.
interface Factory<T> {
fun create(): T
}
class User(val name: String) {
companion object : Factory<User> {
override fun create(): User {
return User("Default")
}
}
}
fun <T> createInstance(factory: Factory<T>): T {
return factory.create()
}
// User의 companion object가 Factory<User>를 구현
val user = createInstance(User)
Extension Function
- companion object에 extension function을 추가할 수 있습니다.
- 외부에서 companion object의 기능을 확장합니다.
class User(val name: String) {
companion object
}
// companion object에 extension function 추가
fun User.Companion.fromCsv(csv: String): User {
val name = csv.split(",").first()
return User(name)
}
val user = User.fromCsv("Kim,25,Seoul")
Object Expression
- object expression은 익명 객체를 생성합니다.
- Java의 anonymous inner class를 대체합니다.
- interface나 abstract class의 일회성 구현에 사용합니다.
interface ClickListener {
fun onClick()
}
fun setClickListener(listener: ClickListener) {
listener.onClick()
}
// object expression으로 익명 객체 생성
setClickListener(object : ClickListener {
override fun onClick() {
println("Clicked!")
}
})
여러 Interface 구현
- 하나의 익명 객체가 여러 interface를 구현할 수 있습니다.
interface A {
fun methodA()
}
interface B {
fun methodB()
}
val obj = object : A, B {
override fun methodA() = println("A")
override fun methodB() = println("B")
}
obj.methodA()
obj.methodB()
Class 상속과 Interface 구현
- class 상속과 interface 구현을 동시에 할 수 있습니다.
open class Base(val x: Int) {
open fun print() = println("Base: $x")
}
interface Printable {
fun printFormatted()
}
val obj = object : Base(10), Printable {
override fun print() = println("Overridden: $x")
override fun printFormatted() = println("Formatted: [$x]")
}
Type 없는 Object Expression
- type 지정 없이 익명 객체를 생성할 수 있습니다.
- local 또는 private 범위에서만 사용 가능합니다.
- property에 직접 접근할 수 있습니다.
fun main() {
val point = object {
val x = 10
val y = 20
}
println("x: ${point.x}, y: ${point.y}")
}
class Example {
// private이므로 내부에서 property 접근 가능
private val data = object {
val name = "Secret"
val value = 42
}
fun printData() {
println("${data.name}: ${data.value}")
}
}
외부 변수 접근
- object expression은 외부 scope의 변수에 접근할 수 있습니다.
- Java anonymous class는
final변수만 접근할 수 있습니다. - Kotlin은
final여부와 관계없이 접근할 수 있고, 값을 수정할 수도 있습니다.
- Java anonymous class는
fun countClicks(button: Button) {
var clickCount = 0 // var : 변경 가능한 변수
button.setOnClickListener(object : ClickListener {
override fun onClick() {
clickCount++ // 외부 변수 수정 가능 (Java에서는 불가능)
println("Clicked $clickCount times")
}
})
}
Object Declaration vs Object Expression
- object declaration과 object expression은 다른 특성을 가집니다.
| 특성 | Object Declaration | Object Expression |
|---|---|---|
| 초기화 시점 | 처음 접근 시 (lazy) | 선언 즉시 |
| instance 수 | singleton (하나) | 매번 새 instance |
| 이름 | 있음 | 없음 (익명) |
| 위치 | top-level 또는 class 내부 | expression이 필요한 곳 |
// Object Declaration : singleton
object Singleton {
val id = System.currentTimeMillis()
}
println(Singleton.id) // 항상 같은 값
println(Singleton.id) // 항상 같은 값
// Object Expression : 매번 새 instance
fun createObject() = object {
val id = System.currentTimeMillis()
}
println(createObject().id) // 매번 다른 값
println(createObject().id) // 매번 다른 값
Java와의 비교
- Kotlin object는 Java의 여러 pattern을 대체합니다.
Static Member 비교
- Kotlin은
statickeyword가 없습니다.- companion object로 static-like 동작을 구현합니다.
// Java : static member
public class User {
private static int count = 0;
private String name;
public User(String name) {
this.name = name;
count++;
}
public static int getCount() {
return count;
}
public static User create(String name) {
return new User(name);
}
}
// Kotlin : companion object
class User(val name: String) {
companion object {
private var count = 0
fun getCount() = count
fun create(name: String) = User(name)
}
init {
count++
}
}
Singleton Pattern 비교
- Kotlin object declaration은 Java의 singleton boilerplate를 제거합니다.
// Java : thread-safe singleton
public class Singleton {
private static volatile Singleton instance;
private String data;
private Singleton() {
data = "initialized";
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public String getData() {
return data;
}
}
// Kotlin : 한 줄로 thread-safe singleton
object Singleton {
val data = "initialized"
}
Anonymous Class 비교
- Kotlin object expression은 Java anonymous class보다 유연합니다.
// Java : anonymous inner class
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// final 변수만 접근 가능
System.out.println("Clicked");
}
});
// Kotlin : object expression
button.setOnClickListener(object : OnClickListener {
override fun onClick(v: View) {
// final 여부와 관계없이 외부 변수 접근 및 수정 가능
println("Clicked")
}
})
// SAM 변환으로 더 간결하게 (Java interface인 경우)
button.setOnClickListener { println("Clicked") }
Java에서 Kotlin Object 접근
- Java code에서 Kotlin object에 접근하는 방법입니다.
// Kotlin
object Utils {
fun doSomething() = println("Done")
}
class User(val name: String) {
companion object {
fun create(name: String) = User(name)
}
}
// Java에서 접근
Utils.INSTANCE.doSomething();
User user = User.Companion.create("Kim");
@JvmStatic으로 Java에서 더 자연스럽게 접근할 수 있습니다.
object Utils {
@JvmStatic
fun doSomething() = println("Done")
}
class User(val name: String) {
companion object {
@JvmStatic
fun create(name: String) = User(name)
}
}
// Java에서 접근 (@JvmStatic 사용 시)
Utils.doSomething(); // INSTANCE 없이 접근
User user = User.create("Kim"); // Companion 없이 접근
Reference
- https://kotlinlang.org/docs/object-declarations.html
- https://kotlinlang.org/docs/object-declarations.html#companion-objects