Kotlin 에는 Java 의 static 키워드가 없다. 대신 companion object 라는 친구가 있기에 Java 의 static 처럼 동작해야하는 변수나 메소드가 필요하다면 companion object 블록을 사용해서 그 내부에 선언하면 된다. 여기까지는 코틀린이라는 프로그래밍 언어를 배운 사람이라면 누구나 아는 내용이다.
그런데 왜, 굳이 static 을 없앴을까? 자바를 오래 사용하다가 코틀린을 처음 접하면 이 생각이 들 수 밖에 없다. static 이 더 편한 것 같은데? 심지어 이름도 더 길어서 타이핑 하기도 불편해.
코틀린을 설계한 사람들은 왜 굳이 자바의 static 을 버렸을까. 이 이유에 대해 누군가 시원하게 답해주길 바랬으나 마땅한 글을 찾지 못해 내 부족한 머리로 나름대로 생각해보고 내렸던 결론을 적어본다.
Java — static
Java 에서 static 이라는 키워드가 어떤 역할을 하는지나 그 기저의 동작 방식에 대해서는 자세히 적지 않겠다. 이에 대해서는 정말 자세히 설명해주신 훌륭한 글들이 많기에 따로 찾아보시면 너무 좋을 것 같다. 간단히 말하면 특정 메소드나 멤버 변수에 인스턴스의 생성 없이도 접근 할 수 있게 만들어주는 키워드다.
이 static 이라는 키워드가 주로 사용되는 use-case 는 아래와 같다.
1. 정적 메소드 (Static Methods)
2. 정적 변수 (Static Variables)
3. 정적 초기화 블록 (Static Initialization Blocks)
4. 정적 내부 클래스 (Static Nested Classes)
5. 유틸리티 클래스 정의
6. 싱글톤 패턴 구현정적 메소드 (Static Methods)
위처럼 자바에서 static 키워드가 사용되는 사례를 코틀린으로 다시 한번 생각해보자. 일단 1~3번은 잠시 제쳐두고 4~6번 부터 보자.
정적 내부 클래스 (Static Nested Classes)
자바에서 정적 내부 클래스는 아래처럼 선언한다.
public class OuterClass {
public static class StaticNestedClass {
void method() {
// OuterClass 멤버 변수 접근 불가능
}
}
}
이걸 코틀린에서 표현하고 싶다면 아래처럼 작성하면 된다.
class OuterClass {
class StaticNestedClass {
fun method() {
// OuterClass 멤버 변수 접근 불가능
}
}
}
기본적으로 클래스 내부에 클래스를 선언하면 자바의 정적 내부 클래스처럼 동작한다. (자바와는 반대라고 생각하면 편하다.)
유틸리티 클래스 정의
자바에서 특정 클래스에 속하기는 애매한 유틸리티성 메소드를 모아놓은 클래스는 보통 아래처럼 작성한다.
public final class StringUtils {
private StringUtils() {} // 인스턴스화 방지
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
public static boolean isEmpty(String str) {
return str == null || str.trim().length() == 0;
}
}
코틀린에선 굳이 이런 유틸리티 성격의 클래스는 필요 없다. 아래처럼 특정 클래스 없이 최상위 메소드로 선언하면 된다.
fun reverse(str: String?): String {
return StringBuilder(str).reverse().toString()
}
fun isEmpty(str: String): Boolean {
return str.trim { it <= ' ' }.isEmpty()
}
싱글톤 패턴 구현
자바에서 싱글톤 패턴을 구현하는 한가지 방법을 살펴보자.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
이걸 코틀린에서 표현하고 싶다면 아래처럼 작성하면 된다.
object Singleton { }
코틀린은 싱글톤 클래스를 간단하게 만들 수 있게 해주는 object 라는 키워드가 존재한다. (이번 글의 주인공이기도 하다.)
사실 이 object 라는 키워드도 처음에는 굳이 싶었다. 자바에서 싱글톤 클래스를 생성하려면 위 코드처럼 보일러플레이트스러운 코드를 매번 작성해야 하는게 번거롭긴하다. 그래서 코틀린의 object 키워드는 그저 이 보일러플레이트 코드를 줄여주는 역할 그 이상 그 이하도 아닌 것으로 보였다. 다만, 그런 역할이었다면 singleton 이라고 지어도 될텐데 object 라고 지은데는 다른 의도가 있지 않았을까?
코틀린 문서를 읽다보면 “everything is object”라는 문장을 간혹 볼 수 있다. 이게 코틀린의 디자인 철학 중 하나인데, 위 object 키워드도 이 철학을 반영한 흔적 중의 하나로 보인다.
단일 싱글톤 인스턴스가 필요한 경우 “클래스 정의 + static 인스턴스”라는 개념을 따로 쓰는 대신, 그저 “하나의 객체”로서 정의하도록 했다. 이렇게 함으로서 “클래스”와 “싱글톤 인스턴스”의 개념을 불필요하게 분리할 필요 없이, “하나의 객체”를 곧바로 선언함으로써 일관된 모델을 제공할 수 있게 된 것이다.
“static 키워드 대신 ‘객체 지향적 관점’에서 일관되게 표현할 수 있도록 object 키워드를 설계했다” 정도로 생각한다.
object 키워드에 대한 설명이 길어졌는데, 다시 위에서 언급했던 자바 static 키워드 사용 사례 여섯개를 보자.
1. 정적 메소드 (Static Methods)
2. 정적 변수 (Static Variables)
3. 정적 초기화 블록 (Static Initialization Blocks)
4. 정적 내부 클래스 (Static Nested Classes)
5. 유틸리티 클래스 정의
6. 싱글톤 패턴 구현
4–6 번 사례의 경우 코틀린에서는 해당 사항이 없다는 것을 방금 확인했다. 1–3 번 사례를 보면 공통점이 하나 있다. 바로 ‘특정 클래스에 종속되어야한다’는 것이다. 조금 더 정확히 말하면 해당 static 키워드가 사용된 변수나 메소드가 의미상 특정 클래스에 붙어있을 수 밖에 없다는 것이다.
여기서 우리가 방금 다뤘던 코틀린의 object 키워드를 떠올려보자. 위 사례의 구현에 앞서, 우리는 싱글톤 객체를 생성하는 object 키워드가 있고 자바의 static 키워드라는 선택지도 있다. 내가 코틀린 설계자의 입장이 되었다고 생각해보자. 굳이 이 두가지 키워드를 모두 사용해야할까? 싱글톤 객체를 생성할 때는 object 키워드를 사용하고 클래스 내부의 정적 변수나 메소드를 선언할 때는 static 키워드를 사용하게 하는게 언어적 차원에서 맞는걸까?
코틀린은 여기서 static 을 버리고 object 를 재사용하기로 한다. 아래처럼 말이다.
class CompanionObjectClass {
object Companion {
val text = "Hello, World!"
fun printText() {
println(text)
}
}
}
위 처럼 클래스 내부에 object 로 싱글톤 객체를 생성하고 해당 객체를 클래스의 짝꿍처럼 사용하는 것이다. 위의 1–3 번 같은 경우는 의미상 분명 특정 클래스에 종속되어야 할 것이고, 코틀린의 설계 철학을 생각했을 때 굳이 static 이라는 키워드를 쓰면서 ‘클래스와는 무관하게, 특정 정적 공간에 존재하는 멤버’ 라는 불편한 인식을 주고 싶지 않을 것 이다. object 키워드를 여기서 활용하기 너무 좋아보인다.
위 처럼 클래스 내부에 object 로 싱글톤 객체를 선언하면 아래처럼 사용할 수 있다.
CompanionObjectClass.Companion.text
CompanionObjectClass.Companion.printText()
매번 짝꿍처럼 붙어서 사용하니 Companion 이라는 이름을 붙여봤다. 하지만 매번 이렇게 사용하자니 조금 불편하다. 아에 별도 키워드로 정의해버리면 어떨까?
kotlin — companion object
class CompanionObjectClass {
companion object {
val text = "Hello, World!"
fun printText() {
println(text)
}
}
}
우리가 익히 봐서 잘 알고있던 모습이 되었다. 아시다시피 이 경우 아래처럼 비교적 간단하게 사용할 수 있다.
CompanionObjectClass.text
CompanionObjectClass.printText()
companion object 를 왜 만들었을까에 대해 고민했던 흔적을 두서없이 적다보니 글이 길어졌다. 간단히 정리해보면 다음과 같다.
- 코틀린의 부가적인 기능과 설계 철학을 생각했을 때 자바의
static
이 꼭 필요하지는 않았다. - 대신 별도의 키워드를 만들어 정말로 “클래스와 밀접하게 엮인 정적 기능”만 그 곳에 담을 수 있게 하면서, 자연스럽게 클래스와 관련된 단 하나의 객체로 취급하게 만든 것
다시 생각해보니 static 은 돌려주시지 않아도 될 것 같습니다.