✏️7.2 컬렉션 캡슐화하기 (Encapsulate Collection)

리팩터링 전

class Person {
  get courses() {return this._courses;}
  set courses(aList) {this._courses = aList;}

리팩터링 후

class Person {
  get courses() {return this._courses.slice();}
  addCourse(aCourse) {...}
  removeCourse(aCourse) {...}

🧷 배경

저자는 가변 데이터를 모두 캡슐화한다. 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬어지기 때문이다.

컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버릴 수 있다.

add()remove()라는 이름의 컬렉션 변경자 메서드를 만든다.

원본 모듈 밖에서는 컬렉션을 수정하지 않는 습관을 갖고 있다면 이런 메서드를 제공하는 것만으로도 충분할 수 있다. 컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는 것이 낫다.

🧷 절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.

  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다. ➡️ 컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션을 복제해 저장하도록 한다.

  3. 정적 검사를 수행한다.

  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.

  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.

  6. 테스트한다.

🧷 리팩터링 전

// Person 클래스...
  constructor(name) {
    this._name = name;
    this._course = [];
  }
  get name() {return this._name;}
  get courses() {return this._courses;}
  set courses(aList) {this._courses = aList;}
  
// Course 클래스...
  constructor(name, isAdvanced){
    this._name = name;
    this._isAdvanced = isAdvanced;
  }
  get name() {return this._name;}
  get isAdvanced() {return this._isAdvanced;}
  


//클라이언트는 Person클래스에서 제공하는 수업 컬렉션에서 수업 정보를 얻는다.

numAdvancedCourses = aPerson.courses
  .filter(c => c.isAdvanced)
  .length
  ;

🧷 리팩터링 후

// 클라이언트
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));

//클라이언트 입장에서는 아래의 코드처럼 수업 목록을 직접 수정하는게 훨씬 편할 수 있다.

// 클라이언트
for(const name of readBasicCourseNames(filename)){
  aPerson.courses.push(new Course(name, false));
}
// Person 클래스...
addCourse(aCourse){
  this._courses.push(aCourse);
}
removeCourse(aCourse, fnIfAbsent = () => {throw new RangeError();}) {
  const index = this._course.indexOf(aCourse)
  if (index === -1) fnIfAbsent();
  else this._courses.splice(index, 1);
}

방금 추가한 메서드를 호출하도록 아래와 같이 코드를 수정한다.

// 클라이언트...
for(const name of readBasicCourseName(filename)) {
  aPerson.addCourse(new Course(name, false));
}

더이상 setCourses()를 사용할 일이 없으니 제거해준다. 세터를 제공해야할 특별한 이유가 있다면 아래의 코드와 같이 복제본을 필드에 저장하도록 한다.

// Person 클래스...
set courses(aList) {this._courses = aList.slice();}
  
// 메서드들을 사용하지 않고 아무도 목록을 변경할 수 없게하고 싶다면 아래와 같이 복제본을 제공하도록 한다.
get courses() {return this._courses.slice();}

컬렉션 관리를 책임자는 클래스라면 항상 복제본을 제공해야 한다.

Last updated