데이터 멤버가 모두 public이 아니라면, 사용자가 어떤 객체에 접근할 수 있는 유일한 수단은 멤버 함수이다. 다시 말해서 클래스의 공개 인터페이스에 있는 것이 함수뿐이라면, 그 클래스의 멤버에 접근하고 싶을 때는 무조건 함수를 쓴다는 것이다.
class AccessLevels {
public:
...
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrtie; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess; // 이 int에는 접근 불가
int readOnly; // 이 int에는 읽기 전용 접근
int readWrite; // 이 int에는 읽기 쓰기 접근
int WriteOnly; // 이 int에는 쓰기 전용 접근
}
함수를 이용한다면 이렇게 세밀하게 데이터 멤버의 접근을 제어할 수 있다. 사실 모든 데이터 멤버에 읽기와 쓰기 함수를 달아 줄 일은 극히 드물기 때문에 이러한 접근 제어는 매우 중요한 포인트이다.
이렇게 함수를 통해서만 데이터 멤버에 접근하도록 구현하면, 데이터 멤버를 계산식으로 대체할 수도 있으며 사용자는 이 클래스 객체에 대해 함부로 접근할 수 없다.
다음은 자동차가 지나가는 속도를 모니터링하는 프로그램에 사용하는 클래스이다.
자동차가 지나갈 때마다 속도를 측정하여, 지금까지 측정한 속도의 평균값을 반환한다.
class SpeedDataCollection {
...
public:
void addValue(int speed); // 새 데이터 멤버 추가
double averageSoFar() const; // 평균 속도 반환
}
averageSoFar
의 구현 방법현재의 평균값 유지하기
평균값을 유지하기 위한 공간 할당이 필요하다. 아마도 현재의 평균값, 누적 총합, 데이터 개수 등이 SpeedDataCollection
의 데이터 멤버로 필요할 것이다. 이렇게 구현하면 averageSoFar
는 이미 계산되어 있는 평균값을 반환하기만 하면 되므로 효율적이다. (+ inline 함수라면 더욱 효율적)
호출될 때마다 평균값 계산하기
averageSoFar
의 속도는 느리지만, SpeedDataCollection
객체 하나의 크기는 첫 번째 방법보다 작을 것이다.
어떤 방법이 좋을 지는 상황에 따라 다르다.
평균값을 빈번하게 사용하고, 속도가 중요하며, 메모리 크기에 많이 구애받지 않는다면 첫 번째 방법이 좋다. 쓸 수 있는 메모리가 한정되어 있거나(ex. 임베디드 기계) 평균값이 자주 필요하지 않은 프로그램이라면 두 번째 방법이 좋다.
중요한 것은 평균값 접근에 멤버 함수를 통하게 한다는 것, 즉 평균값을 캡슐화한다는 것이다. 멤버 변수(평균값)를 캡슐화함으로써 averageSoFar
의 내부 구현을 바꿀 수 있게 된다.
따라서 널리 쓰이는 클래스일수록 내부 구현을 더 좋은 쪽으로 개선할 가능성을 남겨두기 위해 멤버 변수의 캡슐화가 필요하다.
구현상의 융통성을 얻을 수 있다. 예를 들면 데이터 멤버를 읽거나 쓸 때 다를 객체에 알림 메시지 보내기, 클래스의 불변속성 및 사전조건(precondition)/사후조건(postcondition) 검증, 스레딩 환경 동기화 등이 간편해진다.
사용자로부터 데이터 멤버를 캡슐화하면, 불변속성을 보여줄 방법이 멤버 함수 뿐이므로 불변속성을 유지하는 데에 용이하다.
참고: 불변속성이란?
protected는 public보다는 외부로부터 많이 가려져 있지만, 결국에는 public과 동일한 문제를 갖고 있다.
어떤 것이 바뀌면 깨질 가능성을 가진 코드가 늘어날 때 캡슐화의 정도는 그에 반비례해서 작아진다(항목 23). 즉 데이터 멤버가 클래스에서 제거될 때 깨질 수 있는 코드의 양에 반비례해서 그 데이터의 캡슐화 정도가 감소한다.
어떤 public 데이터 멤버를 제거한다고 가정하자. 이 멤버와 관련된 얼마나 많은 코드가 망가질지 파악하기조차 어렵다. 이는 protected 멤버를 제거한 경우도 마찬가지이다. 해당 멤버를 사용하는 파생 클래스까지 영향이 미칠 것이기 때문이다.
이 말은 결국 어떤 데이터 멤버를 public이나 protected로 선언한 순간부터 해당 데이터 멤버에 대해 무언가를 바꾸기(제거하기) 어렵다는 말이다.
캡슐화의 관점에서 접근수준을 나눈다면 private과 private이 아닌 나머지 이렇게 둘 뿐이다.
정리
- 데이터 멤버는 private 멤버로 선언해야 한다.
- 문법적으로 일관성 있는 데이터 접근
- 세밀한 접근 제어
- 클래스의 불변속성 강화
- 내부 구현의 융통성
- protected는 public보다 더 많이 보호받고 있는 것이 절대로 아니다.