최근 수정 시각 : 2024-12-09 14:45:55

상속(프로그래밍)

프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all" <colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C( 포인터 · 구조체 · size_t) · C++( 자료형 · 클래스 · 이름공간 · 상수 표현식 · 특성) · C# · Java · Python( 함수 · 모듈) · Kotlin · MATLAB · SQL · PHP · JavaScript · Haskell( 모나드)
마크업 문법 HTML · CSS
개념과 용어 함수( 인라인 함수 · 고차 함수 · 람다식) · 리터럴 · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 #! · == · === · deprecated · NaN · null · undefined · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

파일:attachment/displayobject_racoon28.jpg
위 그림은 액션스크립트 3.0에서 기본적으로 지원하는 클래스간의 상속 구조도. OOP 언어라면 (당연히) 시스템을 제공하는 사람들도 이렇게 객체지향 구조를 이용한다.

1. 개념2. 상속의 종류
2.1. 구현 상속(implementation inheritance)
2.1.1. 예시
2.2. 인터페이스 상속(interface inheritance)
2.2.1. 예시
3. 상속의 기능
3.1. 재정의3.2. 다중 상속
3.2.1. 죽음의 다이아몬드
4. 구현 상속의 문제점과 해결 방안
4.1. 정보 은닉의 파괴4.2. 동적인 유연성이 떨어짐
4.2.1. 결합도를 크게 늘림
4.3. 엉뚱한 상속 구조의 발생4.4. 부모 자리에 자식이 들어가더라도 정확히 같은 행동을 할까?4.5. 동적 바인딩에서 오는 성능 하락4.6. 해결 방안

1. 개념

  • 객체 지향 프로그래밍(OOP: Object Oriented Programming)에서 크게 3요소로 꼽는 캡슐화, 상속, 다형성 세 가지 중 상속을 일컫는다. 다른 표현으로는 계승, 확장(Java에서 상속할 때 extends라는 키워드를 사용한다.)이라는 단어도 사용된다.
  • 일본, 중국, 북한에서는 계승이라는 단어로 번역했다.
  • 수리 논리학과 연계된다.
  • 파생 클래스가 기초 클래스의 기능을 받아 쓰는 것이라고 이해하면 쉽다. 이럴 땐 파생 클래스는 기초 클래스의 기능을 받았으므로 기초 클래스의 기능도 쓸 수 있게 된다.(하지만 파생된 클래스가 기초 클래스의 노릇을 못하는 경우가 종종 발생한다. 본 문서의 4.4 문단을 참조하라.)
  • 상속하는 클래스는 '슈퍼 클래스', '부모 클래스' 등으로 부르고, 상속받는 클래스는 '서브클래스', '자식 클래스' 등으로 부른다.
  • CSS에서 말하는 상속(부모 태그의 속성 중 상속이 가능한 CSS 속성을 그대로 자식 태그에게 넘겨 주는 기능)과는 다른 개념이다.

2. 상속의 종류

  • 상속은 구현 상속(implementation inheritance)과 인터페이스 상속(interface inheritance)으로 나뉜다.
  • 구현 상속은 코드 상속(code inheritance)으로도 부르며 인터페이스 상속은 서브타이핑(subtyping)으로도 부른다.
  • 구현 상속과 인터페이스 상속의 가장 큰 차이점은 polymorphic substitutability(리스코프 치환 가능성)가 컴파일 타임에 결정되는가 여부이다.
  • 인터페이스 상속은 is-a 관계가 항상 성립하며 따라서 부모 타입을 자식 타입으로 항상 치환 가능하다.
  • 반면에 구현 상속은 방법에 따라 is-a, has-a, 그리고 is implemented in terms of(IIITO) 관계가 성립하는데, 이 중 is-a 관계일 때만 부모 타입을 자식 타입으로 치환할 수 있다.
  • 별다른 말이 없는 경우 상속은 대개 구현 상속을 지칭하는 경우가 많다.

2.1. 구현 상속(implementation inheritance)

2.1.1. 예시

철권 시리즈를 구현한다고 치자. 그러면 당신은 카즈야 헤이하치 같은 캐릭터의 클래스를 정의할 것이다. 만약 당신이 초보 프로그래머(C++을 기준으로 작성하였다.)라면
#!syntax cpp
class 카즈야
{
    체력
    힘
    스피드 
    풍신권
    나락쓸기
    ...
}

class 헤이하치
{
    체력
    힘
    스피드
    뇌신권
    ...
}
대충 이런 식으로 캐릭터마다 클래스를 만들어낼 것이다.

