4. 인터페이스

인터페이스의 역할

  • 객체의 사용방법을 정의한 타입이다.

  • 개발 코드와 객체가 서로 통신하는 접점 역할을 한다.

    • 개발 코드가 인터페이스의 메소드를 호출하면, 인터페이스는 객체의 메소드를 실행시킨다.

    • 개발 코드는 객체 내부 구조를 몰라도 인터페이스의 메소드만 알면 된다.

  • 개발 코드를 수정하지 않고, 사용하는 객체를 변경할 수 있게 해준다.

  • 자바에서 다형성을 제공하는데 큰 역할을 한다.

인터페이스 선언법

클래스와 같은 네이밍 규칙을 사용하고, .java 파일로 생성하면 된다.

[ public ] interface 인터페이스명 { ... }

인터페이스의 구성요소

인터페이스는 오직 상수(static final)메소드만 갖는다.

인터페이스 메소드의 종류

  • 추상 메소드

  • 디폴트 메소드 (자바 8부터)

  • 정적 메소드 (자바 8부터)

interface 인터페이스명 {
// 상수
타입 상수명 = 값;

// 추상 메소드
타입 메소드명(매개변수, ...)

// 디폴트 메소드
default 타입 메소드명(매개변수, ...) {...}

// 정적 메소드
static 타입 메소드명(매개변수) {...}
}

상수 필드(Constant Field)

인터페이스는 아직 구현체가 없으므로, 인스턴스화가 불가능하다. 그러므로, 인스턴스 필드도 사용 불가능하다. 하지만, 상수 필드는 선언 가능하다. 상수 선언 시에는 반드시 초기값을 대입해야 한다.

추상 메소드(Abstract Method)

리턴 타입, 매개변수, 메소드 이름 메소드의 시그니처만 있는 것이다. 구체적인 구현은 없으며, 실제 실행부는 구현체가 갖고 있다.

디폴트 메소드(Default Method)

사실상 인스턴스의 구현체 메소드가 된다. 자바8부터 디폴트 메소드를 허용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서이다.

정적 메소드(Static Method)

일반적인 클래스의 정적 메소드와 동일하게 인스턴스 없이 메소드 호출이 가능하다.

인터페이스의 상수 필드 선언

인터페이스에서 선언하는 필드는 따로 설정하지 않아도 public static final 키워드가 붙어 자연적으로 상수 필드가 된다.

네이밍 규칙은 일반적인 상수 네이밍과 동일하다.

인터페이스 상수는 static { ... } 블록으로 초기화할 수 없기 때문에, 초기 값이 반드시 들어가야 한다.

public interface RemoteControl {
  int MAX_VOLUME = 10;
  int MIN_VOLUME = 0;
}

인터페이스 필드에 위와 같이만 선언해도 public static final이 자동으로 붙은 효과를 내어 상수가 된다.

인터페이스의 추상 메소드 선언

인터페이스의 메소드는 모두 추상 메소드의 성격을 갖기 때문에 입력하지 않더라도 컴파일 과정에서 public abstract 키워드가 앞에 자동으로 붙는다.

public interface RemoteControl {
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volume);
}

인터페이스의 디폴트 메소드 선언

메소드 선언 시 default 키워드가 리턴 타입 앞에 붙는다. 디폴트 메소드는 public 특성을 갖기 때문에, public을 생략하더라도 자동으로 컴파일 과정에서 붙게 된다.

[public] default 리턴타입 메소드명(매개변수, ...) { ... }
package hellojpa.interface_test;

public interface RemoteControl {
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volume);

    default void setMute(boolean mute) {
        if(mute) {
            System.out.println("무음 처리합니다.");
        } else {
            System.out.println("무음 해제합니다.");
        }
    }
}

인터페이스의 정적 메소드 선언

정적 메소드도 마찬가지로 public 특성을 갖기 때문에, 생략해도 자동으로 컴파일 과정에서 public이 붙는다.

[public] static 리턴타입 메소드명(매개변수, ...) { ... }
public interface RemoteControl {
    int MAX_VOLUME = 10;
    int MIN_VOLUME = 0;

    void turnOn();
    void turnOff();
    void setVolume(int volume);

    default void setMute(boolean mute) {
        if(mute) {
            System.out.println("무음 처리합니다.");
        } else {
            System.out.println("무음 해제합니다.");
        }
    }

    static void changeBattery() {
        System.out.println("건전지를 교환합니다.");
    }
}

인터페이스의 구현 클래스 만들기

public class 구현클래스명 implements 인터페이스명 {
  // 인터페이스의 추상 메소드를 구현하면 된다.
}

인터페이스의 추상 메소드 작성 주의점

  • 인터페이스의 모든 메소드가 기본적으로 public 접근 제한을 갖기 때문에 public보다 더 낮은 접근 제한자로 작성할 수 없다.

  • 구현체 구현 시에 public을 생략하면 "Cannot reduce the visibility of the inherited method"라는 컴파일 에러를 만난다.

  • 모든 추상 메소드를 구현 클래스가 작성하지 않으면, 구현 클래스가 될 수 없고 자동적으로 추상 클래스가 된다.

    • 이 경우에는 클래스에 abstract 키워드를 붙여야 한다.

인터페이스의 익명 구현 객체

  • 굳이 클래스 파일을 생성 안하고, 잠시동안 쓸 익명 구현 객체를 만들 수 있다.

  • 자바 8에서 지원하는 람다식에서 익명 구현 객체를 만드는 방식이 많이 쓰인다.

  • 모든 추상 메소드들의 실체를 작성하지 않으면 컴파일 에러가 난다.

  • 실체 메소드 내부에서 필드, 메소드를 추가로 선언할 수도 있다.

