[Java] 12. 인터페이스(Interface)


Study/Java  2020. 5. 11. 11:08

안녕하세요. 명월입니다.


이 글은 Java에서 인터페이스(Interface)를 사용하는 방법에 대한 글입니다.


이전에 클래스와 클래스 상속에 대해서 설명한 적이 있습니다.

링크 - [Java] 08. 클래스의 상속과 this, super 키워드 사용법


일단 상속의 개념은 클래스의 모든 기능을 부여받고 그 기능을 추가 확장하는 것입니다. 그런데 Java에서는 다중 상속, 즉 연관성이 없는 두 개의 클래스를 상속받을 수 없습니다.

아마 이유는 함수의 모호점 때문인 것 같습니다. C/C++에서 자주 발생하던 문제인데.. Java에서는 다중 상속을 아예 막로써 함수의 모호점을 막은 것 같습니다.

// 상위 클래스 AClass
class AClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("AClass func");
  }
}
// 상위 클래스 BClass
class BClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("BClass func");
  }
}
// 클래스, AClass와 BClass를 상속받는다.(실제 이렇게 작성하면 IDE툴에서 에러가 발생함)
public class Example extends AClass, BClass {
  // 함수
  @Override
  public void func() {
    // 상위 클래스 호출
    super.func();
  }
}

위 예제는 실제 이클립스에 작성하면 컴파일 에러가 발생할 것입니다.

Example 클래스를 보면 AClass와 BClass 양쪽을 상속 받고 func 함수를 재정의(Override)했습니다. 재정의까지는 좋은데 func함수 안에 상위 func함수를 호출하는 부분이 있는데.. 이게 어떤 클래스의 func를 가르키는 지 알 수가 없습니다.

C/C++에서는 가장 늦게 로딩되는 함수를 호출합니다. 근데 이것을 어떤게 가장 늦게 로딩되는 지를 알기가 힘들다는 문제점이 있습니다.


이 문제를 해결하기 위해, Java에서는 다중 상속을 애초에 못하게 막은 듯합니다.


그러나 객체 지향으로 넘어오면서, 객체 지향의 가장 큰 특성이 많은 데이터와 객체를 분류하고 리스트화 할 수 있다는 것입니다.

즉, AClass는 AClass끼리, BClass는 BClass끼리 하나의 리스트에 분류해서 관리를 할 수 있는 것입니다.

import java.util.ArrayList;
import java.util.List;
// 상위 클래스 AClass
class AClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("AClass func");
  }
}
// 상위 클래스 BClass
class BClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("BClass func");
  }
}
// AClass를 상속받은 클래스
class CClass extends AClass {
  // 함수
  @Override
  public void func() {
    // 콘솔 출력
    System.out.println("CClass func");
  }
}
// BClass를 상속받은 클래스
class DClass extends BClass {
  // 함수
  @Override
  public void func() {
    // 콘솔 출력
    System.out.println("DClass func");
  }
}
// 실행 클래스 
public class Example {
  // 실행 함수
  public static void main(String... args) {
    // AClass 리스트
    List<AClass> aClassList = new ArrayList<>();
    // BClass 리스트
    List<BClass> bClassList = new ArrayList<>();
    // AClass리스트에 AClass를 할당해서 넣는다.
    aClassList.add(new AClass());
    // CClass는 AClass를 상속받았기 때문에 AClass취급을 할 수 있다.
    aClassList.add(new CClass());
    // BClass리스트에 BClass를 할당해서 넣는다.
    bClassList.add(new BClass());
    // DClass는 BClass를 상속받았기 때문에 BClass취급을 할 수 있다.
    bClassList.add(new DClass());
    // 리스트의 아이템을 취득
    for (int i = 0; i < aClassList.size(); i++) {
      // 아이템 취득
      AClass cls = aClassList.get(i);
      // 콘솔 출력
      cls.func();
    }
    // 리스트의 아이템을 취득
    for (int i = 0; i < bClassList.size(); i++) {
      // 아이템 취득
      BClass cls = bClassList.get(i);
      // 콘솔 출력
      cls.func();
    }
  }
}

먼저 여기서 이해해야 할 것은 AClass와 CClass간의 관계와 BClass와 DClass간의 관계입니다.

먼저 CClass는 AClass를 상속받았습니다. 예전에 Stack과 Heap의 관계에 대해 설명한 적이 있습니다.

링크 - [Java] 10. 메모리 할당(stack 메모리와 heap 메모리 그리고 new)과 Call by reference(포인터에 의한 참조)

먼저 우리가 클래스를 선언할 때, 변수명과 클래스 할당을 같이 사용합니다.

AClass a = new AClass();