하지만 잘 들여다보면 카즈야나 헤이하치는 상당히 많은 공통점을 갖고 있다. 일단 머리, 팔, 다리가 기본적으로 있고 수치상으로 따지고 들어가면 체력과 공격력, 방어력, 회피율 등의 수치가 있다. 쿠마 모쿠진처럼 인간이 아닌 캐릭터와 비교해 봐도 이러한 공통점을 찾을 수 있다. 요컨대 격투 게임 캐릭터는 저마다 기술이나 인종은 다를지 몰라도 공통적으로 체력과 힘, 스피드 같은 요소들을 갖추고 있다는 것이다.

그렇다면 아래와 같이 '캐릭터'라는 클래스를 만든 다음에
#!syntax cpp
class 캐릭터
{
    체력
    힘
    스피드
    ...
}
이렇게 공통되는 부분을 클래스로 만든 다음에
#!syntax cpp
class 카즈야 : public 캐릭터
{
    풍신권
    나락쓸기
    ...
}
이렇게 상속을 받아 개별 캐릭터를 정의해 주면 똑같은 클래스를 몇 번이고 다시 쓰는 수고를 하지 않아도 된다. 때문에 클래스는 설계도나 금형, 붕어빵 틀에 곧잘 비유되곤 한다. 이러한 비유도 썩 적절하지는 않은데, 상속은 여기서 더 나아가 기존 클래스에 새로운 기능을 얼마든지 덧붙이거나 심지어 기존 클래스까지 새롭게 정의할 수 있기 때문이다.

만약 클래스나 상속을 쓰지 않았다면 사소한 수정 사항이라도 발생할 경우 모든 캐릭터의 코드를 일일이 찾아다니며 수정을 해야 하지만 상속을 쓰면 그냥 상속받은 클래스에 새로운 변수를 하나 추가하거나 아래의 재정의 같은 기능을 이용해서 입맛에 맞게 바꿔줄 수도 있다.

자바의 모든 상속은 public 상속이고, 따라서 문법적으로 is-a 관계이다.

하지만 C++의 경우 private inheritance를 지원하는데 이 경우 자식 클래스 멤버 변수의 접근 제한이 변경되므로 부모 위치에 자식을 넣어도 동작을 보장할 수 없다(IIITO using derivation 관계).

2.2. 인터페이스 상속(interface inheritance)

인터페이스 상속은 is-a 관계가 항상 성립한다. 이는 다형성 중 subtyping polymorphism에 해당한다.

2.2.1. 예시

  • 자바 예시
    {{{#!syntax java
interface Show {
public String show();
}

class Bar implements Show {
public int a;
public int b;
public String show() {
return "" + this.a + " " + this.b + "\n";
}
}
}}}
instance Show Bar where
show :: Bar -> String
show (Bar a b) = show a ++ b
}}}
  • 위 두 예시에서 볼 수 있듯이 Bar 타입 객체는 항상 Show 타입 객체이다. 따라서 Show가 쓰인 모든 부분을 Bar로 교체해도 문법적으로 전혀 문제가 없다.

3. 상속의 기능

3.1. 재정의

상속받은 파생 클래스가 부모 클래스의 요소를 재정의해야 할 필요가 있을 것이다. 이를테면 ' 오우거 진파치의 체력을 두 줄로 해주세요.', ' 로저의 경우는 캥거루 꼬리를 다리처럼 쓰는 캐릭터니 다리가 2개가 아니라 3개의 다리가 있는 것처럼 만들어주세요.' 같은 요구 사항이 있을 경우에는 재정의를 사용한다. 영어로는 오버라이드(override)라고 한다.

재정의를 할 수 있는 것은 멤버 함수에 한정되며 이 때문에 함수(또는 연산자) 오버로딩과 종종 혼동되므로 주의해야 한다. 함수 오버로딩은 함수(메서드) 이름만 같고 인수 개수나 타입이 달라 서로 구별할 수 있는 서로 다른 메서드를 만드는 것을 말한다. 혼동이 우려되면 재정의라는 말을 쓰거나 method overriding이라고 하자.

부모 클래스의 오버로딩된 메서드가 여러 개 있는데 그중에 일부만 재정의하면 나머지 메서드는 가려져 호출할 수 없다. 부모의 기능 중 하나만 오버라이드해도 될 때도 다른 메소드도 전부 재정의하자. 그냥 부모의 메소드만 호출해 반환하는 코드 한 줄이면 된다.

