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

[kotlin] 코틀린 차곡차곡 - 4. 클래스 - 기본 구성요소

by 사바라다 2021. 6. 13.

 

안녕하세요. 코틀린 차곡차곡 4번째 시간입니다. 오늘은 코틀린 클래스의 기본에 대해서 알아보는 시간을 가져보도록 하겠습니다. 클래스는 내용이 많기 때문에 한번에 모든 것을 설명드리는 것은 힘들것이라고 생각하며 2번에 나눠서 알아보는 시간을 가져보도록 하겠습니다.

클래스 (Class)

클래스란 객체를 정의하는 틀 또는 설계라는 의미를 가지고 있습니다. 이러한 개념은 여타 언어와 마찬가지로 코틀린도 동일합니다. 아래에서 코틀린 클래스의 내부 구성을 보도록 하겠습니다.

  • 클래스 선언 키워드
    • class 키워드를 통해 클래스를 선언할 수 있습니다.
  • 클래스 이름
    • 클래스 이름을 쓰며 일반적으로 파스칼케이스(PascalCase)를 이름 규칙으로 가져갑니다.
  • 기본 생성자
    • 클래스 이름 옆에 생성자 키워드 constructor을 넣어 생성자를 선언할 수 있습니다.
    • 기본적인 접근제어자는 public 이며 public으로 사용할 때는 constructor을 생략하고 사용할 수 있습니다.
  • 멤버변수
    • val 또는 var 키워드를 통해 내부 변수를 선언할 수 있습니다.
  • 초기화 블럭
    • 객체가 생성될때 초기화 블럭이 실행됩니다.
    • 실행 순서는 위에서 아래로 차례대로 실행되며 init 블럭에서는 해당 블럭보다 위에 선언되어있는 멤버변수, 그리고 생성자 변수만을 사용할 수 있습니다.
  • 내부 함수
    • 클래스의 상태 업데이트 등을 캡슐화하여 사용하기 위해 클래스 내부에 함수를 선언할 수 있습니다.

아래는 위 클래스 이미지의 원본 코드입니다.

class NameService(name: String) {

    private val firstProperty = "First property: $name"

    init {
        println("First initializer block that pr-ints $name")
    }

    fun printName(): String = this.firstProperty
}

또한 이렇게 만들어진 클래스는 아래와 같이 사용할 수 있습니다. 코틀린에서는 객체를 생성할 때 new 키워드를 사용하지 않습니다.

NameService("sabarada") // 결과 : First initializer block that prints 매핑

생성자 (constructor)

코틀린의 생성자에는 기본 생성자(primary constructor)와 보조 생성자(secondary constructors)가 있습니다. 위의 클래스에서 보았듯이 기본 생성자는 클래스 선언부(Head)에 위치하게 됩니다.

class NameService constructor(name: String) { ... }

기본적으로 public의 접근 제어자를 가지게되며 이 경우에는 생성자 키워드인 constructor를 생략하고 클래스 이름 바로 뒤에 ()를 통해 바로 선언할 수도 있습니다.

class NameService(name: String) { ... }

이어서 보조 생성자에 대해서 알아보도록 하겠습니다. 보조 생성자는 아래와 같이 클래스 body 내부에서 constructor 키워드를 통해 생성해 낼 수 있습니다. 또한 기본 생성자가 있으면 반드시 아래처럼 : this() 키워드를 통해 기본 생성자를 호출해 주어야합니다.

class MappingService(name: String) {

    constructor(value: Int) : this(value.toString()) {
        println("Secondary constructor block that prints $value")
    }
}

그렇다면 호출 순서는 어떻게 될까요? 아래의 코드로 테스트 해보도록 하겠습니다.

class NameService constructor(name: String) {

    constructor(value: Int) : this(value.toString()) {
        println("Secondary constructor block that prints $value")
    }

    private val firstProperty = "First property: $name"

    init {
        println("First initializer block that prints $name")
    }

    fun printName(): String = this.firstProperty

}

결과를 확인해보도록 하겠습니다. 결과는 아래와 같습니다. initializer block이 먼저 실행되며 보조 생성자는 그 이후에 생성되는 것을 확인할 수 있었습니다. 즉, 보조 생성자를 호출하면 내부에서 기본생성자를 호출하여 클래스의 초기화를 합니다. 그 후 보조 생성자 block을 실행한다는 사실을 알 수 있었습니다.

