Java 재활 훈련 5일차 - 인터페이스

0

java

목록 보기
5/18

인터페이스

인터페이스는 두 객체를 연결하는 역할을 한다. 객체 A가 인터페이스를 통해서 특정 객체의 메서드를 호출하고 그 결과값을 받는 것이다. 인터페이스는 일관된 사용 방법(method)를 제공해주어, 인터페이스와 연결된 객체가 무엇이든 지 상관없이 객체 A에 실행 결과값을 받을 수 있도록 해준다.

즉, 인터페이스에 연결된 구현체 객체가 객체 B이든 객체 C이든 객체 A는 관계없이 특정 메서드를 호출하여 그 결과를 받을 수 있는 것이다.

이 특징으로 인해 인터페이스는 다형성 구현의 주된 기술로 이용된다. 상속으로 다형성을 구현하는 방법도 있지만, 인터페이스를 사용하는 다형성 구현의 경우가 더 많다.

정리하자면 '언터페이스'는 특정 객체들이 반드시 구현해야할 명세서와 같다.

인터페이스 선언

인터페이스도 .java 형태의 소스 파일을 가지고, .class형태로 컴파일되기 때문에 물리적 형태는 클래스와 동일하다.

class 대신에 interface를 사용하며 접근 제한자로는 클래스와 마찬가지로 패키지 내에서만 사용 가능한 default, 패키지와 상관없이 사용하는 public을 붙일 수 있다.

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

중괄호 안에는 인터페이스가 가지는 맴버들을 선언할 수 있는데, 다음과 같은 종류가 있다.

public interface 인터페이스명 {
    //public static final 필드
    //public 추상 메서드
    //public 디폴트 메서드
    //public 정적 메서드
    //private 메서드
    //private 정적 메서드
}

실제 인터페이스를 만들어 보도록 하자. RemoteControl.java파일을 만들어 interface를 만들도록 하자.

  • RemoteControl.java
public interface RemoteControl {
    public void turnOn();
}

인터페이스는 구현체를 가지고 있어야 하고, 구현체란 하나의 객체이다. 그 객체는 인터페이스에 명시한 메서드를 가지고 있어야 하므로, 명시적으로 인터페이스 메서드를 구현해주어야 한다. 즉, 하나의 추상 메서드인 turnOn()RemoteControl 인터페이스 구현체 객체에서 정의해주어야 한다는 것이다.

이를 위해서 Television이라는 클래스를 만들도록 하자. TelevisionRemoteControl 인터페이스를 구현해야하므로 다음과 같이 implements라는 키워드를 통해서 명시적으로 RemoteControl을 구현해야한다고 써주도록 한다.

  • Television.java
public class Television implements RemoteControl {
    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }
}

Television 클래스는 implements를 통해서 RemoteControl 인터페이스를 구현한다는 명시적인 선언을 한 것이다. 따라서 RemoteControl에 있는 turnOn이라는 추상 메서드를 반드시 구현(재정의)해야한다.

이제 TelevisionRemoteControl의 구현체가 되었으며, RemoteControl 변수에 Television을 객체로 사용할 수 있다.

public class Main {
    public static void main(String[] args) {
        RemoteControl rc; // null
        rc = new Television();
        rc.turnOn(); // TV를 켭니다.
    }
}

참고로 interface의 default값은 null이다. RemoteControl 인터페이스 변수에 Television 객체를 넣어준 것을 볼 수 있다. 이제 RemoteControl 인터페이스의 turnOn 메서드를 실행시키면 Television 객체의 메서드가 실행되는 것이다.

RemoteControl 인터페이스 변수인 rc에는 Television 뿐만 아니라 RemoteControl를 구현체인 객체들은 모두 들어갈 수 있다. 가령, RemoteControl를 구현한 Audio 클래스가 있다고 하자.

  • Audio.java
public class Audio implements RemoteControl{
    @Override
    public void turnOn() {
        System.out.println("Audio를 켭니다.");
    }
}

Audio 클래스가 RemoteControl를 implement하고 있고, RemoteControl의 메서드인 turnOn을 오버라이딩하여 구현하고 있으므로 RemoteControl 인터페이스의 변수인 rc에 대입이 가능하다.

public class Main {
    public static void main(String[] args) {
        RemoteControl rc; // null
        rc = new Television();
        rc.turnOn(); // TV를 켭니다.

        rc = new Audio();
        rc.turnOn(); // Audio를 켭니다.
    }
}

