데이터 조직화

2025. 5. 15. 00:57·Programming/Refactoring

데이터 구조는 프로그램에서 중요한 역할을 수행하니 데이터 구조에 집중한 리팩터링만을 이 챕터에서 묶어 다룬다.

 

변수 쪼개기 (Split Variable)

배경

변수는 다양한 용도로 사용된다. 때로는 반복문에서 루프 인덱스로, 때로는 계산 중간 결과를 저장하기 위해 사용된다.
루프 변수처럼 값을 여러 번 대입해야 하는 상황도 있지만, 중간 결과를 저장하려는 변수라면 값은 한 번만 대입되어야 한다.
그런데 하나의 변수에 여러 번 값을 대입하고, 그 값들이 서로 다른 의미를 가진다면 이는 코드의 명확성을 해치는 신호다.
이럴 경우 변수의 역할을 분리하여 각각을 명확히 드러내야 한다. 하나의 이름으로 여러 의미를 표현하는 변수는 코드를 읽는 사람에게 혼란을 준다.

절차

  1. 변수를 선언한 곳과 처음 값을 대입하는 곳에서 변수 이름을 바꾼다.
  2. 이때, 해당 변수를 불변으로 선언한다 (const, final 등 사용).
  3. 두 번째 대입 시점까지 기존 변수 이름을 새로운 이름으로 모두 변경한다.
  4. 두 번째 대입에서는 기존 이름으로 새 변수를 선언한다.
  5. 테스트를 통해 기능이 동일하게 작동하는지 확인한다.
  6. 이후의 대입도 같은 방식으로 분리해 반복한다.

예시

Before

let distance = station.distance - customer.distance;
if (distance > 0) {
  distance = distance * 0.9;
}

 

After

const initialDistance = station.distance - customer.distance;
let discountedDistance;
if (initialDistance > 0) {
  discountedDistance = initialDistance * 0.9;
}

필드 이름 바꾸기 (Rename Field)

배경

수십 년 전 프레드 브룩스는 “데이터 테이블 없이 플로우차트만 보여줘서는 여전히 혼란스럽다. 하지만 데이터 테이블을 보여준다면 흐름도는 웬만해선 필요 없다”고 말했다.
이 말은 지금도 유효하다.
어떤 프로그램이 무엇을 하는지 이해하려면 데이터 구조를 제대로 이해해야 한다. 레코드나 클래스의 필드 이름은 코드의 의미를 전달하는 핵심 수단이다.
따라서 필드 이름을 명확하게 짓는 일은 단순한 개선이 아니라, 코드 전체의 가독성과 유지보수성을 높이는 중요한 작업이다.
클래스의 게터와 세터 이름도 마찬가지로 의미를 잘 전달해야 하며, 필요한 경우 함께 변경하는 것이 좋다.

절차

  1. 레코드나 필드의 유효 범위가 제한적이라면, 해당 필드를 사용하는 모든 코드를 직접 수정하고 테스트한다. (이 경우 캡슐화는 필요 없다.)
  2. 레코드나 필드가 외부에서 직접 접근되고 있다면, 먼저 해당 데이터를 캡슐화한다.
  3. 캡슐화된 클래스 내부의 private 필드명을 새로운 이름으로 바꾸고, 이에 맞춰 내부 메서드도 수정한다.
  4. 전체 테스트를 실행해 수정이 제대로 반영되었는지 확인한다.

예시

Before

class Order {
  constructor(data) {
    this._data = data;
  }

  get person() {
    return this._data.person;
  }

  get name() {
    return this.person.name;
  }
}

 

After

class Order {
  constructor(data) {
    this._data = data;
  }

  get customer() {
    return this._data.customer;
  }

  get name() {
    return this.customer.name;
  }
}

 

파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)

배경

가변 데이터는 소프트웨어의 안정성과 예측 가능성을 떨어뜨리는 주요 원인 중 하나다.
가변성을 완전히 없앨 수는 없지만, 그 범위를 좁히는 노력은 코드 품질을 크게 향상시킨다.
그 중에서도 쉽게 계산할 수 있는 파생 변수(derived variable)는 굳이 저장해둘 필요가 없다.
대신 그 값을 계산해주는 질의 함수(query function)로 대체하면 변수 갱신 시 생기는 오류 가능성을 줄일 수 있다.
단, 어떤 연산이 데이터 구조를 감싸거나 새로운 데이터를 생성하는 변형(transform)이라면 예외로 둘 수도 있다.

