Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
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
31
Archives
Today
Total
관리 메뉴

로또

28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자. 본문

책/Effective C++

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

아롱로또 2023. 11. 27. 12:10

 

다루는 내용

핸들을 반환하는 함수의 문제점과 그 원인을 알아보자.

 

핸들을 반환하는 코드

사각형을 사용하는 어떤 응용프로그램을 만들고 있다고 하자. 좌상단(upper-left)과 우하단(lower-right) 꼭짓점을 변수로 갖는 Rectangle 클래스를 다음과 같이 만들었다.

// 꼭짓점 하나
class Point{
public:
    Point(int _x, int _y): x(_x), y(_y){}
    void SetX(int _x){x = _x;}
    void SetY(int _y){y = _y;}
    int GetX(){return x;}
    int GetY(){return y;}

private:
    int x, y;
};

// Rectangle을 이루는 꼭짓점 두 개
struct RectData{
    Point ulhc; // upper-left
    Point lrhc; // lower-right
    RectData(Point ul, Point lr): ulhc(ul), lrhc(lr){}
};

class Rectangle{
public:
    Rectangle(Point ul, Point lr){
        p_data = shared_ptr<RectData>(new RectData(ul, lr));
    }
    Point& UpperLeft() const {return p_data->ulhc;}
    Point& LowerRight() const {return p_data->lrhc;}
private:
    shared_ptr<RectData> p_data;
};

Point는 사용자 정의 타입으로 이전에 읽었던 "20. '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다." 를 떠올려 Rectangle 클래스의 멤버 함수인 UpperLeft()와 LowerRight()에서 Point 객체에 대한 참조자를 반환하도록 만들었다.

 

컴파일은 문제없지만, UpperLeft()와 LowerRight()는 상수 멤버 함수로 Rectangle 객체의 정보를 제공하지만 Rectangle 객체를 수정할 수는 없도록 설계하였다. 하지만 private 멤버 변수의 참조자를 public 멤버 함수를 통해 제공하므로 다음과 같이 객체의 정보를 수정할 수 있을 것이다.

void Print(const Rectangle rec){
    cout << rec.UpperLeft().GetX() << ", " << rec.UpperLeft().GetY() << endl;
    cout << rec.LowerRight().GetX() << ", " << rec.LowerRight().GetY() << endl;
}

int main(){
    Point coord_1(0, 0);
    Point coord_2(100, 100);
    const Rectangle rec(coord_1, coord_2);
    cout << "Before " << endl;
    Print(rec);
    rec.UpperLeft().SetX(50);
    cout << "After " << endl;
    Print(rec);
    return 0;
}

실행 결과

 

Rectangle rec는 분명 상수 객체로서 수정이 불가능해야 하는데, 수정이 이루어진 결과를 볼 수 있다. 

 

이러한 경우는 참조자뿐만 아니라, 포인터나 반복자와 같은 핸들(handle, 다른 객체에 손을 댈 수 있게 하는 매개자)에서도 동일하게 발생할 수 있다. 어떤 객체의 내부 요소에 대한 핸들을 반환하는 것은 해당 객체의 캡슐화가 무너질 수 있는 위험에 놓이는 것이다.

내부 요소에는 private, protected로 선언된 데이터 멤버뿐만 아니라 멤버 함수도 포함된다. 때문에 외부 공개가 차단된 멤버 함수에 대해 이들의 포인터를 반환하는 멤버 함수또한 없어야 한다.

 

우리는 여기서 다음과 같은 두 가지 교훈을 얻을 수 있다.

  1. 클래스 데이터 멤버는 아무리 숨겨봤자 그 데이터 멤버의 참조자를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
  2. 어떤 객체에서 호출한 상수 멤버 함수의 참조자 반환 값의 실제 데이터가 그 객체의 바깥에 저장되어 있다면, 이 함수의 호출부에서 그 데이터의 수정이 가능하다.

이러한 문제점은 참조자 대신 상수 참조자를 반환하는 것으로 해결할 수 있다.

class Rectangle{
public:
    Rectangle(Point ul, Point lr){
        p_data = shared_ptr<RectData>(new RectData(ul, lr));
    }
    const Point& UpperLeft() const {return p_data->ulhc;} // 상수 참조자 반환
    const Point& LowerRight() const {return p_data->lrhc;} // 상수 참조자 반환
private:
    shared_ptr<RectData> p_data;
};

실행 결과

 

상수 참조자를 반환하도록 변경하면, 호출부에서 객체의 상태를 변경하지 못하도록 컴파일러 수준에서 막아준다.

 

무효참조 핸들(Dangling Handle)

그럼에도 불구하고, UpperLeft()와 LowerRight()는 여전히 핸들(참조자, 포인터, 반복자 등)을 반환하고 있다. 이는 다른 곳에서 문제가 될 여지가 존재한다. 가장 큰 문제는 무효참조 핸들로, 핸들이 있기는 하지만 해당 핸들을 따라갔을 때 실제 객체의 데이터가 존재하지 않는 문제이다.

 

다음은 GUI 객체의 사각 테두리 영역(bounding box)을 Rectangle 객체로 반환하는 예제이다.

class GUIObject{...};
const Rectangle GetBoundingBox(const GUIObject& obj);

int main(){
    GUIObject* pgo; // 임의의 GUI Object를 가리키는 변수
    ...
    const Point* p_upper_left = &(GetBoundingBox(*pgo).UpperLeft());
    ...
}

 

위 코드에서 GetBoundingBox()가 호출되면 다음과 같은 일이 발생할 것이다.

  1. Rectangle 임시 객체(temp)가 새로 생성된다.
  2. 해당 temp에 대해 UpperLeft()가 호출되어 Point 참조자를 반환한다.
  3. 반환된 참조자에 &연산을 하여 나온 주소값이 p_upper_left에 대입된다.
  4. GetBoundingBox() 함수의 반환값인 temp의 소멸자가 호출된다.

이제, p_upper_left가 가리키는 객체는 존재하지 않게 된다. p_upper_left라는 핸들만 존재하는 것이다.

이러한 문제들 때문에 핸들을 반환하는 함수는 위험하다. 

 

물론, 핸들을 반환하는 멤버 함수를 절대적으로 금지하는 것은 아니다.

실제로 operator[] 연산자는 string이나 vector 등의 클래스에서 개개의 원소를 참조할 수 있게 만드는 용도로 제공되는데, 내부적으로는 각 원소 데이터에 대한 참조자를 반환하기도 한다. 물론 이러한 함수는 예외적인 것으로, 일반적인 규칙은 아니다.

 

이것만은 잊지 말자!

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