티스토리 뷰

얼마 전 자바8 람다 나머지 이야기를 보면서 평소 필자가 알던 Java와는 완전히 달라 보였다. 필자가 알고 있던 Java는 보수적이지만 정통적이라고 생각 해왔는데 과감히 이 생각을 깨졌다.



Java 8 Lambda 에 대해 궁금한 부분은 필자가 예전에 작성한 아티클을 참고하기 바란다.

Java 8 Interface 변경 사항 default 키워드

오라클의 Defining an Interface 문서에 의하면 Java Interface의 정의는 변경되었다. Java Interface는 abstract methods, default methods, static methods 를 정의할 수 있다고 한다.

The interface body can contain abstract methods, default methods, and static methods.

명세에 따르면 Java Interface에 default 키워드를 통해 메서드를 구현할 수 있다. 또, 이를 구현하는 클래스는 Interface의 메서드를 @Override 할 수 있다.

Oracle 문서에서 아래와 같은 예제를 볼 수 있다.

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   default boolean didItWork(int i, double x, String s) {
       // Method body 
   }  
}

Java 8 Interface, 그 인터페이스는 그 인터페이스가 아니다.

원래 객체지향 언어에서 Interface는 그 시그너처와 선언이 변하지 않는다는 것을 전제로 하여 다형성(polymophism)을 정의하는 객체간의 규약이다. 그러나 Java8 Interface는 Interface를 업그레이드하는 개념을 도입하였다.

Now users of your code can choose to continue to use the old interface or to upgrade to the new interface.
Alternatively, you can define your new methods as default methods

객체지향 언어의 객체 규약을 정의함에 있어 항상 논의 되는 것이 Interface와 Abstract 클래스 둘 중 어떤 것을 쓸 것이가에 대한 것이다. 어떤 것을 사용해도 무방하겠지만 객체 간의 규약(서로간의 약속)은 Interface 로 정의해야 한다. 구현체가 없는 온전한 인터페이스 역할을 해야 하기 때문이다.

특히 분산 객체(distributed object)는 분산된 두 객체의 규약, 즉 Interface 의 프록시(proxy)를 통해 분산 객체를 사용한다. Java8 Interface는 구현체가 포함될 수 있으므로 더 이상 분산 객체를 정의하는 규약으로 부적합하다.

이는 반드시 분산 객체 뿐만 아니라, 일반적인 객체로서도 문제의 소지가 충분하다. 아래는 이런 현상을 약간 억지스럽게 구현한 코드이다.

이를 통해 알 수 있는 것은 Java 8 Interface 로 규약된 분산 객체에 구현 코드가 포함이 되므로 더 이상 ‘규약’이라는 표현 자체가 규약이 될 수 없을 것 같다.

interface Duck {
    void say();
    void walk();
}

interface MyDuck extends Duck { }

interface YourDuck extends Duck {
    default void walk() { say(); }
}

class MyDuckImpl implements MyDuck {
    @Override
    public void say() { System.out.println("MyDuck: 꽥~"); }

    @Override
    public void walk() { System.out.println("MyDuck: 뒤뚱~"); }
}

class YourDuckImpl implements YourDuck {
    @Override
    public void say() { System.out.println("YourDuck: 꽥꽥꽥~"); }
}

public class Main {
    public static void main(String[] args) {
        new MyDuckImpl().say();
        new YourDuckImpl().say();

        new MyDuckImpl().walk();
        new YourDuckImpl().walk();
    }
}

// 결과
MyDuck: 꽥~
YourDuck: 꽥꽥꽥~
MyDuck: 뒤뚱~
YourDuck: 꽥꽥꽥~

상대적인 취약해진 Java 8 Interface

일반적으로 실행 파일(executable file)이나 라이브러리(library) 파일은 코드와 데이터 등을 구조적으로 저장하고 링크(link) 과정을 거친 바이너리 파일이다. 반면 Java는 각 클래스 파일을 컴파일하면 .class 확장자를 가진 바이너리 파일을 출력한다.

