티스토리 뷰

728x90

내부에서 사용하는 객체에 대한 ‘핸들’을 반환하는 코드는 되도록 피하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//점을 나타내는 클래스
class Point
{
public:
    void setX(int newVal);
    void setY(int newVal);
};
//Rectangle에 쓰기 위한 점 데이터
struct RectData
{
     Point ulhc;                    //ulhc = “좌측 상단(upper left – hand corner)”
    Point lrhc;                     //lrhc = “우측 하단(lower right – hand corner)”
};
class Rectangle
{
private:
    std::tr1::shared_ptr<RectData>pData;          //tr1::shared_ptr 자원관리 클래스(스마트 포인터)
};
cs

Point가 사용자 정의 타입 입니다.
사용자 정의 타입을 전달할 때는 값에 의한 전달 보다 참조에 의한 전달방식을 쓰는 편이 더 효율적 입니다.
그래서 이들 두 멤버 함수는 스마트 포인터로 물어둔 Point 객체에 대한 참조자를 반환하는 형태로 만들어졌습니다.

 

1
2
3
4
5
6
class Rectangle
{
public:
    Point& upperLeft()const { return pData->ulhc; }
    Point& lowerRight()const { return pData->lrhc; }
};
cs

조금만 들여다보면 자기모순적인 코드임을 알 수 있습니다.
upperLeft 함수와 lowerRight 함수가 상수 멤버 함수 입니다.
하지만 호출부에서 private 멤버인 내부 데이터에 대한 참조자를 써서 내부 데이터를 맘대로 수정해도 좋다는 뜻이 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
void main()
{
    Point coord1(00);
    Point coord2(100100);
    
    //rec은 (0, 0)부터 (100, 100)의 영역에 있는 상수 Rectangle 객체 입니다.
    const Rectangle rec(coord1, coord2);         
                                                     
    //이제 이 rec은(50, 0)부터 (100, 100)의 영역에 있게 됩니다.
    rec.upperLeft().setX(50);                            
}
cs

rec은 상수 객체로 선언 되었지만 upperLeft를 호출한 쪽은 rec의 은밀한 곳에 숨겨진 Point 데이터 멤버를 참조자로 끌어와 바꿀 수 있다는 것입니다.

여기서 두 가지 교훈을 얻을 수 있습니다.

1. 클래스 데이터 멤버는 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화가 정해진다는 점 입니다.
Ulhc와 lrhc는 private로 하는 upperLeft 및 lowerRight 함수가 public 멤버 함수이기 때문입니다.

2. 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면,
이 함수의 호출부에서 그 데이터의 수정이 가능하다는 점입니다.

참조자, 포인터 및 반복자는 모두 핸들이고, 객체의 내부요소에 대한 핸들을 반환하게 만들면
언제든지 그 객체의 캡슐화를 무너뜨리는 위험과 상수 멤버 함수조차도 객체 상태의 변경을 허용하는 지경까지 이를 수 있습니다.

 

해결

호출부에서 객체의 상태를 바꾸지 못하도록 컴파일러 수준에서 막고 있는 방법으로 const 키워드 입니다.

1
2
3
4
5
6
7
class Rectangle
{
public:
    const Point& upperLeft() const {return pData->ulhc;}
    const Point& lowerRight() const {return pData->lrhc;}
};
 
cs

이렇게 설계하면 사용자는 사각형을 정의하는 꼭짓점 쌍을 읽을 수는 있지만 쓸 수는 없게 됩니다.
사용자들이 Rectangle을 구성하는 Point를 들여다보도록 하자는 것은 처음부터 알고 시작한 설계이기 때문에 이 부분은 의도적인 캡슐화 완화라고 할 수 있습니다.

upperLeft 함수와 lowerRight 함수를 보면 내부 데이터에 대한 핸들을 반환하고 있는 부분이 있는데 이것을 남겨두면 다른 쪽에서 문제가 될 수 있습니다.
가장 큰 문제가 무효참조 핸들로서, 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
class GUIObject { … };
 
//Rectangle 객체를 값으로 반환 합니다. 반환 타입에
// const가 붙은 이유는 반환타입을 상수화 시키는 것입니다.
const Rectangle Boundingbox(const GUIObject& obj);       
 
이 상태에서 어떤 사용자가 이 함수를 사용한다고 생각해 보면
GUIObject *pgo; //pgo를 써서 임의의 GUIObject를 가리키도록 합니다.
                 //pgo가 가리키는 GUIObejct 의 사각 테두리 영역으로부터 좌측 상단 꼭짓점의 포인터를 얻습니다.
 
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());       
cs

boundingBox 함수를 호출하면 Rectangle 임시 객체가 새로 만들어 집니다.
이 객체를 temp라고 가정하였을 때 이 temp에 대해 upperLeft가 호출될 텐데 이 호출로 인해 temp의 내부데이터,
두 Point 객체 중 하나에 대한 참조자가 나옵니다.

마지막으로 이 참조자에 &연산자를건 결과 값이 pUpperLeft 포인터에 대입되는 것입니다.
이 문장이 끝날 무렵 temp객체가 소멸되니 그 안에 들어 있는 Point 객체들도 덩달아 없어질 것입니다.
이 문장은 pUpperLeft에게 객체를 달아 줬다가 주소 값만 남기고 몽땅 빼앗아 간 것입니다.

객체의 내부에 대한 핸들을 반환하는 함수는 어떻게든 위험하다는 말이 이래서 나오는 것입니다.
포인터, 참조자, 반복자가 핸들을 반환하는 함수라는 사실만으로 위험하다는 것입니다.
일단 바깥으로 떨어져 나간 핸들은 그 핸들이 참조하는 객체보다 더 오래 살 위험이 있기 때문입니다.

 

결론

어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것은 되도록 피하는 것이 캡슐화 정도를 높이고,상수 멤버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화할 수 있습니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함
250x250