3.2. 다중 상속

한번 파생받은 클래스에서 또 파생되는 경우, 파생 클래스 바로 위의 클래스를 직접 클래스(direct class), 그 위의 클래스를 간접(indrect) 클래스라고 칭한다. 어느 정도 규모가 되는 실 프로젝트에서는 위로 끝없는 클래스의 계층이 펼쳐져 있고 엄청난 수의 간접 클래스와 인터페이스가 가득하다.

한 번에 둘 이상의 클래스를 파생받는 경우, 다시 말해 여러 부모를 둔 경우를 두고 다중 상속(multiple inheritance)이라고 칭한다. 바로 밑의 '죽음의 다이아몬드' 때문에 두 개 이상 타입을 상속해야 하는 경우 구현 상속보다는 인터페이스 상속하는 것이 유리하다.

Java와 C#은 아예 문법적으로 다중 구현 상속을 하지 못하도록 막아 놓았다. 그나마 예외로는 C++의 Signal/Slot이나 GUI에서 부모 자식 관계를 이용한 자동 메모리 관리와 기본 GUI 컴포넌트의 상속, 또는 Qt(프레임워크)의 QObject 정도가 있다. C++ 이외에 파이썬도 다중 구현 상속이 가능하다.

멤버 변수가 있는 클래스를 다중 상속 했을 경우 다중 상속으로 인해 두 번째 이후로 오는 클래스는 첫 번째 클래스 다음 순서로 메모리에 올라오기 때문에 원본 클래스와는 메모리 위치가 다르며, 이에 대한 메모리 주소 처리를 요구하기 때문에 추가적인 오버헤드가 발생한다.

3.2.1. 죽음의 다이아몬드

파일:svvHZGt.jpg

The Deadly Diamond of Death(DDD)

C++에서 super 키워드가 없는 이유.[1]

프로그래밍 언어 컴퓨터에게 내릴 명령을 순서대로 정리해 놓은 문서라고 볼 수 있으며, 가장 중요한 특징 중 하나는 같은 구문이 두 가지 이상의 의미로 해석될 여지가 있어서는 안 된다[2]라는 것이다. 그런데 다중 상속이 허용될 경우 이러한 상황이 발생할 가능성이 있으며, 그중 한 예가 바로 위의 그림과 같은 '죽음의 다이아몬드'이다.

예를 들어 ' 사람'이라는 기본 클래스가 있고, 여기에는 '성격()'과 '키()' 메소드가 있다. 이제 '사람' 클래스를 상속받은 ' 아빠'와 ' 엄마' 클래스가 있고, 이 둘을 동시에 상속받은 ' 자식' 클래스가 있다고 하자. 이렇게 되면 '자식' 클래스에서 '성격()' 혹은 '키()' 메소드를 호출할 때 어느 부모의 메소드를 따라야 하는지가 한 가지로 명확히 정해지지가 않는다. 예컨대 프로그래머는 '아빠'의 "키()" 메소드를 따르고 "엄마"의 "성격()" 메소드를 따르는 것을 의도했지만 전혀 의도하지 않은 정반대의 결과물이 나올 가능성이 존재한다는 것. 이것이 바로 '죽음의 다이아몬드'이다. 상속 관계가 마름모꼴(다이아몬드형)으로 생겼다고 해서 붙여진 이름.

C#(.net)하고 Java에선 이것을 방지하기 위해 다중 상속 자체를 제한해 놨다. 상속은 기본적으로 일반 클래스는 무조건 하나만 가능하다. 두 개의 이상의 클래스를 상속받는 상황을 보조하기 위해 추상 메소드라는 개념이 있다. 다만 이름과 의미만 정해져 있고 실제 내용 없는 빈 깡통으로 정의되며, 여차하면 내용을 참고하지 않으므로 문제를 일으키지 않는다. 예를들어 인터페이스는 추상 메소드만 모아놓은 것이기 때문에 실질적으로 쓰이는 것은 하위 클래스에서 실질적으로 구현된 유일한 메소드뿐이지, 단순히 메소드 이름만 가진 걸로는 아무련 효과가 없다. 즉 실제 내가 무언가 만들기 전까지 아무런 일도 하지 않으므로 죽음의 다이아몬드고 뭐고 뭘 호출할지 모호함이 발생할 여지를 만들지 않는 개념이다.

