- 어떤 객체의 내부요소에 대한 핸들을 반환하는 것은 되도록 피해라. 캡슐화 정도를 높이고, 상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화할 수 있다.
내부에서 사용하는 객체에 대한 핸들을 반환하지 마라
내부에서 사용하는 객체를 제어할 수 있는 핸들을 반환하는 것은 좋지 않다.
사각형을 사용하는 어떤 응용프로그램을 만들고 있다고 가정해 보자.
사각형은 좌측 상단과 우측 하단의 꼭짓점 두 개로 나타낼 수 있다. 이것을 추상화한 Rectangle 클래스를 만들었다.
메모리 부담을 줄이기 위해 꼭짓점을 Rectangle 자체에 넣는 것이 아니라 별도의 구조체로 관리하기로 했다.
class Point {
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData {
Point ulhc; // 좌측 상단
Point lrhc; // 우측 하단
};
class Rectangle {
public:
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
private:
std::tr1::shared_ptr<ReacData> pData;
};
값에 의한 전달보다 참조에 의한 전달방식이 효율적이라 생각하여 위와 같이 구현했다.
컴파일은 잘 된다. 하지만, 자기모순적인 코드가 된다.
upperLeft 함수와 lowerRight 함수는 상수 멤버 함수이다. 즉, Rectangle 객체를 수정할 수 없어야 한다.
하지만, 반환하는 값은 private 멤버 데이터의 참조자이다. 사용자는 이것을 통해 객체를 수정할 수 있게 된다.
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50);
여기서 두 가지를 알 수 있다.
- 클래스 데이터 멤버는 아무리 숨겨봤자 그 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다. ulhc와 lrhc는 private으로 선언되어 있다. 하지만, 실질적으로 public 멤버와 같다. 참조자를 반환하는 upperLeft, lowerRight 함수가 public 멤버 함수이기 때문이다.
- 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다.
참조자뿐만 아니라 포인터나 반복자를 반환하도록 되어 있었다 해도 마찬가지로 문제가 생긴다.
참조자, 포인터 및 반복자 모두 핸들이고, 어떤 객체의 내부요소에 대한 핸들을 반환하게 만들면 언제든지 그 객체의 캡슐화를 무너뜨리게 된다.
어떤 객체의 내부요소는 흔히 데이터 멤버만 생각할 수 있는데, 일반적인 수단으로 접근이 불가능한 멤버 함수도 객체의 내부요소에 들어간다.
그러니 이들에 대한 핸들도 반환하지 말아야 한다. 즉, 외부 공개가 차단된 멤버 함수에 대해, 이들의 포인터를 반환하는 멤버 함수를 만드는 일이 절대로 없어야 한다. 이런 함수가 하나라도 들어가는 순간부터 실질적인 접근 수준이 바뀐다.
당연히, 멤버 함수 포인터를 반환하는 함수의 접근도에 맞춰진다. protected 혹은 private 멤버로 선언된 함수라 해도 사용자 측면에서 얼마든지 포인터를 얻어내어 호출할 수 있다.
그렇다면 Rectangle에서 문제를 해결하려면 반환 타입에 const 키워드만 붙여주면 된다.
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
이렇게 설계하면, 사용자는 사각형을 정의하는 꼭짓점 쌍을 읽을 수는 있지만 수정할 수는 없게 된다.
사용자들이 Rectangle을 구성하는 Point를 알고 싶을 수 있는 것을 알고 설계했기 때문에 이 부분은 의도적인 캡슐화 완화라고 할 수 있다. 이보다 더 중요한 부분은 느슨하게 만든 데에도 제한을 두었다는 것이다.
upperLeft 함수와 lowerRight 함수를 보면 내부 데이터에 대한 핸들을 반환하고 있는 부분이 있다.
이것을 남겨두면 다른 문제가 생길 수 있다. 바로 무효참조 핸들이다.
무효참조 핸들이란 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것이다.
이는 함수가 객체를 값으로 반환할 경우에 가장 흔하게 발생된다.
예를 들어보면 어떤 GUI 객체의 사각 테두리 영역을 Rectangle 객체로 반환하는 함수가 있다고 가정해 보자.
class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj); // Rectangle 객체 반환
GUIObject *pgo;
...
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft()); // pgo가 가리키는 객체의 사각 테두리 꼭짓점 포인터 얻기
boundongBox 함수를 호출하면 Rectangle 임시 객체가 새로 만들어진다. 그리고 그 객체에 대해 upperLeft가 호출될 텐데, 이로 인해 내부 데이터인 Point객체 중 하나에 참조자를 얻을 수 있다. 이 참조자의 주소를 pUpperLeft에 대입하게 된다.
하지만, 이 문장이 끝날 때 boundingBox함수 호출 시에 만들어진 임시 객체가 소멸하게 된다.
당연히 그 안에 들어 있는 Point 객체도 소멸된다. 그럼 사용자는 있지도 않은 객체의 Point를 가리키고 있는 것이다.
따라서, 객체 내부에 대한 핸들을 반환하는 함수는 위험하다는 것이다.
그렇다고 핸들을 반환하는 멤버 함수를 절대로 만들지 말라는 것은 아니다.
예를 들어, operator[] 연산자는 string이나 vector 등의 클래스에서 원소를 참조할 수 있게 만드는 용도로 제공되고 있는데, 실제로 이 연산자는 내부적으로 해당 컨테이너에 들어 있는 원소 데이터에 대한 참조자를 반환하는 식으로 동작한다.