사각형(Rectangle)을 사용하는 응용프로그램을 만드는 중이다.. 사각형은 좌측상단 꼭짓점과 우측하단 꼭짓점으로 나타낼 수 있다. 그리고 메모리 부담을 줄여야 한다. => 사각형의 영역을 정의하는 꼭짓점을 Rectangle 자체에 넣기 보다, 꼭짓점을 별도의 클래스(RecData)에 넣은 후 Rectangle은 이 구조체를 가르키도록 했다.
사용자가 꼭짓점 정보가 필요할 때가 있으니 꼭짓점 정보를 반환하는 upperLeft()와 lowerRight()를 선언했는데, const인데 Point&(꼭짓점에 대한 핸들)를 반환했다. => 함수 자체에선 내부 정보를 안건드니까 const로 해도 에러가 안뜨지만, 반환된 Point 핸들은 아무 제약이 없으므로, 실질적으로 데이터가 바뀔 수 있다.
즉 이런 코드가 가능해짐
분명 rec는 const로 선언되었는데, rec 내부의 꼭짓점 정보를 바꿀 수 있다!!
======================================================================
여기서 알 수 있는 사실
- 클래스 데이터 맴버는 아무리 숨겨봤자(private으로 해봤자), 그 맴버의 핸들(참조자, 포인터, 반복자)를 반환하는 함수들의 최대 접근도에 따라 캡슐화 정도가 정해진다.
지금 경우에 빗대어 설명하자면, ulhc와 lrhc는 private 이지만 실질적으로는 public이다. 왜냐하면 이들의 참조자를 반환하는 upperLeft, lowerRight가 public 함수이기 때문이다. => 비트수준 상수성의 한계(항목 3 참고)
어떤 객채의 내부요소(internals)는 데이터 맴버 + 외부에서 접근 불가능한(protected, private으로 선언된) 맴버 함수 이다. => 즉 이런 맴버 함수에 대한 핸들도 반환하면 안된다. private, protected로 되어 있는 그 어떤 것들에 대한 핸들도 반환하면 안된다.
그래서 upperLeft와 lowerRight는 다음과 같이 바뀌어야 함
참조자가 아닌 const 참조자(즉 상수 객체)를 반환하니 다음과 같은 시도가 먹히지 않는다.
이렇게 설계하면, 사용자는 사각형을 정의하는 꼭짓점 쌍을 읽을 수는 있지만, 쓸 수는 없게 된다. const로 선언한게 드디어 효과를 발휘함
=> 읽기 접근은 허용하고, 쓰기 접근은 허용하지 않음 (읽기 접근의 의도적으로 허용함, 즉 캡슐화를 의도적으로 느슨하게 했지만, 느슨하게 만드는 데에도 제한을 둠)
그래도 찝찝한게, upperLeft와 lowerRight는 내부 데이터에 대한 핸들을 반환하는 부분이 남아 있긴 하다.( return 부분 ) => 문제가 생길 수 있다.
어떤 문제? => dangling handle , 핸들을 따라갔더니 실제 객체가 없는 경우
이해가 안된다 => 예를 보자. 위 인터페이스를 그대로 사용하고, 어떤 GUI 객체의 사각 테투리 영역(bounding box)를 Rectangle 객체로 반환하는 함수가 있다고 생각해보자.
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
이 문장을 보자. boundingBox()를 호출해서 rec이 반환되었고, rec에 대한 upperLeft()가 호출, rec->ulhc 가 반환되고 이 객체의 포인터가 반환될 것이다. 그리고 이 문장이 끝나면 rec은 사라진다!!! => rec->ulhc도 당연히 사라진다. => pUpperLeft는 dangling pointer를 가지게 된다.
포인트에 getter를 하나 추가하고
미정의 동작을 보이게 된다.( pUpperLeft가 가르키는 객체가 사라졌기 때문이다.)
======================================================================
객체 내부에 대한 핸들을 반환하는 함수는 어떻게든 위험하다는 말이 이래서 나오는 것이다. 분명 upperLeft는 const 객체를 반환함에도 이런 일이 발생한다. 즉 핸들이 참조자이든, 포인터이든, 반복자이든, 핸들에 const를 붙였든 상관없다. 중요한 것은 핸들을 반환하는 함수인 것 자체가 중요하다. 일단 바깥으로 떨어져 나간 핸들은(반환된 핸들은) 그 핸들이 참조하는 객체보다 더 오래 살 위험이 있기 때문이다.(그래서 dangling handle 문제가 나오는 것이다.)
그래서 핸들을 반환하는 맴버함수를 절대로 두지마라는 얘기가 아니라, 되도록 피하자라는 이야기이다. 어쩌다 보면 핸들을 반환하는 함수가 필요할 때가 있다. 대표적인 경우가 operator[] 이다. 얘는 애초에 핸들을 반환하도록 설계된 연산자이기 때문이다. 하지만 이 경우는 원소를 저장하는 컨테이너가 사라질 때 원소도 같이 사라지기 때문에 dangling handle 문제가 일어나지 않는다. 하지만 이런 함수는 예외적이다.
- 어떤 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)를 반환하는 것은 되도록 피하자. 캡슐화 정도를 높이고, 상수 맴버 함수가 객체의 상수성을 유지한 채로 동작할 수 있도록 하며, 무효참조 핸들이 생기는 경우를 최소화 할 수 있다.
'Effective C++ > 5. 구현' 카테고리의 다른 글
항목31: 파일 사이의 컴파일 의존성을 최대한 줄이자 (0) | 2021.04.24 |
---|---|
항목 30: 인라인 함수는 미주알고주알 따져서 이해해 두자. (0) | 2021.04.24 |
항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! (0) | 2021.04.24 |
항목 27: 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2021.04.24 |
항목 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.04.24 |