Post

(Kotlin) 싱글턴 object(static) - 객체 선언, 동반 객체, 객체 식 / 익명, 무명

클래스를 정의하면서 동시에 인스턴스를 생성한다.

코틀린에는 static이 없고, 대신 이를 최상위 함수로 처리한다.
그러나 최상위 함수는 말 그대로 최상위에 존재하는 함수이기 때문에 어떤 클래스의 private에는 당연히 접근할 수 없어 클래스 내부에 선언된 static이 클래스의 다른 멤버와 상호 작용하는 경우를 커버할 수 없다.
이렇게, static을 최상위 함수로 대체할 수 없을 때 object를 고려한다.

object를 사용하면 java로 변환시 static으로 컴파일된다.

1. 객체 선언(object declaration) : 싱글턴

객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 생성 을 동시에 처리해주기 때문에, singleton에 사용하기 적합하다.

  • 객체 접근 시점에 객체가 생성된다.
  • 주/부 생성자는 사용할 수 없다. 객체 선언과 동시에 생성자 호출 없이 바로 만들어지기 때문.
  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.

인터페이스를 구현해야 하는데 그 구현 내부에 다른 상태가 필요하지 않은 경우 사용하면 좋다.

  • 객체 선언 안에도 프로퍼티, 메소드, init이 들어갈 수 있다.
  • 싱글턴과 마찬가지로 객체 선언도 대규모 소프트웨어 시스템에서는 적합하지 않을 수 있다.
  • 자바에서 코틀린 객체 선언으로 생성된 인스턴스에 접근하려면 INSTANCE를 사용한다.
  • 클래스 안에서 객체 선언을 사용하더라도 객체 선언의 인스턴스는 단 하나만 생성된다.
1
2
3
4
5
6
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int =
            p1.name.compareTo(p2.name)
    }
}
1
2
3
>>> val persons = listOf(Person("B"), Person("C"), Person("A"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name=A), Person(name=B), Person(name=C)]
Q1 최상위 함수로 정의해도 되는거 아님?

이런 경우 Person에 대해서만 비교하는 것이기 때문에 최상위 함수 보다는 클래스 내부에 정의하는 것이 알맞다.

Q2 Person이 Comparator을 직접 상속 받으면 되지 왜 안에 object를 쓰고 거기서 상속받는가?
  • Comparable이 아니라 Comparator이니까.
  • sortedWith()는 ~를 기준으로 정렬. 이라는 의미이므로 Comparator를 받아야 한다.
  • Comparable을 이용해 1:1 비교 메소드를 정의하는 것과는 다름.
  • 만약 Person이 직접 Comparator를 상속받았다면 다음과 같이 써야해서 매우 부자연스럽다.
1
2
3
>>> val persons = listOf(Person("B"), Person("C"), Person("A"))
>>> println(persons.sortedWith(persons[0]))
[Person(name=A), Person(name=B), Person(name=C)]

2. 동반 객체 companion

  • 객체 선언 처럼 클래스가 로드되는 시점에 인스턴스가 단 하나만 생성된다.
  • 동반 객체 선언도 일반 객체 선언처럼 상속이나 함수, 프로퍼티를 가질 수 있다.
  • 객체 선언은 한 클래스 내에 여러개 존재할 수 있지만, 동반 객체는 단 하나만 존재할 수 있다.
  • 따라서 여러개의 객체 선언을 사용하는게 더 적합할 때도 있다.
  • 동반 객체를 포함한 클래스를 확장해야하는 경우에는 동반 객체 멤버를 하위 클래스에서 오버라이드할 수 없으므로 부 생성자를 사용하는 편이 더 낫다.
  • 동반 객체에 이름을 지정하지 않는 경우 자동으로 Companion이 이름이 된다.
  • 이름을 붙이건 안붙이건 동반 객체 내부의 메소드는 그냥 호출할 수 있다.

코틀린에서 클래스 안의 클래스는 기본적으로 static으로 컴파일 되는 중첩 클래스이며, innerobject를 함께 사용할 수 없기 때문에 클래스 안에 들어있는 object객체 선언일지라도 바깥쪽 클래스의 멤버에 그냥 접근할 수는 없다.

단, 객체를 생성하면서 접근하는 것은 가능한데 이 때문에 private생성자를 kt object 내에서 호출할 수 있어 팩토리 메소드 패턴을 구현하기 적합하다.

이런 식으로 클래스 안에 중첩 클래스로 object가 들어가는 경우, companion을 붙여주면 object의 이름을 명시하지 않고 바깥쪽 클래스의 이름으로 바로 접근할 수 있기 때문에 팩토리 패턴을 구현하는 경우 이를 붙여주는 것이 좋다.

#0 class 내부의 static 변수를 companion에 둘 수 있음
  • 인스턴스가 단 하나만 생성되니까. 여기에 변수를 두면 static처럼 쓸 수 있다.
#1 Factory method pattern
1
2
3
4
5
6
7
8
class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) =
            User(getFacebookName(accountId))
    }
}
1
2
3
4
>>> val u = User.newSubscribingUser("umbum@n.c")
>>> println(u.nickname)
umbum
>>> val u2 = User.Companion.newSubscribingUser("umbum@n.c")
#2 클래스 내부에 뭔가를 상속/구현해야 할 때 동반 객체를 사용할 수 있다.

어떤 시스템에서는 모든 객체를 JSON 역직렬화를 통해 만들어야 하기 때문에, 모든 타입의 객체를 JSON 역직렬화하는 일반적인 방법이 필요하다.
이런 경우 JSON 역직렬화 구현을 제공하는 인터페이스를 사용하게 되는데, 이런 메서드는 static으로 Person.fromJSON() 형태로 호출하는게 자연스럽다.
그래서 1. 객체 선언과 마찬가지로 클래스 자체가 이 인터페이스를 상속하게 하는 것 보다는 객체 선언을 이용해 클래스 내부에 싱글턴 객체가 이를 상속하도록 한다.

