우리는 객체지향 프로그래밍 언어를 공부하면서 클래스 멤버들의 공개 등급이라는 개념을 접한다. 까놓고 말해 public, protected, private은 C++, C#, Java에 모두 공통으로 거의 동일한 용도로 쓰이는 키워드이다.
C++은 이런 공개 등급을 마치 case나 default처럼 뒤에다 콜론을 붙여 일종의 label 형태로 지정하지만, Java/C#은 멤버 함수 및 변수의 선언에 공개 등급이 같이 붙는다. 이 세 등급의 차이는 아래 표와 같다.
출처 \ 공개 등급 | public | protected | private |
외부 | O | X | X |
파생 클래스 내부 | O | O | X |
자기 클래스 내부 | O | O | O |
public이야 멤버의 접근에 아무 제약이 없는 등급이지만 pr*로 시작하는 나머지 두 등급은 그렇지 않다. 단지, protected는 파생 클래스의 내부에서 자기에게 접근을 허용하는 반면, private은 이마저도 허용하지 않는다. 상속 관계에서부터 둘의 차이가 발생하는 것이다. private은 사람으로 치면 자식에게도 물려주거나 공유하지 않는 개인 칫솔, 속옷, 복용하는 약 같은 급의 지극히 '사쩍'인 물건에다 비유할 수 있겠다.
물론 그 어떤 공개 등급이라도 자기가 선언해 놓고 자기 클래스의 멤버 함수에서도 사용하지 못하는 등급은 없다. 심지어 자기 자신 this뿐만 아니라 자기 클래스에 속하는 아무 객체라도 말이다.
즉, 아래의 코드에서 bar(함수)와 priv_member(변수)가 똑같이 Foo의 멤버라면, bar는 o라는 다른 인스턴스의 priv_member에도 저렇게 마음대로 접근할 수 있다.
void Foo::bar(Foo& o)
{
priv_member += o.priv_member;
}
this 포인터가 아예 없는 static 멤버 함수도 있다는 점을 생각한다면 이는 당연한 귀결이라 하겠다. 선언은 자기가 했지만, 접근과 사용은 내가 곧장 못 하고 파생 클래스에서만 가능하다거나 하는 기괴한 개념은 없다.
오히려 반대로 부모 클래스에서는 공개였는데 파생 클래스에서는 부모의 멤버를 감추고, 외부에서는 오로지 파생 클래스가 새로 제공하는 public 멤버만 사용 가능하도록 클래스를 더 폐쇄적인 형태로 바꾸는 건 가능하다.
C++에서는 클래스를 상속할 때 별 생각 없이 파생 클래스의 이름 뒤에다 콜론을 찍고 public Base1, public Base2 이런 식으로 public을 붙이곤 한다. 이때 써 주는 공개 등급은.. 부모 클래스 멤버들의 공개 등급이 파생 클래스에서는 어떻게 되는지를 결정한다. 그 방식은 한편으로는 직관적이어 보이면서도 한편으로는 헷갈리고 어렵다.
상속 방식 \ 기존 공개 등급 | public | protected | private |
public | public | protected | |
protected | protected | protected | |
private | private | private |
public 상속은 부모 클래스 멤버들의 공개 등급을 파생 클래스에다가도 원래 지정되었던 형태 그대로 유지시킨다. 부모 때 public이었던 놈은 파생에서도 public, protected는 protected.. 그런 식이다.
그 반면, protected나 private 상속으로 가면 일단 부모 멤버들은 몽땅 private, 또는 잘해야 protected로 등급이 바뀐다. public 속성이 없어지기 때문에 외부에서 파생 클래스 객체를 대상으로 부모 클래스의 멤버에 접근은 할 수 없어진다. public, protected, private이라는 공개 등급을 각각 3, 2, 1이라는 수라고 생각한다면, 클래스를 상속하는 방식 N은 부모 멤버들의 공개 등급을 N보다 크지 않은 등급으로 재설정하는 셈이다.
다른 예로, 부모 클래스에서 protected였던 멤버가 있는데 자식이 부모를 private로 상속했다고 치자. 그러면 그 멤버는 부모에서의 공개 등급은 protected였기 때문에 자식 클래스의 내부에서 마음대로 접근이 가능하다. (참고로 public 멤버는 부모에서 public이었다 하더라도 non-public 상속을 거치면 파생 클래스에서의 외부 접근이 곧장 차단된다. 내부 접근과 외부 접근의 차단 시기가 서로 차이가 있다.)
하지만 private 상속된 protected 멤버는 파생 클래스에서의 등급이 private로 바뀌었다. 그렇기 때문에 얘로부터 상속받은 손자 클래스에서는 이 멤버에 더 접근할 수 없게 된다.
그리고 한번 private/protected 상속으로 인해 작아져 버린 공개 등급은 후속 파생 클래스가 다시 public으로 상속한다고 해서 다시 커지지 않는다. 상속 과정에서 기존 공개 등급을 동일하게 유지하거나 더 낮출 수는 있어도 도로 높일 수는 없다는 뜻이다.
부모 클래스에서 private 상태였던 멤버들은(처음부터 그리 됐든, 상속 방식 때문에 그리 됐든..) public 등 그 어떤 방식으로 상속을 받더라도 파생 클래스에서는 결코 접근할 수 없다. 위의 표에서 그냥 평범하게 private라고 명시된 놈은 현재 private이기 때문에 다음 파생 클래스에서는 감춰진다는 뜻이며, 취소선이 그어져 있는 private은 이미 접근 불가이고 파생 클래스의 입장에서는 그냥 없는 멤버가 됐음을 의미한다.
C++은 언어 차원에서 POD(단순 데이터 더미)와 객체(생성자 소멸자 가상 함수 등..)의 구분이 모호하다 보니, struct와 class의 언어적 구분도 없다시피한 것으로 잘 알려져 있다. 그냥 디폴트로 지정돼 있는 멤버의 공개 등급이 전자는 public이고 후자는 private이라는 차이만이 있을 뿐이다.
그것처럼, 클래스를 상속할 때 공개 등급을 안 쓰고 그냥 class Derived: Base {} 라고만 쓰면 Base는 private 방식으로 상속된다. C++의 세계에서는 디폴트 공개 등급이 어디서든 private인 셈이다.
class 대신 아예 struct Base { private: int a; } 이런 식으로 클래스를 선언해도 된다. 미관상 보기 좋지 않으니까 안 할 뿐이지.
얘는 클래스 선언 문맥에서는 struct라는 대체제가 있고, 템플릿 인자 문맥에서는 typename라는 대체제가 있으니 위상이 뭔가 애매해 보인다. 전자는 C 시절부터 있었던 키워드요, 후자는 C++98에서 추가된 키워드이다.
코딩을 하다 보면 범용적인 부모 클래스의 포인터로부터 자식 클래스로 static_cast 형변환을 하는 경우가 많다. 파생 클래스가 부모보다 멤버가 더 많고 할 수 있는 일도 더 많기 때문이다. 그런데 protected/private 상속을 한 파생 클래스는 자기만의 새로운 멤버/메소드가 있는 한편으로 부모에게 가능한 조작이 금지되어 버린다.
일반적으로 부모와 자식 클래스 관계는 is-a 관계라고 일컬어지고, 자식 포인터에서 부모 포인터로 형변환은 "당연히" 가능한 것으로 여겨지는데.. 그 당연한 건 public 상속일 때만으로 한정이다. 비공개 상속일 때는 정보 은닉을 제대로 보장하기 위해 자식에서 부모로 가는 게 허용되지 않으며, 심지어 static_cast로도 안 된다! Visual C++ 기준 C2243이라는 고유한 에러까지 난다.
기술적으로는, 객체 내부의 ABI 차원에서는 아무 위험할 것이 없음에도 불구하고 전적으로 객체지향 이념에 위배된다는 이유만으로 자식에서 부모로 못 간다. 무리해서 강제로 부모인 것처럼 취급하려면 reinterpret_cast라는 무리수를 동원해야 한다.
게다가 C++은 다중 상속(=가상 상속) 때문에 일이 더 크고 복잡해진다.
class A { public: int pub_mem; };
class B: virtual protected A {};
class C: virtual public A {};
class D: public B, private C {};
D o; o.pub_mem = 100;
위의 코드에서 저 멤버에다가 100을 대입하는 것은 가능할까?
이걸 판단하기 위해서는 컴파일러는 클래스 D의 가능한 상속 계통을 모두 순회하면서 최대 공개 등급(?)이 public이 되는지 따져 봐야 한다.
(참고로, 둘을 virtual로 상속하지 않았다면, 저 pub_mem이 B에 딸린 놈인지 C에 딸린 놈인지 알 수 없어서 모호하다고 어차피 컴파일 에러가 남..)
B는 A를 protected 상속했기 때문에 여기서 pub_mem이 protected가 되어 버려서 나가리. C는 A를 public으로 상속했지만, 최종 단계인 D에서 C를 private 상속해 버렸기 때문에 private가 된다.
요컨대 A에서 D까지 가는 동안 public이 계속 public으로 유지되는 상속 계통이 존재하지 않으며, 최대 공개 등급은 B 계열인 protected로 귀착된다. 그렇기 때문에 위의 구문은 컴파일 에러가 나게 되며, pub_mem은 D의 멤버 함수 내부에서나 건드릴 수 있다.
Visual C++의 경우, 가상 상속이 아닌 일반적인 상속 공개 등급 위반이라면 C2247 Not accessible because .. uses ... to inherit from 이라는 좀 단정적인 문구의 에러가 뜬다.
그러나 가상 상속 체계에서는 "접근 경로가 없다"라는 표현이 들어간 C2249 No accessible path to ... member declared in virtual base ... 에러가 난다. 공개 등급 체크를 위해서 더 복잡한 계산을 한 뒤에 판정된 에러이기 때문에 그렇다. 매우 흥미로운 차이점이다.
아무튼.. 참 복잡하기 그지없다.
그래서 C++ 이후의 언어들은 상속은 그냥 부모 멤버들의 공개 등급을 그대로 유지하는 public 상속만 생각하는 편이다. 다중 상속을 봉인해 버렸다는 건 더 말하면 입만 아플 것이고. C++이 객체지향 언어로서 여러 실험과 시행착오를 많이 했고 거기서 너무 무리수로 여겨지는 개념들이 후대의 언어에서는 짤렸다고 생각하면 되겠다.
Posted by 사무엘