인터페이스(Interface)

인터페이스는 우리가 전 편에서 본 추상 클래스와 매우 흡사합니다. 인터페이스는 추상 클래스처럼 추상 메서드를 멤버로 가지며 인터페이스도 마찬가지로 인스턴스화 할 수 없습니다. 하지만 아래와 같은 차이점이 있습니다.

  • 인터페이스 내에 선언된 메서드는 public abstract로 선언됩니다.
  • 인터페이스 내에 선언된 변수는 public static final로 선언됩니다. 이와 다르게 추상 클래스는 정적이 아닌 필드, final이 아닌 필드를 선언할 수 있습니다.
  • 인터페이스에는 생성자가 없습니다.
  • 추상 클래스는 extends 키워드를 통해 다른 클래스를 상속받거나 인터페이스를 구현할 수 있지만, 인터페이스는 extends 키워드로 오로지 인터페이스만을 상속받을 수 있습니다.

인터페이스 만들기

인터페이스를 만드려면 아래와 같이 작성해야 합니다.

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

인터페이스를 구현하려면 아래와 같이 implements 키워드를 사용합니다.

구현(implementation)

구현(혹은 구현체)의 정의를 위키백과에서 확인하면 다음과 같습니다.

구현(implementation)은 계획, 아이디어, 모델, 설계, 규격, 표준, 알고리즘 혹은 정책을 수행하거나 애플리케이션을 실현하는 것을 말한다. ... 컴퓨터 공학에서, 구현이란 기술적으로 정리해놓은 규격서(specification)나 알고리즘을 프로그래밍 혹은 소프트웨어 배치를 통해 프로그램이나 소프트웨어의 부분, 다른 컴퓨터 시스템을 구축하는 것을 말한다. 하나의 규격서나 표준에도 다양한 구현이 있을 수 있다.

보통 다소 추상적인 설계를 구체적인 사실로 바꾸는 것을 말하며, 인터페이스를 구현한다는 말은 흔히 클래스에서 인터페이스에 있는 모든 추상 메서드를 오버라이딩하여 메서드 본문에 구체적인 코드를 작성하는 것을 말합니다.

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

class 클래스명 implements 인터페이스명 {
	// ...
}

인터페이스에 다른 인터페이스를 상속받으려면 클래스를 상속받을 때 extends를 써준 것처럼 쓰시면 됩니다.

interface 인터페이스A {
	// ...
}

interface 인터페이스B extends 인터페이스A {
	// ...
}

여러 개의 인터페이스를 구현할 필요가 있을 경우, 아래와 같이 쉼표(,)를 기준으로 인터페이스를 구분합니다. 인터페이스가 다른 인터페이스를 상속받을 때도 마찬가지입니다.

interface 인터페이스A {
	// ...
}

interface 인터페이스B {
	// ...
}

// 인터페이스는 클래스와는 달리 다중 상속이 가능하다.
interface 인터페이스C extends 인터페이스A, 인터페이스B {
	// ...
}

// 클래스는 여러 개의 인터페이스를 구현할 수 있다.
class 클래스명 implements 인터페이스A, 인터페이스B {
	// ...
}

특징 살펴보기

이어서 인터페이스의 특징을 살펴보도록 하겠습니다. 인터페이스도 추상 클래스와 마찬가지로, 인터페이스를 구현하는 클래스는 해당 인터페이스에 선언된 메서드를 모두 구현하거나 추상 클래스로 선언되어야 합니다.

interface Shape {
	double getArea();
	
	double getCircumference();
}

//에러: Rectangle 타입은 상속받은 추상 메서드 Shape.getArea()를 반드시 구현해야 합니다.
class Rectangle implements Shape { }

위에서도 봤지만 인터페이스 내에 선언된 메서드는 public abstract로, 필드는 public static final로 선언됩니다.

interface Shape {
	/* public static final */ double DEFAULT_X = 0;
	
	/* public static final */ double DEFAULT_Y = 0;
	
	/* public abstract */ double getArea();
	
	/* public abstract */ double getCircumference();
}

내부 인터페이스

인터페이스는 클래스나 다른 인터페이스의 멤버로 선언될 수 있습니다. 덧붙여서 이런 인터페이스를 중첩 인터페이스(nested interface)나 멤버 인터페이스(member interface), 내부 인터페이스(inner interface)라고 합니다.

interface OuterInterface {
	/* public static */ interface InnerInterface {
		// ...
	}
}

이런 내부 인터페이스의 대표적인 예로는 자바 표준 라이브러리의 Map.Entry가 있습니다. 두 인터페이스가 강하게 연결되어 논리적으로 그룹화하려는 경우 내부 인터페이스가 유용할 수 있습니다.

package java.util;

public interface Map<K, V> {
	...
	/* public static */ interface Entry<K, V> {
		...
	}
}