1. 객체 선언에서는 Person.NameComparator.compareTo()로 내부 객체를 명시해 주어야 하지만, 동반 객체는 그냥 Person.fromJSON()으로 사용했다는 점에서 차이가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String): Person {
            TODO("not implemented")
        }
    }
}

fun loadFromJSON<T>(factory: JSONFactory<T>): T {
    . . .
}
1
2
>>> val p = Person.fromJSON(json)
>>> loadfromJSON(p)
#3 동반 객체 확장, 모듈 분리

동반 객체의 메소드도 확장 함수로 정의할 수 있기 때문에 함수의 정의를 바깥쪽 클래스와 분리할 수 있다.
Person은 비즈니스 로직 모듈이므로, JSON 역직렬화 함수는 서버/클라이언트 통신 모듈에 두고싶다면

1
2
3
4
// 비지니스 로직 모듈
class Person(val name: String) {
    companion object {}  // 빈 동반 객체
}
1
2
3
4
// 서버/클라이언트 통신 모듈
fun Person.Companion.fromJSON(json: String): Person {
    TODO()
}
1
>>> val p = Person.fromJSON(json)

* 이렇게 분리하는건 장단이 있는 듯. Person만 확인했을 때 동반 객체 안에 뭐가 있는지 바로 확인이 안되니까.

3. 객체 식 : 무명 내부 클래스, 무명 객체, 무명 클래스

이름 없이 쓰면 객체 식이다. Note ) 당연하겠지만 이건 싱글턴이 아니다. 객체 식이 쓰일 때 마다 새로운 인스턴스가 생성된다.

객체 식은 여러 메소드를 오버라이드해야 하는 경우나, 추상 클래스를 구현해야 하는 경우 사용하도록 한다.
SAM(Single Abstract Method), 즉 단일 추상 메소드만 가지고 있는 함수형 인터페이스인 경우에는 무명 객체 대신 람다를 사용하는 편이 좋다.
2017/12/05 - [Coding/Kotlin Java] - [Kotlin] 함수형 인터페이스(SAM)에 람다 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
fun countClicks(window: Window) {
    var clickCount = 0
    window.addMouseListener(
        object : MouseAdapter() {
            override fun mouseClicked(e: MouseEvent) {
                clickCount++    // 둘러싼 함수의 변수에 접근할 수 있다.
                // 자바는 final만 접근할 수 있지만 코틀린은 final이 아니어도 접근 가능하다.
            }
            override fun mouseEntered(e: MouseEvent) { ... }
        }
    )
}

딱 한 번만 사용되는 객체라서 클래스를 정의하기가 부담스러운 경우 객체 식을 변수에 넣어 사용하면 깔끔하게 처리할 수 있다.

1
2
3
4
5
6
7
interface Shape {
    fun onDraw()
}

val triangle = object: Shape {
    override fun onDraw() { ... }
}

### 객체 선언과 동반 객체의 차이

내부적인 구현에 약간 차이가 있기는 하나, 둘 다 static으로 컴파일되며 시스템 전역에 하나만 존재하는 인스턴스라는 점은 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class NCtest (private val name: String) {
    object NotCompanion {
        // val _name = name    이게 가능하려면 inner를 붙여야 하는데 object에는 inner를 붙일 수 없다.
        fun testMethod(c: NCtest) = c.name
    }
}

class Ctest (private val name: String) {
    companion object {
        // val _name = name    역시 위와 같은 이유로 안됨.
        fun testMethod(c: Ctest) = c.name
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class Ctest {
   private final String name;
   public static final Ctest.Companion Companion =
         new Ctest.Companion((DefaultConstructorMarker)null);
  
   public Ctest(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
  
   public static final class Companion {
  
      @NotNull
      public final String testMethod(@NotNull Ctest c) {
         Intrinsics.checkParameterIsNotNull(c, "c");
         return c.name;
      }
  
      private Companion() {}
  
      // $FF: synthetic method**
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class NCtest {
   private final String name;
  
   public NCtest(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
  
   public static final class NotCompanion {
      public static final NCtest.NotCompanion INSTANCE;
  
      @NotNull
      public final String testMethod(@NotNull NCtest c) {
         Intrinsics.checkParameterIsNotNull(c, "c");
         return c.name;
      }
  
      static {
         NCtest.NotCompanion var0 = new NCtest.NotCompanion();
         INSTANCE = var0;
      }
   }
}

### 생성자 파라미터를 사용해야 하는 경우 적용할 수 있는 싱글턴

https://stackoverflow.com/questions/40398072/singleton-with-parameter-in-kotlin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DBHandler private constructor(context: Context)
    : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
    
    companion object {
        // null check 해서 객체 생성해야 하니까 lateinit쓰면 안된다.
        @Volatile private var INSTANCE: DBHandler? = null
        
        // 중복 처럼 보이지만 sync는 꽤 비싸니까 퍼포먼스를 위해.
        // 근데 java로 변환해보면 synchronized block은 비어있고 코드가 밖으로 나와있음. 뭔지.
        fun getInstance(context: Context): DBHandler =
                INSTANCE ?: synchronized(this) {
                   INSTANCE ?: DBHandler(context.applicationContext).also{ INSTANCE = it }
                }
    }
  
    override fun onCreate(db: SQLiteDatabase?) {
    }
}
This post is licensed under CC BY 4.0 by the author.