본문 바로가기

프로그래밍 언어/자바

[Java] 인터페이스

학습할 것

  • 인터페이스 정의하는 방법
  • 인터페이스 구현하는 방법
  • 인터페이스는 Object인가?
  • 인터페이스 레퍼런스를 통해 구현체를 사용하는 방법
  • 인터페이스 상속
  • 인터페이스의 기본 메소드 (Default Method), 자바 8
  • 인터페이스의 static 메소드, 자바 8
  • 인터페이스의 private 메소드, 자바 9

 

인터페이스란?

추상적인 표현을 사용해 클래스의 행동을 정의해 놓는 녀석이다. 클래스의 청사진이라고 할 수 있다. 인터페이스는 abstract methods와 variables를 가지고 있을 수 있다. 그러나 메소드의 바디는 가질 수 없다.

 

인터페이스는 일종의 계약이다. 여러 개발자들이 작업을 수행할 때, 다른 개발자의 코드가 완성될 때까지 기다릴 수 없다. 또한, 다른 개발자가 어떻게 개발했는지 모르더라도 자신이 개발하는 것에 지장이 없어야 한다. 이를 위해 인터페이스를 사용한다.

 

인터페이스에 정의된 모든 변수들은 final, public and static 하다. 코드에 명시하지 않더라도, 컴파일러가 자동으로 위와 같이 만든다.

→ 인터페이스의 변수는 static 이므로, 동일한 인터페이스를 구현한 클래스들은 해당 변수를 공유하는가? 그렇다. 하지만 문제는 없다. 왜냐하면 final이기 때문에 수정이 불가능하기 때문이다.

 

인터페이스를 사용하는 이유는?

  • 추상화를 구현해내기 위해
  • 자바에서는 클래스의 다중 상속을 지원하지 않기 때문에, 인터페이스를 통해 다중 상속이 가능하다.
  • 느슨한 결합을 구현하기 위해
  • 그렇다면 추상클래스가 있는데 왜 인터페이스를 사용할까?
    추상 클래스는 non-final 변수를 가지고 있을 수 있기 때문이다.
    → 추상 클래스는 다중 상속이 불가능 한가? oo
    → 추가적으로, 인터페이스는 모든 메소드가 추상 메소드인 반해, 추상 클래스는 일부 메소드만 추상 메소드일 수 있다.

 

인터페이스 정의하는 방법

인터페이스를 정의하기 위해서 interface 키워드를 사용한다.

interface <interface_name>{
    // declare constant fields
    // declare methods that abstract
    // by defalut
}

인터페이스 소스코드와 바이트코드 비교

컴파일러는 자동으로 필드에 public, static, final 키워드를 부여한다. 또한, 메소드에는 public과 abstract 키워드를 부여한다.

    // class version 52.0 (52)
    // access flags 0x601
    public abstract interface compiler/test/Printable {

      // compiled from: Printable.java

      // access flags 0x19
      public final static I MIN = 5

      // access flags 0x401
      public abstract print()V
    }

 

 

인터페이스는 Object인가?

답변은 아니다. 의문이 들 수 있다. 그렇다면 인터페이스 변수는 어떻게 Object의 메소드를 사용할 수 있는 것일까?

"인터페이스는 인터페이스만 상속받을 수 있다."  라는 조건에 의해, 인터페이스는 (클래스와는 다르게) 암묵적으로 Object 클래스를 상속받지 않는다. 필자는 JLS(Java Language Specification 9.2(Interface Members)) 에서 해답을 찾았다.

 

답변은 다음과 같다. 인터페이스가 extends한 상위 인터페이스가 없을 경우, 암묵적으로 Object에 있는 각각의 메소드들이 해당 인터페이스에 선언된다. 덕분에, 인터페이스는 Object 클래스를 상속받지 않더라도(상속도 불가능 하겠지만) Object 클래스의 메소드를 사용할 수 있는 것이다.

 

