티스토리 뷰

JAVA/whiteship-livestudy

인터페이스

최진영 2021. 4. 14. 16:28

인터페이스 정의하는 방법

 인터페이스는 결국 앞전에 상속에서 배웠던 "추상클래스"이다. 추상클래스란 추상화한 메소드를 가진 메소드 즉, 아직 구현되지 않은 메소드들을 가진 클래스였다. 인터페이스는 이 추상클래스에서 추상화 정도가 매우 높아서 오로지 추상 메소드와 상수만을 멤버로 가진 클래스를 말한다.

 추상클래스가 조금 만들어둔 설계도라고하면 인터페이스는 아예 아무것도 안만든 설계도라고 할 수 있다.

interface 인터페이스이름{
	public static final 타입명 변수이름 = 값;
	public abstract 메소드명 (매개변수, ...);
}

 이때 모든 상수는 반드시 public static final 이어야하며, 생략하여 사용할 수 있다.

 메소드도 마찬가지로 public abstract 이어야하며 생략할 수 있지만 static 메소드와 default 메소드는 예외이다.

(생략된 제어자는 컴파일러가 컴파일시에 알아서 삽입해준다)
package week8;

public interface Exam1 {
    public static final int age = 20;
    int year = 2021;
    
    public abstract void Hello();
    int Bye(int time);
}

 

 그런데 추상클래스는 결국 몇몇 메소드를 추상화하여서 만든 클래스이고 인터페이스는 모든 메소드를 추상화하여서 만든 클래스라고 할 수 있다. 그냥 추상클래스로 다 표현하면 되는데 인터페이스를 왜 굳이 구분지어 놓았을까?

 이는 그 둘의 존재 목적자체가 다르기 때문이다. 추상 클래스는 존재 의의가 추상 클래스를 상속받아서 기능을 이용할 수 있을 뿐만 아니라 확장시키는 것에 있다. 근데 인터페이스는? 모든 메소드를 다 구현해야한다. 즉, 구현을 강제했다는 것이다. 따라서 인터페이스의 존재 목적은 함수의 구현을 강제함으로 써 구현 객체와 같은 동작을 보장하기 위함이다. 이는 아래의 다중상속에 대해 이야기할 때 다시 이야기하도록 한다.

 

인터페이스 장점

  • 개발시간을 단축시킬 수 있다.
  • 표준화가 가능하다.
  • 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.
  • 독립적인 프로그래밍이 가능하다.

 

1. 개발시간을 단축시킬 수 있다.

 동시에 한쪽에서 인터페이스를 구현하는 클래스를 만들고 있을 때 다른 한쪽에서 클래스가 작성될 때까지 기다리지 않더라도 양쪽에서 동시에 개발 할 수 있다.

2. 표준화가 가능하다.

 프로젝트를 진행할 때 기본 틀로써 인터페이스를 작성한 다음, 모든 개발자가 인터페이스를 구현하여 개발을 하게함으로써 일관되고 정형화된 프로그래밍이 가능하다.

3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.

 서로 상속관계가 아니거나, 같은 부모클래스를 받지않아도 인터페이스로 구현함으로써 관계를 맺어줄 수 있다.

4. 독립적인 프로그래밍이 가능하다.

 인터페이스를 통해 클래스 선언(인터페이스쪽)과 구현부(상속받는 클래스쪽)를 구분지을 수 있기 때문에 독립적인 프로그램을 작성하는 것이 가능하다.

 

인터페이스 구현하는 방법

 우리는 보통 상속을 이야기할 때 인터페이스를 상속한다, 클래스를 상속한다고 말한다. 하지만 클래스의 상속은 상속을 의미하지만 엄밀히 따지자면 인터페이스의 상속은 구현을 한다는 것에 더 가깝다.

 메소드 내용 자체를 새로 구현하는 것이 인터페이스를 상속받은 클래스가 하는 역할이기 때문이다. 따라서 인터페이스는 구현할 때 extends가 아닌 implements 키워드를 사용한다.

 

 가장 일반적으로는 말했다시피 클래스에 implements키워드를 사용하는 것이다.


 그럼 우리의 Intellij 컴파일러가 아주 친절하게도 abstract(추상화된) 메소드가 있으니 구현해달라고 한다. 이를 자동완성을 해보면,


와 같은 메소드들이 정의된다. 즉, 인터페이스를 구현하였을 때는 추상화 클래스를 만들었을 때와 같이 추상화된 인터페이스의 모든 메소드들을 일단 받아와서 정의를 해야한다는 점이다.

package week8;

public class Exam2 {
    public static void main(String[] args) {
        Exam3 exam3 = new Exam3();
        exam3.Hello();
        System.out.println(exam3.Bye(10));
    }
}

class Exam3 implements Exam1 {

