nathan_H

인터페이스의 개념과 추상 클래스와의 차이점 본문

Programming Laguage/Java

인터페이스의 개념과 추상 클래스와의 차이점

nathan_H 2020. 6. 2. 15:20

인터페이스란?


  • 인터페이스의 의미 자체는 아래와 같다.
- 전기 신호의 변환(變換)으로 중앙 처리 장치와 그 주변 장치를 서로 잇는 부분. 또는, 그 접속 장치.
- 키보드나 디스플레이 등처럼 사람과 컴퓨터를 연결하는 장치.
- 소프트웨어끼리 접촉·공통되는 부분. 순화어는 `접속'.
  • 즉 인터페이스의 의미를 한 줄로 요약하면 "접속하는 장치" 이다. 그리고 이 의미는 자바에서도 그대로 적용된다.

자바 인터페이스의 개념과 역할


  • 개념
    • 개발 코드와 객체가 서로 통신하는 접점, 객체의 사용 방법을 정의한 타입.
  • 역할
    • 개발 코드가 객체에 종속되지 않도록 방지
    • 개발 코드 변경 없이 리턴값 또는 실행 내용을 다양하게 구현 (다형성)
    • 개발자들 간의 하나의 코드 규약
  • 하나의 프로그램을 제작할 때에는 "설계"와 "구현"을 하게 되는데, 인터페이스는 설계 단계에서 각 객체와 실제 개발 코드를 서로 통신할 수 있는 접점을 만들어 주게 되는 것이다. 그리고 이 기능은 해당 프로그램의 확장성이나 유연성 측면에서 강력한 장점을 가지고 있다.

인터페이스 선언


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

    타입 메소드명(매개변수, ...);

    default 타입 메소드명(매개변수, ...) {..};

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

필드 선언


  • 인터페이스는 상수 필드만 선언이 가능한데, 그 이유는 클래스와 달리 객체를 생성해서 사용할 수 없기 때문이다.(익명 구현 객체 제외) 즉 인터페이스는 클래스가 아니라는 점을 기억해야 한다.

    인터페이스 상수 필드 주의사항

    • 인터페이스에 선언된 필드는 모두 public static final이고, 생략 가능하다.(자동적으로 컴파일 과정에서 붙음)
    • 추가로 상수명은 관례로 대문자로 작성해야하고 서로 다른 단어로 구성되어 있을 경우 '_' 로 연결
    • 선언과 동시에 초기값 지정
      • 정적 클래스 상수값처럼 static 블록을 통한 초기화 불가능함.

메소드 선언


추상 메소드

public abstract void testMethod(args...);
  • 인터페이스 통해 호출된 메소드들은 최종적으로 객체에서 실행이 되어야 한다.
    • 인터페이스는 추상화 껍데기이기 때문에 속은 껍데기를 구현한 객체에서 실행이 되어야 함
  • 기본적으로 실행 블록이 없는 추상메소드로 선언
    • public abstract를 생략하더라도 자동적으로 컴파일 과정에서 붙게 됨

디폴트 메소드

public default void testMethod(args..) { ... }
  • 실행 블록을 가지고 있는 메소드로 반드시 default 키워드를 붙여야 함.
    • 동일 패키지 내에서만 사용 가능
  • 기본적으로 public 접근 제한
    • 생략하더라도 컴파일 과정에서 자동 붙음

정적 메소드

public static void testMethod(args...){...}

static을 사용하는 이유
공유: 여러 인스턴스들이 동일한 데이터를 사용해야 할 때
호출: 인스턴스를 생성하지 않고 클래스로 바로 접근(클래스명.변수명)

인터페이스 구현


mblogthumb-phinf pstatic net

https://m.blog.naver.com/PostView.nhn?blogId=qkrghdud0&logNo=220676883294&proxyReferer=https:%2F%2Fwww.google.com%2F

  • 인터페이스의 추상 메소드 대한 실체 메소드를 가진 객체에서 실제 구현이 이루어진다.
    • 즉 구현 객체를 통해서 인터페이스에서 정의된 추상 메소드가 구현이 이루어짐.
    • 인터페이스 자체로 구현을 할 수는 없음.
  • 주의 사항
    • 오버라이드 된 메소드의 접근 지정자도 public이어야 함
      • 위 내용은 일반 상속 클래스에서의 오버라이딩 규칙이 그대로 적용된 것
        • 상속 오버라이딩 규칙 : 접근 지정자는 부모 객체의 메소드보보다 같거나 혹은 접근 범위가 더 큰 접근 지정자를 사용해야 함

선언


public class 클래스명 implements 인터페이스명 {

    // 인터페이스에 선언된 추상 메소드의 실체 메소드 선언
}
  • 추상 메소드의 실체 메소드를 작성하는 방법
    • 메소드의 선언부가 정확히 일치해야 함
    • 인터페이스의 모든 추상 메소드의 실체 메소드를 작성해야 함
      • 일부만 재정의 할 경우,추상 클래스로 선언(abstract키워드 추가)
  • 인터페이스를 선언에서는 추상 클래스와 달리 인터페이스는 모든 메소드를 "강제"함

예제

인터페이스

public interface RemoteControl {

    public static final int MAX_VOLUME = 10;
    public static final int MIN_VOLUME = 0;

    // abstract method
    public void turnOn();
    public void turnOff();
    public void setVolume(int volume);

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

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

구현 객체

public class Audio implements RemoteControl {

    private int volume;

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

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

    public void setVolume(int volume) {
        if (volume > RemoteControl.MAX_VOLUME) {
            this.volume = RemoteControl.MAX_VOLUME;
        } else if (volume < RemoteControl.MIN_VOLUME) {
            this.volume = RemoteControl.MIN_VOLUME;
        } else {
            this.volume = volume;
        }

        System.out.println("현재 Audio 볼륨 : " + this.volume);
    }
}

익명 구현 객체

  • 명시적인 구현 클래스 작성 생략하고 바로 구현 객체를 얻는 방법으로 일회성 구현 객체 생성 시 사용함
  • 이름 없는 구현 클래스 선언과 동시에 객체 생성.
인터페이스 변수 = new 인터페이스() {
       // 인터페이스에 선언된 추상 메소드의 실체 메소드 선언
};
                RemoteControl rc = new RemoteControl() {
            int volume;
            @Override
            public void turnOn() {
                System.out.println("turn On");
            }

            @Override
            public void turnOff() {
                System.out.println("Turn Off");
            }

            @Override
            public void setVolume(int volume) {
                if (volume > RemoteControl.MAX_VOLUME) {
                    this.volume = RemoteControl.MAX_VOLUME;
                } else if (volume < RemoteControl.MIN_VOLUME) {
                    this.volume = RemoteControl.MIN_VOLUME;
                } else {
                    System.out.println("현재 기기 볼륨 : " + volume);
                }
            }
        };

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


  • 인터페이스는 클래스와 달리 다수의 인터페이스 타입을 사용할 수 있음
public class 클래스명 implements 인터페이스A, 인터페이스B {

    // 인터페이스A에 선언된 추상 메소드의 실체 메소드 선언
    // 인터페이스B에 선언된 추상 메소드의 실체 메소드 선언
}
public interface Searchable {

    void search(String url);
}
public class SmartTelevision implements RemoteControl, Searchable {

    private int volume;

    @Override
    public void turnOn() {
        System.out.println("Turn on TV.");
    }

    @Override
    public void turnOff() {
        System.out.println("Turn off TV.");
    }

    @Override
    public void setVolume(int volume) {
        if (this.volume > RemoteControl.MAX_VOLUME) {
            this.volume = RemoteControl.MAX_VOLUME;
        } else if (this.volume < RemoteControl.MIN_VOLUME) {
            this.volume = RemoteControl.MIN_VOLUME;
        } else {
            this.volume = volume;
        }

        System.out.println("Currently TV Volume : " + this.volume);
    }

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

디폴트, 정적 메소드 사용


  • 디폴트 메소드 사용
    • 인터페이스만으로는 사용이 불가능하고 추상 메소드와 동일하게 구현 객체가 인터페이스에 대입되어야 호출이 가능함.
    • 모든 구현 객체가 가지고 있는 기본 메소드로 사용
      • 필요에 따라 구현 클래스가 디폴트 메소드 재정의하여 사용
      • 추상 메소드와는 달리 재정의 되지 않아도 사용 가능
    • 즉 디폴트 메소드는 인터페이스에서 실행 블록을 가지고 있기는 하지만, 사용은 구현 객체를 통해서 사용 가능하고 필요에 따라 구현 클래스에서 재정의해서 사용함.
  • 정적 메소드 사용
    • 인터페이스로 바로 호출 가능
public class RemoteControlExample {

    public static void main(String[] args) {

        RemoteControl rc;
        rc = new Television(); // 객체의 주소가 저장됨.
        rc.turnOn();
        rc.turnOff();
        rc = new Audio(); // 실제 구현 객체의 주소가 저장됨.
        rc.turnOn();
        rc.turnOff();
//        RemoteControl.setMute(ture); x 안됨
        RemoteControl rc1 = new Television();
        rc1.setMute(true);

        rc1 = new Audio();
        rc1.turnOn();
        rc1.setMute(true);

        RemoteControl.changeBattery();
    }
}
  • Television이라는 구현 객체를 인터페이스에 대입해야 함.

타입변환과 다형성


  • 앞서 언급한 인터페이스를 활용한 프로그램의 확장성과 유연성은 바로 타입변환과 오버라이딩을 활용한 다형성을 통해 드러나게 된다.
  • 즉 인터페이스를 구현한 객체도 클래스 상속처럼 자동 타입 변환과 다형성이 그대로 적용이 된다.
  • 자동 타입변환과 다형성에 대해 리뷰를 하면 다음과 같다.

자동 타입 변환

인터페이스 변수 = 구현객체;
  • 인터페이스로 자동 타입 변환이 됨.

필드의 다형성

  • 다형성은 객체를 부품화 시킴으로써 필드의 다형성을 구현할 수 있음.
public class 클래스명 {
    인터페이스 변수명 = new 인터페이스구현객체1()
    인터페이스 변수명 = new 인터페이스구현객체2()
    인터페이스 변수명 = new 인터페이스구현객체3()
}
public interface Tire {
    public void roll();
}
public class Car {
    Tire frontLeftTire = new HankookTire();
    Tire frontRightTire = new HankookTire();
    Tire backLeftTire = new KumhoTire();
    Tire backRightTire = new KumhoTire();

    Tire[] tires = {
            new HankookTire(),
            new HankookTire(),
            new HankookTire(),
            new HankookTire()
    };

    void run() {
        for (Tire tire : tires) {
            tire.roll();
        }
    }
}

매개변수의 다형성

  • 매개 변수의 타입이 인터페이스인 경우, 어떤 구현 객체도 매개값으로 사용 가능함.
    • 구현 객체에 따라 메소드 실행결과가 달라짐.

강제 타입 변환

  • 인터페이스 타입으로 자동 타입 변환 후, 구현 클래스 타입으로 변환
    • 강제 타입 변환의 경우 구현 클래스 타입에 선언된 다른 멤버를 사용할때 사용.

객체 타입 확인(instanceof 연산자)

  • 강제 타입 변환 전 구현 클래스 타입 조사
  • 매개값으로 어떤 구현 객체가 대입될지 모르기 때문에 강제 타입 전 항상 구현 클래스 타입을 조사한 후에 강제 타입 변환을 실행해야 함.

인터페이스 상속


  • 인터페이스도 일반 클래스 처럼 다른 인터페이스를 상속을 할 수 있는데 인터페이스는 클래스와 달리다중 상속을 허용함
public inteface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2 {..}
  • 만약 다중 상속을 한 하위 인터페이스를 구현 클래스로 만들 때는 상속한 상위 인터페이스의 모든 추상 메소드를 재정의해야 함.
    • 하위 인터페이스의 추상 메소드
    • 상위 인터페이스1의 추상 메소드
    • 상위 인터페이스2의 추상 메소드
  • 인터페이스 자동 타입 변환
    • 해당 타입의 인터페이스에 선언된 메소드만 호출이 가능함. (아래 예시 코드 참고)
public interface InterfaceA {
    public void methodA();
}
public interface InterfaceB {
    public void methodB();
}
public interface InterfaceC extends InterfaceA, InterfaceB{
    public void methodC();
}
public class ImplementationC implements InterfaceC{
    public void methodA() {
        System.out.println("ImplementationC-methodA() 실행");
    }

    public void methodB() {
        System.out.println("ImplementationC-methodB() 실행");
    }

    public void methodC() {
        System.out.println("ImplementationC-methodC() 실행");
    }
}
public class Example {

    public static void main(String[] args) {
        ImplementationC impl = new ImplementationC();

        InterfaceA ia = impl;
        ia.methodA(); // IntefaceA 추상 메소드만 사용 가능
        System.out.println();

        InterfaceB ib = impl;
        ib.methodB(); // IntefaceB 추상 메소드만 사용 가능
        System.out.println();

        InterfaceC ic = impl;
        ic.methodA();   // 모든 추상 메소드만 사용 가능
        ic.methodB();
        ic.methodC();
    }
}
  • 위 예시는 A,B 상위 인터페이스를 상속받은 C가 A,B로 자동 타입 변환을 진행할 경우 해당 타입의 메소드만 실행 가능한 경우의 예시임.

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


  • 디폴트 메소드는 인터페이스에서 선언된 인스턴스 메소드이기 때문에 "구현 객체"가 있어야 사용할 수 있음.
    • 즉 선언은 인터페이스에서 하고, 사용은 구현 객체를 통해서 함.
  • 이와 같이 디폴트 메소드는 모든 구현 객체에서 공유하는 기본 메소드처럼 보이지만, 사실은 인터페이스에서 디폴트 메소드를 허용하는 다른 이유가 존재함.

디폴트 메소드의 필요성


interface04

https://hyuntaekhong.github.io/blog/java-basic21/

  • 인터페이스에서 디폴트 메소드를 호용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서인데, 기존 인터페이스의 이름과 추상 메소드의 변경 없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새롭게 개발하는 클래스는 디폴트 메소드를 사용할 수 있다.
  • 위 그림 예시를 보면 기존에 MyInteface method만 존재한 인터페이스를 구현한 MyClassA가 존재했고, 시간이 지나 Myinterface에 메소드를 추가 해야하는 일이 존재해 method2 메소드를 추가해야하는 상황이 발생했을때, 추상 메소드로 추가로 하면 에러가 발생하게 된다. 왜냐하면 이미 구현된 MyClassA에 method2을 재정의된 추상 메소드가 존재하지 않기 때문이다. 그래서 MyInterface는 추상 메소드가 아닌 디폴트 메소드를 추가해 기존 구현 객체의 코드 변화 없이 메소드를 추가할 수 있게 되는 것이다.
public interface MyInterface {
    public void method1();

    public default void method2() {
        System.out.println("MyInterface-method2 실행");
    }
}
public class MyClassA implements MyInterface {
    @Override
    public void method1() {
        System.out.println("MyClassA-method1() 실행");
    }
}
package chapter8.defualtmethod;

public class MyClassB implements MyInterface {
    @Override
    public void method1() {
        System.out.println("MyClassB-method1() 실행");
    }

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

디폴트 메소드가 있는 인터페이스 상속


  • 부모 인터페이스에서 디폴드 메소드가 정의 되어 있을 경우, 자식 인터페이스에서 디폴트 메소드를 활용하는 방법은 세 가지가 존재.
    • 디폴트 메소드를 단순히 상속만 받음
    • 디폴트 메소드를 재정의해서 실행 내용을 변경
    • 디폴트 메소드를 추상 메소드로 재선언
public interface ParentInterface {
    public void method1();
    public default void method2() {
        /* 실행문 */
    }
}
  • 디폴트 메소드를 단순히 상속만 받은 경우
public interface ChildInterface1 extends ParentInterface {
    public void method3();
}
  • 디폴트 메소드를 재정의해서 실행 내용을 변경하는 경우
public interface ChildInterface2 extends ParentInterface{
    @Override
    default void method2() {
        System.out.println("ChildInterface2 Override ParentInterface Method2");
    }
    public void method3();
}
  • 디폴트 메소드를 선언해서 추상 메소드로 재선언
    • ChildInterface3 을 구현하는 클래스는 method1, method2, method3의 실체 메소드를 가지고 있어야함.
public interface ChildInterface3 extends ParentInterface {
    @Override
    public void method2(); // 추상 메소드로 재선언

    public void method3();
}
public class DefaultMethodExample {

    public static void main(String[] args) {
        MyInterface mi1 = new MyClassA();
        mi1.method1();
        mi1.method2();

        MyInterface mi2 = new MyClassB();
        mi1.method1();
        mi1.method2();

        ChildInterface1 ci1 = new ChildInterface1() {
            @Override
            public void method3() {
                System.out.println("child method3");
            }

            @Override
            public void method1() {
                System.out.println("child method1");
            }
        };

        ci1.method1();
        ci1.method2();
        ci1.method3();

        ChildInterface2 childInterface2 = new ChildInterface2() {
            @Override
            public void method3() {
                System.out.println("child2 method3");
            }

            @Override
            public void method1() {
                System.out.println("child2 method1");
            }
        };

        childInterface2.method1();
        childInterface2.method2();
        childInterface2.method3();

        ChildInterface3 childInterface3 = new ChildInterface3() {
            @Override
            public void method2() {
                System.out.println("child3 method2");
            }

            @Override
            public void method3() {
                System.out.println("child3 method3");
            }

            @Override
            public void method1() {
                System.out.println("child3 method1");
            }
        };

        childInterface3.method1();
        childInterface3.method2();
        childInterface3.method3();
    }
}

추상 클래스 vs 인터페이스


  • 개인적으로 인터페이스를 학습하면서 추상클래스와 많이 떠올랐는데, 추상 클래스와 인터페이스는 선언만 있고 실제 구현 내용이 없는 클래스라는 점에서 차이가 무엇이고 왜 나눠서 사용하는지 의문이 들었다.
  • 하지만 조금만 자세히 생각해보면 추상 클래스와 인터페이스는 엄연히 다른 목적을 가지고 있다. 그 이전에 추상 클래스와 인터페이스의 개념을 다시 상기해보자.

추상 클래스란?
실체 클래스들의 공통되는 필드와 메소드를 정의한 클래스로 0 개 이상의 추상 메소드를 가지고 있고, 일반 메소드, 변수를 가질 수 있음.

인터페이스란?
인터페이스는 하나의 설계도 혹은 명세서로 볼 수 있는데, 모든 메소드가 추상 메소드이고, 일반 변수를 가질 수 없다. (자바 8에서 부터는 default 키워드를 이용해 일반 메소드의 구현도 가능하긴 함)

추상 클래스와 인터페이스의 차이


  • 위 개념을 보면 추상 클래스와 인터페이스 모두 상속받은 클래스 혹은 구현하는 인터페이스 안에서 추상 메소드를 구현하도록 강제한다는 점에서 하는 일 자체는 똑같지만 존재 목적은 명확히 다름
  • 우선 추상 클래스의 경우 존재 목적은 추상 클래스를 상속 받아 기능을 "이용"하고 "확장"하는 목적.
  • 반면 인터페이스는 함수를 명시를 한 후 실제 구현 클래스에서 인터페이스의 함수의 구현을 강제하기 위함에 있다. 즉 인터페이스는 함수 구현을 강제함으로써 객체의 같은 동작을 보장함에 있다. 그래서 명세서 혹은 설계도라구 불리우는 것이다.

그리고 추가로 추상 클래스와 인터페이스는 상호 보완적인 면을 가지고 있는데, 자바는 기본적으로 다중 상속을 지원하지 않는다. 그래서 하나의 클래스는 하나의 클래스만 상속을 할 수 있는데, 인터페이스의 경우 다중 상속을 허용하고 있다. 그럼 왜 추상 클래스는 다중 상속을 허용하지 않고, 인터페이스만 다중 상속이 가능한 것일까?

단순히 클래스의 다중 상속을 보완하기 위해 존재한 것일까라는 의문점을 가질 수 있다. 여기서 다시 한번 추상 클래스와 인터페이스의 목적을 상기시켜 보자. 추상 클래스는 기능을 "이용" 및 "확장"하는 목적이기 때문에 다중 상속을 할 경우 모호성이 발생하게 되는데, 가령 Phone, Computer을 다중 상속받는 MyElectronics이라는 클래스가 존재한다고 하자.

추상 클래스 다중 상속

public abstract class Computer {

    public void turnOn() {
        System.out.println("Turn On Computer!");
    };
}
public abstract class Phone {

    public void turnOn() {
        System.out.println("Turn On Phone");
    };
}
class MyElectronics extends Phone, Computer {

    @Override
    public void play() {
         super.turnOn();
        }
}

위와 같은 코드에서 만약 Phone, Computer 클래스 모두에 turnOn()이라는 메소드를 가지고 있었다면 어떤 메소드가 실행 되어야 할까? 즉 이런 상황에서 다중 상속을 지원할 경우 발생할 수 있는 모호성이 생기게 된다.

반면 인터페이스는 아래와 같이 여러 개의 인터페이스를 할 수 있는데, 인터페이스는 추상 메소드의 구현을 강제하기 때문에 다중 상속에서 발생하는 모호성이 제거되게 된다.

public interface Phone {

    public void turnOn();
}
public interface Computer {

    public void turnOn();
}
public class MyElectronics implements Phone, Computer {

    @Override
    public void turnOn() {
        System.out.println("나의 전자기기 TurnOn!!");
    }
}

그리고 궁극적으로 상속은 부모 클래스를 이용하거나 확장하기 위해 사용되기 때문에, 다중 상속 자체가 모호해지게 되는 것이다. 반면 인터페이스는 해당 인터페이스를 구현한 객체들에 대해서 동일한 동작을 약속하는 것이기 때문에 다중 상속을 하더라도 클래스 다중 상속에서 발생하는 모호성이 발생하지 않게 되는 것이다.

그래서 단순히 추상 클래스, 인터페이스 모두 메소드를 실체 클래스에서 구현하는 관점이 아닌 인터페이스와 추상 클래스의 사용 목적을 명확히 파악한 후 사용하는 것이 중요하다.

참고

자바의 추상 클래스와 인터페이스

추상 클래스 vs 인터페이스 :: 마이구미

이것이 자바다

Comments