해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.
이번 포스팅에선 Chapter07, Chapter08 의 내용에 대해 다룹니다.
- Chapter07 캡슐화
- Chapter08 기능 이동
7,8장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.
Chapter07 캡슐화
1.레코드 캡슐화하기 |
2.컬렉션 캡슐화하기 |
3.기본형을 객체로 바꾸기 |
4.임시 변수를 질의 함수로 바꾸기 |
5.클래스 추출하기 |
6.클래스 인라인하기 |
7.위임 숨김하기 |
8.중개자 제거하기 |
9.알고리즘 교체하기 |
캡슐화의 효과
변경 영향도 최소화: 내부 구현 변경 시 클라이언트 코드 영향 감소
코드 품질 향상: 책임 분리로 응집도 높이고 결합도 낮춤
유지보수 용이성: 명확한 책임 분배로 코드 변경이 용이해짐
테스트 용이성: 작고 독립적인 단위로 테스트 작성 쉬워짐
캡슐화 기법
1.레코드 캡슐화하기
* 데이터를 객체로 감싸고 접근자를 통해서만 조작 가능하게 함
- 데이터 구조 변경 유연성 확보, 유효성 검증 추가 가능성, 관련 동작 함께 배치
//Before
const organization = { name: "Acme Gooseberries", country: "GB" };
// 직접 접근
console.log(organization.name);
organization.name = "Acme Berry Company";
//After
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(value) { this._name = value; }
get country() { return this._country; }
set country(value) { this._country = value; }
}
const organization = new Organization({
name: "Acme Gooseberries",
country: "GB"
});
// 접근자를 통한 접근
console.log(organization.name);
organization.name = "Acme Berry Company";
2.컬렉션 캡슐화하기
* 컬렉션 직접 반환 대신 복사본이나 추가/제거 메서드 제공
- 컬렉션 무결성 보호, 변경 이벤트 발생 가능, 클래스가 컬렉션 관리 책임 가짐
//Before
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get courses() { return this._courses; }
set courses(courses) { this._courses = courses; }
}
// 외부에서 컬렉션 직접 조작
const person = new Person("John");
person.courses.push(new Course("Math", true));
//After
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get courses() { return [...this._courses]; } // 복사본 반환
addCourse(course) { this._courses.push(course); }
removeCourse(course) {
const index = this._courses.indexOf(course);
if (index >= 0) this._courses.splice(index, 1);
}
}
// 메서드를 통한 안전한 조작
const person = new Person("John");
person.addCourse(new Course("Math", true));
3.기본형을 객체로 바꾸기
* 단순 문자열, 숫자 등을 전용 클래스로 변환
- 도메인 개념 명확화, 관련 동작 응집, 유효성 검증 중앙화, 비교 로직 개선
//Before
class Order {
constructor(data) {
this.priority = data.priority; // priority는 문자열: "high", "normal", "low"
}
}
// 우선순위 확인 로직이 여러 곳에 중복됨
if (order.priority === "high" || order.priority === "rush") {
// 긴급 처리
}
//After
class Priority {
constructor(value) {
this._value = value;
}
toString() { return this._value; }
get _index() {
return ["low", "normal", "high", "rush"].indexOf(this._value);
}
higherThan(other) { return this._index > other._index; }
equals(other) { return this._index === other._index; }
}
class Order {
constructor(data) {
this.priority = new Priority(data.priority);
}
get priority() { return this._priority; }
set priority(value) {
this._priority = (value instanceof Priority) ? value : new Priority(value);
}
}
// 의미있는 비교 가능
if (order.priority.higherThan(new Priority("normal"))) {
// 우선 처리
}
4.임시 변수를 질의 함수로 바꾸기
* 임시 변수를 함수로 추출하여 재사용성 높임
- 코드 중복 제거, 가독성 향상, 디버깅 용이성, 최적화 기회 제공
//Before
class Order {
constructor(quantity, price) {
this._quantity = quantity;
this._price = price;
}
getTotal() {
const basePrice = this._quantity * this._price;
const discount = Math.max(0, this._quantity - 500) * this._price * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - discount + shipping;
}
}
//After
class Order {
constructor(quantity, price) {
this._quantity = quantity;
this._price = price;
}
get basePrice() { return this._quantity * this._price; }
get discount() { return Math.max(0, this._quantity - 500) * this._price * 0.05; }
get shipping() { return Math.min(this.basePrice * 0.1, 100); }
getTotal() {
return this.basePrice - this.discount + this.shipping;
}
}
5.클래스 추출하기
* 함께 변경되는 데이터와 메서드를 별도 클래스로 분리
- 단일 책임 원칙 적용, 복잡성 감소, 재사용성 향상, 테스트 용이성
//Before
class Person {
constructor(name, areaCode, number) {
this._name = name;
this._areaCode = areaCode;
this._number = number;
}
get telephoneNumber() {
return `(${this._areaCode}) ${this._number}`;
}
// 기타 메서드들...
}
//After
class Person {
constructor(name, areaCode, number) {
this._name = name;
this._telephone = new TelephoneNumber(areaCode, number);
}
get telephoneNumber() { return this._telephone.toString(); }
}
class TelephoneNumber {
constructor(areaCode, number) {
this._areaCode = areaCode;
this._number = number;
}
toString() { return `(${this._areaCode}) ${this._number}`; }
}
6.클래스 인라인하기
* 불필요한 클래스를 상위 클래스로 통합
- 과도한 분리 해소, 불필요한 위임 제거, 코드 이해도 증가
// Before
class Person {
constructor(name) {
this._name = name;
this._telephoneNumber = new TelephoneNumber();
}
get name() { return this._name; }
get telephoneNumber() { return this._telephoneNumber.telephoneNumber; }
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
get officeNumber() { return this._telephoneNumber.number; }
set officeNumber(arg) { this._telephoneNumber.number = arg; }
}
class TelephoneNumber {
constructor() {
this._areaCode = null;
this._number = null;
}
get telephoneNumber() { return `(${this._areaCode}) ${this._number}`; }
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
}
// After
class Person {
constructor(name) {
this._name = name;
this._areaCode = null;
this._number = null;
}
get name() { return this._name; }
get telephoneNumber() { return `(${this._areaCode}) ${this._number}`; }
get officeAreaCode() { return this._areaCode; }
set officeAreaCode(arg) { this._areaCode = arg; }
get officeNumber() { return this._number; }
set officeNumber(arg) { this._number = arg; }
}
7.위임 숨김하기
* 클라이언트가 위임 객체에 직접 접근하지 않도록 중개 메서드 제공
- 의존성 감소, 내부 구현 변경에 외부 영향 최소화, API 간소화
// Before
class Person {
constructor(name) {
this._name = name;
this._department = new Department();
}
get name() { return this._name; }
get department() { return this._department; }
set department(arg) { this._department = arg; }
}
class Department {
get manager() { return this._manager; }
set manager(arg) { this._manager = arg; }
}
const manager = person.department.manager;
// After
class Person {
constructor(name) {
this._name = name;
this._department = new Department();
}
get name() { return this._name; }
get manager() { return this._department.manager; }
set department(arg) { this._department = arg; }
}
class Department {
get manager() { return this._manager; }
set manager(arg) { this._manager = arg; }
}
const manager = person.manager; // department에 직접 접근하지 않음
8.중개자 제거하기
* 단순 위임만 수행하는 중개 메서드 제거, 직접 접근 허용
- 불필요한 위임 제거로 코드 간소화, 과도한 캡슐화 방지
// Before
class Person {
constructor(name) {
this._name = name;
this._department = new Department();
}
get name() { return this._name; }
get manager() { return this._department.manager; }
get chargeCode() { return this._department.chargeCode; }
get department() { return this._department; }
set department(arg) { this._department = arg; }
}
const manager = person.manager;
const chargeCode = person.chargeCode;
// After
class Person {
constructor(name) {
this._name = name;
this._department = new Department();
}
get name() { return this._name; }
get department() { return this._department; }
set department(arg) { this._department = arg; }
}
const manager = person.department.manager;
const chargeCode = person.department.chargeCode;
9.알고리즘 교체하기
* 복잡한 알고리즘을 더 간단하고 명확한 알고리즘으로 교체
- 가독성 향상, 유지보수성 개선, 성능 최적화, 더 나은 라이브러리 활용
// Before
function foundPerson(people) {
for (let i = 0; i < people.length; i++) {
if (people[i] === "Don") {
return "Don";
}
if (people[i] === "John") {
return "John";
}
if (people[i] === "Kent") {
return "Kent";
}
}
return "";
}
// After
function foundPerson(people) {
const candidates = ["Don", "John", "Kent"];
const found = people.find(p => candidates.includes(p));
return found || "";
}
Chapter08 기능 이동
1. 함수 옮기기 |
2. 필드 옮기기 |
3. 문장을 함수로 옮기기 |
4. 문장을 호출한 곳으로 옮기기 |
5. 인라인 코드를 함수 호출로 바꾸기 |
6. 문장 슬라이드하기 |
7. 반복문 쪼개기 |
8. 반복문을 파이프라인으로 바꾸기 |
9. 죽은 코드 제거하기 |
기능 이동 효과
불필요한 코드를 제거하여 코드베이스를 간소화하고 유지보수성 및 가독성을 향상시킨다.
- 응집도 향상: 관련 코드를 함께 배치하여 모듈성 개선
- 책임 명확화: 코드의 기능과 책임 경계를 명확히 설정
- 가독성 증가: 코드의 의도를 더 명확하게 표현
- 유지보수성 개선: 코드 변경 시 영향 범위 최소화
- 테스트 용이성: 기능별로 분리된 코드는 단위 테스트하기 쉬움
기능 이동 기법
1. 함수 옮기기
* 함수를 관련 데이터가 있는 모듈이나 클래스로 이동
- 함수와 데이터 응집도 향상, 컨텍스트 접근성 개선, 코드 재사용성 증가
// Before
class Account {
get overdraftCharge() {
if (this._type.isPremium) {
const baseCharge = 10;
if (this._daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (this._daysOverdrawn - 7) * 0.85;
}
} else {
return this._daysOverdrawn * 1.75;
}
}
}
// After
class Account {
get overdraftCharge() {
return this._type.overdraftCharge(this._daysOverdrawn);
}
}
class AccountType {
overdraftCharge(daysOverdrawn) {
if (this.isPremium) {
const baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return daysOverdrawn * 1.75;
}
}
}
2. 필드 옮기기
* 필드를 더 적합한 클래스로 이동
- 데이터와 사용 코드 간 거리 최소화, 객체 간 책임 경계 명확화, 결합도 감소
// Before
class Customer {
constructor(name, discountRate) {
this._name = name;
this._discountRate = discountRate;
this._contract = new CustomerContract(new Date());
}
get discountRate() { return this._discountRate; }
}
class CustomerContract {
constructor(startDate) {
this._startDate = startDate;
}
}
// After
class Customer {
constructor(name, discountRate) {
this._name = name;
this._contract = new CustomerContract(new Date(), discountRate);
}
get discountRate() { return this._contract.discountRate; }
}
class CustomerContract {
constructor(startDate, discountRate) {
this._startDate = startDate;
this._discountRate = discountRate;
}
get discountRate() { return this._discountRate; }
set discountRate(arg) { this._discountRate = arg; }
}
3. 문장을 함수로 옮기기
* 특정 함수와 관련된 문장을 해당 함수 내로 이동
- 동일 목적 코드 통합, 중복 제거, 함수의 완전성과 응집도 향상
// Before
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>제목: ${person.photo.title}</p>`);
return result.join("\n");
}
function renderPhoto(photo) {
return `<img src="${photo.url}" />`;
}
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
return result.join("\n");
}
function renderPhoto(photo) {
return [
`<img src="${photo.url}" />`,
`<p>제목: ${photo.title}</p>`
].join("\n");
}
4. 문장을 호출한 곳으로 옮기기
* 여러 함수에서 다르게 동작하는 코드를 호출자로 이동
- 함수 역할 명확화, 맥락에 따른 동작 차별화, 함수 간 책임 경계 재조정
// Before
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(photoData(person.photo));
return result.join("\n");
}
function photoData(photo) {
return [
`<img src="${photo.url}" />`,
`<p>제목: ${photo.title}</p>`
].join("\n");
}
// After
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(`<img src="${person.photo.url}" />`);
result.push(`<p>제목: ${person.photo.title}</p>`);
return result.join("\n");
}
function photoData(photo) {
return `<img src="${photo.url}" />`;
}
5. 인라인 코드를 함수 호출로 바꾸기
* 중복된 코드를 함수로 추출하고 함수 호출로 대체
- 코드 중복 제거, 의도 명확화, 표준 라이브러리 활용으로 가독성 향상
// Before
let appliesToMass = false;
for (const s of states) {
if (s === "MA") appliesToMass = true;
}
// After
appliesToMass = states.includes("MA");
6. 문장 슬라이드하기
* 관련 코드를 함께 모으기 위해 문장 위치 이동
- 코드 응집도 향상, 변수 사용 지점 최적화, 논리적 코드 그룹화
// Before
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
const units = order.units;
let discount;
charge = chargePerUnit * units;
let discountableUnits = Math.max(0, units - pricingPlan.discountThreshold);
discount = discountableUnits * pricingPlan.discountFactor;
if (order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);
// After
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
const units = order.units;
const chargePerUnit = pricingPlan.unit;
let charge = chargePerUnit * units;
let discount = 0;
let discountableUnits = Math.max(0, units - pricingPlan.discountThreshold);
discount = discountableUnits * pricingPlan.discountFactor;
if (order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);
7. 반복문 쪼개기
* 여러 일을 수행하는 반복문을 용도별로 분리
- 단일 책임 원칙 적용, 가독성 향상, 코드 변경 용이성 확보
// Before
function calculateTotals(invoices) {
let averageAge = 0;
let totalSalary = 0;
let count = 0;
for (const invoice of invoices) {
totalSalary += invoice.salary;
averageAge += invoice.age;
count++;
}
averageAge = averageAge / count;
return { averageAge, totalSalary };
}
// After
function calculateTotals(invoices) {
const totalSalary = invoices.reduce((sum, invoice) => sum + invoice.salary, 0);
const count = invoices.length;
const averageAge = invoices.reduce((sum, invoice) => sum + invoice.age, 0) / count;
return { averageAge, totalSalary };
}
8. 반복문을 파이프라인으로 바꾸기
* 반복문을 map, filter 등의 파이프라인 연산으로 변환
- 선언적 프로그래밍으로 의도 명확화, 데이터 변환 과정 가시화, 코드 간결화
// Before
function getYoungestAgeOfAuthors(people) {
let youngest = Number.MAX_VALUE;
let totalSalary = 0;
for (const p of people) {
if (p.job === "author") {
totalSalary += p.salary;
if (p.age < youngest) youngest = p.age;
}
}
return `최연소: ${youngest}, 총급여: ${totalSalary}`;
}
// After
function getYoungestAgeOfAuthors(people) {
const authors = people.filter(p => p.job === "author");
const youngest = Math.min(...authors.map(a => a.age));
const totalSalary = authors.reduce((sum, a) => sum + a.salary, 0);
return `최연소: ${youngest}, 총급여: ${totalSalary}`;
}
9. 죽은 코드 제거하기
* 실행되지 않는 코드 식별 및 제거
- 불필요한 코드를 제거하여 코드베이스를 간소화하고 유지보수성 및 가독성을 향상시킴
// Before
function processOrder(order) {
let result;
// legacy code that's no longer used
if (false) {
const legacyValue = calculateLegacyValue();
result = legacyValue;
}
else {
const newValue = calculateNewValue();
result = newValue;
}
return result;
function calculateLegacyValue() {
// complex calculations that are no longer used
return 100;
}
function calculateNewValue() {
return 200;
}
}
// After
function processOrder(order) {
return calculateNewValue();
function calculateNewValue() {
return 200;
}
}
마무리
책에서 보여주는 예제들은 객체지향 설계의 원칙을 효과적으로 설명하기 위한 것이지만, 실제 구현은 프로젝트의 특성과 팀의 선호도에 따라 다양하게 적용될 수 있습니다.
JavaScript에서 책의 예제처럼 클래스를 활용한 캡슐화가 가능하지만, JavaScript의 본질적 특성을 고려하면 함수형 프로그래밍 패러다임이 더 자연스러울 수 있습니다. 객체 리터럴과 클로저를 활용하여도 캡슐화를 구현할 수 있으며, JavaScript의 특성에 부합하는 코드로 이어집니다.
// 클로저를 이용한 캡슐화 예시
function createPerson(name) {
let courses = []; // 비공개 상태
return {
getName: () => name,
getCourses: () => [...courses],
addCourse: course => courses.push(course)
};
}
TypeScript는 private, protected, public 같은 접근 제어자, 인터페이스와 타입 시스템, 그리고 컴파일 타임 검사 등을 통해 객체지향적 설계와 캡슐화에 더 적합한 도구를 제공합니다. 이러한 특성을 활용하면 대규모 애플리케이션에서 더 견고한 코드 구조를 만들 수 있습니다.
한편, '반복문 쪼개기', '죽은 코드 제거하기' 등의 리팩터링 기법은 프로그래밍 패러다임에 관계없이 코드 품질을 향상시키는 보편적인 방법들입니다. 이러한 기법들은 함수형, 객체지향, 절차적 프로그래밍 어디에서든 적용 가능하며, 코드의 가독성과 유지보수성을 개선할 수 있습니다.
결론적으로, 특정 프로그래밍 패러다임을 맹목적으로 따르기보다 프로젝트의 맥락과 언어가 지닌 고유한 특성을 고려하여 가장 적합한 기법을 선택하는 데 있습니다. 상황에 따라 적절한 접근법을 유연하게 채택함으로써 더 읽기 쉽고 유지보수하기 좋은 코드를 만들어낼 수 있습니다.
'개발' 카테고리의 다른 글
[리팩터링 2판] JavaScript 리팩터링 도서학습 #3 (3) | 2025.04.17 |
---|---|
[리팩터링 2판] JavaScript 리팩터링 도서학습 #2 (4) | 2025.03.29 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #1 (5) | 2025.03.23 |
모듈 페더레이션(Module Federation) 이해하기 (6) | 2025.01.26 |
댓글