절차

  1. 해당 변수가 값이 갱신되는 모든 지점을 찾는다.
  2. 동일한 계산을 수행하는 함수를 작성한다.
  3. 변수와 함수 결과가 항상 같은지 어서션을 추가해 검증한다.
  4. 테스트한다.
  5. 변수를 참조하는 코드를 모두 함수 호출로 대체한다.
  6. 테스트한다.
  7. 변수 선언과 갱신 코드를 제거한다.

예시

Before

class ProductPlan {
  constructor() {
    this._capacity = 10;
    this._used = 5;
    this._availableSpace = this._capacity - this._used;
  }

  get availableSpace() {
    return this._availableSpace;
  }

  adjustUsed(amount) {
    this._used += amount;
    this._availableSpace = this._capacity - this._used;
  }
}

 

After

class ProductPlan {
  constructor() {
    this._capacity = 10;
    this._used = 5;
  }

  get availableSpace() {
    return this._capacity - this._used;
  }

  adjustUsed(amount) {
    this._used += amount;
  }
}

 

참조를 값으로 바꾸기 (Replace Reference with Value)

배경

객체를 다른 객체 내부에 포함시킬 때, 그 내부 객체를 참조(reference)로 다룰지 값(value)으로 다룰지 결정해야 한다.
두 방식은 속성 갱신 방식에서 차이가 뚜렷하다.
참조로 다루는 경우, 내부 객체 자체는 유지한 채 그 속성만 갱신한다. 반면 값으로 다루면, 내부 객체를 수정하지 않고 새 객체로 통째로 교체한다.

값으로 다루는 방식은 객체가 불변(immutable)이면 특히 효과적이다. 값 객체는 대체로 안전하게 복사할 수 있으며, 예상치 못한 공유 상태를 줄일 수 있다.
하지만 값 객체는 공유할 수 없기 때문에, 동일 객체를 여러 곳에서 공유해야 하는 상황이라면 이 리팩터링을 적용해서는 안 된다.

절차

  1. 대상 클래스가 불변인지 확인하거나, 불변 객체로 바꿀 수 있는지 검토한다.
  2. 이 객체를 수정하는 모든 세터(setter)를 제거한다.
  3. 동등성 비교가 필요하다면 필드를 기준으로 동치성(equality) 비교 메서드를 작성한다.

예시

Before

class Address {
  constructor(city) {
    this._city = city;
  }

  get city() {
    return this._city;
  }

  set city(value) {
    this._city = value;
  }
}

class Customer {
  constructor(address) {
    this._address = address;
  }

  get address() {
    return this._address;
  }

  set address(a) {
    this._address = a;
  }
}

const address = new Address("Seoul");
const customer = new Customer(address);
customer.address.city = "Busan"; // 외부 객체까지 함께 바뀜

 

After

class Address {
  constructor(city) {
    this._city = city;
  }

  get city() {
    return this._city;
  }

  equals(other) {
    return other instanceof Address && this.city === other.city;
  }
}

class Customer {
  constructor(address) {
    this._address = address;
  }

  get address() {
    return this._address;
  }

  changeAddress(newCity) {
    this._address = new Address(newCity);
  }
}

const customer = new Customer(new Address("Seoul"));
customer.changeAddress("Busan"); // 새로운 Address 객체로 교체

 

값을 참조로 바꾸기 (Replace Value with Reference)

배경

하나의 데이터 구조 안에 논리적으로 같은 데이터가 여러 개 복제되어 존재할 때가 있다.
예를 들어, 여러 고객 정보에 같은 등급 정보가 포함되어 있지만, 각각 독립적으로 저장되어 있다면 문제가 발생할 수 있다.
이러한 복제된 값들은 개별적으로 갱신되기 때문에,
갱신 시 하나라도 놓치면 데이터 불일치가 생기고 시스템의 신뢰성을 떨어뜨릴 수 있다.