예전에 설명한 대로 AClass a는 stack 메모리에 들어가고 new AClass는 heap 메모리에 할당됩니다.

여기서 앞에 변수에 선언된 타입과 할당된 타입을 맞추어야 하는데 조건이 있습니다.

1. 클래스와 동일한 데이터 타입이면 가능합니다.

2. 클래스보다 상위 클래스나 인터페이스도 정의가 가능합니다.


즉, 위 CClass의 경우는 AClass c = new CClass();가 가능하다는 뜻입니다. 이유는 일단 CClass는 AClass를 모두 상속받았기 때문에 AClass의 모든 기능과 메모리 구조를 가지고 갑니다.

그래서 AClass로 변수를 지정하고 함수를 호출에도 문제가 없습니다. (참고로, 자바에서는 모든 클래스가 Object를 묵시적으로 상속받았기 때문에, 원시 데이터(Primitive type)을 제외하고 모든 데이터가 Object로 선언할 수 있습니다.)

그래서 위의 예제처럼 AClass와 BClass를 list로 분류할 수 있는 것입니다.


근데, 구조를 보면 AClass와 BClass는 같지만 AClass a = new DClass()는 불가능합니다.

이유는 구조가 사람이 보기에만 같은 것이지, 사실 AClass와 DClass는 서로 연관 관계가 없습니다. 물론 AClass와 DClass는 Object로를 상속 받았기 때문에 Object로는 묶을 수 있습니다.

그러나 Object타입은 func함수가 없기 때문에 Object 변수로는 func함수를 호출할 수 없습니다.


그러나 AClass, BClass는 차이를 두더라도 AClass와 DClass를 묶고 싶다. 그럴 때 인터페이스를 맞추면 가능합니다.

import java.util.ArrayList;
import java.util.List;
// 인터페이스
interface IClass {
  // func함수 정의
  public void func();
}

// 상위 클래스 AClass
class AClass implements IClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("AClass func");
  }
}

// 상위 클래스 BClass
class BClass {
  // 함수
  public void func() {
    // 콘솔 출력
    System.out.println("BClass func");
  }
}

// AClass를 상속받은 클래스
class CClass extends AClass {
  // 함수
  @Override
  public void func() {
    // 콘솔 출력
    System.out.println("CClass func");
  }
}

// IClass를 상속받은 클래스
// BClass를 상속받은 클래스
class DClass extends BClass implements IClass {
  // 함수
  @Override
  public void func() {
    // 콘솔 출력
    System.out.println("DClass func");
  }
}

// 실행 클래스 
public class Example {
  public static void main(String... args) {
    // IClass를 변수로.
    List<IClass> IClassList = new ArrayList<>();
    // AClass 할당
    IClassList.add(new AClass());
    // BClass 할당
    IClassList.add(new CClass());
    // DClass 할당
    IClassList.add(new DClass());
    // 리스트의 아이템을 취득
    for (int i = 0; i < IClassList.size(); i++) {
      // 아이템 취득
      IClass cls = IClassList.get(i);
      // 콘솔 출력
      cls.func();
    }
  }
}

위 예제를 보면 DClass는 AClass와 IClass를 상속받았습니다. AClass는 IClass를 상속받았습니다.

즉, DClass와 AClass는 IClass를 상속받고 IClass는 func함수가 정의되어 있으니, list에서 IClass로 정의하고 각 클래스를 할당합니다.

콘솔 출력을 하면 차례로 AClass, BClass, CClass의 결과가 나옵니다.


여기서 인터페이스의 특징은 new를 통해 클래스를 할당(인스턴스 생성)을 할 수 없습니다. 오직 Stack 영역의 변수명으로만 사용됩니다.

인터페이스는 멤버 변수를 설정할 수 없고, 함수의 경우도 함수명까지만 정의할 수 있고 함수의 stack 영역, 즉 실행 코드를 작성하지 않습니다.

즉, 실행 코드가 작성되지 않으니 인터페이스는 다중 상속을 해도 문제가 없습니다. 함수명만 정의되어 있으니 어떤 것을 실행할지는 extends로 상속한 상위 클래스나 할당한 클래스를 실행하면 되기 때문입니다.

인터페이스를 상속 받을 때는 implements 키워드를 사용합니다. 일반적인 클래스 상속은 extends를 사용합니다.


여기서 하나 더 알 수있는 것은 List와 ArrayList, LinkedList의 관계입니다.

링크 - [Java] 05. 배열과 List, Map의 사용법

List는 인터페이스입니다. 그리고 ArrayList와 LinkedList는 클래스의 부분입니다. 즉, List로 변수를 선언하고 ArrayList로 할당하여 사용한 것입니다.


여기까지 Java에서 인터페이스(Interface)를 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.