Kotlin, 어렵지 않게 사용하기 (3) - Object 1

2022. 9. 20. 23:57Spring/Kotlin

kotlin의 객체 사용 방식과 용례를 Java 코드와 비교하며 학습하는 것이 해당 포스팅의 목표입니다.

🔗 Kotlin 시리즈 모아보기

 

안녕하세요. 이번 포스팅에서는 Kotlin의 객체와 관련된 기본 문법을 학습합니다.

Java와의 호환성을 크게 갖기 때문에, Java와 호환하여 정의 및 호출하는 방식을 익히도록 합니다.

 


 

코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다르다.

몇 가지 먼저 이야기 해보자면, 첫 번째로 인터페이스에 프로퍼티 선언이 들어갈 수 있으며

초기화가 필수인 자바와는 달리 코틀린에서는 기본적으로 상태를 갖지 않는다.

두 번째는 자바와 달리 코틀린 선언은 기본적으로 final이며 public이다.

세 번째는 중첩 클래스는 기본적으로는 내부 클래스가 아니다.

즉, 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.

 

이제 하나씩 살펴보자.

 

Interface

✔️  자바 8 인터페이스와 비슷

✔️ 구현이 된 메소드 (= Java의 Default Method)

✔️ 상태(필드) 선언 X, 상태를 저장하지 않음

 

 

자바에서는 extends와 implements 키워드를 사용하지만

코틀린에서는 "Class : Interface" 으로 인터페이스를 구현하고, "Class : (Abstract) Class" 로 확장할 수 있다.

 

interface Clickable {
    fun click()					// 일반 메소드 선언
    fun showOff() = println("I'm clickable!")	// 디폴트 구현이 있는 메소드
}

 

코틀린은 자바 6와 호환되게 설계되었기 때문에 인터페이스의 디폴트 메소드를 지원하지 않는다.

코틀린은 디폴트 메소드가 있는 인터페이스

일반 인터페이스 디폴트 메소드 구현이 정적 메소드로 들어있는 클래스를 조합해 구현한다. 

실제로 컴파일된 .class 파일을 Java 코드로 Decompile 하면 아래와 같다.

 

public interface Clickable {
   void click();

   void showOff();

   public static final class DefaultImpls {
      public static void showOff(@NotNull Clickable this) {
         System.out.println("I’m clickable!");
      }
   }
}

 

위 Clickable 인터페이스를 구현하는 클래스는 click()에 대한 구현을 제공해야 한다.

반면 showOff 메소드의 경우 새로운 동작을 정의할 수도 있고, 생략해서 디폴트 구현을 사용할 수도 있다.

 

interface Focusable {
    fun setFocus(b: Boolean)
    		= println("I ${if (b) "got" else "lost"} focus.")
    fun showOff() = println("I'm focusable!")
}

 

자바 파일로 Decompile 하면 아래와 같다.

 

public interface Focusable {
   void setFocus(boolean b);

   void showOff();

   public static final class DefaultImpls {
      public static void setFocus(@NotNull Focusable this, boolean b) {
         System.out.println("I " + (b ? "got" : "lost") + " focus.");
      }

      public static void showOff(@NotNull Focusable this) {
         System.out.println("I'm focusable!");
      }
   }
}

 

참고로, 인터페이스 멤버에게 본문이 없으면 자동으로 추상 멤버가 되지만, 

그렇더라도 따로 멤버 선언 앞에 abstract 키워드를 덧붙일 필요가 없다.

 

 

Implementation

showOff()를 구현한 두 인터페이스를 구현하면 어떻게 될까?

결론적으로, Compile Error가 발생한다.

 

/* 
    compile error:
    Class 'DoubleButton' must override public open fun showOff(): 
    Unit defined in com.gngsn.kotlindemo.ch4.Clickable 
    because it inherits multiple interface methods of it
*/
class Button : Clickable, Focusable {
    override fun click() = println("Double implement Click")
}

 

코틀린 컴파일러는 두 메소드가 동시에 존재한다면 하위 클래스에서 오버라이드하도록 강제한다.

클래스에서 showOff()를 구현하면 컴파일 에러는 사라지지만, 

