📝 이것이 자바다

Chapter 08. 인터페이스

j_estory 2022. 12. 29. 17:07

🏷 인터페이스 역할

 

인터페이스는 사전적인 의미로 두 장치를 연결하는 접속기를 말한다.

여기서 두 장치를 서로 다른 객체로 본다면, 인터페이스는 이 두 객체를 연결하는 역할을 한다.

아래 그림과 같이 객체 A는 인터페이스를 통해 객체 B를 사용할 수 있다. 

객체 A가 인터페이스의 메소드를 호출하면, 인터페이스는 객체 B의 메소드를 호출하고,

그 결과를 받아 객체 A로 전달해준다.

객체 A가 객체 B의 메소드를 직접 호출하면 간단할텐데 왜 중간에 인터페이스를 거치도록 하는걸까? 

 

만약, 객체 B가 객체 C로 변경이 된다고 가정해보자

 

객체 A는 인터페이스 메소드만 사용하므로 객체 B가 객체 C로 변경되는 것에는 관심이 없다.

만약 인터페이스가 없다면 객체 A의 소스 코드를 객체 B에서 객체 C로 변경하는 작업이 추가로 필요할 것이다. 

 

이러한 특징으로 인해 인터페이스는 다형성 구현에 주된 기술로 이용된다.

상속을 이용해서 다형성을 구현할 수도 있지만, 인터페이스를 이용해서 다형성을 구현하는 경우가 더 많다.

 

✔️ 인터페이스와 구현 클래스 선언

 

인터페이스 선언은 class 키워드 대신 interface 키워드를 사용한다. 

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

public interface 인터페이스명 {
    // public 상수 필드 
    // public 추상 메소드
    public void trunOn();
    // public 디폴트 메소드
    // public 정적 메소드
    // private 메소드
    // private 정적 멧소드
}

위 그림에서 객체 A가 인터페이스의 추상 메소드를 호출하면 

인터페이스는 객체 B의 메소드를 실행한다.

그렇다면 객체 B는 인터페이스에 선언된 추상 메소드와 동일한 선언부를 가진 메소드를 가지고 있어야 한다.

 

여기서 ! 객체 B를 인터페이스를 구현한 객체라고 한다. 

인터페이스에 정의된 추상 메소드에 대한 실행 내용이 구현되어 있기 때문이다. 

객체 B와 같은 구현 객체는 다음과 같이 인터페이스를 구현하고 있음을 선언부에 명시해야 한다.

public class B implements 인터페이스명 {...}

implements 키워드는 해당 클래스가 인터페이스를 통해 사용할 수 있다는 표시이며,

인터페이스의 추상 메소드를 재정의한 메소드가 있다는 뜻이다. 

 

ex. 

public interface RemoteControl {
    public void trunOn();
}

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

 

✔️ 변수 선언과 구현 객체 대입

 

인터페이스도 하나의 타입이므로 변수의 타입으로 사용할 수 있다. 

인터페이스는 참조 타입에 속하므로 인터페이스 변수에는 객체를 참조하고 있지 않다는 뜻으로 null을 대입할 수 있다. 

RemoteControl rc;
RemoteControl rc = null;

 

인터페이스를 통해 구현 객체를 사용하려면,

인터페이스 변수에 구현 객체를 대입해야 한다. 

rc = new Television();

만약 Television이 implements RemoteControl로 선언되지 않았다면,

RemoteControl 타입의 변수 rc에 대입할 수 없다. 

 

인터페이스 변수에 구현 객체가 대입이 되었다면, 변수를 통해 인터페이스의 추상 메소드를 호출할 수 있게 된다. 

rc.trunOn();

이렇게 호출하게 되면 실제로 실행되는 것은 Television에서 재정의된 trunOn() 메소드이다.

 

ex.

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

@Test
public void 변수선언_구현객체_테스트() {
    RemoteControl rc; // 인터페이스 객체 생성
    
    rc = new Television(); // 구현 객체 대입
    rc.trunOn();

    rc = new Audio(); // 구현 객체 대입
    rc.trunOn();
}

// 결과
TV를 켭니다.
Audio를 켭니다.

 

🏷 상수 필드

 

인터페이스는 public static final 특성을 갖는 불변의 상수 필드를 멤버로 가질 수 있다. 

인터페이스에 선언된 필드는 모두 public static final 특성을 갖기 때문에 이를 생략하더라도 자동적으로 컴파일 과정에서 붙게 된다.

public interface RemoteControl {
    /**
     * 상수는 구현 객체와 관련 없는 인터페이스 소속 멤버 이므로,
     * 인터페이스로 바로 접근하여 상수값을 읽을 수 있다.
     */
    int MAX_VOLUMN = 10;
    int MIN_VOLUMN = 0;
    
