[JAVA] 인터페이스

SungBum Park·2021년 3월 7일
0

JAVA

목록 보기
8/9
post-thumbnail

인터페이스 정의하는 방법

인터페이스(Interface) 란?

자바에서 인터페이스는 추상 메서드로만 이루어진 일종의 추상 클래스이다. 물론 추상 클래스와 다른 점은 인터페이스는 오직 추상 메서드와 상수만을 가질 수 있다는 점이다. 따라서 모든 구현은 인터페이스를 구현한 구체 클래스에 책임을 맡긴다. 이와 같은 특징으로 추상 클래스보다는 더 유연한 프로그래밍이 가능하며, 클래스나 추상 클래스와 달리 인터페이스는 여러 개를 구현할 수도 있다.

하지만, 추상 메서드와 상수만을 가질 수 있다는 특징은 자바 8 버전 이전의 이야기이다. 자바 8 버전부터는 인터페이스 내부에 여러가지를 선언할 수 있게 되었다.

인터페이스 정의

public interface 인터페이스 {
	public static final 타입 상수 =;
	public abstract 반환타입 메서드(매개변수);
}
  • 인터페이스의 접근 제어자는 publicdefault 를 사용할 수 있다. 이 외는 컴파일 에러가 발생한다.
  • 상수 선언에서 public static final 을 생략해도, 컴파일 시에 자동으로 이를 추가해준다.(대부분 생략해서 사용함.)
  • 메서드 선언에서 public absract 를 생략해도, 컴파일 시에 자동으로 이를 추가해준다.(대부분 생략해서 사용함.)
  • 상수 선언과 메서드 선언에서 접근 제어자를 privateprotected 를 사용시, 컴파일 에러가 발생한다.

인터페이스 구현하는 방법

public class 구체클래스 implements 인터페이스 {
	@Override
	public 반환타입 메서드(매개변수) {
		구현
	}
}
  • 인터페이스를 구현하기 위해서는 클래스에서 implements 키워드를 사용한다.
  • 구체 클래스에서는 인터페이스에서 정의된 메서드를 반드시 구현(오버라이딩)해야 한다. 이를 어길시 컴파일 에러가 발생한다.
  • 구체 클래스는 여러 개의 인터페이스를 구현할 수 있다.

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

인터페이스는 기본적으로 구체 클래스에서 구현해서 사용해야 하고, 여러 구체 클래스를 가질 수 있어 상속과 같이 인터페이스의 레퍼런스를 통해 구체 클래스를 구현하는 것이 대부분이다.

public interface Fly {
    void fly();
}
public interface Run {
    void run();
}
public class Car implements Run {
    @Override
    public void run() {
        System.out.println("자동차가 달린다.");
    }
}
public class Plane implements Fly, Run {
    @Override
    public void fly() {
        System.out.println("비행기가 날다.");
    }

    @Override
    public void run() {
        System.out.println("비행기가 달린다.");
    }
}
public class App {
    public static void main(String[] args) {
        Fly planeOnlyFly = new Plane();
        Run planeOnlyRun = new Plane();
        
        planeOnlyFly.fly();
        // planeOnlyFly.run();   컴파일 에러
        
        planeOnlyRun.run();
        // planeOnlyRun.fly();   컴파일 에러
    }
}
  • 인터페이스의 레퍼런스를 사용하게 되면, 해당 인터페이스에서 정의된 메서드만을 사용할 수 있다.

인터페이스를 구현한 클래스는 인터페이스의 타입(레퍼런스)으로 선언할 수 있다.(상속과 같은 특징) 그리고 이러한 방법이 다형성을 지키는 것이고, 유연한 코드를 만들 수 있다.

하지만, 위와 같은 문제점이 발생할 수도 있다. 비행기는 날수도 있고 달릴 수도 있기 때문에, 위 코드와 같이 FlyRun 인터페이스를 구현했다. 하지만 둘 다를 하기 위해서는 Plane 구체 클래스를 타입으로 선언할 수 밖에 없다.(물론 오직 날 수만 있는 또는 오직 달릴 수만 있는 비행기라면 위와 같이 사용할 수도 있다.)

인터페이스 상속

위 문제를 매우 간단하게 해결할 수 있는 방법은 인터페이스 상속을 통해 달리고 날 수 있는 인터페이스를 만드는 것이다. 인터페이스 사이에서도 상속이 가능하다.

