interface와 abstract class(인터페이스와 추상클래스)

  1. 인터페이스란?
  2. 인터페이스는 어떨 때 사용할까?
  3. 추상클래스란?
  4. 인터페이스랑 뭐가 달라..?
  5. 결론
  • Kotlin docs에 인터페이스를 읽어보면 다음과 같이 궁금증을 해결해준다

    What makes them different from abstract classes is that interfaces cannot store state. They can have properties, but these need to be abstract or provide accessor implementations.

  • 추상 클래스와 다른 점은 인터페이스가 상태를 저장할 수 없다는 것입니다. 속성을 가질 수 있지만 추상이거나 접근자 구현을 제공해야 합니다.

인터페이스란?

  • Kotlin의 인터페이스는 추상 메서드 선언과 메서드 구현을 포함할 수 있습니다. - docs
  • 이는 추상 메서드와 디폴트 메서드를 가질 수 있는 Java 8 버전의 인터페이스와 같다. 이러한 디폴트 메서드는 재정의 또한 가능하다.
  • 뿐만 아니라 프로퍼티 또한 선언이 가능하다.
  • 인터페이스는 ‘다중 상속’이 가능하다.

인터페이스는 ‘interface’ 키워드를 통해 생성 가능하다.

interface InterfaceTest {
    // 프로퍼티 선언
    val a: Int

    // 메서드 선언
    fun add(a: Int, b: Int): Int

    // 디폴트 메서드
    fun printA() = println(a)
}

인터페이스는 어떨 때 사용할까?

‘인터페이스의 특징인 메서드 선언이 언제 필요할까?’를 생각해보면 된다. 블랙잭 게임에서는 플레이어와 딜러라는 참가자가 존재한다. 이 참가자들의 상태에는 Bust, Hit, Stay, Blackjack 등이 있다. 이러한 상태는 딜러냐 플레이어냐에 따라 다르게 해석되기도, 혹은 같은 의미로 해석되기도 한다. 그러면 우리는 딜러 class와 플레이어 class에서 각각 구현을 해줘할까??? 그럼 너무 불편할 것이다. 같은 의미의 일을 수행하는데 다른 로직으로 처리해야할 때, 사용하면 좋은 것이 interface이다.

interface Participant {
    fun isHit(): Boolean        // 무조건 구현 해줘야 한다.
    ....
}
class Dealer(cards: List<Card>): Participant {
    override fun isHit(): Boolean = cards.sum() <= 16   // 딜러는 카드 합이 16 이하면 카드를 뽑을 수 있는 상태다
    ....
}
class Player(cards: List<Card>): Participant {
    override fun isHit(): Boolean = cards.sum() <= 21   // 딜러는 카드 합이 21 이하면 카드를 뽑을 수 있는 상태다
    ....
}

하지만 인터페이스는 상태를 가질 수 없다. 그렇기에 Participant에는 cards라는 상태를 갖고있지 않다. 추상화를 했는데 결국엔 그것을 구현하는 Dealer와 Player 클래스의 cards를 접근하고 있게 된다. 그러면 어떻게 해야할까?

추상클래스란?

  • 추상 클래스는 구현되지 않고 선언만 된 추상메서드를 가지고 있는 클래스이다.
  • 이러한 추상 클래스는 인터페이스와 마찬가지로 프로퍼티, 디폴트 메서드를 가질 수 있다.
  • 추상 클래스는 ‘abstract class’ 키워드를 클래스명 앞에 붙여 사용하며, 추상 프로퍼티 및 메서드를 선언할 때는 앞에 ‘abstract’ 키워드를 붙이면 된다.
abstract class AbstractTest {
    // 프로퍼티
    abstract val a: Int
    val b = 3

    // 메서드 선언
    abstract fun add(a: Int, b: Int): Int

    // 디폴트 메서드
    fun printA() = println(a)
}

인터페이스랑 뭐가 달라..?

앞서 얘기 나눈 인터페이스의 Kotlin docs 정의를 보면 ‘추상 클래스와 다른 점은 인터페이스가 상태를 저장할 수 없다는 것입니다. 속성을 가질 수 있지만 추상이거나 접근자 구현을 제공해야 합니다.’라고 나와있다.

‘엥? 인터페이스에 상태값을 가질 수 없다고? 아래와 같이 하면 되던데??’

interface InterfaceTest {
    // 프로퍼티
    val a: Int
        get() = 3
}

위 인터페이스는 정말 상태를 가진다고 할 수 있을까? 위 코드를 디컴파일 해보면 다음과 같이 나온다

public interface InterfaceTest {
   int getA();

   @Metadata(
      mv = {1, 8, 0},
      k = 3
   )
   public static final class DefaultImpls {
      public static int getA(@NotNull InterfaceTest $this) {
         return 3;
      }
   }
}

코드를 잘 볼 줄 모르지만, 대충 인터페이스의 a 프로퍼티는 getA라는 디폴트 메서드를 통해 3이라는 값을 갖고 있는 것처럼 보여진다. 즉, a는 상태처럼 보이지만 상태가 아니며, 인터페이스는 위와 같은 방법(디폴트 메서드)으로 속성을 가질 수 있다.

그러나,

추상 클래스는 이와 반대로 상태값을 가질 수 있다. 이는 생성자 또한 가질 수 있다는 의미로 해석 해볼 수 있다.

abstract class AbstractTest {
    // 프로퍼티
    val b = 3
}
public abstract class AbstractTest {
   private final int b = 3;

   public final int getB() {
      return this.b;
   }
}

인터페이스와는 달리 온전히 b라는 변수가 3이라는 값을 가지는 것을 알 수 있다.

그럼 차이점은 이뿐일까??

인터페이스는 기본적으로 다중 상속이 가능하다. 그러나 추상 클래스는 단일 상속만 가능하다. 뿐만 아니라, 인터페이스는 디폴트 메서드를 하위 클래스에서 재정의 할 수 있으나, 추상 클래스에선 하위 클래스에서 디폴트 메서드를 재정의 할 수 없다.

결론

이러한 차이점을 알고, 본인이 작성하고자 하는 코드에 맞게 적절히 사용하는 것이 올바른 쓰임일 것 같다.

글 내용 중 틀린 부분이 있다면 댓글 남겨주세요 :D