물론 추상 메소드만 쓰는 인터페이스 특성상 클래스마다 똑같은 기능을 매번 구현해야 하고 이로 인해 코드의 재사용성이 크게 저하되는 문제점이 있다. 결국 이름만 지정된 아무것도 아닌 메소드를 구현하는 건 다른 클래스에 똑같은 걸 그대로 구현하는 것과 실질적으로 같은 모양새인 셈.

이 문제 때문에 최근에 나오는 언어들은 트레이트나 믹스인같이 일반 메소드가 있어도 다중 상속이 가능한 모델을 고안하고 있다. 위의 죽음의 다이아몬드에 대한 해결책도 새로 나온게 있는데, 바로 상속의 우선순위를 두는 것이다. 앞에 오는 클래스의 메소드만 상속하고 그 외의 클래스의 메소드는 무시해 버리는 것. Python이 이런 방식으로 구현되어 있다. 기존의 다중 상속 방식이 수학적으로는 클래스의 집합 위에 정의해야 했다면, 이런 방식은 클래스의 수열 위에 정의하였다고 볼 수 있다. 이런 경우 앞의 모호함 문제는 해결되지만 상속되는 클래스들의 순서가 바뀌면 가변하는 부분도 있는 등, 프로그래머가 생각해야 할 부분이 늘어난다. 아니면 아예 상속을 받지 않는 조상 객체만 다중 상속이 가능하게 해도 공통 조상이 존재할 수 없어지기 때문에 죽음의 다이아몬드를 방지하는게 가능하다.

4. 구현 상속의 문제점과 해결 방안

  • 객체 지향에서 없어서는 안 될 3요소 중 하나인 상속이지만[3] 구현 상속의 효용성 여부에는 논란이 많다.
  • 자바의 창시자 제임스 고슬링은 자바를 다시 만든다면 구현 상속을 포함하지 않을 것이라고 했다.[4]

4.1. 정보 은닉의 파괴

하위 클래스가 상위 클래스의 정보를 뜯어내 보안상 허점을 만들어낼 수 있다. 이를 방지하기 위해 자식한테도 안 보여줄 것은 private, 자식에겐 보여주되 남에게는 안 보여줄 건 protected, 누구에게나 다 보여줄 건 public으로 선언하도록 구분하는 기능이 기본적으로 지원되지만, 문제는 protected를 미칠 듯이 남용해 대는 프로그래머가 많다는 것.[5] 특히 객체 지향에 갓 입문한 초보 프로그래머의 클래스엔 온통 protected와 public만이 가득한 걸 볼 수 있다.

4.2. 동적인 유연성이 떨어짐

상속은 컴파일 시점에 부모를 지정해 놓으면 런타임 시점에 바꾸는 방법이 없다시피 하므로 유연성이 바닥이다. 동적인 슈퍼클래스 바꾸기를 지원하는 언어가 아주 없는 건 아니지만 그런 걸 지원했다가 발생할 수 있는 위험 요소가 한두 가지가 아닌지라 거의 없다고 봐도 무방하다.

4.2.1. 결합도를 크게 늘림

좋은 객체 지향적 코드는 한 클래스 내 요소들의 응집도는 높이고, 서로 다른 클래스 간의 결합도는 떨어뜨린 코드이다. 그런데 어떤 클래스가 상속으로 부모 자식 관계가 되면, 그 부모 클래스는 몰라도 자식 클래스는 부모 클래스 없이는 그야말로 아무것도 아닌 클래스가 된다. 이는 자식(이 될) 클래스의 부모(가 될) 클래스에 대한 결합도를 크게 높이고, 이에 따라 객체 지향의 핵심 중 하나인 재사용이 힘들어진다. 상속하는 순간 그냥 한 세트가 된다고 생각하면 편하다. 이건 포함도 비슷하지만, 최소한 포함은 최상위 클래스의 인터페이스만 알면, 그와만 결합될 뿐이고 서브클래스는 아웃 오브 안중이 된다.

4.3. 엉뚱한 상속 구조의 발생

