본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 6. 클래스 - static(companion)과 접근 제어자

by 사바라다 2021. 6. 19.

안녕하세요. 오늘은 코틀린 차곡차곡 6번째 시간입니다. 오늘은 이전 시간에 이어서 코틀린 클래스에 대해서 알아보는 3번째 시간이자 마지막 시간입니다. 마지막까지 잘부탁드립니다. :)

정적 클래스 ( static class )

클래스 내부에 정적 클래스 및 변수를 선언하기 위해서는 companion 키워드를 사용하면 됩니다.

companion 클래스를 선언하고 그 내부에 변수 및 함수를 선언하면 이곳에 선언된 요소들은 객체를 만들지 않고도 바로 접근이 가능합니다.

아래코드를 통해 companion object를 좀 더 알아보도록 하겠습니다. 아래 코드는 companion object에 2개의 함수와 1개의 정적 변수를 선언해서 사용하고 있습니다. 이 예제는 정적 함수를 사용하여 객체 생성시에 validation 체크 등을 캡슐화하여 사용할 수 있게 됩니다. 따한 정적 변수 등은 예제에서의 "sabarada"와 같은 magic string 값을 한곳에서 관리할 수 있도록 도와 줄 수 있습니다.

class MappingService private constructor(
    val name: String? = DEFAULT_NAME
) {

    companion object {

        fun create(): MappingService = MappingService()


        fun create(name: String): MappingService = MappingService(name)


        const val DEFAULT_NAME: String = "sabardada"
    }
}

위 클래스는 아래 처럼 사용할 수 있습니다. companion object의 기본 접근 제어자(modifier)는 public 입니다. 따라서 아래처럼 사용할 수 있습니다.

val mappingService_1 = MappingService.create()
println("mappingService_1 = ${mappingService_1.name}") // 결과 : mappingService_1 = sabardada

val mappingService_2 = MappingService.create("SABARADA")
println("mappingService_2 = ${mappingService_2.name}") // 결과 : mappingService_1 = SABARADA

println("mappingService_DEFAULT_NAME = ${MappingService.DEFAULT_NAME}") // 결과 : mappingService_DEFAULT_NAME = sabardada

추가로 companion object에 별도의 이름을 추가할 수도 있습니다. 사용하는 입장에서는 이름을 명시적으로 MappingService.Factory.create() 처럼 언급해도되며 MappingService.create() 이런식으로 생략해도 괜찮습니다.

companion object Factory {
    ...
}

companion object에 선언했을 때 java 코드로는 어떻게 나오는지 한번 decompile 해보도록 하겠습니다. const val로 선언한 변수 부분은 public static final로 함수로 선언했던 부분은 실재로 내부 클래스로 하여 구현이 되어지는것을 확인할 수 있었습니다.

public final class MappingService {

   @Nullable
   private final String name;
   @NotNull
   public static final String DEFAULT_NAME = "sabardada";
   @NotNull
   public static final MappingService.Companion Companion = new MappingService.Companion((DefaultConstructorMarker)null);

   @Nullable
   public final String getName() {
      return this.name;
   }

   private MappingService(String name) {
      this.name = name;
   }

   // $FF: synthetic method
   MappingService(String var1, int var2, DefaultConstructorMarker var3) {
      if ((var2 & 1) != 0) {
         var1 = "sabardada";
      }

      this(var1);
   }

   // $FF: synthetic method
   public MappingService(String name, DefaultConstructorMarker $constructor_marker) {
      this(name);
   }


   public static final class Companion {
      @NotNull
      public final MappingService create() {
         return new MappingService((String)null, 1, (DefaultConstructorMarker)null);
      }

      @NotNull
      public final MappingService create(@NotNull String name) {
         Intrinsics.checkNotNullParameter(name, "name");
         return new MappingService(name, (DefaultConstructorMarker)null);
      }

      private Companion() {
      }

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

 

컴파일 타임 상수(compile-time constants)

const 키워드를 통해 컴파일 타임 상수를 만들어낼 수 있습니다. 컴파일 타임 상수는 class 명세가 method 메모리 영역에 들어갈 때 함께 초기화 되어 객체를 만들지 않아도 사용할 수 있는 상수입니다. 코틀린에서 이 상수는 3가지 조건을 만족시켜 만들 수 있습니다.

  1. Top-level 또는 object 선언 또는 companion object
  2. String 또는 primitive type
  3. getter 함수가 없음

위 조건을 만족시키는 컴파일 타임 상수들은 아래와 같습니다. 첫번째는 companion object를 클래스 내부에 선언 후 그 내부에 const val을 사용하는 방법입니다.

class MappingService {
    companion object {
        const val DEFAULT_NAME: String = "sabardada"
    }
}

두번째는 아래 코드처럼 클래스밖에 top-level에 const val을 선언하는 방법입니다. top-level에 관해서는 별도의 포스팅으로 찾아뵙도록 하겠습니다.

const val DEFAULT_NAME_2: String = "sabardada"

class MappingService private constructor(
    val name: String? = DEFAULT_NAME_2
)

마지막 방법은 object 클래스로 선언하여 그 안에 const val를 선언하는 방법입니다. object로 클래스를 선언하면 SingleTon으로 객체가 생성되어집니다.

object MappingBaseService {
    const val DEFAULT_NAME_3: String = "sabardada"
}

접근 제어자 (Modifier)

클래스의 내부에는 여러 요소가 있는데 이들은 각각 접근제어자(Modifier)를 가질 수 있습니다. 접근제어자란 해당 요소를 접근할 수 있는 권한을 어디까지 허용하냐입니다. 코틀린에서는 private, protected, internal, 그리고 public의 4가지 접근제어자를 가질 수 있습니다.

각각은 접근제어자에 대한 설명은 아래와 같습니다.

  • private - 클래스 외부에서 접근이 불가능합니다.
  • protected - 상속관계에 있는 외부에서는 접근이 가능합니다.
  • internal - 한 모듈안에서 라면 접근이 가능합니다.
  • public - 어디서든 접근이 가능합니다.

아래는 kotlin 공식 문서에 나와있는 예제입니다.

open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // 기본값 : public

    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a는 private이기 때문에 참조 불가
    // b, c, d, e 는 참조 가능

    override val b = 5   // 'b' 는 protected로 오버라이딩 가능
}

class Unrelated(o: Outer) {
    // o.a, o.b 는 접근 불가능
    // o.c 와 o.d 는 접근 가능 (같은 module)
    // Outer.Nested 그리고 Nested::e 는 접근 불가능
}

마무리

오늘도 이렇게 코틀린 클래스의 기본에 대해서 알아보는 시간을 가져보았습니다.

오늘까지 3개의 포스팅을 통해 코틀린 클래스의 기본을 마무리합니다. 아직 못다한 많은 부분이 있지만 그부분은 추후에 다른 이름으로 다시 찾아뵙도록 하겠습니다.

다음 포스팅은 코틀린의 안전한 코딩 기법중 하나인 NPE(NullPointException) 처라하기에 대해서 알아보도록 하겠습니다.

참조

코틀린 인 액션 (Kotlin In Action)

코틀린을 다루는 기술 (The Joy Of Kotlin)

kotlinlang_object-declarations-overview

댓글