해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.
이번 포스팅에선 Chapter11 의 내용에 대해 다룹니다.
- Chapter12 상속 다루기
12장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.
Chapter12 상속 다루기
1.메서드 올리기 |
2.필드 올리기 |
3.생성자 본문 올리기 |
4.메서드 내리기 |
5.필드 내리기 |
6.타입 코드를 서브클래스로 바꾸기 |
7.서브 클래스 제거하기 |
8.슈퍼클래스 추출하기 |
9.계층 합치기 |
10.서브클래스를 위임으로 바꾸기 |
11.슈퍼클래스를 위임으로 바꾸기 |
상속 다루기의 효과
상속은 강력한 객체지향 메커니즘이지만, 잘못 사용하면 복잡성을 증가시킵니다.
상속 리팩터링의 주요 효과
- 계층구조 명확화: 클래스 간의 관계와 책임이 명확해집니다
- 코드 중복 제거: 공통 로직을 상위 클래스로 올려 중복을 줄입니다
- 유지보수성 향상: 변경 사항이 적절한 레벨에서 처리됩니다
- 확장성 개선: 새로운 서브클래스 추가가 용이해집니다
- 캡슐화 강화: 각 클래스의 책임이 명확해져 응집도가 높아집니다
- 다형성 활용: 타입별 동작을 효과적으로 구현할 수 있습니다
상속 다루기 기법
1. 메서드 올리기
- 서브클래스들에 중복된 메서드를 슈퍼클래스로 이동
- 코드 중복 제거, 수정 포인트 단일화, 일관성 보장
// Before
class Employee { /* 기본 정보 */ }
class Salesman extends Employee {
get name() { return this._name; }
getAnnualCost() {
return this.monthlyCost * 12;
}
}
class Engineer extends Employee {
get name() { return this._name; }
getAnnualCost() {
return this.monthlyCost * 12;
}
}
// After
class Employee {
get name() { return this._name; }
getAnnualCost() {
return this.monthlyCost * 12;
}
}
class Salesman extends Employee {
// 중복 메서드 제거됨
// 고유 로직만 유지
}
class Engineer extends Employee {
// 중복 메서드 제거됨
// 고유 로직만 유지
}
2. 필드 올리기
- 서브클래스들이 공통으로 사용하는 필드를 슈퍼클래스로 이동
- 데이터 중복 제거, 일관된 초기화, 공통 필드 관리 용이성
// Before
class Employee { /* 기본 정보 */ }
class Salesman extends Employee {
constructor(name, id, monthlyCost) {
super();
this._name = name;
this._id = id;
this._monthlyCost = monthlyCost;
}
}
class Engineer extends Employee {
constructor(name, id, monthlyCost) {
super();
this._name = name;
this._id = id;
this._monthlyCost = monthlyCost;
}
}
// After
class Employee {
constructor(name, id, monthlyCost) {
this._name = name;
this._id = id;
this._monthlyCost = monthlyCost;
}
}
class Salesman extends Employee {
constructor(name, id, monthlyCost) {
super(name, id, monthlyCost);
// Salesman 고유 초기화만 수행
}
}
class Engineer extends Employee {
constructor(name, id, monthlyCost) {
super(name, id, monthlyCost);
// Engineer 고유 초기화만 수행
}
}
3. 생성자 본문 올리기
- 서브클래스 생성자들의 공통 코드를 슈퍼클래스로 이동
- 초기화 로직 통합, 일관성 보장, 중복 제거
// Before
class Party {
constructor(name) {
this._name = name;
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
}
class Department extends Party {
constructor(name, staff) {
super(name);
this._staff = staff;
}
}
// After
class Party {
constructor(name) {
this._name = name;
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
}
class Department extends Party {
constructor(name, staff) {
super(name);
this._staff = staff;
}
}
// 더 복잡한 경우 - 공통 초기화 코드가 있을 때
// Before
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
this.initializePerformanceTracking(); // 공통 로직
}
initializePerformanceTracking() {
this._performanceData = [];
this._lastReviewDate = new Date();
}
}
class Manager extends Party {
constructor(name, id, monthlyCost, teamSize) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
this._teamSize = teamSize;
this.initializePerformanceTracking(); // 중복된 공통 로직
}
initializePerformanceTracking() {
this._performanceData = [];
this._lastReviewDate = new Date();
}
}
// After
class Party {
constructor(name, id, monthlyCost) {
this._name = name;
if (id !== undefined) this._id = id;
if (monthlyCost !== undefined) this._monthlyCost = monthlyCost;
this.initializePerformanceTracking();
}
initializePerformanceTracking() {
this._performanceData = [];
this._lastReviewDate = new Date();
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name, id, monthlyCost);
// Employee 고유 로직만
}
}
class Manager extends Party {
constructor(name, id, monthlyCost, teamSize) {
super(name, id, monthlyCost);
this._teamSize = teamSize;
// Manager 고유 로직만
}
}
4. 메서드 내리기
- 슈퍼클래스의 메서드가 일부 서브클래스에서만 사용될 때 해당 서브클래스로 이동
- 불필요한 인터페이스 제거, 클래스 역할 명확화, 응집도 향상
// Before
class Employee {
get quota() { return this._quota; }
set quota(value) { this._quota = value; }
// 모든 직원이 quota를 가지는 것은 아님
}
class Engineer extends Employee {
// Engineer는 quota를 사용하지 않음
}
class Salesman extends Employee {
// Salesman만 quota를 사용함
}
// After
class Employee {
// quota 관련 메서드 제거
}
class Engineer extends Employee {
// quota 없음
}
class Salesman extends Employee {
get quota() { return this._quota; }
set quota(value) { this._quota = value; }
// Salesman에게만 필요한 quota 관련 로직
}
5. 필드 내리기
- 슈퍼클래스의 필드가 일부 서브클래스에서만 사용될 때 해당 서브클래스로 이동
- 메모리 효율성, 클래스 책임 명확화, 불필요한 속성 제거
// Before
class Employee {
constructor(name, quota) {
this._name = name;
this._quota = quota; // 모든 직원이 quota를 가지는 것은 아님
}
}
class Engineer extends Employee {
constructor(name) {
super(name, null); // quota 사용하지 않지만 null로 전달
}
}
class Salesman extends Employee {
constructor(name, quota) {
super(name, quota);
}
}
// After
class Employee {
constructor(name) {
this._name = name;
}
}
class Engineer extends Employee {
constructor(name) {
super(name);
// quota 없음
}
}
class Salesman extends Employee {
constructor(name, quota) {
super(name);
this._quota = quota; // Salesman만 quota 필드 소유
}
}
6. 타입 코드를 서브클래스로 바꾸기
- 타입을 구분하는 코드 대신 서브클래스를 사용하여 다형성 구현
- 타입별 동작 캡슐화, 조건문 제거, 확장성 향상
// Before
class Employee {
constructor(name, type) {
this._name = name;
this._type = type;
}
get type() { return this._type; }
toString() {
return `${this._name} (${this._type})`;
}
getBonus() {
switch (this._type) {
case "engineer":
return this.getSalary() * 0.1;
case "manager":
return this.getSalary() * 0.2;
case "salesman":
return this.getSalary() * 0.15 + this.getSalesBonus();
default:
return 0;
}
}
}
// 사용
const emp1 = new Employee("John", "engineer");
const emp2 = new Employee("Jane", "manager");
// After
class Employee {
constructor(name) {
this._name = name;
}
toString() {
return `${this._name} (${this.type})`;
}
// 추상 메서드
getBonus() {
throw new Error("서브클래스에서 구현해야 합니다");
}
}
class Engineer extends Employee {
get type() { return "engineer"; }
getBonus() {
return this.getSalary() * 0.1;
}
}
class Manager extends Employee {
get type() { return "manager"; }
getBonus() {
return this.getSalary() * 0.2;
}
}
class Salesman extends Employee {
get type() { return "salesman"; }
getBonus() {
return this.getSalary() * 0.15 + this.getSalesBonus();
}
getSalesBonus() {
// 영업 보너스 계산 로직
return 1000;
}
}
// 팩터리 함수로 생성
function createEmployee(name, type) {
switch (type) {
case "engineer":
return new Engineer(name);
case "manager":
return new Manager(name);
case "salesman":
return new Salesman(name);
default:
throw new Error(`Unknown employee type: ${type}`);
}
}
// 사용
const emp1 = createEmployee("John", "engineer");
const emp2 = createEmployee("Jane", "manager");
7. 서브클래스 제거하기
- 더 이상 쓸모없어진 서브클래스를 제거하여 계층구조 단순화
- 불필요한 복잡성 제거, 유지보수 부담 감소
// Before
class Person {
constructor(name) {
this._name = name;
}
get name() { return this._name; }
get genderCode() { return "X"; }
}
class Male extends Person {
get genderCode() { return "M"; }
}
class Female extends Person {
get genderCode() { return "F"; }
}
// 사용이 단순하여 서브클래스가 불필요
// After
class Person {
constructor(name, genderCode) {
this._name = name;
this._genderCode = genderCode || "X";
}
get name() { return this._name; }
get genderCode() { return this._genderCode; }
}
// 사용
const john = new Person("John", "M");
const jane = new Person("Jane", "F");
8. 슈퍼클래스 추출하기
- 비슷한 기능을 하는 두 클래스에서 공통 부분을 뽑아 슈퍼클래스 생성
- 코드 중복 제거, 공통 기능 재사용, 일관성 확보
// Before
class Employee {
constructor(name, id, monthlyCost) {
this._id = id;
this._name = name;
this._monthlyCost = monthlyCost;
}
get monthlyCost() { return this._monthlyCost; }
get name() { return this._name; }
get id() { return this._id; }
get annualCost() { return this.monthlyCost * 12; }
}
class Department {
constructor(name, staff) {
this._name = name;
this._staff = staff;
}
get staff() { return this._staff.slice(); }
get name() { return this._name; }
get monthlyCost() {
return this.staff
.map(e => e.monthlyCost)
.reduce((sum, cost) => sum + cost);
}
get annualCost() { return this.monthlyCost * 12; }
}
// After - 공통 슈퍼클래스 추출
class Party {
constructor(name) {
this._name = name;
}
get name() { return this._name; }
get annualCost() { return this.monthlyCost * 12; }
// 추상 메서드
get monthlyCost() {
throw new Error("서브클래스에서 구현해야 합니다");
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
get id() { return this._id; }
get monthlyCost() { return this._monthlyCost; }
}
class Department extends Party {
constructor(name, staff) {
super(name);
this._staff = staff;
}
get staff() { return this._staff.slice(); }
get monthlyCost() {
return this.staff
.map(e => e.monthlyCost)
.reduce((sum, cost) => sum + cost);
}
}
9. 계층 합치기
- 슈퍼클래스와 서브클래스가 거의 비슷할 때 하나로 합치기
- 불필요한 복잡성 제거, 계층구조 단순화
// Before
class Employee {
constructor(name, id) {
this._name = name;
this._id = id;
}
get name() { return this._name; }
get id() { return this._id; }
}
class Salesman extends Employee {
constructor(name, id) {
super(name, id);
}
// 추가 로직이 거의 없음
}
// After - 계층 합치기
class Employee {
constructor(name, id) {
this._name = name;
this._id = id;
}
get name() { return this._name; }
get id() { return this._id; }
// 필요시 타입 구분 필드 추가
// this._type = type;
}
10. 서브클래스를 위임으로 바꾸기
- 상속 관계를 합성(위임) 관계로 변경
- 유연성 증대, 다중 상속 문제 해결, 런타임 동작 변경 가능
// Before - 상속 사용
class Booking {
constructor(show, date) {
this._show = show;
this._date = date;
}
get hasTalkback() { return false; }
get basePrice() { return this._show.price; }
}
class PremiumBooking extends Booking {
constructor(show, date, extras) {
super(show, date);
this._extras = extras;
}
get hasTalkback() { return this._show.hasOwnProperty('talkback'); }
get basePrice() {
return Math.round(super.basePrice + this._extras.premiumFee);
}
get hasDinner() {
return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
}
}
// After - 위임 사용
class Booking {
constructor(show, date) {
this._show = show;
this._date = date;
}
get hasTalkback() {
return this._premiumDelegate
? this._premiumDelegate.hasTalkback
: false;
}
get basePrice() {
let result = this._show.price;
if (this._premiumDelegate) {
result += this._premiumDelegate.extrasBasePrice;
}
return result;
}
get hasDinner() {
return this._premiumDelegate
? this._premiumDelegate.hasDinner
: undefined;
}
_bePremium(extras) {
this._premiumDelegate = new PremiumBookingDelegate(this, extras);
}
}
class PremiumBookingDelegate {
constructor(hostBooking, extras) {
this._host = hostBooking;
this._extras = extras;
}
get hasTalkback() {
return this._host._show.hasOwnProperty('talkback');
}
get extrasBasePrice() {
return Math.round(this._extras.premiumFee);
}
get hasDinner() {
return this._extras.hasOwnProperty('dinner') && !this._host.isPeakDay;
}
}
// 팩터리 함수
function createBooking(show, date) {
return new Booking(show, date);
}
function createPremiumBooking(show, date, extras) {
const result = new Booking(show, date);
result._bePremium(extras);
return result;
}
11. 슈퍼클래스를 위임으로 바꾸기
- 상속이 적절하지 않을 때 합성으로 변경
- IS-A 관계가 아닌 HAS-A 관계일 때, 불필요한 인터페이스 노출 방지
// Before - 부적절한 상속
class List extends Array {
constructor() {
super();
}
last() {
return this[this.length - 1];
}
}
// 문제: Array의 모든 메서드가 노출됨 (push, pop, shift, unshift 등)
// List가 Array의 모든 기능을 제공해야 하는가?
// After - 위임 사용
class List {
constructor() {
this._storage = [];
}
get length() { return this._storage.length; }
last() {
return this._storage[this._storage.length - 1];
}
add(element) {
this._storage.push(element);
}
get(index) {
return this._storage[index];
}
// 필요한 기능만 선택적으로 노출
forEach(callback) {
this._storage.forEach(callback);
}
map(callback) {
return this._storage.map(callback);
}
filter(callback) {
return new List().initWith(this._storage.filter(callback));
}
initWith(array) {
this._storage = [...array];
return this;
}
}
// 사용
const myList = new List();
myList.add(1);
myList.add(2);
console.log(myList.last()); // 2
// myList.push() - 에러! 의도적으로 제한된 인터페이스
마무리
상속 다루기를 학습하면서 프론트엔드 개발자로서 가장 큰 의구심이 들었던 부분은, 과연 이런 복잡한 클래스 구조를 실제 프론트엔드 개발에서 얼마나 활용할 수 있을까 하는 점이었습니다. React, Vue 같은 현대 프론트엔드 프레임워크에서는 컴포넌트 기반 개발이 주류이고, 함수형 프로그래밍 패러다임이 더 선호되는 상황에서 클래스 상속의 필요성에 대해 진지하게 고민해보게 되었습니다.
실제로 프론트엔드에서는 다음과 같은 상황들이 더 일반적입니다
전통적인 클래스 상속 방식
// 공통 기능을 가진 베이스 컴포넌트
class BaseComponent {
constructor() {
this.state = { loading: false };
}
handleClick() {
console.log('Button clicked');
this.trackEvent('button_click');
}
trackEvent(eventName) {
// 공통 분석 로직
analytics.track(eventName, { timestamp: Date.now() });
}
showLoading() {
this.state.loading = true;
}
}
class Button extends BaseComponent {
constructor(text, type = 'primary') {
super();
this.text = text;
this.type = type;
}
render() {
return `
<button
class="btn btn-${this.type} ${this.state.loading ? 'loading' : ''}"
onclick="this.handleClick()"
>
${this.state.loading ? 'Loading...' : this.text}
</button>
`;
}
}
class SubmitButton extends BaseComponent {
constructor(formId) {
super();
this.formId = formId;
}
handleClick() {
super.handleClick(); // 부모의 클릭 로직 실행
this.submitForm();
}
submitForm() {
this.showLoading();
// 폼 제출 로직
document.getElementById(this.formId).submit();
}
render() {
return `
<button
class="btn btn-submit ${this.state.loading ? 'loading' : ''}"
onclick="this.handleClick()"
>
${this.state.loading ? 'Submitting...' : 'Submit'}
</button>
`;
}
}
React 함수형 컴포넌트 + Hooks
// 커스텀 훅으로 공통 로직 분리
const useAnalytics = () => {
const trackEvent = useCallback((eventName, data = {}) => {
analytics.track(eventName, { ...data, timestamp: Date.now() });
}, []);
return { trackEvent };
};
const useLoadingState = () => {
const [loading, setLoading] = useState(false);
const showLoading = useCallback(() => setLoading(true), []);
const hideLoading = useCallback(() => setLoading(false), []);
return { loading, showLoading, hideLoading };
};
// 컴포넌트들이 필요한 기능만 조합해서 사용
const Button = ({ text, type = 'primary', onClick }) => {
const { trackEvent } = useAnalytics();
const { loading, showLoading } = useLoadingState();
const handleClick = useCallback(() => {
console.log('Button clicked');
trackEvent('button_click');
if (onClick) {
showLoading();
onClick();
}
}, [trackEvent, showLoading, onClick]);
return (
<button
className={`btn btn-${type} ${loading ? 'loading' : ''}`}
onClick={handleClick}
disabled={loading}
>
{loading ? 'Loading...' : text}
</button>
);
};
const SubmitButton = ({ formRef }) => {
const { trackEvent } = useAnalytics();
const { loading, showLoading, hideLoading } = useLoadingState();
const handleSubmit = useCallback(async () => {
console.log('Submit button clicked');
trackEvent('form_submit');
if (formRef.current) {
showLoading();
try {
await submitForm(formRef.current);
} finally {
hideLoading();
}
}
}, [trackEvent, showLoading, hideLoading, formRef]);
return (
<Button
text="Submit"
type="submit"
onClick={handleSubmit}
loading={loading}
/>
);
};
// Higher-Order Component 패턴도 가능
const withAnalytics = (WrappedComponent) => {
return (props) => {
const { trackEvent } = useAnalytics();
return <WrappedComponent {...props} trackEvent={trackEvent} />;
};
};
Vue 3 Composition API
<!-- 공통 로직을 컴포저블로 분리 -->
<script>
// composables/useAnalytics.js
import { ref } from 'vue'
export function useAnalytics() {
const trackEvent = (eventName, data = {}) => {
analytics.track(eventName, { ...data, timestamp: Date.now() })
}
return { trackEvent }
}
// composables/useLoadingState.js
export function useLoadingState() {
const loading = ref(false)
const showLoading = () => {
loading.value = true
}
const hideLoading = () => {
loading.value = false
}
return { loading, showLoading, hideLoading }
}
</script>
<!-- Button.vue -->
<template>
<button
:class="[
'btn',
`btn-${type}`,
{ loading: loading }
]"
@click="handleClick"
:disabled="loading"
>
{{ loading ? 'Loading...' : text }}
</button>
</template>
<script setup>
import { useAnalytics } from '@/composables/useAnalytics'
import { useLoadingState } from '@/composables/useLoadingState'
const props = defineProps({
text: String,
type: { type: String, default: 'primary' }
})
const emit = defineEmits(['click'])
const { trackEvent } = useAnalytics()
const { loading, showLoading } = useLoadingState()
const handleClick = () => {
console.log('Button clicked')
trackEvent('button_click')
showLoading()
emit('click')
}
</script>
<!-- SubmitButton.vue -->
<template>
<Button
text="Submit"
type="submit"
@click="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
import Button from './Button.vue'
import { useAnalytics } from '@/composables/useAnalytics'
import { useLoadingState } from '@/composables/useLoadingState'
const props = defineProps({
formData: Object
})
const { trackEvent } = useAnalytics()
const { loading, showLoading, hideLoading } = useLoadingState()
const handleSubmit = async () => {
console.log('Submit button clicked')
trackEvent('form_submit')
showLoading()
try {
await submitForm(props.formData)
} finally {
hideLoading()
}
}
</script>
프론트엔드에서 상태 관리를 할 때도 클래스보다는 함수형 접근법이 더 직관적인 경우가 많습니다
// 클래스 기반 상태 관리 - 복잡하고 무거움
class UserManager {
constructor() {
this.users = [];
this.currentUser = null;
}
addUser(user) {
this.users.push(user);
}
setCurrentUser(userId) {
this.currentUser = this.users.find(u => u.id === userId);
}
}
// 함수형 접근 - 더 가볍고 예측 가능
const useUserManager = () => {
const [users, setUsers] = useState([]);
const [currentUser, setCurrentUser] = useState(null);
const addUser = useCallback((user) => {
setUsers(prev => [...prev, user]);
}, []);
const selectUser = useCallback((userId) => {
setCurrentUser(users.find(u => u.id === userId));
}, [users]);
return { users, currentUser, addUser, selectUser };
};
결국 프론트엔드 개발자로서 내린 결론은, 클래스 상속 패턴을 맹목적으로 따를 필요는 없지만, 이런 구조적 사고방식과 리팩터링 원칙들은 여전히 가치가 있다는 것입니다. React 컴포넌트를 설계할 때도, 커스텀 훅을 만들 때도, 상태 관리 로직을 구성할 때도 "책임의 분리", "공통 로직의 추출", "인터페이스의 일관성" 같은 원칙들은 동일하게 적용되니까요.
다만 프론트엔드에서는 상속보다는 컴포지션, 클래스보다는 함수, 명령형보다는 선언형 접근을 우선 고려하되, 정말 필요한 경우에만 복잡한 클래스 구조를 도입하는 것이 현명한 선택인 것 같습니다. 무엇보다 팀원들과 함께 유지보수해야 하는 코드라는 점을 항상 염두에 두고, 과도한 추상화보다는 명확하고 이해하기 쉬운 구조를 만드는 것이 더 중요하다고 생각합니다.
'개발' 카테고리의 다른 글
[리팩터링 2판] JavaScript 리팩터링 도서학습 #7 (5) | 2025.05.19 |
---|---|
[리팩터링 2판] JavaScript 리팩터링 도서학습 #6 (6) | 2025.05.17 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #5 (7) | 2025.05.15 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #4 (4) | 2025.04.24 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #3 (5) | 2025.04.17 |
댓글