리팩터링 2판 스터디: 9장 '데이터 조직화'

이 글은 리팩터링 2판 스터디 시리즈 중 하나입니다.

리팩터링 2판 스터디의 두 번째 글입니다. 책을 순서대로 읽기보다 관심도 순서대로(그래야 중간에 때려치더라도 제가 관심있는 부분을 최대한 많이 정리해 놓을 수 있죠!) 매번 한 챕터씩 정리하는 식으로 진행하려고 합니다.

그리고 생각보다 엄청나게 분량이 많더라고요! 여기에는 간단히 각 리팩터링 기법의 절차와 요점만 요약해 놓았지만, 책에서 설명해 주는 방대한 예시 없이 이 내용만 읽어서는 이해가 어려울 수 있으니 책을 읽어 보시는 것을 추천합니다. 책을 읽고 나서 나중에 필요한 기법만 찾아볼 때는 이 정리가 유용할 거예요.

목차

정리

9.1 변수 쪼개기

한 개의 변수가 여러 가지 역할을 수행한다면 코드를 이해하기 어렵게 되고, 수정하기도 어려워집니다. 이럴 때는 먼저 변수를 역할에 따라 쪼개야 합니다.

절차 (p.330)

  1. 변수를 선언한 곳과 값을 처음 대입하는 곳에서 변수 이름을 바꾼다.
  2. 가능하면 이 때 불변으로 선언한다.
  3. 이 변수에 두 번째로 값을 대입하는 곳 앞까지의 모든 참조를 새로운 변수 이름으로 바꾼다.
  4. 두 번째 대입 시 변수를 원래 이름으로 다시 선언한다.
  5. 테스트한다.
  6. 마지막 대입까지 반복한다.

요점

역할이 둘 이상인 변수가 있다면 쪼개야 한다. 예외는 없다. 역할 하나당 변수 하나다. (p.330)

9.2 필드 이름 바꾸기

필드의 이름을 이해하기 쉽게 바꿔야 할 때가 있습니다. 그럴 때 아래의 방법을 사용합니다.

절차 (p.334)

  1. 레코드의 유효 범위가 제한적이라면 필드에 접근하는 모든 코드를 수정한 후 테스트한다. 이후 단계는 필요 없다.
  2. 레코드가 캡슐화되지 않았다면 우선 레코드를 캡슐화(7.1절)한다.
  3. 캡슐화된 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다.
  4. 테스트한다.
  5. 생성자의 매개변수 중 필드와 이름이 겹치는 게 있다면 함수 선언 바꾸기(6.5절)로 변경한다.
  6. 접근자들의 이름도 바꿔준다(6.5절).

사실 요즘은 위와 같은 절차를 따르지 않아도 IDE에서 이 작업을 간단하게 처리해 주죠. 하지만 사용 범위가 넓을수록 레코드를 캡슐화하는 게 좋다는 제안은 여전히 유효합니다.

요점

이름은 중요하다. 그리고 프로그램 곳곳에서 쓰이는 레코드 구조체의 필드 이름들은 특히 더 중요하다. [...] 데이터 구조는 무슨 일이 벌어지는지를 이해하는 열쇠다. (p.334)

하나의 데이터가 게터, 세터, 생성자, 내부 필드로 나뉘기 때문에 변경할 사항이 늘어나지만, 각각을 따로 수정하면서 한 번 변경할 때마다 더 적은 부분을 건드릴 수 있기 때문에 더 안전합니다.

상황에 따라 레코드를 캡슐화할지 아니면 단순히 속성(property)의 이름을 바꿀지 결정합시다.

불변 데이터 구조(immutable data structure)의 개념을 제가 정확히 이해하고 있는지 모르겠어요. 이 부분은 좀 더 자세히 공부해 봐야겠습니다.

9.3 파생 변수를 질의 함수로 바꾸기

파생 변수(derived variable)란 다른 변수의 값으로부터 계산되는 변수를 의미합니다. 계산에 쓰이는 변수의 값이 바뀔 때마다 새로 계산되는 식인데, 이런 의존 구조가 복잡하다면 코드가 망가지기 쉽습니다.

