😀 책에서 기억하고 싶은 내용을 써보세요.

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

 

 

배경

  • 저자는 가변 데이터를 모두 캡슐화하는 편이다.
    • 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워진다.
    • 객체 지향 개발자들은 캡슐화를 적극 권장한다.

 

컬렉션을 다룰 때 하는 실수

  • 컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 하면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버린다
    • 이런 문제를 방지하기 위해 컬렉션을 감싼 클래스에 add()와 remove()라는 이름의 컬렉션 변경자 메서드를 만든다. ⇒ 항상 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 한다.

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법1

  • 절대로 컬렉션을 반환하지 않게 하는 방법
  • 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치게 하는 것
  • ex) aCustomer.orders.size() 처럼 접근하는 코드를 aCustomer.numberOfOrders()로 바꾸는 것
  • 저자가 선호하지 않는 방법

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법2

  • 컬렉션을 읽기전용으로 제공하기
  • 자바에서는 컬렉션의 읽기전용 프락시를 반환하게 만들기가 쉽다.
    • 프락시가 내부 컬렉션을 읽는 연산은 그대로 전달하고, 쓰기는 모두 막는 것.
  • 이터레이터(iterator)나 열거형 객체를 기반으로 컬렉션을 조합하는 라이브러리들도 비슷한 방식을 쓴다.

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법3

  • 가장 흔히 사용하는 방식은 컬렉션 데터를 제공하되 내부 컬렉션의 복제본을 반환하는 방식
  • 복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다.
  • 컬렉션이 상당히 크다면 성능 문제가 발생 할 수 있지만 성능에 지장을 줄만큼 컬렉션이 큰 경우는 별로 없었다.

 

 

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다.
    • 컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션을 복제해 저장하도록 만든다.
  3. 정적 검사를 수행한다.
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환.
  6. 테스트한다.

 

 

예시

수업 목록을 필드로 지니고 있는 Person 클래스를 예로 살펴보자.

// Person 클래스
constructor(name) {
	this._name = name;
	this._courses = [];
}

get name() {return this._name;}
get courses() {return this._courses;}
get 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 = readBaiscCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));

 

 

클라이언트 입장에서는 다음처럼 수업 목록을 직접 수정하는 것이 훨씬 편할 수 있다.

// 클라이언트
for(const name of readBasicCourseNames(filename) {
	aPerson.courses.push(new Course(name, false));
}
  • 위와 같은 방식은 Person 클래스가 더는 컬렉션을 제어할 수 없어서 캡슐화가 깨진다.
  • 필드를 참조하는 과정한 캡슐화했을 뿐 필드에 담긴 내용은 캡슐화화지 않은게 원인이다.

 

 

2️⃣ 제대로 캡슐화하기 위해 먼저 클라이언트가 수업을 하나씩 추가하고 제거하는 메서드를 Person 에 추가해보자.

// Person 클래스
addCourse(aCourse) {
	this._courses.push(aCourse);
}
removeCourse(aCourse, fnIfAbsent = () => {throw new RangeError();}) {
	const index = this._courses.indexOf(aCourse);
	if (index === -1) fnIfAbsent();
	else this._courses.splice(index, 1);
}

제거 메서드에서는 클라이언트가 컬렉션에 없는 원소를 제거하려 할 때의 대응 방식을 정해야 한다.

그냥 무시하거나 에러를 던질 수도 있다. 여기서는 기본적으로 에러를 던지되, 호출자가 원하는 방식으로 처리할 여지도 남겨뒀다.

 

 

4️⃣ 그런 다음 컬렉션의 변경자를 직접 호출하던 코드를 모두 찾아서 방금 추가한 메서드를 사용

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

 

2️⃣ 이렇게 개별 원소를 추가하고 제거하는 메서드를 제공하기 때문에 setCourses()를 사용할 일이 없어졌으니 제거한다(세터 제거하기). 세터를 제공해야 할 특별한 이유가 있다면 인수로 받은 컬렉션의 복제본을 필드에 저장하게 한다.

// Person 클래스
set courses(aList) {this._courses = aList.slice();}

이렇게 하면 클라이언트는 용도에 맞는 변경 메서드를 사용하도록 할 수 있다.

 

5️⃣ 하지만 저자는 이 메서드들을 사용하지 않고서는 아무도 목록을 변경할 수 없게 만드는 방식을 선호한다. 다음과 같이 본제본을 제공하면 된다.

// Person 클래스
get courses() {return this._courses.slice();}
  • 저자의 경험에 따르면 컬렉션에 대해서는 어느 정도 강박증을 갖고 복제본을 만드는 편이 예상치 못한 수정이 촉발한 오류를 디버깅하는 것보다 낫다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 기존에 게터를 사용해서 객체를 그대로 반환하고 세터로 수정하는 방식을 사용했었습니다.
    • 복제본을 제공해서 원본을 수정하지 못하게 막는 방식은 처음 봤습니다.
    • 책에서는 접근자 메서드로만으로 캡슐화 했다고 생각하면 안이한 개발자라고 한다.
  • 습관이 아닌 코드로 하지 말아야 할 것들을 미리 방지

 

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 컬렉션이란?
    • 객체의 모음, 그룹
    • 자바에서는 컬렉션 인터페이스(java.util.Collection)로 대표적으로 List오 Set으로 나눠진다. List ⇒ 순서가 있는 저장 공간 Set ⇒ 집합적인 저장 공간

 

 

반응형

+ Recent posts