본문 바로가기
💻 개발 이야기/객체지향 OOP

상속은 무엇인가?

by Jinseong Hwang 2025. 1. 25.

 
Disclaimer

이 글에는 개인적인 경험과 생각이 포함되어 있습니다.
 
 

01 | 자연, 그리고 프로그래밍 언어에서의 상속


상속이라는 단어를 들으면 보통 "부모가 가진 것을 자식에게 물려준다"는 이미지를 떠올리게 된다. 이처럼 상속(Inheritance)은 철학적 관점에서 세대 간의 '전이(傳移)'를 의미하기도 하고, 생물학적 관점에서 '종(種)'과 '속(屬)' 간 유전적 특성이 이어지는 모습과 유사하다는 평가를 받는다. 생물학에서 동물(Animal)이라는 넓은 범주 아래 포유류(Mammal)가 동물의 공통 특성을 물려받고, 여기서 다시 고양잇과(Felidae)가 더 구체적인 특성을 취하는 식의 점진적 구체화가 자연스럽게 일어나는 것처럼, 객체지향 프로그래밍에서도 상위 클래스(부모)가 가진 특성과 동작을 하위 클래스(자식)가 물려받아 확장해 나가는 구조를 구현할 수 있다.
 

https://pixabay.com/photos/cat-pet-feline-animal-fur-kitty-6569156/

 
프로그래밍 언어 차원에서 상속 개념은 1960년대 후반 노르웨이에서 탄생한 Simula(Simulation의 앞부분을 따서 지음)에서 비롯되었다. Simula는 "클래스와 객체"라는 개념을 최초로 제안했고, 프로그램을 실제 세계처럼 객체 단위로 나누어 모델링한다는 새로운 패러다임인 OOP(Object-oriented Programming)를 제안했다. 이때 공통된 속성과 기능을 물려주는 구조가 필요해지면서 상속 개념이 싹을 틔우게 되었다. 이후 1970년대 Smalltalk에서는 "Everything is an object"라는 철학 아래 클래스 간 계층 구조를 적극 채택함으로써 객체지향 프로그래밍을 확립했고, 상속은 코드 재사용과 확장성을 높이는 핵심 기법으로 자리 잡았다. 1980~90년대에 걸쳐 C++과 Java가 등장하며 객체지향 패러다임이 폭넓게 실무에 도입되었고, 이 과정에서 상속은 지금처럼 널리 알려진 대표적 도구가 되었다.
 

02 | 상속은 무엇인가?


이러한 배경 속에서 오늘날의 소프트웨어 개발 현장은 수많은 객체지향 언어와 프레임워크 위에서 돌아간다. 현실 세계의 개념을 효율적으로 담아내기 위해서는 객체지향 설계가 요구되며, 그 핵심에는 상속이 자리 잡고 있다. 흔히 "IS-A 관계이면 상속을 써야 한다"는 말이 있을 정도로 상속은 객체지향 프로그래밍에서 직관적인 쉬운 기능이다. 그러나 막상 실무에서 상속을 다루다 보면, "IS-A 관계라고 다 상속이 옳은 것인가?" 하는 의문에 부딪힐 수 있다. 상속은 언제나 만능이 아니며, 적절히 활용해야만 진정한 가치를 얻을 수 있기 때문이다.
 
상속은 초기의 절차지향 방식 프로그래밍과 달리, 객체 단위로 현실을 쪼개어 각자 역할을 정의하고 협력하도록 만드는 객체지향 패러다임을 확립하는 과정에서 탄생했다. 여러 객체가 공통으로 가지는 속성과 동작을 부모 클래스에 정의하고, 자식 클래스가 이것을 물려받아 필요한 부분을 확장 또는 변경함으로써 중복 코드가 줄어들고 유지보수가 용이해지는 장점이 있다.
 
상속의 기본 개념은 부모 클래스(Super Class)와 자식 클래스(Sub Class)로 나뉜다. 부모 클래스는 여러 자식 클래스가 공통으로 가지는 속성과 동작을 정의해 두는 상위 개념이며, 자식 클래스는 부모가 제공하는 코드를 물려받아(상속받아) 필요한 부분만 재정의(오버라이딩)하거나 추가 기능을 덧붙여 사용한다. 이때 보통 "A는 B의 일종이다(A IS-A B)"라는 관계가 자연스러우면 상속이 적합하다고 알려져 있다. 예를 들어 "고양이(Cat)는 동물(Animal)의 일종이다"라는 말이 어색하지 않다면, Animal을 부모로, Cat을 자식 클래스로 설계할 수 있다.
 
상속의 장점은 크게 네 가지로 요약할 수 있다.

  • 공통 로직을 부모 클래스에 정의함으로써 코드 재사용성을 극대화할 수 있다.
  • 기존 클래스를 수정하지 않고도 자식 클래스를 만들어 기능을 확장하기가 쉬워진다.
  • 상속을 통해 계층 구조(Hierarchical Structure)를 만들 수 있으므로 "일반적인 개념에서 구체적인 개념으로 내려가는" 흐름을 코드로 표현하기에 용이하다.
  • 다형성(Polymorphism)을 구현하기가 쉬워져, 부모 클래스의 타입만 알면 자식 클래스들이 어떤 형태로 구현되어 있어도 동일한 인터페이스로 접근할 수 있게 된다.

 

상속은 구조일까 행위일까?

