[Effective C++]데이터 멤버가 선언될 곳은 private 영역임을 명심하자

설계 및 선언, 다섯 번째 이야기

Posted by SungBeom on December 10, 2019 · 6 mins read

public 데이터 멤버가 안되는 이유

public 데이터 멤버는 왜 안 될까요? 우선 문법적 일관성이 첫 번째 이유입니다. 데이터 멤버가 public이 아니라면, 사용자 쪽에서 어떤 객체를 접근할 수 있는 유일한 수단은 멤버 함수일 것입니다. 어떤 클래스의 공개 인터페이스에 있는 것들이 전부 함수뿐이라면, 그 클래스의 멤버에 접근하고 싶을 때 그냥 함수를 쓰기만 하면 됩니다.

또한 함수를 사용하면 데이터 멤버의 접근성에 대해 훨씬 정교한 제어가 가능합니다. 만일 어떤 데이터 멤버를 public으로 내놨다면 모두가 이 멤버에 대해 읽기 및 쓰기 접근권한을 갖게 되지만, 이 값을 읽고 쓰는 함수가 있으면 접근 불가, 읽기 전용, 읽기 쓰기 접근, 쓰기 전용 접근 등을 여러분이 직접 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AccessLevels {
public:
    ...
    int getReadOnly() const { return readOnly; }
    void setReadWrite(int value) { readWrite = value; }
    int getReadWrite() const { return readWrite; }
    void setWriteOnly(int value) { writeOnly = value; }
 
private:
    int noAccess;   // 이 int에 대해서는 접근 불가
    int readOnly;   // 이 int에 대해서는 읽기 전용 접근
    int readWrite;  // 이 int에 대해서는 읽기 쓰기 접근
    int writeOnly;  // 이 int에 대해서는 쓰기 전용 접근
};
cs

이렇게 세밀한 접근 제어는 나름대로의 중요성을 갖고 있습니다. 어떤 식으로든 외부에 노출시키면 안 되는 데이터 멤버들이 꽤 많기 때문이죠.

또한 캡슐화(encapsulation)의 장점이 있습니다. 함수를 통해서만 데이터 멤버에 접근할 수 있도록 구현해 두면, 데이터 멤버를 나중에 계산식으로 대체할 수도 있을 것이고, 사용자는 절대로 이 클래스를 넘보지 못합니다. 자동화 장치를 사용해서 자동차가 지나가는 속도를 모니터링하는 프로그램으로 예를 들어 보겠습니다. 이 프로그램이 실행되면, 자동차가 지나갈 때마다 속도를 계산한 후에 지금까지 수집한 속도 데이터 집합에 그 속도를 추가합니다.

1
2
3
4
5
6
7
class SpeedDateCollection {
    ...
public:
    void addValue(int speed);     // 새 데이터 값을 추가합니다.
    double averageSoFar() const;  // 평균 속도를 반환합니다.
    ...
};
cs

이제 arverageSoFar 멤버 함수를 어떻게 구현할지 생각해 보도록 합시다. 한 가지 방법으로, 지금까지 수집한 속도 데이터 전체의 평균값을 담는 어떤 데이터 멤버를 클래스 안에 넣어두는 방안이 있습니다. 또 다른 방법으로는 호출될 때마다 평균값을 계산하는 방법이 있습니다. 수집한 데이터를 매번 죽 훑어가는 코드가 들어가게 되겠지요.

첫 번째 방법(현재의 평균값 유지하기)을 사용하면, speedDateCollection 객체 하나의 크기가 좀 커집니다. 평균값을 유지하기 위해 현재의 평균값, 누적 총합, 데이터의 개수 등이 데이터 멤버로 들어가야 할 것이니까요. 하지만 이런 방법으로 구현한 averageSoFar 함수는 현재의 평균값을 반환하기만 하는 인라인 함수이기에 효율 면에서 꽤 짭짤할 수 있습니다. 이와 반대로 호출될 때마다 평균값을 계산하는 방법을 쓰면 함수 자체의 속도는 느려지지만, SpeedDateCollection 객체 하나의 크기는 첫 번째 방법보다 작아질 것입니다. 따라서 환경에 맞게 바람직한 방법대로 구현하면 됩니다.

어쨌든 중요한 포인트는 "평균값 접근에 멤버 함수를 통하게 한다"(다른 말로 평균값을 캡슐화한다)라는 점인데, 이렇게 함으로써 내부 구현을 이렇게 혹은 저렇게 바꿀 수 있게 되고, 사용자 쪽에서는 기껏 해 봤자 컴파일만 다시 하면 끝납니다.

데이터 멤버를 함수 인터페이스 뒤에 감추게 되면 구현상의 융통성을 전부 누릴 수 있습니다. 예를 들어 데이터 멤버를 읽거나 쓸 때 다른 객체에 알림 메시지를 보낸다든지, 클래스의 불변속성 및 사전조건(precondition), 사후조건(postcondition)을 검증한다든지, 스레딩 환경에서 동기화를 건다든지 하는 일이 간편해집니다.

사용자로부터 데이터 멤버를 숨기면(그러니까 캡슐화하면), 클래스의 불변속성을 항상 유지하는 데 절대로 소홀해질 수 없게 됩니다. 불변속성을 보여줄 수 있는 통로가 멤버 함수밖에 없으니까요. 그뿐 아니라 캡슐화는 현재의 구현을 나중에 바꾸기로 결정할 수 있는 권한을 예약하는 셈입니다. C++에서 public이란 '캡슐화되지 않았다'는 뜻이며, 실질적인 측면에서 이야기할 때 '캡슐화되지 않았다'라는 말은 섣불리 '바꿀 수 없다'라는 의미를 담고 있기 때문입니다.

protected 데이터 멤버가 안되는 이유

protected 데이터 멤버의 경우도, 앞서 말한 사정과 비슷합니다. 문법적 일관성과 세밀한 접근 제어에 관한 이야기라면 public 데이터 멤버와 똑같게 됩니다.

어떤 클래스에 public 데이터 멤버가 있고, 이것을 제거한다고 가정합시다. 이 멤버에 매달려 있는 많은 코드들이 망가질 것이고, 이것을 사용하는 사용자 코드는 전부 무사할 수 없을 것입니다. 이젠 어떤 protected 데이터 멤버를 제거한다고 가정해 봅시다. 이것을 사용한 파생 클래스 전부가 망가질 것이며, public 데이터 멤버를 제거할 때와 같은 문제가 발생합니다. 따라서 캡슐화의 관점에서 쓸모 있는 접근 수준은 private(캡슐화 제공)와 private가 아닌 나머지(캡슐화 없음), 이렇게 둘뿐입니다.


정리

데이터 멤버는 private 멤버로 선업합시다. 이를 통해 클래스 제작자는 문법적으로 일관성 있는 데이터 접근 통로를 제공할 수 있고, 필요에 따라서는 세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화할 수 있을 뿐 아니라, 내부 구현의 융통성도 발휘할 수 있습니다.
protected는 public보다 더 많이 '보호'받고 있는 것이 절대로 아닙니다.