들어가며

자바의 함수형 인터페이스(Functional Interface)는 람다식과의 호환성을 위해 하나의 추상 메서드만 가져야 합니다. 람다식은 실제론 "익명 클래스(Anonymous Class)의 객체"와 동등하기 때문에, 람다식이 함수형 인터페이스에 작성된 메서드와 1 : 1로 매핑되기 위해서 함수형 인터페이스는 하나의 추상 메서드만 가져야 한다는 것이죠. 실제로 다음과 같이 함수형 인터페이스에 두 개 이상의 추상 메서드를 작성하면 오류가 발생합니다.

 

함수형 인터페이스에 2개 이상의 추상 메서드를 작성한 경우

 

그러나, 자바에서 함수형 인터페이스로 정의된 Comparator는 다음과 같이 두 개의 추상 메서드를 가지고 있습니다.

 

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    // ..생략
}

 

equals가 추상메서드로 취급되지 않는 모습을 볼 수 있었던 거죠. 뭔가 해서 직접 실험해보니, 함수형 인터페이스를 작성할 때 다음과 같이 equals를 추상메서드로 작성해도 오류가 나지 않는 걸 볼 수 있었습니다. equals는 Object 클래스에 정의된 메서드로, 마찬가지로 toString같은 Object클래스에 정의된 함수를 함수형 인터페이스에서 추상메서드로 작성해도 오류가 없었습니다.

 

함수형 인터페이스인데, 추상메서드를 3개 작성했음에도 오류가 나지 않는다

 

 

함수형 인터페이스는 하나의 추상메서드만 가져야 한다는데, 위에서 분명 저는 3개의 추상메서드를 함수형 인터페이스에 작성했음에도 괜찮았습니다. 왜 이런 결과가 나오는지를 조사해봤고, 이 글에서 소개하려 합니다.

 

 

Object 클래스의 메서드이기 때문이다

이유는 간단하게, equals 등의 메서드는 Object 클래스에 작성된 메서드이기 때문입니다. 그리고 자바의 모든 객체는 Object 클래스를 상속(extends)하죠. 그렇기 때문에 괜찮았습니다.

 

조금 다른 얘기를 먼저 해보겠습니다. 자바에서는 다중 상속을 기본적으로 지원하지 않습니다. A라는 클래스와 B라는 클래스를 상속받을 때 A와 B 둘다 같은 이름의 메서드를 가진다면 모호성이 생기기 때문이죠. 하지만 인터페이스라면, 어차피 구현을 개발자가 원하는대로 할 수 있기 때문에 여러 인터페이스를 구현할 수 있습니다. 

 

이를 참고하면서, 다음 인터페이스를 보겠습니다.

 

interface MyInterface {
    void someMethod1();
    void someMethod2();
}

 

MyInterface를 구현하는 클래스들은 someMethod1과 someMethod2를 둘 다 구현해야 합니다. 이 상황에서, YourInterface라는 인터페이스와 이를 구현한 YourClass라는 클래스를 보겠습니다.

 

interface YourInterface {
    void someMethod2();
}

class YourClass implements YourInterface {
    public void someMethod2() {
        System.out.println("someMethod2");
    }
}

 

YourInterface는 MyInterface와 마찬가지로 someMethod2라는 이름의 추상 메서드를 가지며, YourClass가 YourInterface를 구현한 모습입니다. 이때 다음과 같이 MyInterface를 구현하는 MyClass라는 클래스를 만들되, MyClass가 YourClass를 상속받게 하면서 someMethod1만 오버라이딩하게 한다면 어떨까요?

 

 

분명 MyClass는 MyInterface를 구현하면서 someMethod2라는 추상 메서드도 오버라이딩해야 하지만, 오류가 나지 않습니다. 왜냐하면 MyClass가 상속하는 YourClass에서 이미 someMethod2라는 메서드를 오버라이딩했기 때문이죠. 따라서 MyClass의 인스턴스를 만들어서 someMethod2를 호출하면, YourClass에서 오버라이딩된 메서드가 호출되게 됩니다.

 

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

 

 

이때, MyClass에서 별도로 someMethod2를 오버라이딩하면, MyClass의 인스턴스에서 someMethod2 실행시 오버라이딩된 메서드가 실행되게 되겠죠. (실행결과는 생략..)

 

 

여기서 힌트를 얻을 수 있었습니다. 자바에서 모든 클래스는 Object를 상속받습니다. 즉 별도로 구현하지 않아도, equals 등의 메서드를 가지고 있습니다. 따라서 Object 클래스에 정의된 메서드를 추상메서드로 작성해서 인터페이스를 만든 경우, 해당 인터페이스를 구현하는 클래스에서 별도로 그 메서드를 오버라이딩하지 않아도 문제가 생기지 않는 것이죠. 

 

interface MyInterface {
    void myMethod();
    boolean equals(Object obj);
}


// Object로부터 상속받은 equals를 가지고 있다
class MyClass implements MyInterface {
    public void myMethod() {
        System.out.println("MyClass");
    }
}

 

따라서 equals는 MyInterface에서 추상메서드로 작성되긴 했으나, MyInterface에게 본질적인 추상메서드는 1개(myMethod)인 셈입니다. 따라서 함수형 인터페이스를 만들 때 Object 클래스에서 정의된 메서드를 추상메서드로 작성해도, 본질적인 추상메서드만 1개라면 오류가 발생하지 않는 겁니다.

 

그렇다면, 인터페이스에 Object클래스에 정의된 메서드를 추상메서드로 작성하는 건 어떤 의미를 가질까요? 바로 개발자들로 하여금, 해당 인터페이스를 구현하는 클래스를 만들 때 필요하다면 해당 메서드를 오버라이딩해 작성해야 함을 알려주는 기능과, 이 인터페이스를 구현하는 클래스들이 반드시 이 메서드들을 올바르게 구현해야 한다는 의도를 명확히 전달하는 기능을 합니다. 

+ Recent posts