[리팩터링 2판] 07. 캡슐화

모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있을 것이다.
데이터구조, 클래스 내부정보, 클래스 연결 관계, 함수 캡슐화 등을 알아보자.


7.1. 레코드 캡슐화하기

배경

  • 레코드에는 단점이 있다.
    특히 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.
  • 가변 데이터를 저장하는 용도로는 레코드보다 객체를 선호하는 편이다.
    • 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없다.
    • 필드 이름을 바꿔도 각각 메서도로 제공할 수 있어서, 점진적으로 수정할 수 있다.
  • 값이 불변이면 값을 구해서 레코드에 저장하고, 이름을 바꿀 떄는 그저 필드를 복제한다.
  • 레코드 구조는 두가지로 구분
    • 필드 이름 노출
    • 원하는 이름 사용 (해시, 맵, 딕셔너리 등)
      • 사용하는 곳이 많을수록 불분명함으로 인해 발생하는 문제가 커진다.

절차

  • 레코드를 담은 변수를 캡슐화한다.
  • 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  • 테스트한다.
  • 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  • 레코드를 반환하는 예전 함수를 사용하는 코드를 위에서 만든 새 함수를 사용하도록 바꾼다.
    필드에 접근할 때는 객체의 접근자를 사용한다.
    적절한 접근자가 없다면 추가한다.
    한 부분을 바꿀 때마다 테스트한다.

  • 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 제거한다.
  • 테스트한다.
  • 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.


7.2. 컬렉션 캡슐화하기

배경

  • 게터가 원본 컬렉션을 반환해서, 컬렉션의 원소들이 바뀌어버릴 수 있다.
  • add()remove()라는 컬렉션 변경자 메서드를 만들어서, 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있다.

  • 컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는게 낫다.
  • 읽기 전용으로 제공할 수도 있다.
  • 가장 흔히 사용하는 방식은 컬렉션 게터를 제공하되 내부 컬렉션의 복제본을 반환하는 것이다.
  • 여기서 중요한 점은 코드베이스에서 일관성을 주는 것이다. 컬렉션 접근 함수의 동작 방식을 통일해야 한다.

절차

  • 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  • 컬렉션에 원소를 추가/제거하는 함수를 추가한다.
  • 정적 검사를 수행한다.
  • 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 떄마다 테스트한다.
  • 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.


7.3. 기본형을 객체로 바꾸기

배경

  • 단춘 출력 이상의 기능이 필요해지는 순간 데이터를 표현하는 전용 클래스를 정의한다.

절차

  • 아직 변수를 캡슐화하지 않았다면 캡슐화한다.
  • 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
  • 정적 검사를 수행한다.
  • 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
  • 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.
  • 테스트한다.
  • 함수이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.


7.4. 임시 변수를 질의 함수로 바꾸기

배경

const basePrice = this._quantity * this._itemPrice;	// 기존 임시 변수

get basePrice() {this._quantity * this._itemPrice;}	// 질의 함수로 변경 후

절차

  • 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  • 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
  • 테스트한다.
  • 변수 대입문을 함수로 추출한다.
  • 테스트한다.
  • 변수 인라인하기로 임시 변수를 제거한다.


7.5. 클래스 추출하기

배경

  • 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
  • 작은 일부의 기능만을 위해 서브클래스를 만들거나, 확장해야할 기능이 무엇이냐에 따라 서브클래스를 만드는 방식도 달라진다면 클래스를 나눠야한다는 신호다.

절차

  • 클래스의 역할을 분리할 방법을 정한다.
  • 분리될 역할을 담당할 클래스를 새로 만든다.
  • 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다
  • 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다.
    하나씩 옮길 때마다 테스트한다.
  • 메서드들도 새 클래스로 옮긴다.
    이때 저수준 메서드, 즉 다른 메서드를 호출하기보다는 호출을 당하는 일이 많은 메서드부터 옮긴다.
    하나씩 옮길 때마다 테스트한다.
  • 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  • 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기 (9.4)를 적용할지 고민해본다.


7.6. 클래스 인라인하기

배경

  • 클래스 추출하기를 거꾸로 돌리는 리팩터링

절차

  • 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다.
  • 소스 클래스의 매서드를 사용하는 코드를 모두 타깃 클래스의 위임 매서드를 사용하도록 바꾼다.
    하나씩 바꿀 때마다 테스트한다.
  • 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.
    하나씩 옮길 때마다 테스트한다.
  • 소스 클래스를 삭제하고 조의를 표한다. 🤣


7.7. 위임 숨기기

배경

manager = aPerson.department.manager;

manager = aPerson.manager;
class Person {
    get manager() {return this.department.manager;}
}

절차

  • 위임 객체의 각 매서드에 해당하는 위임 메서드를 서버에 생성한다.
  • 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
  • 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
  • 테스트한다.


7.8. 중개자 제거하기

배경

  • 위임 숨기기의 반대 리팩터링

절차

  • 위임 객체를 얻는 게터를 만든다.
  • 위임 매서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다.
    하나씩 바꿀 때마다 테스트한다.
  • 모두 수정했다면 위임 매서드를 삭제한다.


7.9. 알고리즘 교체하기

배경

  • 기존 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.
  • 매서드를 가능한 잘게 나눴는지 확인하고, 알고리즘 교체를 진행한다.

절차

  • 교체할 코드를 함수 하나에 모은다.
  • 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.
  • 대체할 알고리즘을 준비한다.
  • 정적 검사를 수행한다.
  • 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행한다. 두 결과가 같다면 리팩터링이 끝난다.

댓글남기기