절차 (p.339)

  1. 변수 값이 갱신되는 지점을 모두 찾는다. 필요하면 변수 쪼개기를 활용해 각 갱신 지점에서 변수를 분리한다.
  2. 해당 변수의 값을 계산해주는 함수를 만든다.
  3. 해당 변수가 사용되는 모든 곳에 어서션(assertion)을 추가(10.6절)하여 함수의 계산 결과가 변수의 값과 같은지 확인한다.
    • 필요하면 변수 캡슐화하기(6.6절)를 적용하여 어서션이 들어갈 장소를 마련해준다.
  4. 테스트한다.
  5. 변수를 읽는 코드를 모두 함수 호출로 대체한다.
  6. 테스트한다.
  7. 변수를 선언하고 갱신하는 코드를 죽은 코드 제거하기(8.9절)로 없앤다.

파생 변수를 쓸 때 어떤 오류가 발생한다는 것인지 쉽게 이해가 되지 않을 수 있는데, 예를 들어 다음과 같은 클래스를 생각해 봅시다.

class Name {
  #given = "";
  set given(val) {
    this.#given = val;
    this.#fullName = val + " " + this.#family;
  }
  get given() {
    return this.#given;
  }
  #family = "";
  set family(val) {
    this.#family = val;
    this.#fullName = this.#given + " " + val;
  }
  get family() {
    return this.#family;
  }
  #fullName = ""; // 파생 변수
  get fullName() {
    return this.#fullName;
  }
}

let gildong = new Name();
gildong.given = "Gildong";
gildong.family = "Hong";
console.log(gildong.fullName); // "Gildong Hong"

일단은 잘 동작하지만, 클래스를 고치다가 실수로 세터 함수를 쓰지 않고 직접 #given이나 #family를 할당한다면 대참사가 벌어집니다.2

class Name {
  #given = "";
  set given(val) {
    this.#given = val;
    this.fullName = val + " " + this.#family;
  }
  get given() {
    return this.#given;
  }
  #family = "";
  set family(val) {
    this.#family = val;
    this.fullName = this.#given + " " + val;
  }
  get family() {
    return this.#family;
  }
  #fullName = ""; // 파생 변수
  get fullName() {
    return this.#fullName;
  }
  constructor(given, family) {
    this.#given = given;
    this.#family = family; // fullName을 업데이트하는 걸 까먹음!
  }
}

let gildong = new Name("Gildong", "Hong");
console.log(gildong.fullName); // <empty string>

이 계산이 복잡하지 않다면 파생 변수를 질의 함수(query)로 바꾸는 것도 좋은 방법입니다.

class Name {
  #given = "";
  set given(val) {
    this.#given = val;
  }
  get given() {
    return this.#given;
  }
  #family = "";
  set family(val) {
    this.#family = val;
  }
  get family() {
    return this.#family;
  }
  get fullName() {
    return this.#given + " " + this.#family;
  }
  constructor(given, family) {
    this.#given = given;
    this.#family = family; // fullName을 업데이트하는 걸 까먹음!
  }
}

let gildong = new Name("Gildong", "Hong");
console.log(gildong.fullName); // "Gildong Hong"

요점

가변 데이터의 유효 범위를 가능한 한 좁혀야 한다고 힘주어 주장해본다. 효과가 좋은 방법으로, 값을 쉽게 계산해낼 수 있는 변수들을 모두 제거할 수 있다. 계산 과정을 보여주는 코드 자체가 데이터의 의미를 더 분명히 드러내는 경우도 자주 있으며 변경된 값을 깜빡하고 결과 변수에 반영하지 않는 실수를 막아준다. (p.338)

9.4 참조를 값으로 바꾸기

객체 내부에서 다른 객체를 사용할 때, 해당 객체를 참조(reference)로 취급하면 원본 객체의 값이 변할 때 그 객체를 참조하는 모든 곳이 영향을 받습니다. 여러 곳에서 한 객체의 상태를 공유해야 하거나 객체의 값이 자주 갱신되는 상황이 아니라면, 객체를 값(value)으로 취급하도록 코드를 변경하는 쪽이 예기치 못한 오류를 줄여 줍니다.

