캡슐화를 깰 수 있는 부분을 제거해보자! 👀
안녕하세요! 두두코딩 입니다 ✋
오늘은 Effective C++ 항목 28장에 대해 알아보겠습니다.
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
내부 ‘핸들’ 반환 문제점
사각형을 사용하는 응용 프로그램을 만들고 있다고 가정해보자. 사각형같은 경우 좌측 상단 2개의 꼭지점과 우측 하단 꼭지점 2개로 나눌 수 있으며, 코드로 구현하면 다음과 같다.
class Point {
public:
Point (int x, int y);
...
void setX(int newVal);
void sety(int newVal);
};
// 메모리 최적화를 위해 Point 관리 객체로 분리
struct RectData {
Point ulhc;
Point lrhc;
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData;
};
응용프로그램에서는 영역정보를 사용하기 때문에 좌측 상단 꼭지점을 반환 받는
upperLeft
함수와 우즉 하단 정보를 반환받는 lowerRight
함수를 멤버 변수로
들고 있을 것이다. 해당 함수 같은 경우 항목20에서 배운 내용과 같이, 사용자 타입일 경우 참조로 반환하는 것이 좋아 아래와 같이 구현했다.
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
};
int main() {
Point coord1(0,0);
Point coord2(100,100);
// 0, 0, 100, 100 사각형을 만듬.
const Rectangle rec(coord1, coord2);
// 50, 0, 100, 100 사각형으로 변경됨
rec.upperLeft().setX(50);
}
위 코드 같은 경우 컴파일은 잘된다. const
키워드를 활용해 멤버 변수 내에서 값을
변경하지 못하도록 한다. 즉, 내부 데이터를 변경하지 못하도록 했기 때문에 아무
문제 없어보인다.
하지만, 위 코드에서 보이는 것과 같이 rec
는 const
로 선언된 상수 객체이다.
따라서 해당 객체는 수정이 불가능해야되며, 고유의 상태를 유지해야 되는데..
참조자를 반환하는 upperLeft
함수로 setX
라는 함수를 활용해 x 값이 변경되는 것을 볼 수 있다.
우리는 위의 코드를 통해, 2가지를 알 수 있다.
1. 클래스 데이터 멤버는 캡슐화를 해도, 참조자 객체를 반환하는 캡슐화에 맞춰진다
위의 클래스에서 private
값으로 ulhc
, lrhc
라는 데이터 멤버를 유지한다.
하지만 내부적으로는 upperLeft
와 lowerRight
참조자 반환 함수로 public
접근권한자를 갖게된다.
2. 호출자가 참조자를 반환하는 상수 멤버 함수를 호출할 경우, 외부에서 참조자를 저장할 경우 변경이 가능하다
위 케이스는 비트 수준 상수성의 한계 라는 측면에서 항목 3을 통해 알아봤다.
위의 케이스와 같이 참조자를 반환하는 것만 문제라고 생각할 수 있지만, 포인터나 반복자를 반환하는 케이스에도 모두 동일한 문제가 발생한다. 우리는 위와 같이 객체 내부데이터에 접근이 가능한 매개자를 ‘핸들’이라 정의하며, 어떤 객체이든 ‘핸들’을 반환하게 만들면 캡슐화가 무너지는 경우가 발생하니 주의해서 사용해야한다.
내부적 ‘핸들’ 반환하는 방법
위 문제를 어떻게 해결하는 것이 좋을까에 대해 알아보자.
위의 예시에서 보았던 upperLeft
, lowerRight
같은 경우 참조자를 반환할 경우
캡슐화가 깨질 수 있다. 이를 막기 위해선 const
키워드만 앞에 붙여 주면 된다.
class Rectangle {
public:
...
const Point& upperLeft() const { return pDate->ulhc; }
const Point& lowerRight() const { return pDate->lrhc; }
...
};
위와 같이 선언하면 꼭지점 쌍을 읽을 수 있지만 외부에서는 변경이 불가능해진다.
즉, const
연산자로 호출부에서 객체의 상태를 바꾸지 못하도록 컴파일러 수준에서
막고 있기 때문에 변경이 불가능해진다.
변경이 불가능한 것은 좋은데.. 이렇게 작성하면 캡술화가 깨지는것 아니야? 🤔
위와 같은 생각할 수 있다. 캡슐화 입장에서는 꺠질 수 있는데, 이것은 사용자가
외부에서 Point
라는 값을 들여다 보도록 설계한 것이기 때문에 우리는 최대한
느슨하게 캡슐화를 풀어주되, 제한을 주도록 설계해야 된다.
무효참조 핸들
위와 같이 const
값을 붙여 외부에서 변경하지 못하도록 하더라도, 내부적으로
‘핸들’을 반환하고 있기 때문에 또 다른 문제 무효참조 핸들 문제가 존재한다.
핸들이 있긴 하지만, 핸들을 따라갔을 경우 데이터가 없는 케이스를 우리는 무효참조 핸들이라고 한다.
아래의 코드를 통해 확인해보자.
class GUIObject { ... };
const Rectangle
boundingBox(const GUIObject& obj);
int test () {
GUIObject *pgo;
...
// pgo의 가각 테두리 영역의 좌측 상단 꼭지점 포인터를 반환
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
}
위의 코드를 보면 GUI 객체의 사각형 테두리를 Rectangle
객체로 반환받는
boundingBox
라는 함수가 존재한다.
코드 가장 마지막 문장에서 우리는 boundingBox
함수를 통해 객체를 얻고 해당 객체의
좌측 상단의 꼭지점 포인터를 반환받으려 한다.
마지막 문장을 좀 자세하게 보도록하자. boundingBox(*pgo)
를 호출할 경우 우리는
임시객체를 생성한다. 이후 임시객체에 대한 좌측상단의 꼭지점을 upperLeft
를
통해 포인터로 반환받는다. 해당 포인터의 주소값을 pUpperLeft
라는 포인터에 담게
된다. 이후 다음 문장을 수행하게 되는데.. 여기서 문제가 발생한다.
임시객체 같은 경우 수행하는 문장에서만 임시적으로 생성되고, 존재한다. 즉, 다음
문장으로 넘어가게 될 경우 임시객체는 사라지게 된다. 즉, 우리가 전달한
좌측상단의 포인터 객체도 같이 사라진다. pUpperLeft
가 가리키는 대상이
사라진다는 이야기이다.
위와 같은 경우가 발생하지 않게하기 위해서는 최대한 ‘핸들’을 반환하는 케이스를 만들지 말아야한다. ‘핸들’이 외부로 나가게 되면 핸들이 참조하는 객체보다 오래 살 경우가 발생하기 때문에 최대한 노출을 피하는 것이 좋다.
핸들을 반환하는 경우를 절대로하지 말라고는 할 수 없다. 필요한 경우가 종종 생길 수 있는데.. 그래도 최대한 피하는게 상책이라는 것을 잊지말자!!
기억할 점
👉 어떤 객체의 내부요소에 대한 핸들 (참조자, 포인터, 반복자)를 반환하는 것은 되도록 피하자!!!