본문 바로가기
Android/Kotlin

[Kotlin] Kotlin에서 class와 object의 차이점 및 활용 방안: 싱글톤 패턴과 유연성 비교

by quessr 2024. 5. 28.

 

Kotlin에서는 class와 object 키워드를 사용하여 클래스를 정의할 수 있으며, 이 두 방식은 중요한 차이점을 가지고 있습니다. 특히, object 키워드는 싱글톤 패턴(Singleton Pattern)을 구현하기 위해 사용됩니다. 싱글톤 패턴을 적용하면 애플리케이션 전체에서 단 하나의 인스턴스만 존재하게 되며, 이는 여러 면에서 유용합니다.

 

싱글톤 패턴과 object

object로 선언된 클래스는 애플리케이션 전체에서 오직 하나의 인스턴스만을 가집니다. 이는 특히 상태를 공유하지 않는 유틸리티 메서드들이나 전역적으로 접근해야 하는 자원을 관리할 때 유용합니다. 예를 들어, 네트워크 호출과 같은 작업을 수행하는 NetworkUtils 클래스가 있다고 가정해봅시다. 이 클래스를 object로 선언함으로써 메모리 낭비를 방지하고, 애플리케이션 전역에서 동일한 인스턴스를 통해 메서드를 일관되게 사용할 수 있게 됩니다. 또한, object로 선언된 클래스의 메서드는 클래스 이름을 통해 바로 접근할 수 있어서, 매우 편리합니다. 예를 들어, NetworkUtils.enqueueCall(...)과 같은 방식으로 사용할 수 있습니다.

일반 class와의 비교

반면, class 키워드로 정의된 클래스는 아래와 같이, 사용할 때마다 인스턴스를 생성해야 합니다.

class NetworkUtils {
    fun <T> enqueueCall(call: Call<T>, onSuccess: (Response<T>) -> Unit, onFailure: (Throwable) -> Unit) {
        // 함수 내용
    }
}

// 사용 예시
val networkUtils = NetworkUtils()
networkUtils.enqueueCall(call, onSuccess, onFailure)

 

이는 object와 비교했을 때 더 많은 유연성을 제공하지만, 각 인스턴스가 메모리를 차지하게 되므로 유틸리티 메서드와 같이 상태를 가지지 않는 경우에는 비효율적일 수 있습니다. 그러나, class 내에 companion object를 정의함으로써, 인스턴스 생성 없이 클래스 이름을 통해 메서드에 접근할 수 있는 방법을 제공합니다.

사용예시:

class NetworkUtils {
    fun <T> enqueueCall(call: Call<T>, onSuccess: (Response<T>) -> Unit, onFailure: (Throwable) -> Unit) {
        // 함수 내용
    }
}

// 사용 예시
val networkUtils = NetworkUtils()
networkUtils.enqueueCall(call, onSuccess, onFailure)

 

companion object 내에 정의된 메서드는 정적 메서드처럼 작동하여, NetworkUtils.enqueueCall(...)과 같은 방식으로 직접 호출할 수 있습니다. 이는 object 키워드의 싱글톤 패턴과 유사한 효과를 제공하지만, 필요에 따라 인스턴스를 여러 개 생성할 수 있는 유연성도 함께 제공합니다.

 

정리

object 키워드를 사용하는 싱글톤 패턴은 상태를 공유하지 않는 유틸리티 메서드나 전역 자원을 관리할 이점을 제공합니다. 메모리 낭비를 방지하고, 코드를 간결하게 유지할 있으며, 전역적으로 일관된 접근이 가능해집니다. 반면, class 많은 유연성을 제공하며, companion object 사용하여 정적 메서드처럼 접근할 있는 방법도 있습니다

 

그런데 이쯤되니 갑자기 궁금해 지는 점이 있었습니다.

그렇다면 class에서도 companion object를 통해 정적 메서드처럼 사용할 수 있는데 굳이 object를 사용하는 이유는 뭘까?!?

object의 장점

  1. 단순성:
    • object 키워드를 사용하면 클래스 자체가 싱글톤 인스턴스가 되므로 별도의 인스턴스 생성이나 companion object 선언이 필요 없습니다. 코드가 단순하고 명확해집니다.
    • 예 :
object NetworkUtils {
    fun <T> enqueueCall(call: Call<T>, onSuccess: (Response<T>) -> Unit, onFailure: (Throwable) -> Unit) {
        // 함수 내용
    }
}

// 사용 예시
NetworkUtils.enqueueCall(call, onSuccess, onFailure)

 

 

2.명확한 싱글톤 보장:

  • object로 선언된 클래스는 애플리케이션 전체에서 단 하나의 인스턴스만 존재합니다. 이는 싱글톤 패턴을 명확하게 보장하여 실수로 여러 인스턴스를 생성하는 것을 방지합니다.
  • 반면 companion object 클래스의 정적 메서드처럼 작동하지만, 해당 클래스 자체는 여전히 여러 인스턴스를 가질 있습니다.

3. 초기화의 편리함:

  • object 처음 접근될 자동으로 초기화됩니다. 별도의 인스턴스 생성이나 초기화 로직이 필요 없습니다.
  • 예 :
object DatabaseConfig {
    val url = "jdbc:mysql://localhost:3306/mydb"
    val username = "user"
    val password = "password"
}

// 사용 예시
println(DatabaseConfig.url)

 

class + companion object의 장점

 

1. 유연성:

  • class 사용하면 여러 인스턴스를 생성할 있어 객체 지향 프로그래밍의 유연성을 가질 있습니다. 필요할 경우 companion object 통해 정적 메서드처럼 사용할 있습니다.
  • 예 :
class NetworkUtils {
    companion object {
        fun <T> enqueueCall(call: Call<T>, onSuccess: (Response<T>) -> Unit, onFailure: (Throwable) -> Unit) {
            // 함수 내용
        }
    }
}

// 사용 예시
NetworkUtils.enqueueCall(call, onSuccess, onFailure)

 

2. 상속 및 다형성:

  • class 상속이 가능하지만, object 상속이 불가능합니다. 만약 클래스를 상속받아 확장해야 필요가 있다면 class 사용해야 합니다.
  • 예 :
open class BaseUtils {
    open fun log(message: String) {
        println(message)
    }
}

class NetworkUtils : BaseUtils() {
    companion object {
        fun <T> enqueueCall(call: Call<T>, onSuccess: (Response<T>) -> Unit, onFailure: (Throwable) -> Unit) {
            // 함수 내용
        }
    }
}

 

정리

object 단순하고 명확하게 싱글톤 패턴을 구현할 있는 장점이 있으며, 초기화가 자동으로 이루어져 편리합니다. 반면에 class + companion object 많은 유연성과 상속 가능성을 제공합니다. 따라서, 유틸리티 클래스나 상태를 가지지 않는 전역적으로 사용되는 객체는 object 선언하는 것이 적합하고, 상속이나 여러 인스턴스가 필요한 경우는 class 사용하는 것이 좋습니다.