만약 구현하고자하는 두 인터페이스의 디폴트 메소드를 호출하고자 하면 아래와 같은 방법이 있다.

 

// 방법 1
class Button : Clickable, Focusable {
    override fun click() = println("Double implement Click")
    override fun showOff() = println("I'm Button!")
}

// 방법 2
class Button : Clickable, Focusable {
    override fun click() = println("Double implement Click")
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

 

위와 같이 super를 사용하여 두 디폴트 메소드를 호출할 수있다.

만약, 하나의 부모 메소드 만을 호출하고 싶다면 해당 부분만 호출하면 된다.

 

 

Access Modifier

: 상속 제어 변경자

 

상속과 관련된 키워드를 살펴보자.

 

  desc
final 오버라이드 X. 클래스 멤버의 기본 변경자
open 오버라이드 O
abstract 오버라이드 강제. 추상 클래스의 멤버에만 붙일 수 있으며, 추상 멤버는 구현된 바디가 있으면 안된다.
override 상위 클래스나 인스턴스의 멤버 오버라이드 중. 기본적으로 open을 가진다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해 야 한다.

 

 

 

override fun

자바와 마찬가지로 클래스는 인터페이스를 개수 제한 없이 구현할 수 있지만, 클래스는 하나만 확장할 수 있다.

자바의 @Override는 코틀린의 override 변경자와 비슷하다.

하지만 @Override는 생략 가능한 반면, override 변경자는 반드시 명시해야 한다.

 

class Button : Clickable {
	override fun click() = println("I was clicked") 
}

 

override 변경자는 실수로 상위 클래스의 메소드를 오버라이드하는 경우를 방지해준다.

상위 클래스에 있는 메소드와 시그니처가 같은 메소드를 우연히 하위 클래스에서 선언하는 경우,

컴파일이 안 되기 때문에 override를 붙이거나 메소드 이름을 바꿔야만 한다.

 

 

 

 

final class/fun (default)

자바의 클래스와 메소드는 기본적으로 상속에 대해 열려있지만,

코틀린의 클래스와 메소드는 기본적으로 final이다.

 

"상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라"
  - Effective Java (Addison-Wesley, 2008)

 

코틀린은 위의 이펙티브 자바와 동일한 철학을 따른다. 

클래스 상속은 문제를 일으킬 수 있는데, 바로 취약한 기반 클래스 fragile base class 이다.

 

👉🏻 취약한 기반 클래스

: 상위 클래스를 변경하면서 하위 클래스가 상위 클래스에 대해 가졌던 가정이 깨져버린 경우 발생하는 문제

 

이를 해결하기 위해 자바에서 final 사용할 수 있다.

자바의 final은 명시적으로 상속을 금지한다는 의미이고, 명시적으로 final을 붙여주어야 한다.

 

어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. 

그와 더불어 오버라이드를 허용하고 싶은 메소드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.

 

 

/* kotlin */
class ClassEx {
    fun defineTest() = println("default access modifier test!")
}

 

위의 코틀린 코드를 자바 코드로 변경하면 아래와 동일하다.

 

/* java */
public final class ClassEx {
   public final void defineTest() {
      System.out.println("default access modifier test!");
   }
}

 

어떤 접근 제어자도 붙이지 않은 상태에서 class와 method에 final이 붙은 것을 확인할 수 있다.

 

📌  Smart Cast
기본적인 클래스 상속 가능 상태를 final로 했기 때문에 스마트 캐스트가 가능하다.

스마트 캐스트의 조건은 타입 검사 뒤에 변경될 수 없는 변수이다.
클래스 프로퍼티의 경우, final 이어야만 스마트 캐스트를 쓸 수 있다.
스마트 캐스트는 val이면서 커스텀 접근자가 없는 경우에만 적용되기 때문이다.

프로퍼티의 기본 값이 final이 아니었다면 하위 클래스에서 프로퍼티를 커스텀 접근자로 정의해서
스마트 캐스트의 요구 사항을 깼을 것이다.

뿐만아니라 기본값을 final로 함으로써 코드를 더 이해하기 쉽게 만든다.

 

 

 

open class/fun

: 오버라이드 가능하게 하는 명시자

상속이 필요한 경우 open 으로 명시해서 허용시킬 수 있다.

 

/* kotlin */
open class RichButton : Clickable {  // extends O 
    fun disable() {}                 // final -> override X
    open fun animate() {}            // override O
    override fun click()             // override O, 오버라이드한 메소드는 기본적으로 열려있다. 
        = println("Double implement Click")
}

 

override 함수의 경우, 그 메소드는 기본적으로 열려있다.

오버라이드하는 메소드를 오버라이드 금지시키려면 해당 메소드 앞에 final을 명시해야 한다.

 

위의 코틀린 코드를 자바 코드로 변경해보자.

 

/* java */
public class RichButton implements Clickable {
   public final void disable() {
   }