If an interface has no direct superinterfaces, then the interface implicitly declares a public abstract member method m with signature s, return type r, and throws clause t corresponding to each public instance method m with signature s, return type r, and throws clause t declared in Object, unless an abstract method with the same signature, same return type, and a compatible throws clause is explicitly declared by the interface. - JLS 9.2

 

 

인터페이스 레퍼런스를 통해 구현체를 사용하는 방법

인터페이스의 래퍼런스는 인터페이스를 구현한 객체를 받을 수 있다. 아래의 경우 Animal 인터페이스를 구현한 Cat, Cow 클래스가 있다. Animal 인터페이스를 사용하여 각 구현체에서 구현한 메소드를 호출하는 것을 확인할 수 있다.
다만, Animal 래퍼런스에서는 Animal 인터페이스에서 정의한 abstract method만 사용할 수 있고, 구현체에서 추가로 정의한 메소드는 접근할 수 없다. 즉, runtime에 실행될 메소드가 결정되는 동적 바인딩이 이뤄지고 있다.

// Animal.java
public interface Animal {
    String CODE = "ANIMAL";
    void eat();
}
// Cat.java
public class Cat implements Animal{
    @Override
    public void eat() {
        System.out.println("Eat fish!");
    }

    public void talk(){
        System.out.println("I'm Cat!");
    }
}
// Cow.java
public class Cow implements Animal{
    @Override
    public void eat() {
        System.out.println("Eat grass!");
    }

    public void talk(){
        System.out.println("I'm cow!");
    }
}
// Main.java
public class Main {
    public static void main(String[] args) {
        Animal animal1, animal2;
        animal1 = new Cat();
        animal2 = new Cow();

        animal1.eat();
        animal2.eat();
//        animal1.talk();
//        animal2.talk();
    }
}

출력
Eat fish!
Eat grass!

 

인터페이스 상속

  • 인터페이스 간의 상속이 가능하다. 상속은 extends 키워드를 사용한다.
  • 예시를 위해 TestImplement ← TestInterface ← ParentInterface1
  • 구조를 따른다고 하자
// ParentInterface1.java
public interface ParentInterface1 {
    void parentInterfaceMethod();
}
// TestInterface.java
public interface TestInterface extends ParentInterface1{
    void childInterfaceMethod();
}
// TestImplement.java
public class TestImplement implements TestInterface {
    @Override
    public void parentInterfaceMethod() {
        System.out.println("Parent Interface Method");
    }

    @Override
    public void childInterfaceMethod() {
        System.out.println("Child Interface Method");
    }
}

 

인터페이스를 구현한 TestImplement 클래스는 자식 인터페이스와 부모 인터페이스의 추상 메소드를 모두 오버라이딩해야 한다.

// main.java
public class Main {
    public static void main(String[] args) {
        TestInterface testInterface = new TestImplement();
        testInterface.childInterfaceMethod();
        testInterface.parentInterfaceMethod();

        ParentInterface1 parentInterface1 = new TestImplement();
        parentInterface1.parentInterfaceMethod();
//        parentInterface1.childInterfaceMethod();
    }
}

 

위 main 메소드에서는 자식 인터페이스 래퍼런스와 부모 인터페이스 래퍼런스에서 구현를 받아 메소드를 호출하고 있다.
자식 래퍼런스는 부모의 메소드를 호출할 수 있는 반면에, 부모 래퍼런스는 자식 래퍼런스의 메소드를 호출할 수 없다.

 

인터페이스가 인터페이스를 상속하는 경우는 다음과 같다.

interface TestInterface{
        void print();
}

public class TestImplement implements TestInterface{
        @Override
        void print(){
                System.out.println("print!")
        }
}

 

시간이 흘러 위에 TestInterface에 메소드를 추가하고 싶어졌다고 하자. 만약 위와 같이 하나의 클래스에서만 해당 인터페이스를 구현했다면 큰 문제가 없다.
하지만, 여러 클래스에서 해당 인터페이스를 구현했을 경우, 그리고 그 클래스들을 수정할 수 없는 경우, 추가된 메소드를 구현체에서 추가 구현해야되는 문제가 발생한다. 이를 해결하기 위해 인터페이스 상속을 사용한다.

