배경
Kotlin을 활용해 코딩하다 보면 자주 보게되는 Companion Object
, 과연 그 정체가 무엇인가에 대해 공부하고자 작성하게 되었다.
기본적으로 Companion Object
에 대해 정확하게 파악하기 위해선 Companion Object
의 Object
가 무엇인지, 기존에 사용하고 있는 Object Declaration
과 Obejct Expression
이 무엇인지에 대해 파악한 뒤 Companion Object
순으로 작성하고자 한다.
Kotlin에서 등장하는 Companion Object
와 Java등에서 등장하는 Static
과 어떤점이 같으며 차이점이 무엇인지에 대해 작성하고자 한다.
기초 개념
Object
정의
먼저 Object
란 무엇인가?
사전적 정의는 Object
(객체)란 인식 가능한 물체/물건을 의미하며, 객체들은 각자의 고유한 속성과 동작을 갖고 있다.
소프트웨어 관점
에서 객체란, 서로 연관있는 변수(속성: property)들을 묶어놓은 데이터 덩어리를 뜻한다.
- property는 선언과 동시에 초기화하는것이 원칙이다.
Class
Class
라는 문법을 이용하면, 모양이 같은 객체를 대량 생산할 수 있다. 즉 Class
는 객체를 찍어내기 위한 설계도이다.
Class안에 포함된 Property는 이 Class대로 객체를 만들면, 이렇게 생긴 Property가 들어갈 것이라고 알려주기 위한 모형 변수이다.
그래서 Class를 선언한다 해도, Class속의 property가 곧바로 생성되지는 않는다.
Class
를 선언하면, Class 이름
과 동일한 이름의 타입
이 만들어지며, Class 이름과 동일한 특수 함수
가 같이 선언된다.
Instance
Class로부터 생성된 객체를 특별히 인스턴스(Instance)
라고 부르기도 한다.
Instance
는 ’구체적인 것’이라는 뜻을 갖고있는데, Class라는 ‘틀’에서 ‘구체적으로’ 만들어지기 때문이다.
Singleton
Singleton
은 Instance
가 하나만 있는 클래스를 의미한다.
Object Expressions (표현식)
뒤에 등장하는 Object Declaration
과 달리, object
를 표현식으로 사용하면, Singleton의 형태로 사용하지 않는다.
Object Expressions의 사용
주로 다음과 같은 형태로 사용한다.
1. 익명 클래스
의 객체를 바로 생성하고자 할 때,
2. 추상 클래스, 인터페이스의 구현체를 익명 클래스의 객체로 구현하고자 할 때
// 1. 익명 클래스의 객체를 바로 생성하고자 할 때
val user = object {
val name = "hunseong"
val age = 24
}
println(user.name) // hunseong
println(user.age) // 24
// 2. 추상클래스, 인터페이스의 구현체를 익명 클래스의 객체로 구현하고자 할 때
val myListener: MyInterface = object : MyListener {
//implement interface
}
user.addListener(myListener)
user2.addListner(object : MyListener {
//implement interface
})
Kotlin
에서는 object
를 사용해 무명객체
를 사용할 수 있다.
object
를 클래스에 사용하는게 아닌, 객체에 사용할 경우 매번 새로운 Instance
를 생성하게 된다. 그리고 object
블럭내에서 로컬 변수 값에 접근할 수 있다.
var counter = 10
// OnClickListener의 무명 객체 생성, object객체가 OnClickListener를 구현
btn.setOnClickListener(object : OnClickListener {
override fun onClick(v: View?) {
counter++ // 로컬 변수에 접근 가능
}
})
로컬 변수 counter
에 무명객체
가 접근해서 값을 설정할 수 있다.
Object Declarations (객체 선언)
정의
Kotlin
에는 Java
에는 없는 특별한 Singleton
가 있다. 따라서, 여기선 class
대신 object
라는 키워드를 사용한다.
Kotlin
의 객체 선언방식을 통해 Singleton
패턴을 더욱 쉽게 사용할 수 있다. Object Declearation
을 통해 흔히 Java
에서 사용하는 무명내부클래스 (anonymous inner class)
처럼 사용할 수 있다.
즉, 클래스
를 정의하면서 객체를 생성하는 키워드
이다.
특징
1번만 객체가 생성되게 하므로, 메모리 낭비를 방지할 수 있고, 전역변수이므로 공유도 용이하다.
소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. (위키백과)
이렇게 선언된 객체
는 프로그램 전체에서 공유할 수 있는 1개뿐인 객체이다.
따라서 위와 같이 선언된 객체
는
DataProviderManger
라는 식별자로 바로 접근 가능하며,Thread-safe
하다.
→ 멀티 쓰레드 환경에서 일반적으로 어떤 함수나, 변수, 혹은 객체가 여러 쓰레드로부터 동시에 접근이 이루어져도 프로그램 실행에 문제가 없는 것- 선언되자마자 초기화되지 않고,
처음으로 접근될 때 초기화
된다.(lazy-initializaion)
→ E.g.DataProviderManager.registerDataProvider( … )
로 사용할 수 있다.
사용방법
Object Declaration
은 object
키워드를 통해서만 가능하다.
불가능한 점
Object
키워드가 선언된 클래스는 주/부 생성자를 사용할 수 없다. 객체 생성과 동시에 생성자 호출 없이 바로 만들어지기 때문이다.
가능한 점
1. 중첩 object
선언이 가능하며,
2. 클래스
나 인터페이스
를 상속할 수 있다.
3. object
내에 class
와 같이 멤버/메소드
를 가질 수 있다.
따라서, object
를 Declaration
으로 사용하게 되면, Java
의 Singleton
패턴과 같이 객체를 생성해 사용할 수 있다.
object Singleton : MyClass(), MyInterface { // class, interface 상속 가능
private val name: String = "Hunseong" // 멤버 변수
override fun hi() {
...
}
}
Companion Object (동반 객체)
만약 Class Instance
없이, 어떤 클래스 내부에 접근하고 싶다면, 클래스 내부에 객체 선언시에 companion
식별자를 붙인 object
를 선언하면된다.
companion object
는 클래스가 메모리에 적재되면서 함께 생성되는 동반(companion)되는 객체이며, 클래스 내부의 객체 선언을 위한 object 키워드이다.
한 마디로, 클래스 내부에서 Singleton
패턴을 구현하기 위해 사용한다.
Companinon Object
를 이야기할 때 꼭 Java와 같이 함께 설명된다. Java나 C#과 달리, Kotlin에는 static 멤버 변수나, 함수가 없다. 그렇기에 kotlin
에서의 static
이 companion object
라 생각할 수 있지만 이는 잘못된 생각이다.
companion object
를 선언하면, companion object
의 멤버들은 생성자 없이 단순히 companion object
Class 이름을 식별자로 사용함으로써, Java
나 C#
에서 static 메소드를 호출했던 것과 같이 해당 객체를 사용할 수 있다.
이 과정에서 Companion
언급 없이 축약표현이 가능함에 따라 static
과 혼동이 생긴다.
접근범위
Outer class는 companion object에 접근 가능하나, companion object 내에서는 Outer class에 접근할 수 없다.
Companion Object는 Static이 아니라 Object
Companion object
의 멤버가 Static으로 선언된 변수처럼 보이지만, static으로 선언되는것이 아니라Companion object
는 RunTime
시에 실제 객체의 인스턴스로 실행된다.
→ JVM 상에서 @JvmStatic
Annotation과 함께 쓰일때 제외
→ 따라서 인터페이스를 구현할 수 있다.
class MyClass2{
companion object{
val prop = "나는 Companion object의 속성이다."
fun method() = "나는 Companion object의 메소드다."
}
}
fun main(args: Array<String>) {
println(MyClass2.Companion.prop)
println(MyClass2.Companion.method())
//이렇게 변수에 할당하는 것은 자바의 클래스에서 static 키워드로 정의된 멤버로는 불가능한 방법
val comp1 = MyClass2.Companion //--(1)
println(comp1.prop)
println(comp1.method())
val comp2 = MyClass2 //--(2)
println(comp2.prop)
println(comp2.method())
}
위와 같은 코드를 통해 Companion Object
가 객체임을 알 수 있다.
1번 주석과 같이, Companion Object
를 변수에 할당할 수 있고
2번 주석과 같이, .Companion
를 빼고 클래스안에 들어가 있는 Companion Object
를 변수에 할당을 할 수 있는것도 볼 수 있다.
즉 Static과는 다르게 Companion Object는 독립된 객체로 생성되는 것임을 알 수 있다.
Companion Object 이름 짓기
compainon object
의 기본이름은 Companion
이지만, 이름을 변경할 수도 있다.
class MyClass3{
companion object MyCompanion{ // -- (1)
val prop = "나는 Companion object의 속성이다."
fun method() = "나는 Companion object의 메소드다."
}
}
fun main(args: Array<String>) {
println(MyClass3.MyCompanion.prop) // -- (2)
println(MyClass3.MyCompanion.method())
val comp1 = MyClass3.MyCompanion // -- (3)
println(comp1.prop)
println(comp1.method())
val comp2 = MyClass3 // -- (4)
println(comp2.prop)
println(comp2.method())
val comp3 = MyClass3.Companion // -- (5) 이름이 바뀌었으므로 에러발생!!!
println(comp3.prop)
println(comp3.method())
}
이름을 바꾸었기 때문에 5번처럼 사용할 경우 에러가 발생한다.
주석 1번과 같이 Companion Object
에 이름을 줄 수 있고, 이러한 것이 2,3번에서 사용될 수 있는것을 볼 수 있다.
또한, 4번처럼 여전히 생략해서 사용할 수도 있다.
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
val instance = MyClass.create()
// 3가지 방법 모두 사용가능
MyClass.create()
MyClass.Factory.create()
Factory.create()
이 지점이 static과 혼동하게 되는 가장 큰 이유인데, 언어적으로 지원하는 축약 표현때문에 companion object와 static을 혼동할 수 있는 것이다.
Companion Object의 갯수제한
companion object
는 어떤 Class의 모든 Instance가 공유하는 객체를 만들고 싶을 때 사용하며, Class내 1개만 가질 수 있다.
이는 당연한 것인게 Kotlin은 클래스명만으로도 참조가 가능하므로, 한 클래스안에 2개의 Companion Object
가 들어가있으면 무엇을 가져와야할지 결정을 못하기 때문이다.
이는, 위에 처럼 서로 다른 이름을 할당해주더라도 변함없는 사실이다.
Class 상속간의 Companion Object는?
상속 관계에서 Companion Object
멤버는 같은 이름일 경우, 가려진다 → Shadowing
open class Parent{
companion object{
val parentProp = "나는 부모값"
}
fun method0() = parentProp
}
class Child:Parent(){
companion object{
val childProp = "나는 자식값"
}
fun method1() = childProp
fun method2() = parentProp
}
fun main(args: Array<String>) {
val child = Child()
println(child.method0()) // 나는 부모값
println(child.method1()) // 나는 자식값
println(child.method2()) // 나는 부모값
}
위에 처럼 부모/자식의 Companion Object
의 멤버가 다른 이름이라면, 자식이 부모의 Companion Object
멤버를 직접 참조할 수 있다. 그러나, 같은 이름인 경우는 다음과 같이
open class Parent{
companion object{
val prop = "나는 부모"
}
fun method0() = prop //Companion.prop과 동일
}
class Child:Parent(){
companion object{
val prop = "나는 자식"
}
fun method1() = prop //Companion.prop 과 동일
}
fun main(args: Array<String>) {
println(Parent().method0()) // 나는 부모
println(Child().method0()) // 나는 부모
println(Child().method1()) // 나는 자식
println(Parent.prop) //나는 부모
println(Child.prop) //나는 자식
println(Parent.Companion.prop) //나는 부모
println(Child.Companion.prop) //나는 자식
}
같은 이름인 경우, 자식 클래스의 companion object
속성인 prop
이 부모에서 정의되어 있지만, 가려져서 무시되는 것을 볼 수 있다.
즉, method0()
메소드는 Parent것이기 때문에, 부모의 Companion Object
의 prop
값인 "나는 부모"가 출력된다.
open class Parent{
companion object{
val prop = "나는 부모"
}
fun method0() = prop
}
class Child:Parent(){
companion object ChildCompanion{ // -- (1) ChildCompanion로 이름을 부여했어요.
val prop = "나는 자식"
}
fun method1() = prop
fun method2() = ChildCompanion.prop
fun method3() = Companion.prop
}
fun main(args: Array<String>) {
val child = Child()
println(child.method0()) //나는 부모
println(child.method1()) //나는 자식
println(child.method2()) //나는 자식
println(child.method3()) // -- (2) "나는 부모"
}
위와 같이 자식 클래스의 Companion Object
에 이름을 부여하게 되면 child.method0()
은 부모의 메소드이므로, 어렵지 않게 “나는 부모”가 출력된 것을 예상할 수 있다.
그리고, child.method1()
와 child.method2()
도 역시 자식의 companion object
의 속성을 가리킨다는 것을 알 수 있다.
2번에서 볼수 있듯이, 자식의 companion object
를 뛰어넘어 부모의 companion object
에 직접 접근이 가능하게 되었는데, 이는 자식이 이름을 바꾸었기 때문에 부모의 Companion
을 가져올 수 있게 된 것이다.
Interface 내에서의 Companion Object 정의
Kotlin Interface 내에서 Companion Object
를 정의할 수 있다.
덕분에, Interface 수준에서 상수항을 정의할 수 있고, 관련된 중요 로직을 기술할 수 있다.
interface MyInterface{
companion object{
val prop = "나는 인터페이스 내의 Companion object의 속성이다."
fun method() = "나는 인터페이스 내의 Companion object의 메소드다."
}
}
fun main(args: Array<String>) {
println(MyInterface.prop)
println(MyInterface.method())
val comp1 = MyInterface.Companion
println(comp1.prop)
println(comp1.method())
val comp2 = MyInterface
println(comp2.prop)
println(comp2.method())
}
Companion Object에서의 Interface 구현
앞서 언급했듯 Companion Object
도 인터페이스를 구현할 수 있다.
// JSONFactory 인터페이스 - 추상메서드 fromJSON 포함
interface JSONFactory {
fun fromJSON(jsonText: String) : Person
}
class Person(val name: String) {
// 동반 객체에 JSONfactory Interface 구현해서 사용
companion object : JSONFactory{
// 추상메서드 재정의
override fun fromJSON(jsonText: String): Person {
return Person(jsonText)
}
}
}
Companion Object
가 Interface
를 구현시, 컴파일 시점에 Person.Companion으로 바뀌고 아래 loadFromJSON(…)함수
에 사용된다.
fun loadFromJSON(factory: JSONFactory) : T {
//... }
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용 방법
loadFromJSON(Person)
중첩 클래스에서의 Companion Object
중첩 클래스도 Companion object
를 정의할 수 있는데, 중첩 클래스
는 위치만 다른 뿐, 일반 클래스와 다를게 없기 때문이다.
중첩 클래스 내에서의 Companino Object 예시
class OuterClass{
class NestedClass{
companion object{
fun method() =
"나는 중첩 클래스의 Companion object의 메소드다."
}
}
fun method() = NestedClass.Companion.method()
}
fun main(args: Array<String>) {
//나는 중첩 클래스의 Companion object의 메소드다.
println(OuterClass().method())
//나는 중첩 클래스의 Companion object의 메소드다.
println(OuterClass.NestedClass.method())
//나는 중첩 클래스의 Companion object의 메소드다.
println(OuterClass.NestedClass.Companion.method())
}
위 코드에서 main()
은 함수 내에 3줄은 전부 같은 값을 출력한다.
1번째 줄에서 OuterClass().method()
는 OuterClass
클래스의 객체를 생성하자마자 객체 메소드를 바로 호출
2, 3번째 줄은, 중첩 클래스의 Companion object
에 정의된 메소드를 직접 접근하여 호출하였다.
OuterClass
안에 NestedClass
가 정의되었을 뿐, NestedClass
도 완전한 일반 클래스처럼 동작함을 확인할 수 있다.
class OuterClass{
companion object{
private val a = 1
private val d = 2
}
private val b = 2
class NestedClass{
private val c = 3
companion object{
private val d = 4
fun getA1() = a
fun getA2() = OuterClass.a
fun getA3() = OuterClass.Companion.a
// 에러 : Unresolved reference: b
fun getB() = b
// 에러 : Unresolved reference: c
fun getC() = c
fun getD1() = d
fun getD2() = OuterClass.d
fun getD3() = OuterClass.Companion.d
}
fun getA1() = a
fun getA2() = OuterClass.a
fun getA3() = OuterClass.Companion.a
// 에러 : Unresolved reference: b
fun getB() = b
fun getC() = c
fun getD1() = d
fun getD2() = OuterClass.d
fun getD3() = OuterClass.Companion.d
}
}
fun main(args: Array<String>) {
// -- (1) 1
println(OuterClass.NestedClass.getA1())
// -- (2) 1
println(OuterClass.NestedClass.getA2())
// -- (3) 1
println(OuterClass.NestedClass.getA3())
// -- (4) 4
println(OuterClass.NestedClass.getD1())
// -- (5) 2
println(OuterClass.NestedClass.getD2())
// -- (6) 2
println(OuterClass.NestedClass.getD3())
val i = OuterClass.NestedClass()
println(i.getA1()) // -- (7)
println(i.getA2()) // -- (8)
println(i.getA3()) // -- (9)
println(i.getC()) // -- (10)
println(i.getD1()) // -- (11)
println(i.getD2()) // -- (12)
println(i.getD3()) // -- (13)
}
OuterClass
의Companion object
의private
멤버a
,d
속성 정의OuterClass
자신에는b
속성 정의Nested Class
자신에는c
속성 정의Nested Class
의Companion Object
의private
멤버 d 정의
NestedClass
의 Companion Object
에 정의된 getA()
, getD()
메소드 → 각각 OuterClass
와NestedClass
의 companion object
에 접근한다.
getB()
, getC()
→ ERROR
(1)의 결과가 1인 이유 → NestedClass
의 Companion Object
에는 a
가 없으므로, 자등으로 OuterClass
의 companion object
의 a
를 참조
(4)의 결과가 4인 이유 → 자신의 Companion Object
의 속성 d
가 이미 있기 때문에 (1)의 결과와 다르게 OuterClass Companion object
의 d
는 shadowing되기 때문이다.
(5), (6)의 결과가 2인 이유 → NestedClass
의 Companion Object
의 d
가 아닌 Shadowing된 OuterClass
의 Companion Object
의 d
에 직접 접근하기 위해 OuterClass.d
또는 OuterClass.Companion.d
를 썼다.
중첩 클래스에서 Companion Object를 함께 사용하는 이유
위의 예시를 통해 이유가 명확해졌다.
OuterClass
에 정의된 companion Object
의 private
속성에 제약없이 접근 가능한 다른 클래스가 필요한 경우 중첩 클래스를 사용하면 된다.
이렇게 하면 클래스 간 공유하는 필요한 로직이나 값이 외부에 노출되지 않고 내부에서만 처리하기 때문에 효율적인 정적 데이터 은닉과 기능의 캡슐화가 가능해진다.
Inner Class(내부 클래스)에서의 Companion Object
내부 클래스에서는 Companion Object
를 사용및 정의할 수 없다.
사용 불가능한 이유
이유는 그 의미가 모호하기 때문이다.
일반 클래스와 중첩 클래스에서 companion object
는 가상 머신안에서 companion object
가 딱 하나만 존재하는 것을 의미한다.
그러나, 내부 클래스는 외부 클래스의 참조를 가지므로, 내부 클래스에 Companion object
를 정의할 수 있다면 이것이 외부 클래스 단위로 하나만 있는 것인지, 아니면 일반 클래스와 마찬가지로 가상 머신 안에서 하나만 있는 것인지 판단하기 어려워 진다.
따라서 이러한 모호함으로 인해 companion object
를 사용할 수 없게 되었다.
동일한 이유로 Java에서도 내부 클래스에 static 키워드를 사용하는것이 금지되어 있다.
Inner Class가 있는 외부 클래스에서의 Companin object 정의와 그 참조 관계
내부 클래스 인스턴스가 생성 → 외부 클래스 인스턴스 참조 를 가지기 때문에 내부 클래스내 에서 외부 클래스의 companion object
속성에 어떤 관계로 참조가 가능한지 확실히 알아볼 필요가 있다.
class OuterClass{
companion object{
private val a = 1
private val b = 2
private val c = 3
private val d = 4
}
private val b = 20
private val d = 40
inner class InnerClass{
private val c = 300
private val d = 400
fun getA() = a //--(1)
fun getB() = b //--(2)
fun getC() = c //--(3)
fun getD() = d //--(4)
}
fun print() {
val inner = InnerClass()
println(inner.getA()) //1
println(inner.getB()) //20
println(inner.getC()) //300
println(inner.getD()) //400
}
}
fun main(args: Array<String>) {
//에러! 내부클래스는 외부에서 생성 불가!
val inner = OuterClass.InnerClass()
OuterClass().print()
}
주석 1~4번이 각각 a~d의 값을 반환한다.
주석 1번에서 a
값을 반환하는 이유 → a
는 InnerClass
가 아닌 OuterClass
의 Companion object
에 정의되어 있다. 그렇기 때문에 1
이 출력
주석 2번에서 b
값을 반환하는 이유 → b
는 InnerClass
에는 없지만 OuterClass
의 멤버 b
와 companion object
에 b
가 정의되어 있다. 결과값으로 20
이 나온 것으로 볼 때 companion object
의 b
는 Shadowing 되고, OuterClass
의 멤버 b
값이 출력되는 것을 볼 수 있다
→ InnerClass
의 getB()
가 Instance
멤버가 Instance
가 우선되는 것으로 해석 가능할듯
주석 3번에서 c
값을 반환하는 이유 → c
는 IneerClass
의 멤버이고, OuterClass
의 Companion object
의 멤버인데, 가장 가까운 속성이 출력된다.
주석 4번에서 d
값을 반환하는 이유 → d
는 InnerClass
, OuterClass
, OuterClass Companion object
의 멤버이다. 이것도 마찬가지로 가장 가까운 자신의 멤버 값이 출력된다.
결과를 보면 중첩 클래스와 달리 OuterClass Companion object
멤버뿐만 아니라, OuterClass
의 멤버 모두 접근이 가능하다는 것을 확인할 수 있다.
추가적으로 main()
함수를 보면, 내부 클래스
는 외부에서 생성이 불가하다는 것을 다시 확인할 수 있다.
Inner Class가 있는 외부 클래스에서의 Companin object 예시
class OuterClass{
companion object{
private val a = 1
private val b = 2
private val c = 3
private val d = 4
}
private val b = 20
private val d = 40
inner class InnerClass{
private val c = 300
private val d = 400
// -- (1) 1
fun getA() = a
// -- (2) 20
fun getB() = b
// -- (3)
fun getC() = this.c
// -- (4)
fun getD() = this.d
// -- (5) 20
fun getOuterB() = this@OuterClass.b
// -- (6) 40
fun getOuterD() = this@OuterClass.d
// -- (7)
fun getOuterCompA() = OuterClass.a
// -- (8)
fun getOuterCompC() = OuterClass.c
}
fun print() {
val inner = InnerClass()
println(inner.getA()) //1
println(inner.getB()) //20
println(inner.getC()) //300
println(inner.getD()) //400
println(inner.getOuterB()) //20
println(inner.getOuterD()) //40
println(inner.getOuterCompA()) //1
println(inner.getOuterCompC()) //3
}
}
fun main(args: Array<String>) {
OuterClass().print()
}
주석 3, 4번
은 this를 붙였다. 이 this
는 InnerClass
의 인스턴스 컨텍스트(context)를 지칭한다. this를 빼도 무방하다. 그러나, (1)과 (2)에는 this를 붙힐 수 없다. a
와 b
는 InnerClass의 속성이 아니기 때문이다
주석 5, 6번
은 this@OuterClass.(멤버참조)
형태로 쓰고 있는데, 결과가 각각 20, 40이 나오는 것으로 보아 여기서 쓰인 b
와 d
는 OuterClass의 멤버를 지칭한다. 즉, this@OuterClass
는 InnerClass
컨텍스트를 감싸는 OuterClass
의 컨텍스트를 가리킨다.
이 점이 매우 중요한데, 외부클래스 인스턴스
가 내부클래스의 인스턴스
를 참조함과 동시에 내부클래스 인스턴스가 외부클래스의 인스턴스를 참조한다(순환참조).
내부클래스 인스턴스가 어딘가에 참조되어 있다면 외부클래스 객체는 결코 메모리에서 삭제될 수 없다는 것도 알 수 있다. 이 점때문에 내부 클래스를 잘못쓰면 메모리 누수가 발생하거나 가비지 컬렉팅(GC)의 성능을 나쁘게 하는 문제가 생겨나기도 한다.
반면 이전에 언급한 중첩클래스는 외부클래스의 companion object
만 참조할 수 있으므로 원천적으로 이 문제가 발생하지 않는다.
주석 7, 8번
부분은 OuterClass
의 companion object
의 멤버에 접근했으므로 쉽게 값 1, 3을 출력함을 알 수 있다.
Companion Object는 언제 사용하는 것이 좋을까?
상수를 정의할 때 주로 Companion Object
를 사용해오곤 했는데, 그러면 언제 Companion Object
를 사용하는게 좋을까?
3가지
정도를 들 수 있다
- private 생성자와 함께 Factory 메소드를 작성할 때
- static constants와 메소드 제공
- 최상위 property및 메소드 범위 지정(Scoping top-level properties and function)
Factory 메소드와 함께 사용
Companion Object
는 OuterClass
의 private
속성, 메소드및 생성자에 접근할 수 있다.
→ 이는 즉 클래스에 private 생성자가 있을 때에도 해당 Companion Object
에서 이를 사용해 클래스를 인스턴스화 할수 있음을 의미한다. 이를 통해 Factory 메소드를 작성해 클래스 생성 방법을 제어할 수 있다.
코틀린은 일반적으로 최상위 메소드와 property를 선호하지만, Companion Object
를 톤해, 이러한 property와 메소드를 둘러싸는(Scoping) 클래스로 지정할 수 있다. 이러면 네임스페이스 오염을 방지할 수 있다.
Companion Object + Factory 패턴 예시
companion object
는 팩토리 패턴을 구현하는데 효과적이다.
클래스를 생성할 때, 여러 생성자(constructor)를 만들어서 객체를 생성할 수 있지만, 생성자가 많아지면 복잡해지고 헷갈릴때가 많아진다.
이럴 경우, 팩토리 패턴을 사용하면 클래스를 생성할 때 어떤 목적으로 만들지 생성자를 선택하는데 있어 도움이 될 수 있다.
다음은 private 생성자인 클래스를 만들고, companion object
블럭에서 User()
를 생성하는 팩토리를 구현한 예시이다.
class User private constructor(val name: String) { //private 주 생성자
companion object {
// 이메일(email)을 기준으로 아이디를 분리해서 User 인스턴스 생성
fun newSubscribingUser(email:String) = User(email.substringBefore("@"))
// ID를 사용해서 User 인스턴스 생성
fun newFacebookUser(id:Int) = User("${id}")
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용 방법
User.newSubscribingUser("UserId@gmail.com")
User.newFacebookUser(1)
위와 같이 팩토리 패턴을 사용하면, User Instance
를 생성할 때, SubscribingUser
/ FacebookUser
클래스가 따로 존재하는 경우에도 필요에 맞는 클래스를 생성하여 리턴할 수 있다
만약 하위클래스에서 override
가 필요할 경우에는 companion object
를 사용할 수 없다
Companion Object
는 오버라이드가 불가능하기 때문에, 아래와 같이 여러 생성자를 생성하고 상속받은 하위클래스에서 부모의 생성자 중에 골라서 사용하는게 효율적이다.
open class User {
val name:String
// 보조생성자 1 - String 인자
constructor(email: String) {
this.name = email.substringBefore("@")
}
// 보조 생성자 2 - Int 인자
constructor(id:Int) {
this.name = "${id}"
}
}
// 상속이 필요한 경우 동반객체는 상속이 불가능하기에 여러개의 보조 생성자를 생성하는 편이 효율적
class UserTest : User {
constructor(email: String) : super(email) // 보조 생성자 1 사용
constructor(id: Int) : super(id) // 보조 생성자 2 사용
}
Companion Object 확장함수 사용
Companion Object
에 확장함수(Extends Function)을 사용하려면 비어있는 Companion Object를 선언하고 확장함수를 선언한다.
class Person(val name: String) {
// 동반 객체가 JSONfactory Interface 구현해서 사용
companion object : JSONFactory{
override fun fromJSON(jsonText: String): Person {
return Person(jsonText)
}
}
}
// 동반 객체에 확장함수 방법
fun Person.Companion.TestLog(){
Log.d(TAG, "TestLog 메서드")
}
// 일반 확장함수 방법
fun Person.TestLog(){
Log.d(TAG, "TestLog 메서드")
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용 방법
Person.Companion.TestLog()
Object Declaration VS Companion Object
두 선언 방식의 차이점은 앞선 개념만 봐도 알 수 있다.
Object Declaration
은 Class 전체가 하나의 Singleton 객체로 선언되지만, (Singleton 형태)Companion Object
는 Class 내에 일부분이 Singleton 객체로 선언되는 것이다.
또한 둘은 초기화 시점이 다른데,Object Declaration
이 선언된 Class는 해당 Class가 사용될 때 초기화되지만, (lazy initialized)Companion Object
는 해당 Class가 속한 Class가 Load될 때 초기화가 이루어진다.
정리하면 아래와 같다
Object Declaration
- Singleton 형태
- Thread-Safe
- Lazy Initailized (실제로 사용시에 초기화 됨)
- const val로 선언된 상수는 Static 변수
- object 내부에 선언된 변수와 함수들은 java의 static이 아님. 단 아래의 케이스는 static
- const val로 상수 선언한 것들
@JvmStatic
또는@JvmField
의 Annotation이 붙은 변수및 함수들
Companion Object
- 해당 클래스(companion object는 클래스 내부에 들어가는 블럭이므로)자체가 static이 아님
→ 즉 CompanionObjectTest()로 생성할 때마다 객체의 주소값은 다름 - 해당 클래스가 로드될 때 초기화됨
- const val로 선언된 상수는 static 변수
- companion object 내부에 선언된 변수와 함수들은 java의 static이 아님 단 아래의 케이스는 static
- const val로 상수 선언한 것들
@JvmStatic
또는@JvmField
의 Annotation이 붙은 변수및 함수들
Static
Kotlin에는 없는 Static
, 과연 Static
이란 무엇일까? 이에 대해선 간단하게만 짚고 넘어가고자 한다.
정의
Static
은 Block
(블럭), Variable
(변수), Method
(메소드), Nested Class
(중첩 클래스)에서 사용 가능한 키워드이다.
static이 붙은 변수와 메소드
→ 각각 클래스 변수, 클래스 메소드라 부르는 반면,static이 붙지 않은 변수와 메소드
→ 각각 인스턴스 변수, 인스턴스 메소드라고 부른다.
static이 붙은 멤버는 클래스가 메모리에 적재될 때 자동으로 함께 생성되므로, Instance 생성 없이도 클래스명 다음에 점을 붙여 바로 참조할 수 있다. 즉, 어떤 변수, 메소드 등이 static
키워드로 선언되면, 해당 멤버는 자신이 속한 클래스의 어떤 객체가 생성되기 전에도 접근 가능하다.
→ 결론적으로 static
한 멤버는 Class
의 Instance
와 별개로 사용될 수 있다.
Static Block
Java는 Static Clause
라고도 불리는 Static Block
을 지원한다.
일반적인 초기화 블록이 Instance가 실행될 때마다, 실행되는 것과 달리 Static Block 내부의 코드는 다음의 경우에 1번만 실행된다.
- 해당 Block이 선언된 Class의 첫 번째 객체를 만들 때
- 해당 Class의
Static Member
에 처음으로 접근할 때
(Class 객체를 만들었는가 여부와는 관계없다)
참고
Kotlin: Object Declaration, 그리고 Companion Object (feat.static)
[Kotlin] Object vs Companion object
[Kotlin] static, object, companion object 차이
[Kotlin] 코틀린 기본 - object / Companion Object(동반 객체)
[kotlin] Companion Object (1) - 자바의 static과 같은 것인가? - Bsidesoft co.
[kotlin] Companion Object (2) - 중첩클래스(Nested Class)와 내부클래스(Inner Class) - Bsidesoft co.
'IT > Kotlin' 카테고리의 다른 글
[Kotlin/Coroutine] 4. Coroutine의 구성요소: Coroutine의 생성과 활용 (0) | 2023.04.22 |
---|---|
[Kotlin/Coroutine] 3. launch와 async의 Job과 Deferred (0) | 2023.04.07 |
[Kotlin/Coroutine] 2. Kotlin에서의 Coroutine: launch와 async (0) | 2023.03.24 |
[Kotlin/Coroutine] 1. Coroutine이란 무엇인가 (0) | 2023.03.16 |
[Kotlin] Delegate Pattern (0) | 2023.02.07 |