특정 기능이 필요하기는 한데 상식적으로 전혀 is-a 관계가 아님에도 불구하고 억지로 상속을 사용하면 엉뚱한 상속 구조가 탄생한다. 대개 코드의 재사용을 위해 이런 식으로 구성하는 경우가 많다. 예를 들면 카페의 매출 관리 프로그램을 만들며 '커피'라는 클래스를 만들고, 멤버 변수로 가격과 사이즈를, 메소드로 getPrice()와 getSize()를 주었다고 하자. 얼마 후 이 카페에서 새로 녹차 메뉴를 팔며 그대로 이 '커피' 클래스를 상속하여 사용했다. 얼추 보기에 가격과 사이즈라는 특성을 동일하게 가지기에 문제가 없어 보이며 아마 최초에는 문제없이 작동할 것이다. 하나 만약 후에 새로 도입한 바리스타 로봇을 이 프로그램과 연동하기 위하여 '커피' 클래스에 '원두량'이라는 멤버 변수를 추가했다고 하자. 커피 클래스를 상속한 녹차 클래스는 어떻게 해야 할까? 그냥 '원두량'을 0으로 설정해 버리면 아마 로봇은 녹차를 시켰을 때 그저 뜨거운 물만을 내어줄 것이다. 이 경우, '녹차'는 별도 클래스로 분리하여 '녹차 가루양'과 같은 별도의 멤버 변수를 주는 것이 옳을 것이며, 인터페이스를 통해 로봇이 클래스에 따라 다른 레시피를 처리할 수 있게 하는 것이 더 바람직하다. 이처럼 상속은 부모와 자식 간 관계(A is a B-A는 B인가?)를 고려했을 때 자식이 부모에 완전히 포함되는 집합일 때만 사용하는 것이 좋다.

4.4. 부모 자리에 자식이 들어가더라도 정확히 같은 행동을 할까?

is-a 관계가 확실하다고 하더라도 문제가 발생할 소지는 남아있다. 상술했듯이 자식 클래스는 부모 클래스에 명시된 어떤 행동(메소드, 함수)을 물려받아 그대로 쓸 수도 있고, 재정의(override)할 수 있다. 여러 이유로 인해 자식 클래스의 행동이 보여주는 결과나 결과가 가지는 조건, 의미 등이 부모 클래스 때와는 딴판으로 달라질 수도 있다. 만약 자식 클래스가 부모 클래스 메소드를 물려받아 부모 클래스가 하던대로 똑같이 동작만 해준다면 다른 객체에서 '부모 클래스 객체의 어떤 메소드를 사용한다'는 자리에 자식 클래스 객체를 넣더라도 문제가 발생하지 않겠지만, 그렇게 되지 않을 가능성도 얼마든지 있다.

예를 하나 들어보자. '사람'이라는 기본 클래스가 있고 여기에는 '악수' 메소드가 있다. 즉, '사람' 클래스를 상속받은 모든 클래스는 '악수'를 할 수 있는 것이다. 보통 사람들이라면 악수를 하는 걸로 상대에게 해를 끼치는 경우는 거의 없다. 그런데 '사람' 클래스를 상속받은 ' 가위손 에드워드' 클래스가 있다고 하자. 에드워드도 일단은 '사람' 클래스를 상속받았으니 당연히 악수를 할 수 있지만 손이 가위로 돼 있기 때문에 다른 사람이 에드워드의 손을 잡으면 다치는 상황이 발생하는 것이다. 그러나 많은 프로그래밍 언어는 '에드워드'가 '사람' 클래스고 '악수'를 할 수 있다는 정도를 따지지, 손 대신 나오는 게 위험한 가위손일 수도 있다는 점은 신경 쓰지 않고 컴파일을 해준다. 프로그래밍 언어 기준으로 말하자면 사람 클래스 + 악수 메소드 기준으로 만들어졌던 예전 코드들에 가위손 클래스의 객체를 넣으면 예전 코드에서 악수 메소드를 잘 쓰던 부분이 몽땅 망가질 수 있다는 뜻이다. 물론 프로그래밍 언어가 허용한다면 사람 클래스를 상속받은 어떤 클래스가 '악수' 행동을 할 때 가위손이 나오든 기관총을 발사하든 컴파일 오류는 안 나겠지만, 가위손과 같이 자신은 멀쩡한데 다른 쪽에서 망가지는 상황은 모두가 겪고 싶지 않을 것이다. 즉 논리적 오류가 난다. 상속을 할 수 있다고 하면 그만인 것이 아니라 다른 객체들을 고려해서 신중하게 해야 하는 이유이기도 하다.

상술된 사람과 가위손의 악수 차이를 학문에서 표현한 것이 리스코프 치환 원칙(Liskov Substitution Principle)이다. 즉 S가 T의 하위형(subtype)일 때 필요한 프로그램의 속성[6]을 변경하지 않고도 자료형 T의 객체를 자료형 S로 교체할 수 있다면 원칙이 만족된다는 것이다.

4.5. 동적 바인딩에서 오는 성능 하락