이러한 특징을 활용하여 특정 메서드의 입력 타입을 인터페이스로 만들어, 다양한 객체가 들어가게 할 수 있다. 사용자 입장에서는 동일한 사용 방법(메서드)를 통해서 서로 다른 결과를 얻을 수 있게 되는 것이다.

상수 필드

인터페이스는 public static final 특성을 갖는 불변의 상수 필드를 맴버 변수로 가질 수 있다. 참고로 인터페이스에 선언된 필드는 모두 public static final 특성을 가지기 때문에 해당 키워드들을 생략하더라도 컴파일 과정에서 붙게된다.

  • RemoteControl.java
public interface RemoteControl {
    int MAX_VOLUME=10; // public static final int MAX_VOLUME = 10;
    int MIN_VOLUME=0; // public static final int MIN_VOLUME = 10;
}

다음과 같이 RemoteControl인터페이스에 상수를 선언할 수 있다.

상수이므로 인터페이스 변수없이, 인터페이스 자체를 통해서 바로 접근이 가능하다.

public class Main {
    public static void main(String[] args) {
        System.out.println(RemoteControl.MAX_VOLUME); // 10
        System.out.println(RemoteControl.MIN_VOLUME); // 0
    }
}

추상 메서드

인터페이스는 구현 클래스가 오버라이딩해야 하는 public 추상 메서드를 맴버로 가질 수 있다. 추상 메서드는 리턴 타입, 메서드 명, 매개변수만 기술되고 중괄호 {}를 붙이지 않는다. public abstract를 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.

참고로 인터페이스의 추상 메서드는 기본적으로 public 접근 제한을 갖는다. public보다 더 낮은 접근 제한으로 재정의할 수 없다.

  • RemoteControl.java
public interface RemoteControl {
    int MAX_VOLUME=10;
    int MIN_VOLUME=0;

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

구현 클래스인 TelevisionAudio는 인터페이스에 선언된 모든 추상 메서드를 오버라이드해서 실행 코드를 가져야 한다.

Default 메서드

인터페이스에는 완전한 실행 코드를 가진 default 메서드를 선언할 수 있다. 추상 메서드는 실행부인 {}가 없지만, default 메서드는 실행부가 있다.

default 메서드는 실행부에 상수 필드를 읽거나 추상 메서드를 호출하는 코드를 작성할 수 있따. RemoteControl 인터페이스에서 무음 처리 기능을 제공하는 setMute default 메서드를 선언해보자.

  • RemoteControl.java
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("무음 처리");
            setVolume(MIN_VOLUME);
        } else {
            System.out.println("무음 해제");
        }
    }
}

default 메서드는 구현 객체가 필요한 메서드이다. 따라서 RemoteControlsetMute 메서드를 호출하려면 구현 객체인 Television 객체를 다음과 같이 인터페이스 변수에 대입하고 나서 setMute를 호출해야한다.

  • Television.java
public class Television implements RemoteControl {
    private int volume;

    @Override
    public void turnOn() {
        System.out.println("TV를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("TV를 끕니다.");
    }

    @Override
    public void setVolume(int volume) {
        if (volume > RemoteControl.MAX_VOLUME) {
            volume = MAX_VOLUME;
            return;
        }

        if (volume < RemoteControl.MIN_VOLUME) {
            volume = MIN_VOLUME;
            return;
        }
        this.volume = volume;
        System.out.println("현재 TV 볼륨:" + this.volume);
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        RemoteControl rc = new Television();
        rc.setMute(true); // 무음 처리 현재 TV 볼륨:0
    }
}

구현 클래스는 디폴트 메서드를 오버라이드해서 자신에게 맞게 수정할 수도 있다. 오버라이드 시에 주의할 점은 public 접근 제한자를 반드시 붙이고 default 키워드를 생략해야한다.

  • Audio.java
public class Audio implements RemoteControl{
    private int volume;
    @Override
    public void turnOn() {
        System.out.println("Audio를 켭니다.");
    }

    @Override
    public void turnOff() {
        System.out.println("Audio를 끕니다.");
    }

    @Override
    public void setVolume(int volume) {
        if (volume > RemoteControl.MAX_VOLUME) {
            volume = MAX_VOLUME;
            return;
        }

        if (volume < RemoteControl.MIN_VOLUME ) {
            volume = MIN_VOLUME;
            return;
        }
        this.volume = volume;
        System.out.println("현재 Audio 볼륨:" + this.volume);
    }

