[Refactoring] 8장 데이터 체계화
본 게시물은 마틴 파울러의 리팩토링: 코드 품질을 개선하는 객체지향 사고법 도서를 읽고 정리한 내용입니다.
데이터 체계화 리펙토링은 데이터 연동을 더 간편하게 해준다.
1. 필드 자체 캡슐화(Self Encapsulate Field)
필드용 읽기/쓰기(setter, getter) 메서드를 작성해서 두 메서드를 통해서만 필드에 접근하게 만든다. 이 방식을 사용하면 하위 클래스가 메서드에 해당 정보를 가져오는 방식을 재정의 하거나, 데이터 관리의 유연성을 확보 할 수 있다.
Before
class SomeClass {
constructor() {
this.low;
this.high;
}
includes(number) {
return number >= this.low && number <= this.high
}
}
After
class SomeClass {
constructor(low, high) {
this._low = low;
this._high = high;
}
get low() {
return this._low
}
set low(number){
this._low = number
}
get high() {
return this._high
}
set high(number) {
this._high = number
}
includes(number) {
return number >= this.low && number <= this.high
}
}
2. 데이터 값을 객체로 전환(Replace Data Value with Object)
프로젝트의 규모가 커질수록 처음에는 단순했던 데이터가 복잡해진다. 이에따라 데이터에 요구되는 항목이나 데이터도 늘어나게 된다. 데이터나 기능을 더 추가해야 할 때에는 해당 데이터를 객체로 만든다.
Before
class Order {
constructor(customer) {
this._customer = customer // 문자열 형식
}
get customer() {
return this._customer
}
set customer(str) {
this._customer = str
}
}
After
class Customer {
constructor(name){
this._name = name
}
get name() {
return this._name
}
}
class Order {
constructor(customerName) {
this._customer = new Customer(customerName)
}
get customerName() {
return this._customer.name
}
set customer(customerName) {
this._customer = new Customer(customerName)
}
}
3. 값을 참조로 전환(Change Value to Reference)
객체는 참조 객체와 값 객체로 분류 할 수 있다. 값 객체 클래스에 같은 인스턴스가 많이 들어있을 경우 해당 객체를 참조 객체로 전환한다.
Before
class Customer {
constructor(name){
this._name = name
}
get name() {
return this._name
}
}
class Order {
constructor(customerName) {
this._customer = new Customer(customerName)
}
get customerName() {
return this._customer.name
}
set customer(customerName) {
// 이름이 동일하더라도 다른 고객으로 판단됨(항상 new를 통해 정의하기 때문에)
this._customer = new Customer(customerName)
}
}
After
class Customer {
static create(name) {
return new Customer(name)
}
constructor(name){
this._name = name
}
get name() {
return this._name
}
}
class Order {
constructor(customerName) {
this._customer = Customer.create(customerName)
}
get customerName() {
return this._customer.name
}
set customer(customerName) {
this._customer = new Customer(customerName)
}
}
4. 참조를 값으로 전환(Change Reference to Value)
참조 객체를 사용한 작업이 복잡해질 경우 값 객체로 전환한다. 값 객체는 변경 할 수 없어야 한다는 주요 특성이 있다.
Before
class Currency {
constructor(code) {
this._code = code
}
get code() {
return this._code
}
}
// 참조 객체는 객체가 저장되어 있는 메모리 주소로 비교한다.
new Currency('USD') === new Currency('USD') // false
After
class Currency {
constructor(code) {
this._code = code
}
equals(currency) {
if (!(currency instanceof Currency)) return false
return this.code === currency.code
}
get code() {
return this._code
}
}
// 값 객체는 객체가 가지고 있는 값을 이용해 비교한다.
new Currency('USD').equals(new Currency('USD')) // true
5. 배열을 객체로 전환(Replace Array with Object)
배열은 비슷한 값 또는 객체의 컬렉션 용도로 사용해야 한다. 만약 배열을 구성하는 원소가 각 인덱스마다 특별한 의미를 가질 땐 그 배열을 객체로 전환한다.
Before
const row = ['armadillo', 15] // 0: 이름, 1: 승 수
After
class Performance {
constructor(name, wins) {
this.name = name
this.wins = wins
}
}
const row = new Performance('armadillo', 15)
6. 관측 데이터 복제(Duplicate Observed Data)
도메인 메서드가 데이터에 접근해야 할 땐, 데이터를 도메인 객체로 복사하고, 양층의 데이터를 동기화하는 observer를 작성해서 비즈니스 로직 처리 코드와 사용자 인터페이스 처리 코드를 분리한다.
7. 클래스의 단방향 연결을 양방향으로 전환(Change Unidirectional Association to Bidirectional)
두 클래스가 서로의 기능을 사용해야 하는데 한 방향으로만 연결되어 있을 땐, 역포인터를 추가하고 서로 업데이트 할 수 있게 접근 한정자(private, protected, public)를 수정한다. 이 리펙토링 기법은 그 방법이 까다로워 반드시 테스트를 거쳐야 한다.
Before
class Order {
constructor() {
this._customer = null
}
get customer() {
return this._customer
}
set customer(customer) {
this._customer = customer
}
}
// Order 클래스를 참조하는 코드가 존재하지 않음
class Customer {
...
}
After
class Order {
...
get customer() {
this._customer
}
set customer(customer) {
if (this._customer !== null) {
this._customer.friendOrders().delete(this)
}
this._customer = customer
if (this._customer !== null) {
this._customer.friendOrders().add(this)
}
}
}
//Customer 클래스에서도 Order 클래스를 참조
class Customer {
constructor() {
this._orders = new Set()
}
friendOrders() {
return this._orders
}
addOrder(order){
this._orders.add(order)
}
removeOrder(order) {
this._orders.delete(order)
}
}
8. 클래스의 양방향 연결을 단방향으로 전환(Change Bidirectional Association to Unidirectional)
두 클래스가 양방향으로 연결되어 있는데, 한 클래스가 다른 클래스의 기능을 더 이상 사용하지 않을 땐 불필요한 방향의 연결을 끊는다. 이를 통해 전체 복잡도와 결합도를 줄인다.
Before
// Customer 클래스가 존재해야지만 있을 수 있음
class Order {
...
get customer() {
return this._customer
}
set customer(customer) {
if (this._customer !== null) {
this._customer.friendOrders().delete(this)
}
this._customer = customer
if (this._customer !== null) {
this._customer.friendOrders().add(this)
}
}
get discountPrice() {
return this.getGrossPrice() * (1 - this._customer.getDiscount())
}
}
class Customer {
constructor() {
this._orders = new Set()
}
getPriceFor(order) {
return order.getDiscountPrice()
}
...
}
After
// _this.customer 삭제
class Order {
...
get discountPrice(customer) {
return this.getGrossPrice() * (1 - customer.getDiscount())
}
}
class Customer {
...
getPriceFor(order) {
return order.getDiscountPrice(this)
}
...
}
9. 마법 숫자를 기호 상수로 전환(Replace Magic Number with Symbolic Constant)
특별한 의미를 가진 리터럴 숫자가 있을 땐, 의미를 살린 이름의 상수로 작성한다.
Before
function potentialEnergy(mass, height) {
return mass * 9.81 * height
}
After
const GRAVITATIONAL_CONSTANT = 9.81
function potentialEnergy(mass, height) {
return mass * GRAVITATIONAL_CONSTANT * height
}
10. 필드 캡슐화(Encapsulate Field)
데이터를 public
타입으로 만들면 데이터가 있는 객체가 모르는 사이에 다른 객체가 데이터 값을 읽고 변경할 수 있다. public
필드가 있을 땐 그 필드를 private
로 전환하고 필드용 읽기, 쓰기 메서드를 작성한다.
Before
//Typescript
public _name: string
After
priviate _name:string
public get name(): string { return this._name }
public set name(value: string) { this._name = value }
11. 컬렉션 캡슐화(Encapsulate Collection)
메서드가 컬렉션을 반환할 땐 그 메서드가 읽기전용 뷰를 반환하게 수정하고 추가 메서드와 삭제 메서드를 작성한다.
Before
class Person {
constructor () {
this._courses = []
}
getCourses() {
return this._courses
}
setCourses(courses) {
this._courses = courses
}
}
After
class Person {
...
getCourses() {
return JSON.parse(JSON.stringify(this._courses))
}
addCourse(course) {
this._courses.push(course)
}
removeCourse(course) {
this._courses.splice(this._courses.indexof(course), 1)
}
}
12. 레코드를 데이터 클래스로 전환(Replace Record with Data Class)
레코드 구조를 이용한 인터페이스를 제공해야 할 땐 레코드 구조를 저장할 덤 데이터 객체를 작성하자.
13. 분류 부호를 클래스로 전환
기능에 영향을 미치는 숫자형 분류 부호가 든 클래스가 있을 땐 새 클래스로 바꾸자. 분류 부호를 사용하는 메서드들은 상징적인 이름이 아닌 숫자만을 인자로 받는다. 때문에 코드를 이해하기 힘들어지게 된다.
Before
// typescript
class Person {
public static readonly O: number = 0
public static readonly A: number = 1
public static readonly B: number = 2
public static readonly AB: number = 3
private _bloodGroup: number
constructor(bloodGroup: number) {
this._bloodGroup = bloodGroup
}
}
After
class BloodGroup {
public static readonly O: BloodGroup = new BloodGroup(0)
public static readonly A:BloodGroup = new BloodGroup(1)
public static readonly B:BloodGroup = new BloodGroup(2)
public static readonly AB:BloodGroup = new BloodGroup(3)
public static readonly _values:BloodGroup[] = [
BloodGroup.O,
BloodGroup.A,
BloodGroup.B,
BloodGroup.AB
]
private _code:number
constructor (code) {
this._code = code
}
private getCode (): number {
return this._code
}
private static code(index: number) {
return this._values[index]
}
}
class Person {
...
constructor(bloodGroup: BloodGroup) {
this._bloodGroup = bloodGroup
}
get bloodGroup() {
return this._bloodGroup
}
set bloodGroup(bloodGroup: BloodGroup) {
this._bloodGroup = bloodGroup
}
}
14. 분류 부호를 하위 클래스로 전환(Replace Type Code with Subclasses)
클래스 기능에 영향을 주는 변경 불가 분류 부호가 있을 땐 분류 부호를 하위 클래스로 만든다. 기능에 영향을 주지 않을 경우에는 분류 부호를 클래스로 전환 기법을 실시한다.
Before
// typescript
class Employee {
private _type:number
static readonly ENGINEER = 0
static readonly SALESMAN = 1
static readonly MANAGER = 2
consturcotr(type: number) {
this._type = type
}
...
}
After
class Employee {
private _type:number
static readonly ENGINEER = 0
static readonly SALESMAN = 1
static readonly MANAGER = 2
private consturcotr(type: number) {
this._type = type
}
create (type: number): Employee {
switch(type) {
case Employee.ENGINEER:
return new Engineer()
case Employee.SALESMAN:
return new Salesman()
case Employee.MANAGER:
return new Manager()
default:
throw new Error('허용되지 않는 분류 부호')
}
}
get type {
return this._type
}
...
}
class Engineer extends Employee {
get type(): number {
return Employee.ENGINEER
}
}
class Salesman extends Employee {
get type(): number {
return Employee.SALESMAN
}
}
class Manager extends Employee {
get type(): number {
return Employee.MANAGER
}
}
15. 분류 부호를 상태/전략 패턴으로 전환(Replace Type Code with State/Strategy)
분류 분호가 클래스의 기능에 영향을 주지만, 하위 클래스로 전환할 수 없을 땐 그 분류 부호를 상태 객체로 만든다. 이 방법은 분류 부호가 객체 수명주기 동안 변할 때나 다른 이유로 하위 클래스를 만들 수 없을 때 사용한다.
Before
// typescript
class Employee {
private _type:number
static readonly ENGINEER = 0
static readonly SALESMAN = 1
static readonly MANAGER = 2
consturcotr(type: number) {
this._type = type
}
payAmount() {
switch(this._type) {
case Employee.ENGINEER:
return this._monthlySalary
case Employee.SALESMAN:
return this._monthlySalary + this._commission
case Employee.MANAGER:
return this._monthlySalary + this._bonus
default:
throw new Error('허용되지 않는 분류 부호')
}
}
...
}
After
class Employee {
...
private _type: EmployeeType
payAmount() {
switch(this.type) {
case EmployeeType.ENGINEER:
return this._monthlySalary
case EmployeeType.SALESMAN:
return this._monthlySalary + this._commission
case EmployeeType.MANAGER:
return this._monthlySalary + this._bonus
default:
throw new Error('허용되지 않는 분류 부호')
}
}
get type () {
return this._type.getTypeCode()
}
set type(type) {
this._type = EmployeeType.newType(type)
}
}
class EmployeeType {
static readonly ENGINEER = 0
static readonly SALESMAN = 1
static readonly MANAGER = 2
newType(code: number) {
switch(code) {
case EmployeeType.ENGINEER:
return new Engineer()
case EmployeeType.SALESMAN:
return new Salesman()
case EmployeeType.MANAGER:
return new Manager()
default:
throw new Error('허용되지 않는 분류 부호')
}
}
}
class Engineer extends Employee {
get typeCode() {
return EmployeeType.ENGINEER
}
}
class Salesman extends Employee {
get typeCode() {
return EmployeeType.SALESMAN
}
}
class Manager extends Employee {
get typeCode() {
return EmployeeType.MANAGER
}
}
16. 하위클래스를 필드로 전환(Replace Subclasses with Fields)
여러 하위클래스가 상수 데이터를 반환하는 메서드만 다를 땐 각 하위클래스의 메서드를 상위 클래스 필드로 전환하고 하위 클래스는 전부 삭제한다.
Before
// typescript
abstract class Person {
abstract isMale(): boolean
abstract getCode(): string
}
class Male extends Person {
isMale(): boolean {
return true
}
getCode(): string {
return 'M'
}
}
class Famale extends Person {
isMale(): boolean {
return false
}
getCode(): string {
return 'F'
}
}
After
class Person {
constructor (isMale, code) {
this._isMale = isMale
this._code = code
}
createMale() {
return new Person(true, 'M')
}
createFamle() {
return new Person(false, 'F')
}
isMale() {
return this._isMale
}
getCode() {
return this._code
}
}
Comments