    @Override
    public void Hello() {
        System.out.println("hello");
    }

    @Override
    public int Bye(int time) {
        System.out.println(time);
        return age;
    }
}
//    [출력]
//    hello
//    10
//    20

 재정의해서 쉽게 사용이 가능하다. 단, 눈여겨볼점은 인터페이스의 상수이다. 인터페이스에서 상수로 지정했던 age를 특별한 선언없이 상속받았기 때문에 바로 사용할 수 있음을 확인할 수 있다.

 자주 사용하는 방식은 아니지만 상속에서 추상클래스를 사용할 때 객체화 동시에 메소드 재정의를 하면 굳이 상속받지 않아도 객체로써 사용할 수 있었는데 인스턴스도 마찬가지로 객체화 동시에 메소드를 재정의한다면 사용할 수 있다.

public class Exam2 {
    public static void main(String[] args) {
        Exam1 exam1 = new Exam1() {
            @Override
            public void Hello() {
                
            }

            @Override
            public int Bye(int time) {
                return 0;
            }
        };
    }
}

 

 

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

 상속에서 우리는 다형성을 사용하는 방법에 대해서 배웠었다. 쉽게 말해서 인스턴스화를 했더라도 내가 선언한 참조클래스라는 틀에 따라서 참조 변수가 가르키는 내용이 달라지게 되는 것이다.

 인터페이스는 그자체로 사용할 수 없는 이유가 메소드가 "정의"되지 않았기 때문이다. 즉, 인스턴스화를 해서 쓰고싶어도 정의되지 않은 클래스이기 때문에 상속을 하거나, 위처럼 인스턴스화를 하는 동시에 메소드 정의를 해주었어야 했다. 그럼 인터페이스 참조 변수에 인터페이스를 상속받은 클래스를 인스턴스화한 객체를 넣으면?

package week8;

public class Exam4 {
    public static void main(String[] args) {
        Exam1 exam5 = new Exam5();
        Exam1 exam6 = new Exam6();

        exam5.Hello();
        System.out.println(exam5.Bye(10));
//        System.out.println(exam5.num5);


        exam6.Hello();
        System.out.println(exam6.Bye(10));
    }
}

class Exam5 implements Exam1 {
    int num5 = 6;

    @Override
    public void Hello() {
        System.out.println("hello5");
    }

    @Override
    public int Bye(int time) {
        return time +5;
    }
}

class Exam6 implements Exam1 {

    @Override
    public void Hello() {
        System.out.println("hello6");
    }

    @Override
    public int Bye(int time) {
        return time + 6;
    }
}

//    [출력]
//    hello5
//    15
//    hello6
//    16

 내가 인스턴스화한 객체가 Exam5, Exam6를 인터페이스인 Exam1의 참조변수에 넣으면 인터페이스일지라도 다형성에 의해서 사용할 수 있음을 확인할 수 있다. 클래스 상속과 마찬가지로 참조변수가 가르킬 수 있는 인터페이스의 상수와 메소드만 사용할 수 있다.

 

인터페이스 상속

 좀전에 인터페이스를 implements"구현"한다고 했었다. 그럼 인터페이스를 extends"상속"받으면 어떻게 될까?


 불가능하니까 implements로 바꾸라한다. 결국 우리가 인터페이스를 상속(extend)한다가 아니라 구현(implement)한다가 맞는 말인 것이다.

(인터페이스 상속이라는 말을 자주 쓰게 되더라도 인터페이스 상속 == 구현이라는 것을 알고 있어야한다.)

 

 클래스 상속을 이야기할 때 클래스의 다중 상속이 자바에서 금지되는 이유를 기억해보면, 두 클래스에게 상속받았을 때 두 클래스가 같은 이름의 메소드를 가지고 있을 때 어떤 것을 사용하는지 확인이 불가능하기 때문에 다중 상속을 금지했었다.

 하지만 인터페이스에서는 다중 상속이 가능하다! 왜 그럴까?

 동일한 메소드를 가진 인터페이스 둘과 이를 상속받은 클래스를 살펴보자.

package week8;

public class Exam8 {
    public static void main(String[] args) {
        Exam11 exam11 = new Exam11();
        exam11.hello();
        exam11.bye();
//        System.out.println(exam11.time);
        System.out.println(exam11.age9);
        System.out.println(exam11.age10);
    }
}
interface Exam9 {
    public static final int time = 9;
    public static final int age9 = 90;
    void hello();
    void bye();
}
interface Exam10 {
    public static final int time = 10;
    public static final int age10 = 100;
    void hello();
    void bye();
}
class Exam11 implements Exam9, Exam10 {

    @Override
    public void hello() {
        System.out.println("hello11");
    }