interface TestInterface{
        void print();
}

public class TestImplement implements TestInterface{
        @Override
        void print(){
                System.out.println("print!");
        }
}

interface ChildInterface extends TestInterface{
        void move();
}

public class TestImplement2 implements ChildInterface{
        @Override
        void print(){
                System.out.println("print!");
        }

        @Override
        void move(){
                System.out.println("move!");
        }
}

위와 같이 인터페이스를 상속하므로서, 기존 인터페이스를 구현했던 구현체에 영향을 주지 않으면서, 인터페이스를 확장할 수 있게 된다.

 

인터페이스의 기본 메소드 (Default Method), 자바 8

기본 메소드 또한 위와 같이 인터페이스에 메소드를 추가할 때 유용하게 사용된다.

interface TestInterface{
        void print();

        default void move(){
                System.out.println("move!");
        }
}

public class TestImplement implements TestInterface{
        @Override
        void print(){
                System.out.println("print!")
        }
}

default 로 지정하므로서, 구현체는 해당 메소드를 구현할 필요없이 사용할 수 있다. 마치 abstract 클래스에서의 abstract가 아닌 기본 메소드와 동일한 효과를 준다.

default 메소드를 정의한 인터페이스를 상속받은 인터페이스는 해당 default 메소드를 abstract 메소드로 재정의할 수 있다. 유사하게, 부모 인터페이스의 abstract 메소드를 default 메소드로 재정의할 수도 있다.

 

인터페이스의 static 메소드, 자바 8

static 키워드를 사용하여 static 메소드를 인터페이스 내에서 정의할 수 있다. 인터페이스를 구현하는 구현체에서 공통적으로 사용되는 유틸리티를 static 메소드로 정의할 수 있다.

interface TestInterface{
        static void hello(){
                System.out.println("hello!");
        }
}

 

그렇다면, 인터페이스에 static 메소드를 지원하게된 이유는 무엇일까?
지원 이전, 인터페이스와 관련된 유틸리티 기능들을 그룹화하기 위해 별도의 클래스를 생성하였다. 이때 유틸리티 클래스는 정적 메소드들의 집합이기 떄문에 인스턴스 생성이 의미가 없다. 따라서, private 생성자로 외부에서 객체 생성을 막아놓았다. 인터페이스에 static 메소드를 생성가능해지면서, 인터페이스와 관련된 클래스 생성 없이 유틸리티 메소드를 지원할 수 있게 되었다.

참고자료 : https://blog.hexabrain.net/122#%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4

 

인터페이스의 private 메소드, 자바 9

자바9부터

  • private method
  • private static method

를 지원한다.

private method는 다음과 같은 규칙을 따라야 한다.

  1. private interface method는 abstract이어서는 안된다.
  2. private method는 오직 인터페이스 내부에서만 사용가능하다.
  3. private static method는 인터페이스 내부의 다른 static 메소드와 static이 아닌 메소드에서 모두 사용가능하다.
  4. private non-static 메소드는 private static 메소드에서 사용될 수 없다.

 

 


참고자료
- https://www.geeksforgeeks.org/interfaces-in-java/

- https://www.javatpoint.com/interface-in-java

- https://blog.hexabrain.net/122#%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-vs-%EC%B6%94%EC%83%81-%ED%81%B4%EB%9E%98%EC%8A%A4

 

'프로그래밍 언어 > 자바' 카테고리의 다른 글

[Java] 멀티쓰레드 프로그래밍  (1) 2023.11.28
[Java] 예외 처리  (1) 2023.11.28
[Java] 패키지  (0) 2023.11.28
[Java] Enum  (0) 2023.11.21
[Java] 상속  (0) 2023.11.21