인터페이스 변수 = new 인터페이스() {
  // 추상 메소드 구현
}
public class InterfaceMain {
    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl() {
            @Override
            public void turnOn() {
                System.out.println("키자");
            }

            @Override
            public void turnOff() {
                System.out.println("끄자");
            }

            @Override
            public void setVolume(int volume) {
                System.out.println("소리를 " + volume + "에 맞춘다.");
            }
        };

        remoteControl.turnOn();
        remoteControl.turnOff();
        remoteControl.setVolume(10);
    }
}

익명 구현 객체 바이트코드

익명 구현 객체도 자바 컴파일러에 의해 .class 파일이 생성된다.

클래스명$1.class 와 같은 형식으로 .class 파일이 생성된다. 익명 구현 객체가 많으면 클래스명$2.class, 클래스명$3.class 와 같은 네이밍으로 생성된다.

다중 인터페이스 구현 클래스

public class 구현클래스명 implemetns 인터페이스A, 인터페이스B {
  // 모든 추상 메소드의 구현
}
  • 인터페이스는 다중 상속이 가능하다.

  • 모든 추상 메소드를 구현하지 않으면, abstract 클래스로 선언해야 한다.

타입 변환과 다형성

  • 인터페이스를 통해 다형성을 구현하기 용이하다.

    • 상속보다 인터페이스를 통한 다형성 구현이 더 많은 추세이다.

  • 나머지 소스는 그대로고 구현체의 종류만 바꾸는 형식으로 다형성을 구현한다.

인터페이스의 자동 타입 변환

  • 인터페이스를 상속한 구현체가 인터페이스의 타입으로 타입 변환되는 것을 자동 타입 변환이라고 할 수 있다.

  • 인터페이스 구현 클래스를 상속해서 자식 클래스를 만들었다면, 자식 객체도 인터페이스 타입으로 자동 타입 변환할 수 있다.

인터페이스 변수 = 구현객체;
// ex)
Controller controller = AudioController();
Controller controller = VideoController();

Controller라는 인터페이스에 구현체인 AudioController, VideoController 등 다양한 구현체를 끼워넣어 기존 코드를 변경하지 않고 구현체만 바꾸어 자동 타입 변환이 이뤄지는 형식으로 다형성을 구현할 수 있다.

인터페이스의 강제 타입 변환

  • 클래스의 강제 타입 변환과 같다.

  • 인터페이스가 자동 타입 변환된 상태라면, 다시 강제 타입 변환을 이용하여 이전의 타입으로 되돌릴 수 있다.

객체 타입 확인(instanceof)

  • 인터페이스를 강제 타입 변환하기 전에 자동 타입 변환이 이루어진 상태인지 판단하기 위해 instanceof로 확인이 가능하다.

  • 강제 타입 변환을 안전하게 하려면 instanceof로 확인을 반드시 하는 것이 좋다.

public class Driver {
  public void drive(Vehicle vehicle) {
    if(vehicle instanceof Car) {
      Car car = (Car) vehicle;
      car.changeGearToDrive();
    }

    vehicle.run();
  }
}

위 예제는 Car라는 객체가 Vehicle로 자동 타입 변환이 이루어졌을 때를 상정해서 작성한 코드이다.

Car라면 달리기 전에 기어를 Drive로 바꾸기 위해 .changeGearToDrive()메소드를 호출한다.

인터페이스가 인터페이스를 상속할 때

인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와 달리 다중 상속을 허용한다.

public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2 { ... }

이 경우에도 당연히 구현체는 모든 상위 인터페이스의 추상 메소드를 구현해야 한다.

디폴트 메소드와 인터페이스 확장

디폴트 메소드는 인터페이스에 선언된 인스턴스 메소드라서 구현체가 있어야 사용 가능한데, 이는 인터페이스의 기본 개념과 맞지 않다. 왜 자바8에서 이러한 디폴트 메소드가 생기게 되었을까?

디폴트 메소드의 필요성

만일 인터페이스를 생성하고 아주 많은 클래스를 만들어 해당 인터페이스에 대한 구현체를 다 만들어놓았다.

그런데, 갑자기 해당 인터페이스를 상속한 모든 구현체에 기능 메소드를 추가해야 하는 일이 생겼다.

이 때, 인터페이스에 추상 메소드를 추가하면, 모든 클래스를 돌아다니며 해당 메소드를 구현해야 한다.

그런데, 해당 메소드는 모든 클래스에서 하는 일이 그다지 다르지 않다면?

이 때, 디폴트 메소드를 추가하여 쉽게 해결할 수 있다.

디폴트 메소드의 오버라이드 활용

모든 클래스에서 하는 일이 비슷한 메소드가 추가할 일이 있어서 디폴트 메소드를 추가했는데, 어떤 한 클래스에서는 조금 다른 일을 해야 한다면? 그럴 때는 디폴트 메소드를 오버라이드하여 해당 클래스에 맞게 수정하면 된다.

자식 인터페이스에서 디폴트 메소드를 활용하는 방법

자식 인터페이스에서 디폴트 메소드를 활용하는 방법은 다음과 같다.

  • 디폴트 메소드를 단순히 상속만 받는다.

  • 디폴트 메소드를 재정의(Override)해서 실행 내용을 변경한다.

  • 디폴트 메소드를 추상 메소드로 재선언한다.

    • 이 경우는 인터페이스를 상속한 다른 인터페이스에서는 매번 이 디폴트 메소드에 해당하는 메소드를 다시 구현하는 것이 낫다고 판단되면 이용할 수 있다.

Last updated