    private int memoryVolume;

    @Override
    public void setMute(boolean mute) {
        if (mute) {
            this.memoryVolume = this.volume;
            System.out.println("무음 처리");
            setVolume(MIN_VOLUME);
        } else {
            System.out.println("무음 해제");
            setVolume(this.memoryVolume);
        }
    }
}

setMute를 오버라이드 한 것을 볼 수 있다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        RemoteControl rc = new Television();
        rc.setMute(true); // 무음 처리 현재 TV 볼륨:0

        rc = new Audio();
        rc.setVolume(5); // 현재 Audio 볼륨:5
        rc.setMute(true); // 무음 처리 현재 Audio 볼륨:0
        rc.setMute(false); // 무음 해제 현재 Audio 볼륨:5
    }
}

정적 메서드

인터페이스에는 정적 메서드도 선언이 가능하다. 추상 메서드와 디폴트 메서드는 구현 객체가 필요하지만, 정적 메서드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다. 접근 지시자는 기본적으로 public 접근 지시자이다. 생략해버리면 public이 추가된다. private도 가능하다.

  • RemoteControl.java
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("무음 처리");
            setVolume(MIN_VOLUME);
        } else {
            System.out.println("무음 해제");
        }
    }

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

인터페이스 변수가 아닌, 인터페이스 자체인 RemoteControl.changeBattery로 호출이 가능하다.

Private 메서드

인터페이스의 상수 필드, 추상 메서드, 디폴트 메서드, 정적 메서드는 모두 public 접근 제한을 갖는다. 이 맴버들을 선언할 때에는 public을 생략하더라도 컴파일 과정에서 public 접근 제한자가 붙어 항상 외부에서 접근이 가능하다. 또한, 인터페이스에 외부에서 접근할 수 없는 private 메서드 선언도 가능하다.

구분설명
private 메서드구현 객체가 필요한 메서드
private 정적 메서드구현 객체가 필요 없는 메서드

private 메서드는 default 메서드 안에서만 호출이 가능한 반면에 private 정적 메서드는 default 메서드 뿐만 아니라, 정적 메서드 안에서도 호출이 가능하다. private 메서드의 용도는 default와 정적 메서드들의 중복 코드를 줄이기 위함이다.

  • Service.java
public interface Service {

    // 디폴트 메서드
    default void defaultMethod1() {
        System.out.println("defaultMethod1 종속 코드");
    }

    default void defaultMethod2() {
        System.out.println("defaultMethod2 종속 코드");

    }

    // private 메서드
    private void defaultCommon() {
        System.out.println("defaultMethod 중복 코드A");
        System.out.println("defaultMethod 중복 코드B");
    }

    // 정적 메서드
    static void staticMethod1() {
        System.out.println("staticMethod1 종속 코드");
        staticCommon();
    }

    static void staticMethod2() {
        System.out.println("staticMethod2 종속 코드");
        staticCommon();
    }

    // private 정적 메서드
    private static void staticCommon() {
        System.out.println("staticMethod 중복 코드C");
        System.out.println("staticMethod 중복 코드D");
    }
}

defaultCommon는 private 메서드로 default 메서드에서 공통적으로 호출되는 부분을 담당하는 것을 알 수 있다. staticCommon는 private 정적 메서드로 정적 메서드에서 공통적으로 호출되는 부분을 담당하는 것을 알 수 있다.

다중 인터페이스 구현

구현 객체는 여러 개의 interfaceimplements 할 수 있다. 구현 객체가 인터페이스 A와 인터페이스 B를 구현하고 있다면 각각의 인터페이스를 통해 구현 객체를 사용할 수 있다.

public class 구현클래스 implements 인터페이스A, 인터페이스B {
    // 모든 추상 메서드 재정의
}

두 인터페이스를 구현하고 있는 '구현클래스'는 '인터페이스A' 타입 변수, '인터페이스B' 타입 변수 모두에 대입이 가능하다.

인터페이스A 변수 = new 구현클래스();
인터페이스B 변수 = new 구현클래스();

다음과 같이 RemoteControl interface와 Searchable interface를 모두 구현한 SmartTelevision 클래스를 작성해보자.

  • RemoteControl.java