일반적으로 코드와 데이터가 모두 포함된 바이너리 파일(일반적으로 portable executable)은 가리키는 offset 에 따라 RVA(relative virtual address)/VA(virtual address)와 매핑된다. 따라서 바이너리 파일을 비정상적으로 수정(삽입/삭제)하면 offset 정보도 함께 변경해 주어야 한다. 그렇지 않으면 메모리에 로드될 때 코드가 정상적으로 동작하지 않을 수 있다.

반면 Java 의 컴파일 된 바이너리는 클래스 별로 컴파일되어 .class 파일로 출력되기 때문에 이런 offset 정보가 필요가 없다. 컴파일된 코드가 논리적인 구조에 따라 물리적인 파일을 생성하지 않는다. 할 수 있는 건 Stack Size 정도 늘리거나 줄일 수 있다.

따라서 Java 8 Interface 의 .class 파일에 악의적으로 default 메서드 구현을 임의로 수정하면 간편하게 응용 프로그램 전체에 영향을 끼치도록 소정의 목적을 달성할 수 있다. (상대적으로 Java의 바이너리가 더 취약하다는 의미이다.)

다시 불거지는 다중 상속 문제

Java 8 Interface는 다시 다중 상속 문제에서 자유롭지 못하게 되었다.

Java는 단일 상속만 가능하지만 Interface는 다중으로 구현할 수 있다. 이는 C++의 다중 상속으로 인해 발생하는 문제점과 복잡성으로 Java는 단일 상속 구조를 택하게 되었다.

다중 상속으로 발생하는 모호성에 대해서도 Java 8 Interface 가 내놓은 해결책은 C++ 의 해결책과 다를 바가 없다.

아래는 Java 8 Interface 다중 상속(구현)으로 인한 모호성 문제 해결 방법이다.

public interface OperateCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}
public interface FlyCar {
    default public int startEngine(EncryptedKey key) {
        // Implementation
    }
}

public class FlyingCar implements OperateCar, FlyCar {
    public int startEngine(EncryptedKey key) {
        FlyCar.super.startEngine(key);
        OperateCar.super.startEngine(key);
    }
}

아래는 C++ 의 다중 상속으로 인해 발생하는 모호성을 해결 하는 방법이다.

#include <iostream>  
using namespace std;  

class OperateCar {  
public:  
    int startEngine(EncryptedKey *key) {  
        // Implementation
    }  
};  

class FlyCar {  
public:  
    int startEngine(EncryptedKey *key) {  
        // Implementation
    }  
};  

class CCC : public OperateCar, public FlyCar {  
public:  
    int startEngine(EncryptedKey *key){  
        OperateCar::startEngine(key);  
        FlyCar::startEngine(key);
    }  
};  

위의 Java와 C++ 소스 코드를 비교해 보면 결국 Java 8 Interface는 다중 상속의 개념이 다시 도입된 것을 알 수 있다.

참 아이러니 하다.

결론

어느 것이 답이 될 수는 없다. 이전까지 지향하고 고수하던 객체지향이 반드시 정답은 아닐 것이다. 다만, Java가 추구하던 Interface의 개념이 그 동안 알고 있던 개념과 이치에 맞지 않고, 어쩔 도리 없이 구현한 스팩일 수 있다.

필자처럼 C# 5.0의 가장 최신 스팩까지 쭉 경험한 개발자라면 도저히 용납할 수 없을 지도 모르겠다.

까마득한 2007년도에 릴리즈한 C# 3.0 스팩의 Lambda, LINQ, Extension Methods 의 언어 스팩과 비교해보면 Java 8 는 너무나도 기대에 미치지 못한 방법으로 구현해 놓았다는 게 실망스럽다.

아직 Java 8 모든 것을 훓어 본 것이 아니므로 필자가 오해하고 있는 부분은 정정해 주길 바란다. (이념, 철학, 사상적인 부분은 사양한다)

참고로 이해를 돕기 위해 2007년도 C# 3.0 스팩과 2014년 Java 8 스팩을 비교하면 다음과 같다.

  • C# Lambda = Java Lambda
  • C# Extension Methods = Java Stream API
  • C# LINQ = Java 엔 없다!


댓글