    @Override
    public void bye() {
        System.out.println("bye11");
    }
}//    [출력]
//    hello11
//    bye11
//    90
//    100

 인터페이스는 아예 메소드의 구현부 자체가 없는 설계도 그자체이다. 따라서 여러 개의 인터페이스를 다중 상속받았더라도 어짜피 메소드의 구현부는 부모 클래스가 아닌 상속받은 자식클래스에서 정의한 것에 의해 따라간다. 클래스 상속에서의 "모호성"이 인터페이스 상속에서는 존재하지 않는 것이다.

 단, 인터페이스 상수는 얘기가 좀 다르다. 앞전에 인터페이스를 상속받았을 때 상수를 그대로 쓸 수 있기는 했다. 근데 만약에 다중 상속을 받았을 때 상수의 변수명이 같으면...?

 위 Exam9의 상수 time과 Exam10의 상수 time처럼 이름이 같은 상수를 사용한다면 클래스 메소드의 "모호성"이 상수에서도 적용된다. 내가 두 인터페이스를 상속받아서 time상수를 사용했는데 Exam9도 있고 Exam10도 있다. 모호하다... 따라서 자바에서는 이러한 중복된 상속 상수에 대해서도 사용을 금지한다. 중복되지 않은 상수는 그대로 사용이 가능하다.

 

 다중 상속이 가능하다고 했는데 그럼 인터페이스와 클래스를 다중상속 받을 수 있을까? 가능하다. 단, 알다시피 클래스의 메소드 구현부는 존재하고 있기 때문에, 클래스에 존재하는 메소드가 우선 상속된다.

 

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

 처음 우리가 이야기할 때 인터페이스와 추상 클래스의 차이는 추상화의 정도에 따라 다르다고 이야기했다. 추상 클래스는 일부만 추상화되어있고, 인터페이스는 추상화 정도가 높아서 추상 메소드와 상수만 가진다고 말이다. 근데... JDK 1.8부터는 말이 조금 달라진다.

 Default Method와 Static 메소드를 인터페이스에서 사용이 가능하다. (static 메소드는 밑에서 설명한다.)

 

 일반 조상 클래스에서 메소드를 새로 추가하는 것은 크게 어려운 일이 아니다. 어짜피 자식클래스는 이를 받아서 쓰기만하면 되고, "메소드 강제 구현"이라는 제약없이 부모 메소드를 바로 내려서 쓰면되기 때문에 필요에 의해서 overriding하는 경우 말고는 영향이 없다고 볼 수 있다.

 근데 인터페이스는? 인터페이스를 상속받은 모든 클래스들이 이 메소드 추가됐다고 클래스를 다 돌아다니면서 구현해야한다. 인터페이스는 설계도 그자체이기 때문에 인터페이스가 변경되는 일은 없어야하는 것이 사실 맞다. 하지만 강제로 제한해두었다고 해도 변경될 일이 어떻게든 생기기 때문에 JDK 1.8에서는 default 메소드를 추가한다.

default method : 기본적인 구현이 제공되어 있는 인터페이스의 메소드

 인터페이스의 일반적인 메소드와는 다르게 default 메소드는 "구현"이 이미 된 상태를 상속한다. 일반 클래스 상속처럼 말이다.

package week8;

public class Exam12 {
    public static void main(String[] args) {
        Exam14 exam14 = new Exam14();
        exam14.hello();
        exam14.bye();
    }
}

interface Exam13 {
    void hello();

    default void bye() {
        System.out.println("bye13");
    }
}
class Exam14 implements Exam13 {

    @Override
    public void hello() {
        System.out.println("hello14");
    }
}
//    [출력]
//    hello14
//    bye13

 default 메소드의 접근제어자는 public이며 생략가능하다.

 default 메소드는 구현부가 정의된 메소드이기 때문에 추가되더라도 상속받은 클래스에서 이를 반드시 구현해야할 일은 없다. 단, 강제적인 구현이 아닌 일반 클래스처럼 메소드 구현을 했기 때문에 충돌하는 상황이 발생한다.

 

1. 인터페이스 다중 상속에서 인터페이스 둘 다 동일한 이름의 메소드를 사용한다.

interface Exam13 {
    void hello();

    default void bye() {
        System.out.println("bye13");
    }
}
interface Exam14 {
    void hello();
    
    default void bye() {
        System.out.println("bye14");
    }
}

 둘을 상속받게 되면 bye()메소드가 무관한 상속을 하였다하여 에러가 나게된다.

 이 상황에서는 다중 상속받은 default 메소드를 Override를 통해 재정의해야 한다.

class Exam15 implements Exam13, Exam14 {

    @Override
    public void hello() {
        System.out.println("hello14");
    }

    @Override
    public void bye() {
        
    }
}

 