    public void trunOn();
}

@Test
public void 인터페이스_상수_테스트() {
    System.out.println("리모콘 최대 볼륨: " + RemoteControl.MAX_VOLUMN);
    System.out.println("리모콘 최저 볼륨: " + RemoteControl.MIN_VOLUMN);
}

 

🏷 추상 메소드

 

인터페이스는 구현 클래스가 재정의해야 하는 public 추상 메소드를 멤버로 가질 수 있다.

추상 메소드는 리턴 타입, 메소드명, 매개변수만 기술되고 중괄호는 붙이지 않는 메소드를 말한다. 

public abstract를 생략하더라도 컴파일 과정에서 자동으로 붙게 된다.

 

public interface RemoteControl {
    /**
     * 상수는 구현 객체와 관련 없는 인터페이스 소속 멤버 이므로,
     * 인터페이스로 바로 접근하여 상수값을 읽을 수 있다.
     */
    int MAX_VOLUMN = 10;
    int MIN_VOLUMN = 0;

    // 추상 메소드 선언
    public void trunOn();
    public void trunOff();
    public void setVolume(int volume);
}

이와 같이 추상 메소드를 선언하게 되면 구현 클래스에서 위의 3가지의 추상 메소드를 모두 재정의해야 한다. 

 

🚨 재정의 시 주의할 점 !! 

 

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

그래서 재정의되는 메소드에는 모두 public이 추가되어 있다.

 

또한, 인터페이스 객체에 각 구현 객체를 대입하게 되면

인터페이스 객체로 각 구현 객체의 재정의된 추상 메소드를 호출 할 수 있다.

@Test
public void 추상메소드_테스트() {
    RemoteControl rc;
    // 인터페이스 객체에 각 구현 객체를 대입하게 되면, 각 구현 객체에 재정의 된 메소드가 호출되게 된다.
    rc = new Television();
    rc.trunOn();
    rc.setVolume(5);
    rc.trunOff();

    rc = new Audio();
    rc.trunOn();
    rc.setVolume(5);
    rc.trunOff();
}

 

🏷 디폴트 메소드

 

인터페이스에는 완전한 실행 코드를 가진 디폴트 메소드를 선언할 수 있다. 

추상메소드는 실행부 ({...})가 없지만, 디폴드 메소드는 실행부가 있다. 

 

선언 방법은 클래스 메소드와 동일한데, 차이점은 default 키워드가 리턴 타입 앞에 붙는다.

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

디폴트 메소드의 실행부에는 상수 필드를 읽거나, 추상 메소드를 호출하는 코드를 작성할 수 있다. 

 

ex. 

// 디폴트 인스턴스 메소드
default void setMute(boolean mute) {
    if (mute) {
        System.out.println("무음 처리합니다.");
        // 추상 메소드 호출하면서 상수 필드 사용
        this.setVolume(MIN_VOLUMN);
    } else {
        System.out.println("무음 해제합니다.");
    }
}

default 메소드는 구현 객체가 필요한 메소드이다.

따라서 위 메소드를 호출하기 위해서는 구현 객체인 television 객체를 인터페이스 변수에 대입하고 나서 호출해야 한다.

@Test
public void default_메소드_테스트() {
    RemoteControl rc;

    rc = new Television();
    rc.trunOn();
    rc.setVolume(5);

    // 디폴트 메소드 호출
    rc.setMute(true);
    rc.setMute(false);
}

 

구현 클래스는 디폴트 메소드를 재정의해서 자신에게 맞게 수정할 수도 있다.

재정의 시 주의점은 public 접근 제한자를 반드시 붙여야 하고, default 키워드를 생략해야 한다. 

 

🏷 정적 메소드

 

인터페이스에는 정적 메소드도 선언이 가능하다.

추상 메소드와 디폴트 메소드는 구현 객체가 필요하지만, 정적 메소드는 구현 객체가 없어도 인터페이스만으로 호출할 수 있다.

 

선언 방법은 클래스 정적 메소드와 완전 동일하다. 

public static 리턴타입 메소드명(매개변수, ...) {...}

정적 메소드의 실행부를 작성할 때 주의할점은 default 메소드 및 추상 메소드 등을 호출할 수 업다는 것이다.

이들은 구현 객체가 필요한 인스턴스 메소드이기 때문이다.

 

🏷 private 메소드

 

인터페이스의 상수 필드, 추상 메소드, 디폴트 메소드, 정적 메소드 모두 public 접근 제한을 갖는다. 

이 멤버들은 선언 시, public을 생략하더라도 컴파일 과정에서 public 접근 제한자가 붙어 항상 외부에서 접근이 가능하다. 