public interface RemoteControl {
    void turnOn();
    void turnOff();
}
  • Searchable.java
public interface Searchable {
    void search(String url);
}
  • SmartTelevision.java
public class SmartTelevision implements RemoteControl, Searchable {

    @Override
    public void turnOn() {
        System.out.println("TV를 켠다.");
    }

    @Override
    public void turnOff() {
        System.out.println("TV를 끈다.");
    }

    @Override
    public void search(String url) {
        System.out.println(url + "을 검색");
    }
}

두 인터페이스를 implements하였고, 추상 메서드들을 모두 구현하였다. 이제 실행해보도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        RemoteControl rc = new SmartTelevision();
        rc.turnOn(); // TV를 켠다.
        rc.turnOff(); // TV를 끈다.

        Searchable searchable = new SmartTelevision();
        searchable.search("www.naver.com"); // www.naver.com을 검색
    }
}

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있으며, 클래스와는 달리 다중 상속을 허용한다. 다음과 같이 extends 키워드 뒤에 상속할 인터페이스들을 나열하면 된다.

public interface 자식인터페이스 extends 부모인터페이스1, 부모인터페이스2 { ... }

자식 인터페이스의 구현 클래스는 자식 인터페이스의 메서드뿐만 아니라, 부모 인터페이스의 모든 추상 메서드를 재정의해야한다. 그리고 구현 객체는 다음과 같이 자식 및 부모 인터페이스 변수에 대입될 수 있다.

자식인터페이스 변수 = new 구현클래스(...);
부모인터페이스1 변수 = new 구현클래스(...);
부모인터페이스2 변수 = new 구현클래스(...);

구현 객체가 자식 인터페이스 변수에 대입되면, 자식 및 부모 인터페이스의 추상 메서드를 모두 호출 할 수 있으나, 부모 인터페이스 변수에 대입되면 부모 인터페이스에 선언된 추상 메서드만 호출 가능하다.

  • InterfaceA.java
public interface InterfaceA {
    // 추상 메서드
    void methodA();
}
  • InterfaceB.java
public interface InterfaceB {
    // 추상 메서드
    void methodB();
}
  • InterfaceC.java
public interface InterfaceC extends InterfaceA, InterfaceB{
    void methodC();
}

InterfaceCInterfaceA, InterfaceB를 상속받아, methodA, methodB를 추상 메서드로 가지고 있는 것으로 볼 수 있다. 따라서, InterfaceC를 구현하는 구현 클래스는 methodA, methodB, methodC를 오버라이드해야한다.

  • IntefaceCImpl.java
public class InterfaceCImpl implements InterfaceC{
    @Override
    public void methodC() {
        System.out.println("IntefaceC methodC 실행");
    }

    @Override
    public void methodA() {
        System.out.println("IntefaceA methodA 실행");
    }

    @Override
    public void methodB() {
        System.out.println("IntefaceB methodB 실행");
    }
}

이제 실행시켜보도록 하자.

  • Main.java
public class Main {
    public static void main(String[] args) {
        InterfaceCImpl impl = new InterfaceCImpl();

        InterfaceA ia = impl;
        ia.methodA(); // IntefaceA methodA 실행

        InterfaceB ib = impl;
        ib.methodB(); // IntefaceB methodB 실행

        InterfaceC ic = impl;
        ic.methodC(); // IntefaceC methodC 실행
        ic.methodA(); // IntefaceA methodA 실행
        ic.methodB(); // IntefaceB methodB 실행
    }
}

InterfaceCInterfaceA, InterfaceB를 상속받았기 때문에 InterfaceC의 구현체인 InterfaceCImplInterfaceA, InterfaceB 타입 변수에 대입될 수 있다. 단, InterfaceA 변수는 실행할 수 있는 메서드가 methodA뿐이고, InterfaceB 변수는 실행할 수 있는 메서드가 methodB뿐이다.

단, InterfaceC는 변수는 methodA, methodB, methodC를 모두 호출할 수 있다.

타입 변환

인터페이스 타입 변환은 인터페이스와 구현 클래스 간에 발생한다. 인터페이스 변수에 구현 객체를 대입하면 구현 객체는 인터페이스 타입으로 자동 타입 변환된다. 반대로, 인터페이스 타입을 구현 클래스 타입으로 변환하고 싶다면 강제 타입 변환이 필요하다.

