Kotlin에는 Static이 존재하지 않는다.

5 분 소요

Static은 어디로 갔을까? 그리고 왜 사라졌을까?


최근 현대 언어들은 static 이 Primitive Type 의 명시적 지원중단 같은 이유로 static을 삭제 했다.

자바의 static 의 경우 필드와 메소드가 그들의 인스턴스가 아닌 클래스를 통해 접근이 가능하다. static 멤버들은 instance 멤버들과 분리되어 있는것이다. 그리고 다른 규칙으로 적용된다.

  Instance methods Static methods
Scope Instance Class
Can override open methods in superclass Yes No (but can shadow them)
Can implement methods in interface Yes No
Dispatch Dynamic, by actual runtime type Static, by type known at compile-time

클래스에서 한편으론 각 인스턴스가 동적으로 행동하고 한편으론 전역에 정적으로 있다는것. 즉 생성자가 합쳐진 두개의 다른개념을 가진다는것은 굉장히 난해하다.

그래서 코틀린에서는 클래스는 인스턴스로의 행동만 정의한다. 정적인 값이나 전역 함수은 클래스 밖으로 분리해서 정의한다.

결국 Kotlin 은 static이 존재 하지 않는다. 대신에 이것들을 활용해야 한다.

  • Top-Level Function
  • Constants
  • Object Instance
  • Companion Objects

Top-Level Function


자바의 경우 클래스 내부에 정의했겟지만 코틀린은 Top-Level Function 을 제공한다. 어디든 .kt 파일에 정의하면 소스가 내부에서 자동 생성된다.

Kotlin

TopLevelFunction.kt

fun topLevelTest() {}

Java

public final class TopLevelFunctionKt {

   public static final void topLevelTest() {
   }
}

자바 내부적으로 파일명Kt 라는 클래스를 만들어 static 변수와 함수를 만들어준다. 이러한 점을 활용해 여러가지에 응용할 수 있다. 예를 들면 유틸 클래스를 대체할수 있다. 확장함수까지 사용하면 더 유용하게 사용할 수 있지만 여기서는 다루지 않는다.

Kotlin

package topLevel

fun lowerCaseCount(value: String): Int = value.count { it.isLowerCase() }

Java

public class StringUtils {
  private StringUtils() { /* Forbid instantiation of utility class */ }   

  public static int lowerCaseCount(String value) {
      return value.chars().reduce(0, 
              (count, current) -> count + (Character.isLowerCase(current) ? 1 : 0));
  }

Top-Level Function 과 프로퍼티는 패키지와 연관되어있다. 즉 2개의 Top-Level Function 와 프로퍼티를 같은 이름으로 같은 패키지의 다른파일에서 생성할수는 없다.

Constants (상수)


프로퍼티 또한 Top-Level 에 선언이 가능하다. 하지만 자바랑 다르게 상수나 변하지 않는 값(val) 만 사용하는걸 권장한다.

Kotlin

TopLevelFunction.kt

const val CONSTANT_STRING = "CONSTANT"
val READONLY_LIST = listOf("value1", "value2")

// NOT OK: avoid public mutable top-Level properties
var mutableValue = "currentValue"
val mutableList = mutableListOf("value1", "value2")

Java

public final class TopLevelFunctionKt {
   @NotNull
   public static final String CONSTANT_STRING = "CONSTANT";
   @NotNull
   private static final List READONLY_LIST = CollectionsKt.listOf(new String[]{"value1", "value2"});
   @NotNull
   private static String mutableValue = "currentValue";
   @NotNull
   private static final List mutableList = CollectionsKt.mutableListOf(new String[]{"value1", "value2"});

   @NotNull
   public static final List getREADONLY_LIST() {
      return READONLY_LIST;
   }

   @NotNull
   public static final String getMutableValue() {
      return mutableValue;
   }

   public static final void setMutableValue(@NotNull String var0) {
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      mutableValue = var0;
   }

   @NotNull
   public static final List getMutableList() {
      return mutableList;
   }
}

만약 Kt가 붙는게 싫으면 package 위쪽에 annotation @file:JvmName: 을 사용하면 된다.

ex) @file:JvmName(“TopLevelFunction”)

Object singleton


코틀린은 Object 선언을 통해 싱글톤 패턴을 지원한다.

Kotlin

object ObjectTest{}

Java

public final class ObjectTest {
   @NotNull
   public static final ObjectTest INSTANCE;

   private ObjectTest() {
   }

   static {
      ObjectTest var0 = new ObjectTest();
      INSTANCE = var0;
   }
}

Object를 선언한것 만으로도 내부적으로 Singleton 인스턴스를 만들어준다. Kotlin 에선 기본적으로 클래스에 open 을 쓰지않으면 final 클래스로 생성되지만 Object는 원래 open 제어자를 붙일수 없게 되어있으므로 기본 final로 생성되고 내부에는 static 초기화 블럭을 사용해 인스턴스를 만들어두고 사용한다. Object 는 클래스나 인터페이스로 확장도 가능하다.

Kotlin

interface Counter {
    fun count(value: String): Int
}

object LowerCaseCounter : Counter { // can implement an interface
    override fun count(value: String) = value.count { it.isLowerCase() }
}
object UpperCaseCounter : Counter { // another implementation of the same interface
    override fun count(value: String) = value.count { it.isUpperCase() }
}

fun main() {
    // Functions on singleton objects can be called like Java static methods
    println(LowerCaseCounter.count("Hello World")) // prints 8
    println(UpperCaseCounter.count("Hello World")) // prints 2
    // But singletons are values and can be assigned to variables and passed as arguments
    val someCondition = true
    val counter = if (someCondition) LowerCaseCounter else UpperCaseCounter
    println(counter.count("Hello Kotlin Everywhere")) // dynamic dispatch
}