또한 인터페이스에 외부에서 접근할 수 없는 private 메소드 선언도 가능하다. 

 

private 메소드는 디폴트 메소드 안에서만 호출이 가능한 반면, private 정적 메소드는 디폴트 메소드 뿐만 아니라
정적 메소드 안에서도 호출이 가능하다. 

 

private 메소드의 용도는 디폴트와 정적 메소드의 중복 코드를 줄이기 위함이다. 

 

ex.

public interface Service {

    // 디폴트 메소드 선언
    default void defaultMethod1() {
        System.out.println("defaultMethod1 종속 코드");
        this.defaultCommon();
    }

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

    // private 메소드
    private void defaultCommon() {
        System.out.println("중복 코드1");
        System.out.println("중복 코드2");
    }

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

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

    // private 정적 메소드
    private static void staticCommon() {
        System.out.println("static 중복 코드1");
        System.out.println("static 중복 코드2");
    }
}

// default 메소드 및 정적 메소드 모두 구현체에서 재정의가 필수가 아니므로 아래와 같이 사용해도 문제 없음.
public class ServiceImpl implements Service{
}

 

🏷 다중 인터페이스 구현

 

구현 객체는 여러 개의 인터페이스를 implements 할 수 있다.

구현 객체가 인터페이스 A, 인터페이스 B를 구현하고 있다면, 각각의 인터페이스를 통해 구현 객체를 사용할 수 있다.

 

구현 클래스는 다음과 같이 인터페이스 A와 인터페이스 B를 implements 뒤에 쉼표로 구분해서 작성하여,

모든 인터페이스가 가진 추상 메소드를 재정의해야 한다.

public class 구현클래스명 implements 인터페이스A, 인터페이스B {
...}

ex.

public class SmartTelevision implements RemoteControl, Searchable{
    @Override
    public void trunOn() {
        System.out.println("스마트 TV를 켭니다.");
    }

    @Override
    public void trunOff() {
        System.out.println("스마트 TV를 끕니다.");
    }