자동 타입 변환은 다음과 같은 조건에서 발생한다.

인터페이스 변수 = 구현객체;

구현 객체가 인터페이스 타입으로 자동 변환되면, 인터페이스에 선언된 메서드만 사용 가능하다. 이러한 문재를 해결해주기 위해서 인터페이스 변수에 들어간 객체의 구현 클래스 타입으로 강제 변환을 해주어야 한다.

강제 타입 변환은 캐스팅을 통해서 인터페이스 타입을 구현 클래스 타입으로 변환시키는 것을 말한다.

구현클래스 변수 = (구현클래스) 인터페이스변수;

Vehicle 인터페이스를 만들어서, Bus가 구현하도록 하자. 단, Bus 클래스에는 Bus만의 메서드인 checkFare가 있다.

  • Vehicle.java
public interface Vehicle {
    void run();
}
  • Bus.java
public class Bus implements Vehicle {
    @Override
    public void run() {
        System.out.println("bus가 달립니다.");
    }

    public void checkFare() {
        System.out.println("요금 체크");
    }
}
  • Main.java
public class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Bus();
        vehicle.run(); // bus가 달립니다.

        // vehicle.checkFare() 불가

        Bus bus = (Bus) vehicle;
        bus.run(); // bus가 달립니다.
        bus.checkFare(); // 요금 체크
    }
}

Vehicle 인터페이스 변수인 vehicle의 구현체로 Bus가 들어갔지만, Vehicle에서 호출 가능한 메서드는 run밖에 없으므로 checkFare는 실행 못한다. 따라서, 강제 형 변환을 통해서 vehicle 변수를 Bus 타입으로 강제 형변환하여 checkFare가 실행될 수 있도록 한다.

다형성

현업에서는 상속을 통한 다형성 보다는 인터페이스를 통한 다형성이 더 많이 사용된다. 다형성이란 사용 방법은 동일하지만 다양한 결과가 나오는 성질을 말한다.

상속의 다형성과 마찬가지로 인터페이스 역시 다형성을 구현하기 위해서 오버라이드와 자동 타입 변환 기능을 이용한다.

다형성 = method override + 자동 타입 변환

인터페이스의 추상 메서드는 구현 클래스에서 오버라이드 해야 하며, 오버라이딩되는 내용은 구현 클래스마다 다 다르다. 구현 객체는 인터페이스 타입으로 자동 타입 변환되고, 인터페이스 메서드 호출 시 구현 객체의 오버라이드된 메서드가 호출되어 다양한 실행 결과를 얻을 수 있다.

객체 타입 확인

상속에서 객체 타입을 확인하기 위해서 instanceof 연산자를 사용했듯이 인터페이스도 사용할 수 있다. 가령, Vehicle 인터페이스 변수에 대입된 객체가 Bus인지 확인하는 코드는 다음과 같다.

if (vehicle instanceof Bus) {
    Bus bus = (Bus) vehicle;
}

java 12부터는 instanceof 연산의 결과가 true일 경우 우측 타입 변수를 사용할 수 있다. 이때 강제 타입 변환은 필요 없다.

if (vehicle instanceof Bus bus) {
    // bus 사용
}

보인된 인터페이스(sealed interface)

java15부터는 부모 인터페이스를 상속하는 자식 인터페이스의 제한을 위해서 봉인된(sealed) 인터페이스를 사용할 수 있다. InterfaceA 상속은 자식 인터페이스인 IntefaceB만 가능하고, 그 이외는 자식 인터페이스가 될 수 없도록 다음과 같이 InterfaceA를 봉인된 인터페이스로 선언할 수 있다.

public sealed interface InterfaceA permits InterfaceB { }

sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 인터페이스를 지정해야 한다. 봉인된 InterfaceA를 상속하는 InterfaceBnon-sealed 키워드로 다음과 같이 선언하거나 sealed 키워드를 사용해서 또 다른 봉인 인터페이스로 선언해야한다. 즉, 봉인된 클래스를 상속하는 자식 클래스는 다음의 2가지 중 하나를 해야한다.
1. sealed로 봉인하거나 해야하거나
2. non-sealed로 봉인을 해제하거나

public non-sealed interface InterfaceB extends InterfaceA {...}

non-sealed는 봉인을 해제한다느 것이기 때문에 InterfaceB를 다른 인터페이스에서 상속할 수 있다.

0개의 댓글