이럴 때는 복제된 값을 공통 저장소에서 관리되는 참조(reference)로 바꾸는 것이 좋다.
이렇게 하면 갱신은 한 곳에서만 이루어지고, 참조된 모든 곳에 일관되게 반영된다.

절차

  1. 동일한 부류에 속하는 객체들을 보관할 저장소(collection)를 만든다.
  2. 생성자에서 저장소에서 해당 객체를 정확히 식별할 수 있는 방식이 있는지 확인한다.
  3. 호스트 객체의 생성자를 수정하여, 값을 직접 할당하지 않고 저장소에서 참조를 가져오도록 한다.
  4. 각 단계 후 테스트하여 참조 변경이 의도대로 작동하는지 확인한다.

예시

Before

class Customer {
  constructor(name, type) {
    this._name = name;
    this._customerType = new CustomerType(type);
  }

  get customerType() {
    return this._customerType;
  }
}

class CustomerType {
  constructor(type) {
    this._type = type;
  }

  get type() {
    return this._type;
  }
}

 

After

class Customer {
  constructor(name, typeCode) {
    this._name = name;
    this._customerType = CustomerTypeRepository.get(typeCode);
  }

  get customerType() {
    return this._customerType;
  }
}

class CustomerType {
  constructor(type) {
    this._type = type;
  }

  get type() {
    return this._type;
  }
}

class CustomerTypeRepository {
  static _types = {
    regular: new CustomerType("Regular"),
    premium: new CustomerType("Premium"),
  };

  static get(typeCode) {
    return this._types[typeCode];
  }
}

 

매직 리터럴 바꾸기 (Replace Magic Literal)

배경

매직 리터럴(Magic Literal)은 이름 없는 숫자나 문자열 같은 리터럴 값을 의미한다.
예를 들어, 물리 상수 9.806, 성별을 의미하는 "M"과 "F", 상태를 의미하는 0, 1, 2 등이 있다.

이런 리터럴은 의도를 파악하기 어렵고, 코드 전반에 중복되어 있으면 유지보수에 큰 위험을 초래한다.
값이 변경될 경우 모든 사용 위치를 일일이 찾아야 하고, 같은 숫자라도 다른 의미로 쓰였을 가능성이 있어 실수가 발생할 수 있다.

따라서 리터럴을 의미 있는 이름의 상수로 추출해 사용하는 것이 좋다.
만약 비교 로직에서 사용된다면, 상수 대신 함수로 추출하는 것도 고려할 수 있다.


절차

  1. 리터럴 값을 대입한 상수를 선언한다.
  2. 해당 리터럴이 사용된 모든 위치를 찾는다.
  3. 각 위치에서 리터럴이 같은 의미로 사용되었는지 확인한 후, 동일한 의미일 경우 상수로 대체한다.
  4. 대체 후 테스트한다.

'Programming > Refactoring' 카테고리의 다른 글

조건부 로직 간소화  (0) 2025.05.19
기능 이동  (0) 2025.05.08
캡슐화  (0) 2025.04.24
기본적인 리팩터링 (3)  (0) 2025.04.16
기본적인 리팩터링 (2)  (0) 2025.04.10
'Programming/Refactoring' 카테고리의 다른 글
  • 조건부 로직 간소화
  • 기능 이동
  • 캡슐화
  • 기본적인 리팩터링 (3)
Seung-o
Seung-o
공부한 것들을 정리하는 개발 블로그입니다.
  • Seung-o
    조그만 사람의 개발 주머니
    Seung-o
  • 전체
    오늘
    어제
    • 분류 전체보기 (52) N
      • Database (20)
        • Real MySQL (4)
        • High Performance MySQL (14)
      • Programming (27) N
        • Protocol (2)
        • Designing Data-Intensive Ap.. (5)
        • Unit Testing (4)
        • Refactoring (11) N
        • Langchain (4)
      • Etc (5)
        • Thought (2)
        • Git (1)
        • Jira (1)
        • Experience (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • Github
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Seung-o
데이터 조직화
상단으로

티스토리툴바