   public void animate() {
   }

   public void click() {
      System.out.println("Double implement Click");
   }

   public void showOff() {   // Clickable method
      DefaultImpls.showOff(this);
   }
}

 

 

 

 

abstract class/fun

자바처럼 코틀린에서도 클래스를 abstract로 선언할 수 있다.

 

✔️ 추상 클래스는 인스턴스화 X

✔️ 추상 클래스/메소드 오버라이드 강제

✔️ 추상 멤버는 항상 열려있기 때문에 open 변경자를 명시할 필요가 없다.

 

abstract class Animated {           // 인스턴스 생성 X 
    val duration: Long = 0
    abstract fun animate()          // 추상 함수. 구현 X. 오버라이드 필수
    open fun stopAnimating() {}     // override O
    fun animateTwice() {}           // 기본적으로 final -> override X
}

 

위의 코틀린 코드를 자바 코드로 변경해보자.

 

public abstract class Animated {
   private final long duration;

   public final long getDuration() {
      return this.duration;
   }

   public abstract void animate();

   public void stopAnimating() {
   }

   public final void animateTwice() {
   }
}

 

 

 

 

Visibility modifiers

: 가시성 변경자. default - public  

 

 

Modifier Class memeber Top-level declaration
public (default) Visible everywhere Visible everywhere
internal Visible in a module Visible in a module
protected Visible in a subclass X
private Visible in a class Visible in a class

 

public (default)

가시성 변경자는 기본적으로 자바와 비슷하다.

다만, 자바에서의 default 가시성인 패키지 전용(package-private)은 코틀린에 없다.

코틀린은 패키지를 네임스페이스를 관리하는 용도로만 사용하기 때문이다.

 

internal

: 모듈 내부에서만 볼 수 있음

 

패키지 전용 가시성에 대한 대안으로 코틀린에는 internal이라는 새로운 가시성 변경자가 도입되었다.

 

모듈(module)은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.

모듈 내부 가시성은 모듈의 구현에 대해 진정한 캡슐화를 제공한다는 장점이 있다.

 

자바에서는 패키지가 같은 클래스를 선언하기만 하면,

외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근할 수 있다는 단점이 있다.

때문에 모듈의 캡슐화가 쉽게 깨지며, 이를 보완하기 위해 internal을 사용할 수 있다.

 

 

protected

오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다. 

클래스의 확장 함수는 그 클래스의 private이나 protected 멤버에 접근할 수 없다.

 

protected는 최상위 레벨에서는 정의할 수 없다.

 

 

private

자바와 달리 코틀린에서는 최상위 선언에 대해 private 가시성(비공개 가시성)을 허용한다.

즉, 최상위 선언에는 클래스, 함수, 프로퍼티 등을 private으로 정의할 수 있다는 의미이다.

최상위 선언은 그 선언이 들어있는 파일 내부에서만 사용할 수 있다

 

 

/*
public: 모든 곳에서 볼 수 있다.
internal: 같은 모듈 내에서만 볼 수 있다.
protected: 하위 클래스 내에서만 볼 수 있다. (최상위 선언 불가)
private: 같은 클래스 내에서만 볼 수 있다. (최상위 선언 시, 같은 파일 내)
*/
internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk! ")
}

fun TalkativeButton.giveSpeech() {  // 오류: "public" 멤버가 자신의 "internal" 수신 타입인 TalkativeButton 을 노출함
    yell()                          // 오류: "yell"에 접근할 수 없음: "yell"은 "TalkativeButton"의 "private" 멤버임
    whisper()                       // 오류: "whisper"에 접근할 수 없음: "whisper"는 "TalkativeButton"의 "protected" 멤버임
}

 