Java


// Counter.java
public interface Counter {
   int count(@NotNull String var1);
}

public final class LowerCaseCounter implements Counter {
   @NotNull
   public static final LowerCaseCounter INSTANCE;

   public int count(@NotNull String value) {
      Intrinsics.checkNotNullParameter(value, "value");
      CharSequence $this$count$iv = (CharSequence)value;
      int $i$f$count = false;
      int count$iv = 0;
      CharSequence var5 = $this$count$iv;

      for(int var6 = 0; var6 < var5.length(); ++var6) {
         char element$iv = var5.charAt(var6);
         int var9 = false;
         boolean var11 = false;
         if (Character.isLowerCase(element$iv)) {
            ++count$iv;
         }
      }

      return count$iv;
   }

   private LowerCaseCounter() {
   }

   static {
      LowerCaseCounter var0 = new LowerCaseCounter();
      INSTANCE = var0;
   }
}

public final class UpperCaseCounter implements Counter {
   @NotNull
   public static final UpperCaseCounter INSTANCE;

   public int count(@NotNull String value) {
      Intrinsics.checkNotNullParameter(value, "value");
      CharSequence $this$count$iv = (CharSequence)value;
      int $i$f$count = false;
      int count$iv = 0;
      CharSequence var5 = $this$count$iv;

      for(int var6 = 0; var6 < var5.length(); ++var6) {
         char element$iv = var5.charAt(var6);
         int var9 = false;
         boolean var11 = false;
         if (Character.isUpperCase(element$iv)) {
            ++count$iv;
         }
      }

      return count$iv;
   }

   private UpperCaseCounter() {
   }

   static {
      UpperCaseCounter var0 = new UpperCaseCounter();
      INSTANCE = var0;
   }
}
// TopLevelFunction.java
package kuma.crawler.kotlinstudy;

import kotlin.Metadata;
import kotlin.jvm.JvmName;
@JvmName(
   name = "TopLevelFunction"
)
public final class TopLevelFunction {
   public static final void main() {
      int var0 = LowerCaseCounter.INSTANCE.count("Hello World");
      boolean var1 = false;
      System.out.println(var0);
      var0 = UpperCaseCounter.INSTANCE.count("Hello World");
      var1 = false;
      System.out.println(var0);
      boolean someCondition = true;
      Counter counter = (Counter)LowerCaseCounter.INSTANCE;
      int var4 = counter.count("Hello Kotlin Everywhere");
      boolean var2 = false;
      System.out.println(var4);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

내부에 실제 함수들이 다 구현되어있고 실제 호출은 해당 싱글톤을 통해 만든 실제 Instance.count 로 호출되고 있다.

Companion Objects


Object 선언 자체로도 괜찮지만 만약 자바 클래스의 static 함수가 내부의 private 멤버들에 접근을 하려면 어떻게 해야할까?

Kotlin

class Rocket private constructor() {
    private fun ready() {}

    companion object {
        fun build(): Rocket {
            val rocket = Rocket()  // can call private constructor
            rocket.ready() // can call private Function
            return rocket
        }
    }

    fun main() {
        val rocket = Rocket.build() // Companion function called using the accompanied class name
    }
}

Java

public final class Rocket {
   @NotNull
   public static final Rocket.Companion Companion = new Rocket.Companion((DefaultConstructorMarker)null);

   private final void ready() {
   }

   public final void main() {
      Rocket rocket = Companion.build();
   }

   private Rocket() {
   }

   public Rocket(DefaultConstructorMarker $constructor_marker) {
      this();
   }

   public static final class Companion {
      @NotNull
      public final Rocket build() {
         Rocket rocket = new Rocket((DefaultConstructorMarker)null);
         rocket.ready();
         return rocket;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

private 으로 생성자를 만들경우 해당 Rocket 클래스는 외부에선 생성할 수 없다. 보통 Builder Pattern에서 자주 쓰이는 방식인데 외부에서 직접 해당 클래스의 생성자를 만드는게 아니라 내부 클래스의 build 메소드를 통해 해당 클래스의 인스턴스를 얻는것이다. 여기선 Companion Object를 사용해 내부에 static class 를 구현하고 코틀린상에선 Rocket 으로 접근해도 build 메소드에 접근할수 있다. 물론 실제로는 Rocket.Companion.build() 지만 코틀린자체에선 생략해서 Rocket.build()로 표기하거나 Rocket.Companion.build() 해도 상관없다 으로 붙여도 상관없다. 만약 함수에 @JvmStatic 을 붙일 경우 자바에서도 Rocket.Companion이 아닌 Rocket 으로 바로 붙을 수 있다.

Kotlin

companion object {
        @JvmStatic
        fun build(): Rocket {
            val rocket = Rocket()  // can call private constructor
            rocket.ready() // can call private function
            return rocket
        }
    }

Java

Rocket rocket = Rocket.build();

결론


static 을 제거함으로 Kotlin은 개념이 섞이는걸 피하고 더 강력하고(Object , Companion 객체) 더 적게 표기하지만 같은 기능 (Top-Level Functions And Properties 등)을 사용할 수 있다. 하지만 코틀린을 처음 사용하는 경우 static을 사용하는 유즈케이스 등을 통해 익숙해고 배울 필요가 있다.

참조


https://jelmini.dev/post/from-java-to-kotlin-life-without-static/

https://kotlinlang.org/docs/functions.html#local-functions

https://kotlinlang.org/docs/object-declarations.html#object-declarations-overview

https://kotlinlang.org/docs/object-declarations.html#companion-objects

카테고리: ,

업데이트:

댓글남기기