코틀린에는 static 개념이 없다. 사실 개념이 없다기 보다는 static keyword가 없기 때문에 Java의 static 개념을 코틀린에서 표현할 수 없다. 그래서 이를 어떻게 표현하는지 중점적으로 살펴보겠다.

  • 싱글톤을 정의하는 방법
  • 동반 객체 companion object를 이용한 팩토리 메소드 구현
  • 익명 클래스 선언

위의 3가지를 object keyword를 이용해 표현한다.

싱글톤

코틀린에서는 object를 이용하여 클래스를 정의함과 동시에 객체를 생성할 수 있다. 말 그대로 싱글톤을 쉽게 구현할 수 있다. 이해하기 쉽게 간단한 예제를 만들었다.

1
2
3
4
5
6
7
8
9
class SharedPreference{
public static SharedPreference INSTACNE=null;

public SharedPreference(){
if(INSTACNE == null){
INSTACNE = SharedPreference();
}
}
}

위 코드는 자바에서 사용할 수 있는 간단한 싱글톤 패턴 구현 코드이다. 그럼 이제 코틀린에서 object를 사용해 바꿔보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object SharedPreference{

private const val NAME = "Test"
private const val MODE = Context.MODE_PRIVATE
private latedinit var preferences: SharedPreferences

fun init(context: Context) {
preferences = context.getSharedPreferences(NAME, MODE)
}

// 생략.
}

fun main(args: Array<String>){
SharedPreference.init(applicationContext)
}

위의 코드처럼 object로 선언하면 클래스 선언과 동시에 객체가 생성된다. 따라서 객체 이름을 통해 property나 메소드에 접근할 수 있다.

object 클래스는 가장 바깥 클래스로 선언될 수도 있고, 내부에 중첩된 클래스 형태로 선언될 수도 있다. 하지만, 어떤 방식으로 선언되었던 간에 존재하는 object는 단일 객체만 존재한다.

companion object

코틀린에서는 static을 지원하지 않는 대신 top-level function을 통해 같은 효과를 낼 수 있다. 단, top-level function은 class 내부에 선언된 private property에는 접근할 수 없는 제한을 받는다.

이를 해결하기 위해서 companion object라는 개념이 존재한다. 클래스의 인스턴스 생성과 상관없이 호출해야 하지만 class의 내부 정보에 접근할 수 있는 함수가 필요할 때 companion obejct를 class 내부에 선언한다. Java로 따지면 class 내부에 static 함수를 넣는다고 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
class A{
companion object{
fun print(){
println("Companion obejct call!!")
}
}
}

fun main(args: Array<String>){
A.print()
}

이처럼 A 클래스 내부에서 선언된 companion object는 호출할 때 클래스 이름으로 바로 호출할 수 있다. (Java의 static 함수와 동일한 형태이다.)

또한, companion object는 외부 클래스의 private property에도 접근이 가능하기 때문에, factory method를 만들 때 적합하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User private constructor(val nickname: String){
companion object{
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))

fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
}
}

fun main(args: Array<String>){
val subscribingUser = User.newSubscribingUser("jhsw0375@gmail.com")
val facebookUser = User.newFacebookUser(4)

println(subscribingUser.nickname)
}

위에서 User 클래스는 private constructor를 가지기 때문에 외부에서 생성할 수 없다. 따라서 외부에서는 companion으로 제공되는 factory method를 이용해서만 객체를 생성할 수 있도록 제한할 수 있다.

companion object의 사용

companion object는 클래스 내부에 정의된 일반 객체이다. 따라서 아래와 같은 작업이 가능하다.

  • companion object에 이름 명명
  • companion object 내부에 확장 함수나 property 정의
  • 인터페이스 상속
1
2
3
4
5
6
7
8
9
10
class Person(val name: String){
companion object Loader{
fun fromJSON(jsonText: String): Person = ... 생략.
}
}

fun main(args: Array<String>){
person = Person.Loader.fromJSON("{name: 'kim'}")
person = Person.fromJSON("{name: 'lee'}")
}

companion object에 이름을 붙일 수 있으며, 이름을 통해서 호출할 수도 있고 그냥 호출할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}

class Person(val name: String){
companion object: JSONFactory{
override fromJSON(jsonText: String): Person = ... 생략
}
}

fun loadFromText<T>(factory: JSONFactory<T>): T{
... 생략.
}

fun main(args: Array<String>){
loadFromText(Person)
}

위의 코드처럼 companion object가 특정 interface를 구현할 수도 있고, 이 interface를 넘겨줄 때는 외부 class 이름을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ClickListener{
fun onClick()
}

fun main(args: Array<String>){
setClickAction(object: ClickListener{
override fun onClick(){
println("clicked!")
}
})
}

fun setClickAction(clickListener: ClickListener){
clickListener.onClick()
}

위의 코드에서 익명 클래스는 singleton이 아니다. 따라서 호출시 매번 객체가 생성된다는 점과 익명 클래스 내에서는 외부 클래스의 변수에 접근하여 값을 수정할 수도 있다.

SharedPreferences 예제

Android에서는 간단한 값을 저장하기 위해서 SharedPreferences를 사용한다. 프로그램 어디서나 이 객체를 사용할 수 있어야 하기 때문에 일반적으로 Singleton을 이용해 구현하곤 한다. 자바에서는 싱글톤을 손쉽게 구현할 수 있다. 마찬가지로 코틀린에서도 object 개념을 사용해서 간단하게 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object SharedPreferenceManager{
private const val PREF_TOKEN = "token"
private const val NAME = "Test"
private const val MODE = Context.MODE_PRIVATE
private latedinit var preferences: SharedPreferences

fun init(context: Context){
preferences = context.getSharedPreferences(NAME, MODE)
}


// 확장 함수를 사용한다. 따라서 edit(), apply() 함수를 호출할 필요가 없다. 모든 작업을 이 함수 하나로 대체할 수 있다.
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit){
val editor = edit()
operation(edit)
editor.apply()
}

var token: String
get() = preferences.getString(PREF_TOKEN,"")
set(value) = preferences.edit{
it.putString(PREF_TOKEN, value)
}
}

간단하게 토큰을 저장하는 예제이다. 저장해서 확인해보는 과정까지 거친 코드이므로 잘 동작한다. 확장 함수를 만들어서 이를 통해 edit(), apply() 함수를 직접 호출할 필요가 없다. 작성한 함수만 사용하면 되기 때문이다.

또한, 여러 개의 함수를 만들 필요 없이 하나의 함수만 사용하면 되고 저장할 값이 필요하다면 token 처럼 만들어서 사용자 지정 get,set을 사용하여 값을 가져오고 저장하는 과정을 거치면 된다.

이 클래스를 만들기 위해서 처음에 어떻게 잘 짤 수 있을까를 먼저 고민해보았다. 그런데 바보 같은 생각이라는 걸 깨달았다. 처음부터 잘 짤 수는 없는 것이다. 완벽한 코드는 없으면 코드를 짜면서 공부를 하면서 수정하면 되는 것이다.

그러니 처음부터 완벽한 코드를 짜려고 애쓰지 않도록 마음 먹었다. 리팩토링을 하면 나의 코드를 더 발전시켜 나갈 수 있으니 말이다. 오늘은 여기까지!

참고