public interface FlyAndRun extends Fly, Run {
}
public class Plane implements FlyAndRun {
    @Override
    public void fly() {
        System.out.println("비행기가 날다.");
    }

    @Override
    public void run() {
        System.out.println("비행기가 달린다.");
    }
}
public class App {
    public static void main(String[] args) {
        FlyAndRun plane = new Plane();

        plane.fly();
        plane.run();
    }
}
  • 인터페이스 사이의 상속은 extends 키워드를 사용한다.
  • 인터페이스 사이의 상속은 다중 상속이 가능하다.
    • 인터페이스는 실제 구현체가 없어, 상속에서 다중 상속을 했을 때의 문제점이 발생하지 않는다.(자바 8 버전부터는 인터페이스에서 구현체를 만들 수 있다. 이 부분은 아래에서 다룬다.)

비행기는 날수도 달릴수도 있으므로, 이를 정의한 FlyAndRun 인터페이스를 만들고 기존의 FlyRun 인터페이스를 상속받았다. 비행기는 이를 구현하여 쉽게 수정이 가능하다.

자바 8에서 추가된 인터페이스 기능

인터페이스에서 정의된 메서드는 반드시 구현 클래스에서 구현되어야 한다. 인터페이스와 구현 클래스를 자신이 개발하는 입장에서는 이를 지키는 것이 매우 간단하다. 인터페이스에 새로운 메서드를 추가하고 구현 클래스에서 이를 구현하면 된다.

하지만, 만약 공개된 라이브러리 또는 프레임워크 내부의 인터페이스를 수정한다면, 이를 사용하는 모든 프로그램(애플리케이션)에서 오류가 발생할 것이다. 따라서 라이브러리 또는 프레임워크 내부에 내장된 인터페이스를 수정하기는 매우 어려운 작업이 될 것이다.(사용자가 이를 모두 수정해야 하기 때문이다.)

이를 해결하기 위해 자바 8 버전부터는 인터페이스 내부에 기본 구현을 포함한 메서드를 만들 수 있는 디폴트 메서드(default method)정적 메서드(static method)가 추가되었다.

공개된 API 내부의 인터페이스를 수정하면? (바이너리 호환성, 소스 호환성, 동작 호환성)
공개된 API 내부의 인터페이스를 수정하더라도, 사용자가 추가된 인터페이스의 메서드를 호출하지 않는다면 문제가 발생하지 않는다. 왜냐하면 바이너리 호환성이 유지되기 때문이다. 바이너리 호환성은 새로 추가된 메서드를 호출하지만 않으면 새로운 메서드 구현이 없이도 기존 클래스 파일 구현이 정상적으로 동작한다는 의미이다.
하지만 사용자가 새로 컴파일을 한다면, 컴파일은 인터페이스의 추가된 메서드의 구현이 없으므로 에러가 발생할 것이다. 이는 소스 호환성을 지키지 못했기 때문이다. 소스 호환성은 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일해야 한다.
마지막으로 동작 호환성이 있다. 동작 호환성은 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램은 같은 동작을 해야 한다. 인터페이스를 수정해도 기존에는 추가된 메서드를 호출할 일이 없으므로 동작 호환성은 유지된다고 볼 수 있다.

  • 출처: 자바 8 인 액션 -

인터페이스의 기본 메소드 (Default Method)

기본 메서드의 사용 방법은 간단하다.

public interface Hello {
	default void print() {
		System.out.println("Hello Default Method");
	}
}
  • default 키워드를 사용하여 기본 메서드를 구현할 수 있다.
  • 기본 메서드를 선언했다면, 이를 구현해야 한다.
  • 기본 메서드도 해당 인터페이스를 구현하는 클래스에서 오버라이딩(재정의)을 할 수 있다.

비행기는 하늘을 날수도 땅을 달릴 수도 있다. 위 예제에서는 이 문제를 인터페이스의 다중 구현으로 해결했다. 하지만 기본 메서드를 사용해서 해결할 수도 있다.

public interface Fly {
    void fly();

    default void run() {
        System.out.println("달린다.");
    }
}

물론, 설계 관점에서는 매우 안좋을 수 있지만, 인터페이스 내부에서도 구현 메서드를 삽입하여 기능을 추가할 수 있게 되었다. 하지만, 단순히 위와 같이 추가하면 FlyAndRun 인터페이스에서 컴파일 에러가 발생한다.

