java

자바 8 - interface 1

Jan 28, 2020

인터페이스를 정의하고 사용해보자.

목차

1. 인터페이스
1.1 인터페이스 선언하기
1.2 인터페이스 구현하기
1.3 인터페이스 타입으로 변환하기
1.4 타입 변환과 instanceof 연산자
1.5 인터페이스 확장하기
1.6 여러 인터페이스 구현하기
1.7 상수

2. 정적 메서드와 기본 메서드
2.1 정적 메서드
2.2 기본 메서드
2.3 기본 메서드의 충돌 해결하기


1. 인터페이스 소개

최근 들어 함수형 프로그래밍의 중요성이 높아졌다. 병행 프로그래밍과 이벤트 구동(혹은 반응) 프로그래밍에 적합하기 때문이다. 자바에서는 객체 지향과 함수형 프로그래밍 사이의 간극을 메워주는 함수 표현식을 지원한다.

인터페이스를 사용하면 구현하지 않고도 수행해야 할 일을 명시할 수 있다.

1.1 인터페이스 선언하기

정수 시퀀스에서 처음 n개 값의 평균을 구하는 서비스를 생각해보자.

public static double average(IntSequence seq, int n)

시퀀스는 다양한 형태를 취할 수 있다.

모든 종류의 시퀀스를 다룰 단일 메커니즘을 구현해보자. 먼저 정수 시퀀스 사이의 공통점을 표현해보면 시퀀스를 다루는 메서드는 적어도 두 가지가 필요하다.

인터페이스를 선언하고 메서드 선언부를 작성한다.

public interface IntSequence {
    boolean hasNext();
    int next();
}

인터페이스의 모든 메서드는 기본적으로 public이 된다. 굳이 public 키워드를 사용할 필요는 없으나, 의도를 명확하게 하기 위해 public으로 선언하는 개발자들도 있다.

인터페이스에 있는 메서드로도 average 메서드를 구현할 수 있다.

public interface IntSequence {
    boolean hasNext();
    int next();

    static double average(IntSequence seq, int n) {
        int count = 0;
        double sum = 0;

        while(seq.hasNext() && count < n) {
            count ++;
            sum += seq.next();
        }
        return count == 0? 0 : sum / count;
    }
}



1.2 인터페이스 구현하기

아까 구현한 average 메서드의 파라미터로 사용할 클래스들을 구현해본다. 이들 클래스는 IntSequence 인터페이스를 구현해야 한다.

public class SquareSequence implements IntSequence {

    int i;

    @Override
    public boolean hasNext() {
        return true;
    }

    @Override
    public int next() {
        i++;
        return i * i;
    }
}
public class DigitSequence implements IntSequence {

    private int number;

    public DigitSequence(int number) {
        this.number = number;
    }

    @Override
    public boolean hasNext() {
        return number != 0;
    }

    @Override
    public int next() {
        int result = number % 10;
        number /= 10;
        return result;
    }

    public int rest() {
        return number;
    }
}
public class Main {

    public static void main(String[] args) {

        // 처음 사각형 100개의 평균 구하기
        SquareSequence squares = new SquareSequence();
        double avg = average(squares, 100);
        System.out.println(avg);

        // 가장 낮은 자릿수부터 시작하는 양수로 된 숫자 출력 (9, 2, 7 ,1)
        DigitSequence digitSequence = new DigitSequence(1729);
        while (true) {
            if(digitSequence.hasNext()){
                System.out.println(digitSequence.next());
            } else {
                break;
            }
        }
    }
}

앞에 구현한 두 클래스는 IntSequence 인터페이스의 모든 메서드를 구현했는데, 만약 일부의 메서드만 구현하고 싶다면 abstract를 붙여 추상클래스를 사용해야 한다.

1.3 인터페이스 타입으로 변환하기

IntSequence digits = new DigitSequence(1729);
System.out.println(average(digits, 100));
// (9 + 2 + 7 + 1) / 4 = 4.75