여기서 알아둬야 할 점은 내부 인터페이스는 기본적으로 정적(static)이라는 사실입니다. 따라서 외부 인터페이스를 구현하는 클래스는 내부 인터페이스의 메서드를 구현하지 않아도 컴파일 에러가 발생하지 않습니다.

interface OuterInterface {
    void foo();
    interface InnerInterface {
        double bar();
    }
}

class OuterInterfaceImpl implements OuterInterface {
    public void foo() { ... }
}

class InnerInterfaceImpl implements OuterInterface.InnerInterface {
	public double bar() { ... }
}

그리고 인터페이스에 private, protected 접근 제어자를 붙일 수 없는데 이런 인터페이스에는 접근할 수 없으므로 의미가 없기 때문입니다. 내부 인터페이스도 이와 마찬가지입니다. 하지만 클래스 내의 내부 인터페이스에는 private, protected를 붙일 수 있습니다.

class OuterClass {
    /* static */ private interface InnerInterface {
        double bar();
    }

	// 중첩 클래스는 뒤에서 살펴본다.
    class InnerClass implements InnerInterface {
        public double bar() { return 0; }
    }
}

// InnerInterface의 접근 제어자가 private이기 때문에 에러가 발생한다. 그 외의 접근 제어자로 변경하면 접근할 수 있게 되어 에러는 발생하지 않는다.
class ChildClass implements OuterClass.InnerInterface {
    public double bar() { return 0; }
}

정적 메서드(static method)

자바 8부터는 정적 메서드를 인터페이스에 추가할 수 있습니다. 클래스의 정적 메서드와 거의 동일하지만, 차이점이 있다면 정적 메서드는 인터페이스를 구현하는 클래스에 상속되지 않습니다.

interface A {
	static void doSomething() {
		// ...
	}
}

class B implements A { }

public class InterfaceExamples {
	public static void main(String[] args) {
		B obj = new B();
		
		A.doSomething();
		// 에러: 타입 B에 메서드 doSomething()가 정의되지 않았습니다.
		// obj.doSomething();
	}
}

인터페이스에 정적 메서드가 추가된 이유는 무엇인가요?

정적 메서드를 지원하기 이전에는 인터페이스와 관련된 유용한 기능들을 그룹화하려면 별도의 유틸리티 클래스를 만들어야 했습니다. 여기서 유틸리티 클래스는 모든 메서드가 정적 메서드이며, 개발 도중 자주 사용하는 유용한 메서드들을 모아둔 클래스입니다. 하지만 유틸리티 클래스는 단순히 관련있는 정적 메서드의 집합이기 때문에 인스턴스화를 하는 것은 의미가 없으므로, private 생성자로 외부에서 객체를 생성하는 것을 막곤 했습니다. 인터페이스를 사용하면 별도의 클래스를 만들지 않고도 유틸리티 메서드를 제공할 수 있습니다.

디폴트 메서드(default method)

자바 8부터는 추상이 아닌, 즉 본문이 있는 메서드를 인터페이스에 추가할 수 있습니다. 이 메서드를 디폴트 메서드라고 하며, 가상 확장 메서드(virtual extension method) 또는 디펜더 메서드(defender method)라고 부르기도 합니다. 디폴트 메서드를 선언하는 방법은 다음과 같습니다.

interface 인터페이스명 {
	default 반환형 메서드명() {
    	// ...
    }
}

디폴트 메서드는 어디에 사용할 수 있을까요?

디폴트 메서드의 필요성을 오라클에선 이렇게 설명하고 있습니다.

만약 컴퓨터로 제어되는 자동차를 만드는 제조업체들이 자사의 자동차에 비행 같은 새로운 기능을 추가한다면 어떨까요? 그러면 다른 회사(예를 들어, 전자지도 기기 제조업체 등)가 자신의 소프트웨어를 하늘에 나는 자동차에 맞도록 고칠 수 있도록, 자동사 제조업체는 새로운 메서드를 명시할 필요가 있을 것입니다. 자동차 제조업체들은 비행과 관련된 새로운 메서드들을 어디에 선언할까요? 만약 제조업체들이 새로운 메서드들을 기존의 인터페이스에 추가한다면, 그 인터페이스를 구현했던 프로그래머들은 인터페이스를 다시 구현해야 합니다. 만약 새로운 메서드들을 정적 메서드로 추가하면 프로그래머들은 추가된 정적 메서드를 필수적인 핵심 메서드가 아니라 유틸리티 메서드로 생각할 것입니다.

디폴트 메서드를 사용하면 라이브러리의 인터페이스에 새로운 기능을 추가하고 이 인터페이스의 이전 버전을 구현한 코드와의 이진 호환성(binary compatibility)을 보장할 수 있습니다.