다중 상속 문제

위 그림은 FlyAndRun 의 상속 관계이다. 그림에서도 알 수 있듯이, run() 메서드가 2개이다. 따라서 FlyAndRun 입장에서는 어떤 run() 을 써야 할 지 모른다.

이 문제는 구현체가 없는 추상 메서드만 있는 순수 인터페이스에서는 전혀 문제가 되지 않는다. 구현체가 없으므로 단순히 구현 클래스에서 해당 메서드를 구현만 하면 되기 때문이다. 하지만 구현이 포함된 기본 메서드는 어떤 구현을 써야할지 컴파일러가 알 수 없다.

이처럼 기본 메서드가 즉 구현체가 생기는 순간 다중 상속 문제가 발생한다. 심지어, 인터페이스의 기본 메서드와 클래스의 메서드가 충돌할 수도 있다. 자바 8은 이 문제를 다음과 같은 세 가지 규칙으로 해결하였다.

  1. 클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드가 기본 메서드보다 우선권을 갖는다.
  2. 1번 규칙 이외의 상환에서는 서브 인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. (만약 클래스나 슈퍼클래스에서 해당 메서드의 정의가 없을 때는 서브 인터페이스가 우선권을 갖는다.)
  3. 1번과 2번 규칙으로 우선순위를 정하지 못하는 상황에서는 해당 메서드를 명시적으로 오버라이딩해야 한다.

FlyAndRun 인터페이스의 문제는 3번 규칙에 해당한다. 클래스가 관여하지 않았고, FlyRun 은 상속관계를 갖고 있지 않기 때문이다. 따라서 다음과 같이 해결해야 한다.

public interface FlyAndRun extends Fly, Run {

    @Override
    default void run() {
				// ...
    }
}

3번 규칙에 따라 충돌이 발생하는 run() 메서드를 직접 오버라이딩해야 컴파일 에러를 해결할 수 있다.

1번과 2변 규칙에 해당하는 간단한 예제를 살펴보자.

public class AClass {
    public void hello() {
        System.out.println("Hello from A Class");
    }
}
public interface B {
    default void hello() {
        System.out.println("Hello from B Interface");
    }
}
public class C extends AClass implements B {
}

AClass, B, C 의 관계는 위와 같다.

public class App {
    public static void main(String[] args) {
        new C().hello();
    }
}

이 상황에서 Chello() 메서드를 호출하면 무엇이 호출될까?

Hello from A Class

1번 규칙에 따라 인터페이스보다 클래스가 항상 우선권을 갖고 있으므로, AClasshello() 메서드가 호출된다.

다음 예를 보자.

public interface A {
    default void hello() {
        System.out.println("Hello from A Interface");
    }
}
public interface B extends A {
    default void hello() {
        System.out.println("Hello from B Interface");
    }
}
public class C implements A, B {
}

public class App {
    public static void main(String[] args) {
        new C().hello();
    }
}

이번에는 BA 를 상속하는 서브 인터페이스이므로, 2번 규칙이 적용된다.

Hello from B Interface

따라서 결과는 서브 인터페이스인 Bhello() 메서드가 선택된다.

인터페이스의 static 메소드

자바 8버전부터 인터페이스 내부의 static 메서드를 추가할 수 있다. 이는 기존의 static 메서드와 같이 인스턴스를 생성하지 않고 사용할 수 있다. 이를 인터페이스 내부에서 구현체와 함께 정의할 수 있게 되었다.

public interface A {
    static void hello() {
        System.out.println("Hello static method");
    }
}
public class App {
    public static void main(String[] args) {
        A.hello();
    }
}
Hello static method

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

자바 9 버전부터는 private methodprivate static method 를 사용할 수 있게 되었다.

이는 이전 자바 8에서 인터페이스에 내부 구현 메서드를 만들 수 있었는데, 이는 반드시 public 으로 공개되어야 했다. 하지만, 구현체가 생긴다면 중복 코드 제거나 캡슐화 등으로 메서드를 감출 수 있어야 더 편리하게 사용할 수 있다. 이를 위해 자바 9는 private 접근 제어자를 인터페이스 내부에서 사용할 수 있도록 했다.

참고자료


profile
https://parker1609.github.io/ 블로그 이전

0개의 댓글