절차 (p.344)

  1. 후보 클래스가 불변인지, 혹은 불변이 될 수 있는지 확인한다.
  2. 각각의 세터를 하나씩 제거(11.7절)한다.
  3. 이 값 객체의 필드들을 사용하는 동치성(equality) 비교 메서드를 만든다.
    • 대부분의 언어는 이런 상황에 사용할 수 있도록 오버라이딩 가능한 동치성 비교 메서드를 제공한다.

요점

값 객체는 대체로 자유롭게 활용하기 좋은데, 특히 불변이기 때문이다. 일반적으로 불변 데이터 구조는 다루기 더 쉽다. 불변 데이터 값은 프로그램 외부로 건네줘도 나중에 그 값이 나 몰래 바뀌어서 내부에 영향을 줄까 염려하지 않아도 된다. (p.343)

9.5 값을 참조로 바꾸기

바로 앞 절과 반대의 상황입니다. 객체의 상태를 여러 곳에서 공유하고, 그 값이 갱신되어야 한다면 해당 객체의 참조를 유지하는 것이 좋습니다. 클라이언트들이 지속적으로 하나의 객체를 참조할 수 있도록 단일한 저장소(repository)를 만들고, 이 저장소에서 필요한 객체의 참조를 받아올 수 있도록 합시다.

절차 (p.348)

  1. 같은 부류에 속하는 객체들을 보관할 저장소를 만든다.
  2. 생성자에서 이 부류의 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다.
  3. 호스트 객체의 생성자들을 수정하여 필요한 객체를 이 저장소에서 찾도록 한다. 하나 수정할 때마다 테스트한다.

책에서는 '클라이언트'와 '호스트'라는 표현을 사용하고 있는데, 이는 굳이 웹에서 말하는 서버-클라이언트가 아니더라도 우리가 리팩터링할 코드에서 가지고 있는 객체를 사용하는 코드나 클래스를 말합니다.

요점

논리적으로 같은 데이터를 물리적으로 복제해 사용할 때 가장 크게 문제되는 상황은 그 데이터를 갱신해야 할 때다. 모든 복제본을 찾아서 빠짐없이 갱신해야 하며, 하나라도 놓치면 데이터 일관성이 깨져버린다. 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는 게 좋다. (p.347)

9.6 매직 리터럴 바꾸기

매직 리터럴은 소스 코드에서 자주 사용하는 일반적인 리터럴 값을 말하는데, 3.141592 같은 것이 대표적이죠. 아니면 부가세율 0.1 같은 것일지도 모릅니다. 전자와 같은 경우는 거의 바뀌지 않겠지만, 후자는 조금 문제가 있습니다. 부가세율이 바뀌어 상품 가격의 부가세를 계산하는 모든 코드가 먹통이 될 수도 있고, 다른 팀원이 도대체 왜 온갖 곳에 price * 0.1 같은 코드가 산적해 있는지 이해하지 못할 수도 있죠. 웬만하면 매직 리터럴을 상수로 따로 선언해 주도록 합시다.

절차 (p.352)

  1. 상수를 선언하고 매직 리터럴을 대입한다.
  2. 해당 리터럴이 사용되는 곳을 모두 찾는다.
  3. 찾은 곳 각각에서 리터럴이 새 상수와 똑같은 의미로 쓰였는지 확인하여, 같은 의미라면 상수로 대체한 후 테스트한다.

요점

의미를 알고 있다고 해도 결국 각자의 머리에서 해석해낸 것일 뿐이라서, 이보다는 코드 자체가 뜻을 분명하게 드러내는 게 좋다. 상수를 정의하고 숫자 대신 상수를 사용하도록 바꾸면 될 것이다. (p.351)

각주

1

9.6절은 원서 2판에는 포함되어 있지 않다고 합니다. 웹 버전에 있다고 하네요.

2

앞에 #을 붙여서 private 필드를 선언하는 기능은 ES2022에 추가되었습니다.




Dani Soohan Park (@heartade)

Follow this blog at Fediverse: @heartade@blog.heartade.dev

Follow my shorter shoutouts at Fediverse: @heartade@social.silicon.moe

Follow me at Bluesky: @heartade.dev