예를 들어서, A라는 인터페이스가 있고 그 인터페이스를 구현하는 수백 개의 클래스가 있다고 합시다. 인터페이스를 확장하고 싶어서 새로운 추상 메서드를 A에 추가하게 되면, 그 인터페이스를 구현하는 수백 개의 클래스에는 새롭게 추가된 추상 메서드에 대한 구현이 없으므로 컴파일 에러가 발생할 것입니다. 기존의 클래스를 수정하지 않고 인터페이스에 새로운 기능을 추가하려면 디폴트 메서드를 사용할 수 있습니다. 즉, 디폴트 메서드의 원래 목적은 하위 호환성을 지키기 위한 것입니다.

다이아몬드 문제

자바 7 이전에는 인터페이스의 다중 상속에는 다이아몬드 문제가 없었습니다. 인터페이스에는 클래스와는 달리 구현이 없는 메서드 선언만 있기 때문에 모호할 게 전혀 없기 때문입니다. 하지만 자바 8 이후부터는 디폴트 메서드가 추가되면서 인터페이스도 다이아몬드 문제를 피해갈 수 없게 되었습니다.

interface MyInterface1 {
	default void myMethod() { /* ... */ }
}

interface MyInterface2 extends MyInterface1 {
	default void myMethod() { /* ... */ }
}

interface MyInterface3 extends MyInterface1 {
	default void myMethod() { /* ... */ }
}

// 에러: ()와 () 매개변수가 있는 myMethod라는 이름의 중복 디폴트 메서드를 MyInterface3과 MyInterface2 타입에서 상속받고 있습니다.
class MyClass implements MyInterface2, MyInterface3 { }

 자바에서는 이런 모호함을 해결하기 위해서 여러 개의 규칙을 도입하여 다이아몬드 문제를 피했습니다. Java 8 in Action에선 규칙을 다음과 같이 설명하고 있습니다.

  1. 클래스나 부모 클래스의 메서드 선언을 디폴트 메서드 선언보다 언제나 우선한다.
  2. 여전히 가릴 수 없을 때, 상속 관계를 갖는 인터페이스에서 동일한 시그니처를 가진 메서드가 있으면 자식 인터페이스의 메서드 선언을 우선한다. (즉, 인터페이스 B가 인터페이스 A를 상속받으면 B를 더 우선한다)
  3. 마지막으로, 여전히 메서드를 선택할 수 없다면 여러 인터페이스를 상속받는 클래스는 메서드를 오버라이딩하고 원하는 메서드를 명시적으로 호출하여 어떤 디폴트 메서드의 구현을 고를지 선택해야 한다.

아래 코드처럼 어떤 디폴트 메서드의 구현을 고를지 선택할 수 있습니다.

interface MyInterface1 {
	default void myMethod() { 
		System.out.println("MyInterface1.myMethod()");
	}
}

interface MyInterface2 extends MyInterface1 {
	default void myMethod() { 
		System.out.println("MyInterface2.myMethod()");
	}
}

interface MyInterface3 extends MyInterface1 {
	default void myMethod() { 
		System.out.println("MyInterface3.myMethod()");
	}
}

class MyClass implements MyInterface2, MyInterface3 { 
	public void myMethod() {
		// 상속받은 인터페이스의 디폴트 메서드를 명시적으로 호출함
		MyInterface2.super.myMethod();
		MyInterface3.super.myMethod();
	}
}

public class InterfaceExamples {
	public static void main(String[] args) {
		MyClass myClass = new MyClass();
		
		myClass.myMethod();
	}
}

private 메서드

자바 8에서 인터페이스에 디폴트 메서드가 추가된 후에, 자바 9에서 private 메서드를 사용하는 것을 허용했습니다. private 메서드를 사용하면 세부적인 내용을 외부에 노출시키지 않은 채 디폴트 메서드에서만 접근할 수 있는 메서드를 만들 수 있습니다.

interface MyInterface {
	default void defaultMethod() {
		privateMethod("디폴트 메서드가 호출되었습니다.");
	}
	
	private void privateMethod(final String string) {
		System.out.println(string);
	}
}

class MyClass implements MyInterface { }

public class InterfaceExamples {
	public static void main(String[] args) {
		MyClass myClass = new MyClass();
		
		myClass.defaultMethod();
	}
}

인터페이스 vs 추상 클래스

기술적인 관점

원래 추상 클래스에선 필요에 따라서 메서드를 구현할 수 있지만 인터페이스는 그럴 수 없었습니다. 하지만 자바 8부터는 인터페이스에 정적 메서드를 추가할 수 있고, 디폴트 메서드란 개념이 추가되면서 인터페이스와 추상 클래스의 경계가 조금 모호해졌습니다.

interface MyInterface {
	default void myMethod() {
		System.out.println("myMethod()가 호출되었습니다.");
	}
}