First initializer block that prints 5
Secondary constructor block that prints 5

초기화 블럭(initializer blocks)

기본 생성자는 클래스의 헤드에 들어갑니다. 보시면 아시겠지만 여게에는 블럭이 없어서 어떠한 코드도 포함할 수 없게 되어있습니다. 따라서 이런 역할을 해줄 별도의 블럭 이 필요할 수 있습니다. 그것이 바로 초기화 블럭(initializer blocks)입니다.

이런 초기화블럭은 아래와같이 사용할 수 있습니다. 초기화 블럭과 맴버변수는 아래에서 위를 참조하는 것은 불가능하고 아래에서 위를 참조하는 것은 가능하므로 주의가 필요합니다.

class MappingService constructor(name: String) {

    private val firstProperty = name

    init {
        println("First initializer block that prints $name")
    }
}

맴버 변수 (Properties)

코틀린 클래스에서 맴버 변수 var 또는 val 키워드로 선언할 수 있습니다. var 키워드로 선언하면 읽기와 쓰기가 가능하지만 val 키워드로 선언하면 이후 읽기만 가능합니다.

class MappingService constructor(name: String) {
    val firstProperty: String = name
}

또한 기본 생성자에서 var과 val 키워드를 붙일 수가 있으면 이렇게하면 해당 property를 생성하는것과 동일한 효과를 가집니다. 즉 아래와 같이 선언하면 맴버변수로 val firstProperty: String = firstProperty가 묵시적으로 추가되어있다고 생각하시면 됩니다.

class MappingService(val firstProperty: String){

}

위의 2개의 예제 모두 아래의 코드로 실행했을 때 동일한 결과를 줍니다.

val mappingService = MappingService("H")
println("firstProperty = ${mappingService.firstProperty}") // 결과 : firstProperty = H

그렇다면 여기서 의문점이 하나 듭니다. 이렇게 선언하면 객체 변수에 직접 접근을 해서 사용하는 걸까요 ? 그렇지 않습니다. val와 var로 선언된 맴버변수에는 보이지 않는 암묵적으로 선언되어있는 부분이 하나 있습니다. 바로 getter와 setter입니다. property가 가질 수 있는 full syntax는 아래와 같습니다.

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

var로 선언할 경우 컴파일시 기본(default) getter와 setter 메서드가가 생깁니다. 아래는 위의 코틀린 예제를 bytecode로 만든 후 java로 decompile했을 때 나오는 결과입니다. val 멤버변수 firstProperty에 대해서 getter 함수가 자동적으로 만들어진 것을 확인할 수 있었습니다.

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 4, 2},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0004\u0018\u00002\u00020\u0001B\r\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004R\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006¨\u0006\u0007"},
   d2 = {"Lpersonal/project/lighthouse/mapping/service/NameService;", "", "firstProperty", "", "(Ljava/lang/String;)V", "getFirstProperty", "()Ljava/lang/String;", "lighthouse.main"}
)
public final class NameService {
   @NotNull
   private final String firstProperty;

   @NotNull
   public final String getFirstProperty() {
      return this.firstProperty;
   }

   public MappingService(@NotNull String firstProperty) {
      Intrinsics.checkNotNullParameter(firstProperty, "firstProperty");
      super();
      this.firstProperty = firstProperty;
   }
}

이 것은 아래처럼 커스터마이징 할 수 있습니다. 각각 get() / set() 메서드를 통해 커스터마이징할 수 있습니다.

var firstProperty: String
get() = this.toString()
set(value) {
    setDataFromString(value) // parses the string and assigns values to other properties
}

함수

클래스 내부에서 함수를 정의해서 사용할 수 있습니다. 함수에 관련한 내용은 이전 [kotlin] 코틀린 차곡차곡 - 3. 함수 시간에 자세하게 설명을 드렸었습니다. 해당 부분을 참고해주시면 감사하겠습니다. 추가로 함수에서는 같은 클래스 내부의 어떠한 맴버변수에 대해서 접근 가능하다라는 사실만 기억해주시면 좋을것 같습니다.

마무리

오늘은 이렇게 코틀린의 클래스에 대해서 알아보았습니다. 이번 시간에는 클래스에 대한 아주 기초적인 부분입니다.

다음시간에 한번더 클래스에 대해서 다양하게 알아보도록 하겠습니다.

감사합니다.

참조

코틀린 인 액션 (Kotlin In Action)

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

kotlinlang_control-flow

댓글