digits의 타입은 DigitSequence가 아닌 IntSequence이다. IntSequence타입 변수는 IntSequence를 구현한 어떤 클래스의 객체라도 참조할 수 있다. 구현한 인터페이스의 타입으로 변수를 선언하고, 선언한 변수에 객체를 할당할 수 있다. 또한, 객체는 해당 인터페이스를 기대하는 메서드에 전달할 수 있다.

IntSequence 인터페이스는 IntSequence 인터페이스를 구현한 모든 클래스의 슈퍼타입이다.

변수를 인터페이스의 타입으로 선언할 수 있지만, 타입이 인터페이스인 객체는 만들 수 없다. 모든 객체는 클래스의 인스턴스여야 한다.

1.4 타입 변환과 instanceof 연산자

가끔은 반대로 변환 (슈퍼타입 -> 서브타입)하는 것도 필요하다. 이때는 타입 변환(cast)을 해야 한다.

IntSequence sequence = new DigitSequence(1897);
DigitSequence digits = (DigitSequence)sequence;
System.out.println(digits.rest());

rest()는 DigitSequence의 메서드이므로 타입을 변환해야 한다.

객체는 실제 클래스나 해당 클래스의 슈퍼타입으로만 타입을 변환할 수 있다. 만일 실수하면 컴파일 시간 오류나 클래스 캐스트 예외(ClassCastException)가 일어난다.

예외를 피하려면 instanceof 연산자로 객체가 원하는 타입인지 검사한다.

객체 instanceof 타입

if(sequence instanceof DigitSequence) {
    DigitSequence digits = (DigitSequence) sequence;
}



1.5 인터페이스 확장하기

인터페이스는 또 다른 인터페이스를 확장해서 원래 있던 메서드 외의 추가 메서드를 제공할 수 있다.

public interface Closeable {
    void close();
}

public interface Channel extends Closeable {
    boolean isOpen();
}

Closeable 인터페이스는 예외가 일어날 때 리소스를 닫는 중요한 인터페이스이다. Channel 인터페이스는 Closeable 인터페이스를 확장한다.

Channel 인터페이스를 구현하는 클래스는 반드시 두 메서드를 모두 구현해야 하며, 해당 클래스의 객체를 두 인터페이스 타입 중 어느 것으로도 변환할 수 있다.

1.6 여러 인터페이스 구현하기

public class FileSequence implements IntSequence, Closeable {
    ...
}

클래스는 인터페이스를 몇 개든 구현할 수 있다. 에를 들어, 파일에서 정수를 읽어오는 FileSequence 클래스는 IntSequence, Closeable 인터페이스를 구현한다.

이렇게 구현하면 FileSequence 클래스는 IntSequence, Closeable 인터페이스를 슈퍼타입으로 둔다.

1.7 상수

인터페이스에 정의한 변수는 자동으로 public static final 이다.

public interface SwingConstants {
    int NORTH = 1;
    int NORTH_EAST = 2;
    int EAST = 3;
}

main {
    System.out.println(SwingConstants.NORTH);
    System.out.println(SwingConstants.NORTH_EAST);
    System.out.println(SwingConstants.EAST);
}
// 1
// 2
// 3

클래스가 SwingConstants를 구현하면 SwingConstants를 생략하고 NORTH를 바로 사용가능한데 일반적인 방법은 아니며, 상수 집합에는 Enum을 사용하는 것이 훨씬 좋다.

인터페이스 안에는 인스턴스 변수를 둘 수 없다. 인터페이스는 객체의 상태가 아닌 동작을 명시한다.


2. 정적 메서드와 기본 메서드

자바 8 이전에는 인터페이스의 모든 메서드가 추상 메서드여야 했다. 다시 말해 구현부가 없어야 했다.
자바 8은 실제 구현이 있는 메서드를 추가할 수 있다.

2.1 정적 메서드

기술적으로만 보면 인터페이스에 정적 메서드가 포함되지 못할 이유는 없었다. 하지만 인터페이스를 추상 명세로 보는 관점에서 맞지 않았다. 그런데 이제는 이런 사고가 진화했다. 특히, 펙토리 메서드는 인터페이스에 아주 잘 맞는다. 예를 들어, IntSequence 인터페이스는 다음과 같이 주어진 정수의 숫자를 만들어내는 정적 메서드 digitOf를 둘 수 있다.