다중 상속이 필요할 때는 아래와 같이 인터페이스를 사용할 수 있습니다. 추상 클래스로는 다중 상속을 할 수 없습니다.

interface Bird { /* ... */ }

interface Horse { /* ... */ }

interface Pegasus extends Bird, Horse { /* ... */ }

그리고 인터페이스에선 일반 필드를 선언할 수 없습니다. 따라서, 정적 필드나 final 필드가 아닌 필드가 필요한 경우 추상 클래스를 사용할 수 있습니다. 또한, 추상 클래스에는 생성자를 만들 수 있지만 인터페이스는 그럴 수 없습니다.

interface HolidayCalendar {
	// 에러: 인터페이스는 생성자를 가질 수 없습니다.
	public HolidayCalendar() { }
}

abstract class HolidayCalendar {
	public HolidayCalendar() { }
}

설계적인 관점

들어가기 전에, IS-A 관계 혹은 CAN-DO 관계는 어떤 상황에서나 추상 클래스를 사용할지 인터페이스를 사용할지 가릴 수 있는 만능 도구는 아닙니다. 때로는 인터페이스로 IS-A 관계를 나타낼 수도 있고 CAN-DO 관계를 나타낼 수도 있습니다. 하지만 헷갈릴 때는 이런 접근법이 유용할 수 있습니다.

IS-A 관계

'A는 B이다'라는 문장을 나타낼 수 있는 경우에는 추상 클래스나 상속 구조를 사용합니다. 여기서 클래스 A는 클래스 B의 자식 클래스라고 할 수 있습니다. 상속 구조에서 위로 올라갈수록 추상적이 됩니다.

완벽한 방법은 아니지만 보통은 위와 같이 접근하면 쉽게 상위 개념과 하위 개념을 구분할 수 있습니다. IS-A 관계보다 더 정확하게 검사하려면 SOLID 원칙 중 하나인 리스코프 치환 원칙을 사용할 수 있습니다.

원-타원 문제(Circle–ellipse problem)

모든 상황에서 IS-A 관계를 상속 관계에 적용할 수 있는 것은 아닙니다. 위키피디아에서 원-타원 문제를 살펴보면 다음과 같습니다.

소프트웨어 개발에서 원-타원 문제(사각형-직사각형 문제라고도 함)는 객체 모델링에서 서브타입 다형성을 사용할 때 일어날 수 있는 몇 가지 함정을 분명하게 보여준다. [...] 문제는 원과 타원(정사각형과 직사각형도 마찬가지)을 나타내는 클래스 사이에 어떤 상속 관계나 서브타이핑이 있어야 하는지에 관한 것이다. [...] 이 예에서 원의 집합은 타원 집합의 부분 집합이며, 원은 장축과 단축의 길이가 같은 타원으로 정의할 수 있다. 따라서 도형을 모델링하는 객체 지향 언어로 쓰인 코드는 Circle 클래스를 Ellipse 클래스의 자식 클래스로, 즉 Circle 클래스를 상속받도록 선택하는 경우가 많다. [...] 타원은 원보다 상태를 더 많이 만들어야 하는데, 타원은 장축과 단축의 길이와 회전을 명시하는 속성이 필요한 반면에 원은 반지름만 필요하기 때문이다.

원은 타원이지만 타원이 할 수 있는 걸 원은 할 수 없습니다. 예를 들어서, 타원은 늘어날 수 있는 반면에, 원은 늘어날 수 없습니다.

CAN-DO 관계

'A는 B이다'가 어색하고 'A는 B를 할 수 있다'와 같이 B가 A의 능력을 나타내는 경우에는 추상 클래스가 아닌 인터페이스를 사용할 수 있습니다. 예를 들어서 '새는 날 수 있다', '비행기는 날 수 있다'의 경우에는 Flyable 인터페이스를 정의하여 비행기나 새를 나타내는 클래스에 fly() 메서드를 구현하도록 강제할 수 있습니다.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	...
}

자바 표준 라이브러리 API에서 살펴보면 Runnable, Cloneable, Comparable, Serializable, Iterable, Accessible와 같이 '~할 수 있는'이라는 뜻을 지닌 -able(ible) 접미사가 붙는 이름들을 자주 볼 수 있습니다. 하지만 이는 공식적이지도 않고 엄격하게 따라야 하는 인터페이스 명명 규칙도 아니므로 더 적절한 이름이 있다면 그 이름을 붙일 수 있습니다.

의존관계 역전 원칙 그리고 인터페이스라는 이름의 계약

내용이 너무 길어져서 의존관계 역전 원칙인터페이스라는 이름의 계약 편을 따로 올렸습니다. 인터페이스의 용도가 와닿지 않는다면 두 게시글을 꼭 읽어보시길 권장해 드립니다.