리팩터링 실전 강의 교재¶
12장 — 상속 다루기¶
대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → Before/After → 절차 → 함정 → 체크리스트 → 퀴즈 전제 환경: Java 17+, Spring Boot 3.x
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 상속 계층을 위·아래로 다듬는 11가지 기법.
- 타입 코드 → 서브클래스(12.6) 와 그 반대 서브클래스 제거(12.7).
- 가장 중요한 ★★ — 서브클래스/슈퍼클래스를 위임으로(12.10·12.11) — 상속 → 합성 전환.
- 상속의 양날의 검 인식 — LSP 위반·취약한 기반 클래스 문제.
0.2 큰 그림 — 위·아래·횡¶
[ 위로 올리기 ] [ 아래로 내리기 ] [ 횡 변환 ]
12.1 메서드 올리기 12.4 메서드 내리기 12.6 타입 코드 → 서브클래스
12.2 필드 올리기 12.5 필드 내리기 12.7 서브클래스 제거 (반대)
12.3 생성자 본문 올리기 12.8 슈퍼클래스 추출
12.9 계층 합치기 12.10 서브클래스 → 위임 ★★
12.11 슈퍼클래스 → 위임 ★
비유 — 상속은 "가문"입니다.
가문의 자산(메서드·필드) 을 어느 세대로 옮길지(올리기·내리기), 멀어진 친척과 합칠지(슈퍼클래스 추출), 가문을 떠나 계약으로 협력 할지(위임으로) — 12장의 결정들.
0.3 현업에서 왜 중요한가¶
- 상속을 잘못 쓰면 취약한 기반 클래스 — 부모 변경이 자식 다 깨뜨림.
- LSP 위반은 가장 잡기 어려운 버그.
- Effective Java Item 18(상속 대신 컴포지션)·19(상속 고려 설계)·20(인터페이스 우선) 의 실전.
- 오브젝트 11장(합성과 유연한 설계)·13장(서브클래싱과 서브타이핑) 직접 적용.
12.1 메서드 올리기 (Pull Up Method)¶
한 줄 정의¶
여러 자식 클래스에 중복된 메서드 를 부모로.
// Before
public class Engineer extends Employee {
public Money monthlyPayout() { return baseSalary; }
}
public class Manager extends Employee {
public Money monthlyPayout() { return baseSalary; } // 중복
}
// After
public abstract class Employee {
public Money monthlyPayout() { return baseSalary; }
}
동기¶
- 중복 코드 (악취 3.2) 제거의 정석.
- 부모가 책임지면 새 자식이 자동 상속.
절차¶
- 자식들의 메서드 시그니처·본문 비교 (완전히 같아야).
- 다르면 통합 가능한 형태로 먼저 맞춤.
- 부모로 복사.
- 자식들에서 제거.
12.2 필드 올리기 (Pull Up Field)¶
한 줄 정의¶
같은 필드가 여러 자식에 → 부모로.
절차¶
12.1과 동일. 보통 12.2가 먼저, 그 다음 12.1.
12.3 생성자 본문 올리기 (Pull Up Constructor Body)¶
한 줄 정의¶
여러 자식 생성자에 공통 본문 → 부모 생성자에서 처리, 자식은 super(...) 호출.
12.4 메서드 내리기 (Push Down Method)¶
한 줄 정의¶
부모에 있지만 일부 자식만 쓰는 메서드 를 그 자식으로 내림.
동기¶
- 상속 포기 (악취 3.23) 처방의 한 형태.
- 부모가 자식별 차이까지 알 필요 없게 됨.
12.5 필드 내리기 (Push Down Field)¶
한 줄 정의¶
12.4의 필드 버전.
12.6 타입 코드를 서브클래스로 바꾸기 (Replace Type Code with Subclasses)¶
한 줄 정의¶
type 필드로 분기하는 클래스를 타입별 서브클래스 로.
// Before
public class Employee {
private String type; // "engineer" / "manager"
public Money payout() {
return switch (type) {
case "engineer" -> baseSalary;
case "manager" -> baseSalary.plus(bonus);
default -> throw new IllegalStateException();
};
}
}
// After
public abstract class Employee {
public abstract Money payout();
}
public class Engineer extends Employee {
public Money payout() { return baseSalary; }
}
public class Manager extends Employee {
public Money payout() { return baseSalary.plus(bonus); }
}
동기¶
- 반복되는 switch (3.12) 처방의 구조 변경 — 10.4 다형성 변환의 선행 단계.
- 새 타입 = 새 클래스.
함정¶
- 객체의 타입이 런타임에 바뀌어야 한다면 상속 부적합 → State/Strategy 패턴 (12.10 후속).
12.7 서브클래스 제거하기 (Remove Subclass)¶
한 줄 정의¶
차이가 없어진 서브클래스를 부모의 필드/메서드 로 흡수. 12.6의 반대.
// Before
public class Person {
public String genderCode() { return "X"; }
}
public class Male extends Person {
public String genderCode() { return "M"; }
}
public class Female extends Person {
public String genderCode() { return "F"; }
}
// After — 굳이 클래스로 나눌 가치 X
public class Person {
private final Gender gender; // enum
public String genderCode() { return gender.code(); }
}
동기¶
- 성의 없는 요소 (3.14)·추측성 일반화 (3.15) 처방.
- 단순 enum이 더 명료.
12.8 슈퍼클래스 추출하기 (Extract Superclass)¶
한 줄 정의¶
서로 다른 두 클래스에 비슷한 부분이 보이면 공통 부모 추출.
// Before
public class Department { ... }
public class Employee { ... }
// 둘 다 name·annualCost 가짐
// After
public abstract class Party {
protected String name;
public abstract Money annualCost();
}
public class Department extends Party { ... }
public class Employee extends Party { ... }
동기¶
- 중복 제거.
- 다형성 가능해짐 —
Party타입으로 일관 처리.
Effective Java 연결¶
Item 20: 슈퍼클래스가 인터페이스 면 더 좋음. 추상 클래스보다 우선.
12.9 계층 합치기 (Collapse Hierarchy)¶
한 줄 정의¶
부모와 자식 사이에 의미 있는 차이가 없으면 한 클래스 로.
동기¶
- 12.7과 비슷한 의도 (단계가 다름).
- 추측성 일반화 제거.
12.10 서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate) ★★¶
한 줄 정의¶
상속을 합성 으로 바꿈 — extends 대신 필드로 들고 위임.
// Before
public class Booking { ... }
public class PremiumBooking extends Booking { ... } // 일부 케이스만
// After
public class Booking {
private final PremiumDelegate premium; // null이면 일반
public boolean hasTalkback() {
return premium != null
? premium.hasTalkback()
: show.hasOwnTalkback();
}
}
public class PremiumDelegate {
public boolean hasTalkback() { return true; }
public Money price(Money basePrice) { return basePrice.plus(extension); }
}
동기¶
- 상속의 함정 회피 — LSP·취약한 기반 클래스·다중 상속 제한.
- 런타임에 동작 교체 가능 (
premium = new ...또는 null). - Effective Java Item 18 "상속 대신 컴포지션" 의 실전 변환.
절차¶
- 위임할 인터페이스 정의.
- 부모 클래스에 위임 객체 필드 추가.
- 자식 동작을 위임 객체로 옮김.
- 부모 메서드에서 "위임 있으면 위임, 없으면 기본" 분기.
- 자식 클래스 제거.
함정¶
- 위임 객체가 부모의 protected 필드에 접근해야 할 수 있음 → 시그니처 조정 필요.
- 모든 상속을 위임으로 바꿀 필요는 없음. 취약성·다중 변종 시.
오브젝트 연결¶
11장(합성과 유연한 설계) — 핸드폰 과금 사례가 정확히 12.10의 적용.
12.11 슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate) ★¶
한 줄 정의¶
자식이 부모를 extends 대신 필드로 들고 필요한 메서드만 위임.
// Before
public class Stack extends ArrayList { ... } // List의 모든 메서드 노출됨
// After
public class Stack {
private final List delegate = new ArrayList();
public void push(Object o) { delegate.add(o); }
public Object pop() { return delegate.remove(delegate.size() - 1); }
// ArrayList의 add(index, ...) 같은 부적절한 메서드는 노출 X
}
동기¶
- 불필요한 인터페이스 상속 (취약한 기반 클래스의 한 형태) 회피.
Stack extends ArrayList→ 외부가stack.add(0, x)같은 LIFO 깨뜨리는 메서드 호출 가능.- 위임은 필요한 메서드만 의도적으로 노출.
Effective Java 연결¶
Item 18 "상속 대신 컴포지션" 의 가장 자주 인용되는 사례 — java.util.Properties extends Hashtable 의 실패.
오브젝트 연결¶
11.01 "java.util.Properties와 java.util.Stack" 사례와 동일.
12장 종합 정리¶
한눈에 보는 결정 가이드¶
| 상황 | 선택 |
|---|---|
| 자식들에 중복 메서드 | 메서드 올리기(12.1) |
| 자식들에 중복 필드 | 필드 올리기(12.2) |
| 일부 자식만 쓰는 부모 메서드 | 메서드 내리기(12.4) |
type 필드 분기 |
타입 코드 → 서브클래스(12.6) |
| 차이 사라진 서브클래스 | 서브클래스 제거(12.7) |
| 두 클래스에 비슷한 부분 | 슈퍼클래스 추출(12.8) |
| 부모-자식 의미 차이 없음 | 계층 합치기(12.9) |
| 상속의 함정·런타임 교체 필요 | 서브클래스 → 위임(12.10) ★★ |
| 부모의 메서드 다 노출되면 위험 | 슈퍼클래스 → 위임(12.11) ★ |
종합 체크리스트 (코드 리뷰용)¶
- 상속 계층에 중복된 코드가 자식 여럿에 있는가
- 부모에 있지만 일부 자식만 쓰는 메서드/필드가 있는가
-
type필드로 분기하는 거대 switch가 있는가 -
extends ArrayList/extends HashMap같이 상속으로 자료구조를 확장하고 있는가 → 위임으로 전환 검토 - 자식이 부모 메서드를
UnsupportedOperationException던지며 무시하는가 → 상속 포기, 위임으로
종합 퀴즈¶
Q1. 12.10·12.11이 ★ 인 이유? — Effective Java Item 18과의 관계?
A. 상속의 가장 큰 함정(취약한 기반 클래스·LSP 위반·다중 상속 제한·런타임 교체 불가) 을 합성으로 우회. Effective Java Item 18 "상속 대신 컴포지션" 의 가장 직접적 적용 절차. 오브젝트 11장의 핵심 변환과 동일.
Q2. 타입 코드 → 서브클래스(12.6) 후 객체의 타입이 런타임에 바뀌어야 한다면?
A. 상속으로는 불가 — 한 번 정해진 자바 객체의 클래스는 못 바꿈. State 또는 Strategy 패턴 (필드로 정책 객체를 들고 교체) 이 답. 12.10 (서브클래스 → 위임) 으로 자연스럽게 연결.
Q3. java.util.Stack extends Vector 의 실패 사례는 어떤 악취·어떤 리팩터링?
A. 상속 포기 (악취 3.23) — Stack이 LIFO를 보장해야 하는데, Vector의 add(int, E) 같은 메서드가 노출되어 LIFO를 깨뜨림. 처방은 12.11 슈퍼클래스 → 위임. Properties도 같은 사례 (Hashtable의 임의 키 추가).
Q4. 메서드 올리기(12.1)와 슈퍼클래스 추출(12.8)의 차이?
A. 12.1은 이미 같은 부모를 둔 자식들 에서 중복 메서드를 부모로. 12.8은 부모-자식 관계가 없는 두 클래스 에 비슷한 부분을 보고 새 부모를 추출. 추출이 먼저, 올리기가 나중인 게 자연스러운 순서.
다음 단계 — 책 전체 마치며¶
카탈로그 활용 사이클 (4장 + 5장 + 6~12장)¶
1. 코드 본다
2. 3장 24 악취로 진단
3. 5장 카탈로그 사용법으로 후보 리팩터링 결정
4. 4장 테스트로 안전망 확인 (없으면 먼저 구축)
5. 6~12장 절차 따라 작은 단계로 적용
6. 단계마다 테스트 → 초록이면 다음
7. 한 PR = 한 리팩터링 = 한 의도
책 전체 6원칙¶
- 자가 테스트가 모든 리팩터링의 전제 (4장)
- 24 악취가 트리거 (3장)
- 이름·전제·절차가 있는 변경 (5장)
- 모든 리팩터링은 쌍 — 양방향 도구 (5장)
- CQS·불변 우선 (11·12장)
- 상속보다 합성 (12.10·12.11)
다음 책으로¶
- Effective Java — 매일 짤 때의 90 권고 (entity-effective-java)
- 오브젝트 — 책임 주도 설계의 한국어 표준 (entity-object)
- Working Effectively with Legacy Code (Feathers) — 모듈·서비스 단위 리팩터링
- Domain-Driven Design (Evans / Vernon) — 도메인 모델링
강의 교재 12장 완료. 같은 형식으로 4권 도서 ingest 패턴이 자리잡음.