public interface IntSequence {
    ...

    static IntSequence digitsOf(int n) {
        return new DigitSequence(n);
    }
}

이 메서드는 IntSequence 인터페이스를 구현한 클래스 중에서 인스턴스 하나를 주지만, 호출자는 이 인스턴스가 어느 클래스의 인스턴스인지 신경 쓸 필요가 없다.

2.2 기본 메서드

인터페이스의 메서드 중 어느 것에든 기본 구현을 작성할 수 있다. 기본 메서드에 반드시 default 제어자를 붙인다.

public interface IntSequence {
    // 기본적으로 시퀀스는 무한이다.
    default boolean hasNext() { return true; }
    int next();
    ...
}

이 인터페이스를 구현하는 클래스는 hasNext 메서드를 오버라이드 하거나 기본 구현을 상속하는 방법 중 하나를 선택한다.

기본 메서드는 고전적인 패턴에 종지부를 찍었다. 요즘에는 그냥 메서드를 인터페이스에 구현한다.

기본 메서드의 중요한 용도는 인터페이슬르 진화시키는 것이다. 오랜 세월 자바의 일부로 존재해 온 Collection 인터페이스를 예를 들자. 예전 방식으로 구현한다면 다음과 같이 클래스를 구현해야 했다.

public class Bag implements Collection

이후 자바 8에서는 stream 메서드를 Collection 인터페이스에 추가했다.

Stream 메서드가 기본 메서드가 아니라고 하면 Bag 클래스는 새로운 메서드를 구현하지 않으므로 컴파일되지 않는다. 인터페이스에 기본 메서드가 아닌 메서드를 추가하는 일은 소스수준에서는 호환되지 않는다.

그런데 여기서 클래스를 다시 컴파일하지 않고 Bag 클래스가 포함된 기존 JAR 파일을 그대로 사용한다고 해보자. 빠진 메서드가 있는데도 여전히 클래스를 제대로 로드한다. 프로그램에서는 여전히 Bag 인스턴스를 생성할 수 있고 전혀 문제가 없다. (인터페이스에서 메서드를 추가하는 일은 바이너리 수준에서 호환된다.) 하지만 프로그램에서 Bag 인스턴스에 stream 메서드를 호출하면 AbsractMethodError가 일어난다.

메서드를 default로 선언하면 이런 문제를 해결할 수 있다. 메서드를 기본 메서드로 만들면 Bag 클래스가 다시 컴파일된다. 다시 컴파일 하지 않고 클래스를 로드한 후 Bag 인스턴스에서 stream 메서드를 호출하면 Collection.stream 메서드가 호출된다.

2.3 기본 메서드의 충돌 해결하기

두 개의 인터페이스를 구현하는 클래스가 있고 두 인터페이스에 같은 이름과 파라미터를 가진 메서드(기본이든 아니든)가 있다고 하면 충돌을 해결해야 한다.

public interface Person {
    String getName();
    default int getId() { return 0; }
}
public interface Identified {
    default int getId() { return Math.abs(hashCode()); }
}
public class Employee implements Person, Identified {

    @Override
    public String getName() {
        return null;
    }

    @Override
    public int getId() {
        return Identified.super.getId();
    }
}

super 키워드로 슈퍼타입 메소드를 호출할 수 있다.

이번에는 Identified 인터페이스의 getId를 기본 메서드로 구현하지 않는다고 해보자.

interface Identified {
    int getId();
}

Employee 클래스가 Person 인터페이스의 기본 메서드를 상속받을 수 있을까? 언뜻 보면 그럴 수 있을 것 같다.

Identified.getId가 수행하길 기대하는 동작을 Person.getId 메서드가 실제로 하는 지 컴파일러가 알 수 없다.

자바 설계자들은 안전성과 일관성을 따르기로 했다. 두 인터페이스가 어떻게 충돌하는 지는 문제되지 않는다. 적어도 한 인터페이스에서 구현을 제공하면 컴파일러는 오류를 보고하며, 모호성을 해결하는 일은 개발자 몫이다.