우테코 미션을 하다보면, 추상클래스를 쓸 때도 있고 인터페이스를 쓸 때도 있다. 둘의 차이를 내 생각과 함께 면밀히 정리해보겠다. 용어 정의에 바탕한 이론을 다루는 글임을 미리 밝힌다. 학창 시절 교수님께 배웠던 C++의 추상클래스와 비교하며 원론적인 정의를 내려보겠다.
1. Dynamic Method Dispatch (vs. Dynamic Binding)
먼저, Dynamic Method Dispatch 라는 개념을 통해 추상 이란 용어에 대해 이해해보자. 긴 용어로 들으면 뭐지 싶을 수 있는데, 해석해보면 어렵지 않다. “동적으로 호출할 메서드를 결정한다”라는 뜻이다. 반댓말론 Static Method Dispatch 가 있다. 이는 “정적으로 호출할 메서드를 결정한다”라는 뜻이다.
정적이라 함은 컴파일 시점에 컴파일러가 특정 메서드를 호출할 것을 명확히 알고 있는 경우를 의미하고, 동적이라 함은 컴파일러는 어떤 메서드를 호출해야 하는 지 모르고 런타임 시점에 호출할 메서드를 결정하는 경우를 의미한다.
그렇다면 어떻게 해야 동적으로 메서드 호출을 결정할 수 있을까? 바로, 인터페이스나 추상 클래스에 정의된 추상 메서드를 사용하면 된다. 같은 클래스를 상속하고 있는 여러 클래스 중 어느 서브 클래스를 사용할 것인가를 런타임 시점까지 미룸으로서, 클래스 재사용성을 높이는 것이다.
여기까지 읽고 나면 이거 Dynamic Binding 이랑 완전 똑같은 거 아니야? 싶을 수 있다. 허나, Dynamic Method Dispatch 와 Dynamic Binding 은 엄밀히 말하자면 다른 개념이다. 아래 링크로 들어가보면 깔끔하게 이를 정의내려주는데, 요약하자면 Dynamic Binding은 런타임 시점에 특정 메서드 구현을 결정하는 메커니즘이고, Dynamic Method Dispatch는 Dynamic Binding을 사용해 런타임 시점에 특정 메서드 구현을 결정하고 이를 찾아 호출하는 과정까지를 포함한다고 한다.
Dynamic dispatch is different from late binding (also known as dynamic binding). Name binding associates a name with an operation. A polymorphic operation has several implementations, all associated with the same name. Bindings can be made at compile time or (with late binding) at run time. With dynamic dispatch, one particular implementation of an operation is chosen at run time. While dynamic dispatch does not imply late binding, late binding does imply dynamic dispatch, since the implementation of a late-bound operation is not known until run time.
Dynamic Binding(==late binding)과 Dtnamic Method Dispatch의 차이에 대해 더 자세히 알고 싶은 사람은 아래 글도 추천한다.
2. Abstarct Class: Abstract vs. Virtual
추상 클래스란 뭘까? 이를 정확히 이해하기 위해 C++과 Java에서 말하는 추상 클래스 정의를 비교해보자. IBM 문서에 따르면 C++의 추상 클래스는 하나 이상의 순수 가상 함수(pure virtual function)가 포함되어 있다고 명시한다. 반면, Oracle 문서에 따르면 Java의 추상 클래스는 추상 클래스는 abstract 키워드가 붙은 클래스로, 추상 메서드(abstract methods)를 포함할 수도 있고 안할 수도 있다 고 명시한다.
전자인 C++의 추상클래스 정의가 사실 일반적인 추상 클래스 정의에 더 가깝다. 자바는 왜 포함할 수도 있고, 안할수도 있다란 애매한 말을 사용했을까? 사실 둘의 차이는 pure virtual function 과 abstract function 의 차이에 있다. 결론부터 말하자면 둘은 기능은 같지만 영향력이 다르다. 먼저 C++이 사용하는 pure virtual function에 대해 설명해보겠다.
C++에선 Polymorphism 구현을 위해 추상 클래스를 사용한다. 정확하겐 overriding과 dynamic binding을 통해 구현한다. 여기서 Polymorphism이란 하나의 메시지에 대해 서로 다른 객체가 서로 다른 방법으로 응답할 수 있는 기능을 뜻한다. 보통 One Interface(super class), Multipler Implementations(sub class)로 설명하는데 외부에 공개되는 인터페이스는 1개고, 실제 구현은 여러 개인 형태로 구현된다.
C++의 추상클래스에 존재하는 pure virtual function은 몸체가 정의되지 않은 함수로 subclass에서의 오버라이딩을 통한 구현이 필수로 요해진다. 이는 superclass에서도 정의 가능한 virtual functon 보다 엄격한 개념이다. pure virtual function과 virtual function의 차이에 대해 더 궁금한 사람은 아래 글을 추천한다.
다시 본론으로 돌아와서 C++의 Polymorphism 구현 과정을 예시를 들어 살펴보겠다. 택시 드라이버와 버스 드라이버라는 두 객체가 존재하고, 두 객체엔 이름은 동일하지만 내용은 다른 메서드와 이름과 내용이 모두 동일한 메서드가 존재한다 가정해보자. 예를 들면 월급을 계산하는 로직이 전자일테고, 드라이버의 이름을 get해오는 로직이 후자일테다.
class TaxiDriver {
private:
char driverName[10];
int baseSalary;
int bonusMoney;
public:
char* getDriverName() const; // return driverName
int getSalary() const; // return baseSalary + bonusMoney
}
class BusDriver {
private:
char driverName[10];
int workingHours;
int payPerHour;
public:
char* getDriverName() const; // return driverName
int getSalary() const; // return workingHours * payPerHour
}
C++
복사
이 코드의 문제점은 모듈화가 제대로 이뤄지지 않아 Driver들을 사용하는 측에서 TaxiDriver와 BusDriver 를 모두 알야 한단 점이 있다. 외부 코드에서 모든 subclass의 구조를 알아야 사용 가능한 것이다.
이를 해결하기 위해선 아래와 같이 polymorphism을 구현해줄 수 있다. 상속 관계를 만들어 공통 부분은 superclass로 빼주고, 이름은 동일하지만 내용은 다른 함수를 subclass에 재정의해 overriding 시킨다. 이때, overriding은 subclass에 동일함수를 재작성해 superclass의 함수를 덮어씌우는 걸 말한다. 제대로 덮어씌우게 하려면 overriding 한 함수들에 virtual 키워드를 붙여주어야 한다. 단순히 overriding 만 하고 virtual 키워드를 붙이지 않으면 superclass의 함수가 호출되어 버리기 때문이다. overriding 후 virtual 키워드까지 붙여주어야 dynamic binding이 일어나 polymorphism이 온전히 구현된다.
class Driver {
private:
char driverName[10];
public:
char* getDriverName() const;
virtual int getSalary() const; // virtual 키워드 붙여서 dynamic binding
}
class TaxiDriver : public Driver {
private:
int bonusSalary;
int bonusMoney;
public:
virtual int getSalary() const; // 함수 재정의 통해 overriding
}
class BusDriver : public Driver {
private:
int workingHours;
int payPerHour;
public:
virtual int getSalary() const;
}
C++
복사
좋다. 그럼 이제 C++에서 말하는 abstract class와 pure virtual function이 무엇인지는 완전히 이해했다. 그럼 Java가 말하는 abstract class와 abstract method는 뭐가 다른걸까? Java에선 모든 non-static 메서드는 기본적으로 virtual function이다. 위에 언급했듯 virtual function은 subclass에서의 구현이 가능하지만 superclass에서도 정의 가능한 함수를 말한다.
즉 Java는 본래 모든 메서드가 virtual 키워드가 붙은 상태이기에 상속 구조를 만들어 overriding만 시켜주면 자동으로 polymorphism이 구현된다. 다만, 일반 메서드와 구분이 안되고 강제성이 없기에 pure한 virtual function임을 표시하기 위한 게 메서드 앞에 붙는 abstract 키워드인 것이다. abstract method 앞에 abstract 키워드가 없다고 해서 polymorphism이 구현되지 않는 게 아니기에(팩트) Java 진영에선 굳이 그 개수를 제한할 필요가 없었지 않았을까 싶다(나의 추측).
헷갈리시는 분들이 있을까봐 한 가지 명확히 짚고 가자면 java의 abstract 메서드는 c++의 pure virtual functions와 동일한 기능을 뜻하는 개념이다. 다만 미치는 영향력이 다르다.
용어 정리
•
virtual function: 하위 클래스의 메서드가 상위 클래스의 메서드를 덮어씌우는 함수임. 다만 상위 클래스에서도 메서드 구현이 가능한 함수임
•
pure virtual function: 상위 클래스에 메서드가 구현되지 않고, 하위 클래스만 구현됨
•
abstract method: java 진영은 모든 메서드가 default로 virtual function이라 오버라이딩만 해주면 pure virtual function을 만들 수 있지만, 이를 강제하고 싶을 땐 abstract 키워드를 붙여줌
마지막으로 추상 클래스의 특징을 살펴보겠다. 추상 클래스는 abstract로 선언된 클래스로, abstract method(==pure virtual function)를 포함할 수도 있고 포함하지 않을 수도 있는 클래스라 언급했었는데, 주의할 점은 반대로 abstract 메서드를 하나라도 가지고 있다면 해당 클래스는 추상으로 선언되어야 한단 점이다. 따라서 추상 클래스를 상속 받는 자식 클래스는 부모의 모든 추상 메서드를 재정의해 구현해야 하며, 만일 구현하지 않는다면 자식 클래스 역시 추상으로 선언되어야 한다.
3. Interface
인터페이스란 뭘까? 추상 클래스에서 많은 걸 이미 언급했기에 매우 간단히 설명 가능하다. C++의 abstract class가 하나 이상의 pure virtual function을 포함하는 클래스라고 설명했었는데, Java의 interface는 pure virtual function 만으로 구성된 클래스다. 물론 이 역시 앞에서 언급했듯 자바의 non-static 메서드들은 default로 virtual function이기에 C++처럼 virtual 키워드를 붙이진 않는다.
abstract class에선 pure virtual function임을 강제하기 위해 메서드앞에 abstract 키워드를 붙였었지만, interface는 그 자체가 pure virtual function을 강제하기에 abstract 키워드를 생략해줘도 된다. abstract class의 모든 메서드가 abstract 메서드라면 인터페이스로 바꿔볼 수 있다. 즉, 인터페이스는 추상 클래스의 일종으로 모든 메서드가 abstract method(==pure virtual function)이다. 다만 interface 그 자체가 기능적으로 몸체가 정의되지 않고 subclass에서의 오버라이딩을 통한 구현이 필수로 요해지는 메서드들만 가질 수 있기에 굳이 abstract 키워드는 붙이지 않아도 되는 것이다.
인터페이스는 추상클래스와 다르게 일반 메서드와 일반 멤버 변수를 가질 수 없다. 따라서 모든 메서드는 public abstract 여야 하고, 모든 멤버 변수는 public static final 이어야 한다. 다만 예외적으로 JDK1.8부터 static 메서드와 default 메서드는 허용한다고 하니 알아두자.
인터페이스 관련해 특이점을 한 가지만 더 짚고 넘어가자면, 인터페이스는 인터페이스로부터만 상속 받을 수 있는데, 이떄 클래스와 달리 다중상속이 가능하다. staitc 상수만 정의할 수 있어서 조상의 멤버 변수와 충돌할 일이 거의 없기에 허용해준 것 같다. 물론 클래스와 마찬가지로 자손은 조상의 모든 멤버를 상속받는다.