2. 인터페이스의 default 메소드와 조상 클래스의 메소드 이름을 동일하게 사용한다.

 앞전에 인터페이스 메소드와 조상 클래스의 메소드가 동일할 때는 조상 클래스의 메소드를 우선적으로 따른다고 했다.

 근데 default 메소드가 생긴 시점에서 이 둘을 같이 쓰게 된다면? 동일하게 조상 클래스의 메소드를 우선적으로 따르며 default 메소드는 무시된다.

package week8;

public class Exam12 {
    public static void main(String[] args) {
        Exam15 exam15 = new Exam15();
        exam15.bye();
    }
}

interface Exam13 {
    default void bye() {
        System.out.println("bye13");
    }
}
class Exam14 {
    public void bye() {
        System.out.println("bye14");
    }
}
class Exam15 extends Exam14 implements Exam13 {
}
//    [출력]
//    bye14

 

물론 다 머리아프다면 상속받은 메소드 중 동일한 부분을 Override으로 복사붙여넣기하면 만능이긴하다...

 

 

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

 JDK 1.8의 인터페이스 주요 업데이트 중 static 메소드이다.

 static 메소드는 해당 클래스의 고유한 메소드가 되며, 상속되면서 사용되지 않기 때문에 인터페이스에서 호출하는 것이외에는 사용되지 않는다. (Overriding 사용이 불가하다.) 즉, 컴파일 시점에서 Dispatch되기 때문에 인터페이스 고유의 메소드가 되는 것이다.

package week8;

public class Exam12 {
    public static void main(String[] args) {
        Exam15 exam15 = new Exam15();
        Exam13.hello();
    }
}

interface Exam13 {
    static void hello() {
        System.out.println("exam13");
    }
}

class Exam15 implements Exam13 {
//    @Override
//    static void hello() {
//        System.out.println("exam14");
//    }
}
//    [출력]
//    exam13

 단, 클래스 상속에서의 static 메소드 활용과 인터페이스 상속에서의 static 메소드 활용이 조금 다르다.

 클래스 상속에서의 static 메소드 이야기는 다음 링크를 통해서 먼저 알고 오면 편하다.

 클래스 상속에서는 super 키워드가 메인이 되어서 자식 생성자를 생성할 때 부모 생성자도 같이 생성되어서 서로 연결되는 형식이었다. 근데 인터페이스는 super를 기본적으로 다루지 않는다.

 따라서 인터페이스는 설계도로써의 역할만 자식 클래스에게 제공하지 나머지 모든 상속으로써의 지원은 하지 않는다. 따라서 인터페이스는 Override를 제외한 기능은 사용하지 않는다고 생각하면 편하다. 인터페이스는 "구현"에 포커싱되어있기 때문에 부모 생성자가 없기 때문이다.

 자식클래스에서 인터페이스의 static메소드를 사용하고싶다면 항상 메소드 이전에 인터페이스를 Exam13.hello()와 같이 명시해주어야 한다.

 

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

 우리가 private 접근 제어자를 사용하는 이유가 뭘까? 내부에서 처리해야할 메소드임에도 불구하고 public으로 공개되기 싫기 때문에 private로 만든다.

 JDK 1.8 이전의 인터페이스는 인터페이스에 메소드 구현부를 정의하지 않았기 때문에 굳이 이러한 고민이 사실 필요없었다. 근데 JDK 1.8에서 default 메소드와 static 메소드를 지원하기 시작했기 때문에 인터페이스에서의 private 사용에 대한 요구사항을 자바에게 요구하게 된 것이다.

 즉, 인터페이스 내부에서는 사용해야할 메소드이지만 다른 인터페이스나 클래스가 인터페이스를 상속받아 이를 사용하는 것을 원하지 않기 때문에 JDK 1.9에서는 이를 업데이트하였다.

private methodprivate static method를 사용할 수 있으며 자세한 내용은 코드를 통해 이해하도록 한다.

package week8;

public class Exam12 {
    public static void main(String[] args) {
        Exam14 exam14 = new Exam14();
        exam14.test();
        Exam13.test2();
    }
}
class Exam14 implements Exam13 {

}
interface Exam13 {
    default void test() {
        hello();
        bye();
    }
    static void test2() {
//        hello();
        bye();
    }
    private void hello() {
        System.out.println("hello");
    }
    private static void bye() {
        System.out.println("bye");
    }
}
//    [출력]
//    hello
//    bye
//    bye

 static 메소드와 default 메소드에 대한 이해를 통틀어서 할 수 있다.

'JAVA > whiteship-livestudy' 카테고리의 다른 글

예외 처리  (0) 2021.08.26
패키지  (0) 2021.04.09
상속  (0) 2021.03.28
클래스  (0) 2021.03.24
제어문  (0) 2021.03.17
댓글
최근에 올라온 글
Total
Today
Yesterday