상속이라는 단어에 대해 곰곰이 생각해봤다. 상속은 본래 "상속하는 행위"를 의미하는 것 같다. (이는 한국어가 모국어인 자의 직감일 뿐이다.) 하지만 객체지향 언어에서는 계층적 구조를 표현하는 데 사용되므로 기본적으로는 "계층적 구조"를 의미한다고 볼 수 있다. 다만, 이러한 계층적 구조를 통해 부모 클래스의 행위를 자식 클래스가 사용할 수 있고, 이를 확장할 수도 있다는 점에서 구조에 의한 행위의 확장도 이루어진다고 할 수 있다.

 

03 | 모든 것은 Trade-off, 상속을 의심해 보자


그러나 종종 "IS-A 관계니까 상속을 써야 한다"는 통념에 매달린 채 무리하게 상속을 적용하면, 오히려 코드가 복잡해지고 유지보수가 어려워진다. 자식 클래스가 부모의 메서드 전부를 자연스럽게 물려받을 수 있는지, 상속 계층이 과도하게 깊어지지 않는지, 상속보다 조합(Composition)을 쓰는 편이 유연하고 단순하지는 않은지 등을 면밀히 검토해야 한다. 예컨대 SNS에서 '사용자(User)'와 '관리자(Admin)'는 둘 다 사람이지만, 관리자에게는 일반 사용자와는 전혀 다른 권한과 책임이 필요할 수 있다. 이 경우 상속만으로 모든 요구사항을 표현하려다 보면 중복되는 기능이나 맞지 않는 메서드를 억지로 물려받아야 해 설계가 꼬이기 쉽다. 이처럼 IS-A 관계가 보이더라도 행동 방식이나 책임이 크게 다르면, 상속은 피하고 필요한 기능들을 조합해서 구현하는 방식이 더 깨끗할 수 있다.

https://gazar.dev/clean-code/composition-over-inheritance-typescript-best-practice

 
상속을 설계하는 과정에서 균형을 맞추려면, 상속 계층이 깊어질 때마다 추상화를 다시 검토하는 습관이 필요하다. "이 클래스들은 정말 공통된 행동을 공유하고 있는가?", "계층이 늘어날수록 얻을 수 있는 이점과 복잡도의 증가 중 어느 쪽이 더 큰가?" 같은 질문을 통해, 필요 이상으로 상속을 남발하지 않도록 설계해야 한다. 또한 자식 클래스들이 부모로부터 불필요한 로직을 물려받고 있지는 않은지, 상속 대신 인터페이스나 조합 방식으로 대체할 수는 없는지도 고민해보아야 한다.
 
실무 현장에서는 상속을 활용해 코드 중복을 획기적으로 줄이거나 유지보수를 단순화한 성공 사례가 많다. 여러 종류의 '직원(Employee)'을 관리하는 시스템을 예로 들면, Employee 클래스를 부모로 잡고 공통 속성과 기능을 정의해 두고, 정규직/계약직/인턴 등 다양한 형태의 자식 클래스를 만들면 관리가 무척 편리해진다. 반면, IS-A 관계처럼 보이는 상황에서 무작정 상속을 적용했다가, 계층 구조가 너무 깊어져서 결합도가 올라가고 유연성이 떨어지는 문제에 직면해 뒤늦게 조합 방식으로 리팩토링을 진행하는 경우도 있다. "기계적으로 상속을 쓰기보다는, 설계의 유연성과 유지보수성을 염두에 두고 필요할 때만 도입하는 게 바람직하다"는 교훈을 얻는 이유가 바로 여기에 있다.
 

이펙티브 자바에서도 상속에 대해 주의할 것을 권하고 있다.

 

04 | 구조를 유지/개선하기 위한 몇 가지 질문과 답변


Q1) 상속 계층이 깊어졌을 때 구조를 단순화하기 위한 리팩토링 전략은 무엇부터 고려하면 좋을까?
 
먼저 각 단계를 거치면서 추상화가 적절했는지 확인해야 한다. 자식 클래스가 부모 클래스의 기능을 거의 사용하지 않는다면, 상속보다는 별도 클래스로 분리하고 필요한 부분만 조합하거나 인터페이스로 뽑아 쓸 수 있다. 중복 로직을 헬퍼나 유틸 클래스로 추출하고, 인터페이스나 디자인 패턴을 도입해 계층을 단순화하는 방법도 있다.
 
Q2) 조합(Composition) 방식에서 자주 겪는 문제 중 하나인 "객체 간 의존관계가 복잡해진다"는 것을 어떻게 최소화할 수 있을까?
 
조합 방식은 객체를 느슨하게 연결해 유연성을 높이지만, 그만큼 객체 간 협력 관계가 늘어나 복잡해질 위험이 있다. 이를 예방하려면 각 객체의 역할과 책임을 명확히 정의하고, 인터페이스나 추상 클래스에 의존하도록 설계하며, 의존성 주입(DI)과 IoC 컨테이너를 적절히 활용해 의존관계를 한눈에 파악할 수 있게 하는 것이 좋다. 더불어 중재자(Mediator)나 퍼사드(Facade) 같은 패턴을 도입해 협력 과정을 중앙에서 관리하거나, 객체 라이프사이클을 엄격히 관리하는 것도 의존성 문제를 완화하는 데 도움이 된다.
 

참고자료