    @Override
    public void setVolume(int volume) {
        System.out.println("현재 볼륨: " + volume);
    }

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


@Test
public void 다중_인터페이스_테스트() {
    RemoteControl rc = new SmartTelevision();
    rc.trunOn();
    rc.trunOff();
    Searchable searchable = new SmartTelevision();
    searchable.search("https://www.naver.com");
}

 

🏷 인터페이스 상속

 

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

 

다음과 같이 extends 키워드 뒤에 상속할 인터페이스를 나열하면 된다.

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

 

자식 인터페이스의 구현 클래스는 자식 인터페이스 메소드뿐만 아니라 부모 인터페이스의 모든 추상 메소드를 재정의해야 한다.

그리고 구현 객체는 다음과 같이 자식 및 부모 인터페이스 변수에 대입할 수 있다.

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

구현 객체가 자식 인터페이스 변수에 대입되면 자식 및 부모 인터페이스의 추상 메소드를 모두 호출할 수 있으나,

부모 인터페이스 변수에 대입되면 부모 인터페이스에 선언된 추상 메소드만 호출 가능하다.

 

ex.

public interface InterfaceA {
    void methodA();
}

public interface InterfaceB {
    void methodB();
}


public interface InterfaceC extends InterfaceA, InterfaceB{
    void methodC();
}

public class InterfaceImplC implements InterfaceC {
    @Override
    public void methodA() {
        System.out.println("methodA 실행");
    }

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

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

@Test
public void 인터페이스_상속_테스트() {
    InterfaceImplC interfaceImplC = new InterfaceImplC();

    InterfaceA interfaceA = interfaceImplC;
    interfaceA.methodA();
    // methodB or methodC 호출 불가

    InterfaceB interfaceB = interfaceImplC;
    interfaceB.methodB();
    // methodA or methodC 호출 불가

    // 인터페이스A,B를 상속받은 자식 인터페이스C는 모두 호출 가능
    InterfaceC interfaceC = interfaceImplC;
    interfaceC.methodA();
    interfaceC.methodB();
    interfaceC.methodC();
}

 

🏷 타입 변환

 

인터페이스의 타입 변환은 인터페이스와 구현 클래스 간에 발생한다. 

인터페이스 변수에 구현 객체를 대입하면 구현 객체는 인터페이스 타입으로 자동 타입 변환된다. 

 

 

반대로, 인터페이스 타입을 구현 클래스 타입으로 변환시킬 수 있는데, 이때는 강제 타입 변환이 필요하다.

 

✔️ 자동 타입 변환

 

의미 그대로 자동으로 타입 변환이 일어나는 것을 의미한다.

// 자동 타입 변환
인터페이스 변수 = 구현 객체;

부모 클래스가 인터페이스를 구현하고 있다면, 

자식 클래스도 인터페이스 타입으로 자동 타입 변환 될 수 있다.

 

 

✔️ 강제 타입 변환

 

캐스팅 기호를 사용하여 인터페이스 타입을 구현 클래스 타입으로 변환시키는 것을 말한다.

// 자동 타입 변환
구현클래스 변수 = (구현 클래스) 인터페이스 변수;

구현 객체가 인터페이스 타입으로 자동 변환되면, 

인터페이스에 선언된 메소드만 사용이 가능하다. 

이렇게 자동 타입 변환 후에 Television의 setTime()과 record() 메소드를 호출하고 싶다면

다음과 같이 캐스팅 기호를 사용하여 원래 Television으로 강제 타입 변환해야 한다.

RemoteControl rc = new Television();
rc.trunOn();
rc.trunOff();
rc.setVolumne(S);

Television tv = (Television) rc;
tv.turnOn();
tv.trunOff();
tv.setVolumn(S);
tv.setTime();
tv.record();

 

🏷 다형성

 

인터페이스의 다형성을 구현하기 위해서는 재정의와 자동 타입 변환 기능을 이용한다.

메소드 재정의 + 자동 타입 변환 = 다형성

 

인터페이스의 추상 메소드는 구현 클래스에서 재정의해야 하며,

재정의되는 내용은 구현 클래스마다 다르다. 

구현 객체는 인터페이스 타입으로 자동 타입 변환이 되고,

인터페이스 호출 시 구현 객체의 재정의된 메소드가 호출되어 다양한 실행 결과를 얻을 수 있다. 

 

✔️ 필드의 다형성

 

자동차를 설계할 때 다음과 같이 필드 타입으로 타이어 인터페이스를 선언하면

필드값으로 한국 타이어 또는 금호 타이어 객체를 대입할 수 있다.

이는 자동 타입 변환 때문이다.

public class Car {
    Tire tire1 = new HankookTire();
    Tire tire2 = new KumhoTire();
}

 

✔️ 매개변수의 다형성

 

메소드 호출 , 매개값을 다양화하기 위해서 상속에서는 매개변수 타입을 부모 타입으로 선언하,

호출할 때에는 다양한 자식 객체를 대입했다. 

이것은 자동 타입 변환 때문인데, 비슷한 원리로 매개변수 타입을 인터페이스로 선언하면,

메소드 호출 시 다양한 구현 객체를 대입할 수 있다.

 

Vehicle 인터페이스가 다음과 같이 선언되어 있다고 가정해보자

public interface Vehicle {
	void run();
}

운전자 클래스인 Driver는 다양한 Vehicle 구현 객체를 운전하기 위해

Vehicle 인터페이스를 매개변수로 가지는 drive() 메소드를 다음과 같이 선언하였다.

public class Driver {
// 구현 객체가 대입할 수 있도록 매개변수를 인터페이스 타입으로 선언
	void drive(Vehicle vehicle) {
    	vehicle.run();
    }
}

dirve() 메소드를 호출할 때 인터페이서 Vehicle을 구현하는 어떠한 객체라도 매개값으로 줄 수 있는데,

어떤 객체를 주느냐에 따라서 run() 메소드의 실행 결과는 다르게 나온다.

 

이유는 구현 객체에서 재정의된 run() 메소드의 실행 내용이 다르기 때문이다.

이것이 매개변수의 다형성이다.

 

🏷 객체 타입 확인

 

상속에서 객체 타입을 확인하기 위해 instanceof 연산자를 사용했는데, 인터페이스에서도 사용할 수 있다.

예를들어, Vehicle 인터페이스 변수에 대입된 객체가 Bus인지 확인하는 코드는 다음과 같다.

if(vehicle instaceof Bus) {
	// vehicle에 대입된 객체가 Bus일 경우 실행
}

🏷 봉인된 인터페이스

 

java 15부터는 무분별한 자식 인터페이스 생성을 방지하기 위해 sealed 인터페이스를 사용할 수 있다.

InterfaceA의 자식 인터페이스는 InterfaceB만 가능하고, 

그 이외는 자식 인터페이스가 될 수 없도록 다음과 같이 InterfaceA를 봉인된 인터페이스로 선언할 수 있다.

// sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 인터페이스를 지정해야 한다.
public sealed interface InterfaceA permits InterfaceB { ... }

// 봉인된 InterfaceA를 상속하는 InterffaceB는 non-sealed 키워드로 선언하면 된다.
public non-sealed interface InterfaceB extends InterfaceA { ... }