상속을 사용하면, 필연적으로 오버라이딩을 사용할 것이다. 이 오버라이딩이야말로 상속이 다형성을 가져다주는 요인인데, 문제는 오버라이딩을 하려면 필연적으로 메소드(함수)가 동적으로 바인딩되어야 한다는 것이다. 실행될 메소드를 컴파일 타임에 결정하는 정적 바인딩 방식으로 구현할 경우, 부모 클래스 또는 상위 인터페이스의 메소드 호출 시 자식 클래스의 메소드가 동작하지 않고 부모의 메소드가 호출될 것이다. 이는 아무런 다형성도 가져다줄 수 없다. 때문에, 런타임에 오버헤드가 생기게 되며 이것은 객체 지향 방식이 절차적 방식에 비해 느린 이유 중 하나이다.

4.6. 해결 방안

위와 같은 문제 때문에 is-a 관계가 의미론적으로 올바른 경우에만 구현 상속 한다. 위에 언급된 커피와 녹차의 예처럼, 언뜻 보기에 비슷해 보인다고 상속을 해서는 안된다. 또한 가위손의 예처럼, 부모와 is-a 관계이고 부모와 같은 행동을 할 수 있지만 다른 객체가 예상치 못한 결과를 초래하는 일이 없어야 한다.

대안으로는 구성[7]을 사용할 수 있다.(composition over inheritance)
예를 들면 의존성 주입할 때 많이 보이는 패턴인데 생성자에서 자식 타입 인스턴스 받아서 부모 타입(주로 interface) 멤버 변수에 할당하는 것이 있다.

상속관계를 설계할때 저렇게 is-a 관계가 아니지만 관련성이 있는 경우는 일반적으로 둘을 포괄하는 또다른 상위 클래스를 만드는 것이 해결책이다. 커피와 녹차의 예를 들면 둘 모두의 부모 클래스인 음료를 만들고 이를 상속하게 하면 된다.

[1] super는 자신의 상위 클래스를 나타내는 키워드다. 예를 들어, 만약 이 예시를 단일 상속이라는 가정하에 DigitalRecorder가 CDBurner만 상속받은 상태였다면 DigitalRecorder의 super.burn()은 CDBurner의 burn()을 호출했을 것이다. 그러나 C++은 다중 상속이 허용된 프로그래밍 언어라, 위의 예시대로라면 DigitalRecorder의 super.burn()은 CDBurner의 burn()인지 아니면 DVDBurner의 burn()인지 모호해지기 때문에 super라는 키워드가 C++에 존재했었다고 해도 모호성에 의해 컴파일을 실패했을 것이며, 이런 이유 때문에 super 키워드가 존재하지 않는다. 다만, C++을 사용하는 프레임워크라고 무조건 super 키워드가 없는 것은 아니다. 예를 들면, 언리얼 엔진은 C++을 스크립트 언어로 사용하는데도 super 키워드가 존재한다. 당연한 소리지만 이는 언리얼 엔진의 C++에서는 다중 상속을 허용하지 않는다는 의미다. 무엇보다도, C++은 다중 상속이 '허용'되어 있는 것뿐이지, C++에서도 다중 상속을 하는 걸 추천하지 않는다. [2] 이를 프로그래밍 용어로 모호성 이라고 표현한다. [3] 객체 지향의 3요소 다형성 중 '서브타이핑 다형성'은 대부분 상속으로 구현한다. 다형성 구현에 상향 형 변환(upcasting, 부모 클래스 참조 변수로 자식 클래스 객체 지칭)이 전제되고, 상향 형 변환 구현에 동적 바인딩이 전제되고, 동적 바인딩 구현에 상속이 전제되기 때문이다. [4] https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming) [5] get()이나 set() 계열의 메소드를 구현해서 값은 private로 설정해 놓고 은닉성을 지킬 수 있다고 믿고 get과 set을 미칠 듯이 남용해 대는 프로그래머가 많은데, get과 set을 이용해서 클래스 내부에서 처리해야 하는 로직을 외부에서 구현하는 게 가능하고 이로 인해 단일 책임 원칙을 위반하며 은닉성이 떨어지게 된다. 값만 은닉한다고 정보 은닉이 지켜지는게 아니다. 가능하면 둘 다 쓰지 말고, 필요하다면 get만 쓰는것이 좋다. [6] OOP에서 '속성'은 데이터, 필드, 멤버 변수 등을 의미한다. '행위'는 메소드를 일컫는다. [7] 조합이나 합성 등으로도 알려져 있기도 하다. 아직 표준어가 결정되지는 않은 듯하다.