핵심 인사이트 (3줄 요약)
- 본질: 상속 거부 (Refused Bequest) 는 자식 클래스가 부모의 메서드나 속성을 사용하지 않거나 빈 구현으로 오버라이드함으로써 리스코프 치환 원칙 (LSP: Liskov Substitution Principle) 을 위반하는 패턴이다.
- 가치: "is-a 관계"가 실제로는 성립하지 않는 상속을 발견·제거함으로써 시스템의 다형성 (Polymorphism) 신뢰성을 회복한다.
- 판단 포인트: "부모 타입 변수에 자식 인스턴스를 대입해도 프로그램이 올바르게 동작하는가?" — No라면 LSP 위반이다.
Ⅰ. 개요 및 필요성
바바라 리스코프 (Barbara Liskov) 가 1987년 제안한 원칙으로, "서브타입은 그것의 베이스타입으로 치환 가능해야 한다"는 원칙이다. 즉, T 타입의 객체가 요구되는 모든 곳에 S extends T 타입의 객체를 대입해도 프로그램의 정확성이 변하지 않아야 한다.
[ 상속 거부 사례 — 정사각형/직사각형 문제 ]
┌────────────────────────────────────────────────────────┐
│ class Rectangle { │
│ int width, height; │
│ void setWidth(int w) { this.width = w; } │
│ void setHeight(int h) { this.height = h; } │
│ int area() { return width * height; } │
│ } │
│ │
│ class Square extends Rectangle { ← "is-a?" NO! │
│ @Override │
│ void setWidth(int w) { │
│ this.width = w; this.height = w; // 거부: height │
│ } │
│ @Override │
│ void setHeight(int h) { │
│ this.width = h; this.height = h; // 거부: width │
│ } │
│ } │
└────────────────────────────────────────────────────────┘
Rectangle r = new Square(5);
r.setWidth(3); // height도 3으로 변경 → 기대 위반!
assert r.area() == 15; ← FAIL (실제: 9)
다형성 (Polymorphism) 을 활용하는 코드는 부모 타입으로 객체를 다룬다. LSP가 위반되면 런타임에 예상치 못한 동작이 발생하고, 타입 체크 (instanceof) 코드가 급증해 OCP (개방-폐쇄 원칙, Open-Closed Principle) 도 함께 위반된다.
- 📢 섹션 요약 비유: "포유류" 카드게임에서 "고래"를 뽑았더니 "육지에서 달려라" 명령을 수행하지 못한다 — 상속 계층이 잘못 설계된 것이다.
Ⅱ. 아키텍처 및 핵심 원리
┌────────────────────────────────────────────────────────────┐
│ LSP 위반 유형 분류 │
├────────────────────┬───────────────────────────────────────┤
│ 유형 │ 설명 및 예시 │
├────────────────────┼───────────────────────────────────────┤
│ 사전 조건 강화 │ 자식이 더 엄격한 입력 조건 요구 │
│ (Precondition │ 부모: accept(n >= 0) │
│ Strengthening) │ 자식: accept(n > 0) ← 위반 │
├────────────────────┼───────────────────────────────────────┤
│ 사후 조건 약화 │ 자식이 더 약한 결과 보장 │
│ (Postcondition │ 부모: return list non-empty │
│ Weakening) │ 자식: return null 가능 ← 위반 │
├────────────────────┼───────────────────────────────────────┤
│ 불변식 위반 │ 자식이 클래스 불변 조건 파괴 │
│ (Invariant │ Square가 width≠height 상태 허용 │
│ Violation) │ │
├────────────────────┼───────────────────────────────────────┤
│ 예외 규칙 추가 │ 자식이 부모가 던지지 않는 예외 추가 │
│ (Exception │ 자식 override에서 새 예외 throw │
│ Addition) │ │
└────────────────────┴───────────────────────────────────────┘
[ 처방 — 구성 방식 재설계 ]
┌────────────────────────────────────────────────┐
│ interface Shape { int area(); } │
│ │
│ class Rectangle implements Shape { │
│ int width, height; │
│ int area() { return width * height; } │
│ } │
│ │
│ class Square implements Shape { │
│ int side; │
│ int area() { return side * side; } │
│ } │
│ // Rectangle과 Square는 더 이상 상속 관계 없음 │
└────────────────────────────────────────────────┘
| 항목 | 설명 | 포인트 |
|---|---|---|
| 핵심 역할 | 입력·상태·출력을 분리하는 책임 경계 | 구현보다 경계를 먼저 본다. |
| 제어 지점 | 조건, 이벤트, 정책이 만나는 곳 | 병목과 결합이 생기는 곳이다. |
| 검증 포인트 | 테스트·로그·모니터링으로 확인할 지점 | 운영 가능성이 설계 품질을 결정한다. |
- 📢 섹션 요약 비유: 스마트폰이 "전화기"를 상속받아 "팩스 기능 거부"를 오버라이드하는 것보다, 스마트폰이 전화 기능을 "내장"하고 별도로 존재하는 게 더 자연스럽다.
Ⅲ. 비교 및 연결
| 구분 | 상속 (Inheritance) | 구성 (Composition) |
|---|---|---|
| 관계 | is-a | has-a |
| 결합도 | 높음 (부모 변경 → 자식 영향) | 낮음 (인터페이스 변경만 영향) |
| 유연성 | 낮음 (컴파일 타임 결정) | 높음 (런타임 교체 가능) |
| LSP 위반 위험 | 높음 | 낮음 |
| 권장 원칙 | is-a 관계 명확할 때만 | 기본값으로 구성 선택 |
상속 거부 (Refused Bequest) 는 여러 SOLID 원칙과 동시에 충돌한다.
| SOLID 원칙 | 위반 방식 |
|---|---|
| LSP (리스코프 치환 원칙) | 직접 위반 — 치환 불가 |
| OCP (개방-폐쇄 원칙) | 타입 체크 코드 급증 |
| ISP (인터페이스 분리 원칙) | 불필요한 메서드 강제 구현 |
- 📢 섹션 요약 비유: 채식주의자를 "인간"의 서브클래스로 만들고 "고기 먹기" 메서드를 오버라이드해 비어두는 것 — 생물학 교과서가 틀린 게 아니라 설계가 틀린 것이다.
Ⅳ. 실무 적용 및 기술사 판단
Java 표준 라이브러리의 Stack<E> 가 Vector<E> 를 상속하는 것은 역사적 LSP 위반 사례다. Stack 에 add(index, element) 같은 Vector 메서드를 호출하면 스택의 불변식(후입선출, LIFO: Last-In-First-Out)이 깨진다. 현재는 Deque 인터페이스와 ArrayDeque 구현을 사용하도록 권고한다.
-
빈 메서드 구현: 오버라이드 메서드 내부가 비어있거나
UnsupportedOperationException만 던지면 신호 -
상속 계층 깊이: 3단 이상 상속은 잠재적 LSP 위반 가능성이 높음
-
코드 리뷰 체크:
@Override메서드에서throw new UnsupportedOperationException검색 -
상속 계층 재설계: "상속 → 인터페이스 + 구성" 전환을 설계 개선 방안으로 제시
-
계약 설계 (Design by Contract, DbC): 선행 조건 (Precondition), 후행 조건 (Postcondition), 불변식 (Invariant) 문서화
-
인터페이스 분리 (ISP: Interface Segregation Principle): 거대 인터페이스 대신 역할별 소형 인터페이스
판단 체크리스트
- 변경 전 동작을 고정할 테스트가 준비되었는가?
- 냄새의 원인이 구조 문제인지 일회성 구현인지 구분했는가?
- 리팩토링 단위를 작게 나눠 롤백 가능하게 했는가?
- 명명·모델·패키지 경계가 함께 개선되는가?
- 📢 섹션 요약 비유: 알바생을 뽑을 때 "모든 업무 가능자"로 채용했는데 특정 업무는 아예 못 한다면, 처음부터 역할을 나눠 채용했어야 한다 — 인터페이스 분리 원칙이다.
Ⅴ. 기대효과 및 결론
| 지표 | LSP 위반 유지 | LSP 준수 |
|---|---|---|
| instanceof 체크 코드 수 | 많음 | 없음 |
| 다형성 적용 가능 범위 | 제한적 | 전체 계층 |
| 새 서브타입 추가 비용 | 높음 (기존 체크 코드 수정) | 낮음 (새 클래스만) |
| 런타임 예외 발생률 | 높음 | 낮음 |
상속 거부 (Refused Bequest) 는 "is-a 관계를 착각한 설계"의 결과다. LSP (Liskov Substitution Principle) 는 다형성이 실제로 작동하기 위한 논리적 전제 조건이다. 실무에서 상속 계층을 설계할 때는 "이 자식 클래스가 부모의 모든 계약을 이행하는가?"를 먼저 검토하고, 의심스러우면 구성 (Composition) 을 선택해야 한다.
확장 방향은 ① 정적 분석 자동화, ② 아키텍처 적합성 검증, ③ 작은 단위의 상시 리팩토링 문화 정착이다.
- 📢 섹션 요약 비유: 운전면허증이 있으면 모든 차를 운전할 수 있어야 한다 — 특정 차(서브클래스)는 면허(계약)를 무시하고 싶다면 면허 제도 자체가 무의미해진다.
📌 관련 개념 맵
| 관계 | 개념 | 설명 |
|---|---|---|
| 상위 개념 | 코드 스멜 (Code Smell) | 상속 거부는 주요 스멜 |
| 상위 개념 | SOLID 원칙 | LSP는 SOLID의 'L' |
| 핵심 원칙 | LSP (Liskov Substitution Principle) | 치환 가능성 보장 |
| 연관 원칙 | OCP (Open-Closed Principle) | LSP 위반 시 함께 위반 |
| 연관 원칙 | ISP (Interface Segregation Principle) | 불필요 메서드 강제 방지 |
| 처방 | 구성 우선 (Composition over Inheritance) | 상속 대신 구성 |
| 연관 개념 | 계약 설계 (Design by Contract, DbC) | 선행/후행 조건, 불변식 |
📈 관련 키워드 및 발전 흐름도
상속 오남용 → 상속 거부와 LSP 위반 → 조합 중심 설계
👶 어린이를 위한 3줄 비유 설명
- 모든 새는 날 수 있다고 배웠는데, 타조는 날지 못한다 — 타조를 "날 수 있는 새" 서브클래스로 만들면 거짓말이 된다.
- "새" 카테고리에서 "날기" 기능은 선택 사항으로 분리하거나, 타조는 다른 카테고리에 넣어야 한다.
- 상속 거부 제거는 이처럼 "진짜 is-a 관계인지" 다시 확인하는 과정이다.