public fun giveSpeech --X--> internal open class TalkativeButton

더 낮은 레벨의 가시성은 접근할 수 없다는 기본적인 가시성 제한 규칙이다.

 

 

public class TalkativeButton implements Focusable {
   private final void yell() {
      System.out.println("Hey!");
   }

   protected final void whisper() {
      System.out.println("Let's talk!");
   }

   public void setFocus(boolean b) {
      DefaultImpls.setFocus(this, b);
   }

   public void showOff() {
      DefaultImpls.showOff(this);
   }
}

 

internal 변경자는 바이트코드상에서는 public이 된다.

다른 모듈에 정의된 internal 클래스나 internal 최상위 선언을 모듈 외부의 자바 코드에서 접근할 수 있다.

또한 코틀린에서 protected로 정의한 멤버를 코틀린 클래스와 같은 패키지에 속한 자바 코드에서는 접근할 수 있다

(이는 자바에서 자바 protected 멤버에 접근하는 경우와 같다).

하지만 코틀린 컴파일러가 internal 멤버의 이름을 보기 나쁘게 바꾼다(mangle). 

 

이름을 바꾸는 이유는 두가지가 있다.

1. 한 모듈에 속한 어떤 클래스를 모듈 밖에서 상속한 경우 그 하위 클래스 내부의 메소드 이름이 우연히 상위 클래스의 internal 메소드와 같아져서 내부 메소드를 오버라이드하는 경우를 방지하기 위함

2. 실수로 internal 클래스를 모듈 외부에서 사용하는 일을 막기 위함

 

 

 

 

Inner/Nested Class

코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같다.

이를 내부 클래스로 변경해서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다. 

Java와 다른 점은 코틀린은 외부 클래스가 내부 클래스 나 중첩된 클래스의 private 멤버에 접근할 수 없다는 점이다.

 

 

자바와 코틀린 사이의 차이

클래스 B 안에 정의된 클래스 A Java Kotlin
중첩 클래스
(바깥쪽 클래스에 대한 참조를 저장하지 않음)
static class A class A
 내부 클래스
(바깥쪽 클래스에 대한 참조를 저장함)
class A inner class A

 

 

코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법이 자바와 다르다.

내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer 라고 써야 한다.

 

/* kotlin */
class Outer {

    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }

    class Nested {
        fun getOuterReference(): Outer = Outer()
    }
}

 

자바처럼 코틀린에서도 클래스 안에 다른 클래스를 선언할 수 있다.

클래스 안에 다른 클래스를 선언하면 도우미 클래스를 캡슐화하거나,

코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용하다.

 

자바와의 차이는 코틀린의 중첩 클래스nested class는 명시적으로 요청하지 않는 한

바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다는 점이다.

 

View 요소를 하나 만들고, 그 View의 상태를 직렬화해야 한다고 가정해보자.

뷰를 직렬화하는 일은 쉽지 않지만 필요한 모든 데이터를 다른 도우미 클래스로 복사할 수는 있다.

이를 위해 state 인터페이스를 선언하고 Serializable을 구현한다.

View 인터페이스 안에는 뷰의 상태를 가져와 저장할 때 사용할 getCurrentState와 restoreState 메소드 선언이다.

 

버튼의 상태를 직렬화하면 java.io. NotSerializableException: Button이라는 오류가 발생한다.

직렬화하려는 변수는 Buttonstate 타입의 state였는데 왜 Button을 직렬화할 수 없다는 예외가 발생할까?

자바에서 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스inner class가 된다.

Buttonstate 클래스는 바깥쪽 Button 클래스에 대한 참조를 묵시적으로 포함한다.

그 참조로 인해 Buttonstate를 직렬화할 수 없기 때문에, 버튼에 대한 참조가 Buttonstate의 직렬화를 방해한다.

 

이 문제를 해결하려면 Buttonstate를 static 클래스로 선언해야 한다. 

자바에서 중첩 클래스를 static으로 선언하면 그 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적 인 참조가 사라진다.
코틀린에서 중첩된 클래스가 기본적으로 동작하는 방식은 방금 설명한 것과 정반대다.