23: 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자
웹브라우저를 나타내는 클래스가 하나 있다고 가정하자. 캐시를 비우는 함수, 기록을 없애는 함수, 쿠키를 제거하는 함수 같은 이런 저런 함수를 통해 제공하는 기능이 많을 것이다.
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
};
이 세 함수를 모아서 불러주는 함수도 준비해 둘 수 있을 것이다.
class WebBrowser {
public:
void clearEverything(); // clearCache, clearHistory, removeCookies를 호출
};
물론 이 기능은 비멤버 함수로 제공해도 된다. 객체의 멤버 함수를 순서대로 불러주기만 하면 되는 것이다.
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
위의 두 방법 중 어느 쪽이 더 괜찮을까? 멤버 버전일까? 비멤버 버전일까?
멤버 버전인 clearEverything은 비멤버 버전인 clearBrowser보다 캡슐화 정도에서 형편없다. 그리고 비멤버 함수를 사용하면 WebBrowser관련 기능을 구성하는 데 있어서 패키징 유연성(packaging flexibility)이 높아지는 장점이 있고, 이로 인해 얻게 되는 추가적인 이점으로 컴파일 의존도도 낮추고, WebBrowser의 확장성도 높일 수 있다.
캡슐화
캡슐화하는 것이 늘어나면 그만큼 밖에서 볼 수 있는 것들이 줄어든다. 밖에서 볼 수 있는 것들이 줄어들면, 그것들을 바꿀 때 필요한 유연성이 커진다. 쉽게 말해, 이미 있는 코드를 바꾸더라도 제한된 사용자들밖에 영향을 주지 않는 융통성을 확보할 수 있다는 뜻입니다. 캡슐화 정도는 어떤 데이터를 접근하는 함수가 많으면 낮다.
항목 22는 데이터 멤버는 private 멤버이어야 한다는 내용이었다. private 멤버에 접근할 수 있는 함수의 개수는 예측이 쉽다. 그 클래스의 멤버 함수의 개수에 프렌드 함수의 개수를 더하면 딱 맞다. 캡슐화 정도가 더 높은 쪽을 고른다면 단연 비멤버 비프렌드 함수이다. 왜냐하면 비멤버 비프렌드 함수는 어떤 클래스의 private 멤버 부분을 접근할 수 있는 함수의 개수를 늘리지 않기 때문이다. 즉, 클래스에 대한 캡슐화 정도가 더 높은 것을 고른 것이다.
여기서 주의해야 할 점이 있다. 첫째, 이 이야기는 비멤버 비프렌드(non-friend) 함수에만 적용된다는 것이다. 프렌드 함수는 private 멤버에 대한 접근권한이 해당 클래스의 멤버 함수가 가진 접근권한과 똑같기 때문에, 캡슐화에 대한 영향 역시 같다.
두 번째는, 캡슐화에 대한 이런저런 이야기 때문에 "함수는 어떤 클래스의 비멤버가 되어야 한다"라는 주장이 "그 함수는 다른 클래스의 멤버가 될 수 없다"라는 의미가 아니라는 것이다. clearBrowser 함수를 다른 유틸리티 클래스 같은 데의 정적 멤버 함수로 만들어도 된다는 이야기이다. 어쨌든 이 함수가 WebBrowser의 멤버 혹은 프렌드가 아니기만 하면 된다.
패키징 유연성, 컴파일 의존도, 확장성
C++로는 더 자연스러운 방법을 구사할 수 있다. clearBrowser를 비멤버 함수로 두되, WebBrowserStuff와 같은 네임스페이스 안에 두는 것이다.
namespace WebBrowserStuff {
class WebBroswer {...};
void clearBrowser(WebBrowser& wb);
}
사실 이건 자연스러움보다 몇 걸음 더 나아간 방법이다. 왜냐하면 네임스페이스는 클래스와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있기 때문이다. 지금 이 부분이 굉장히 중요한데, clearBrowser 같음 함수들은 편의상 준비한 함수들(이하 편의 함수)이기 때문이다.
이런 종류의 편의 함수가 꽤 많이 생길 수 있다. 그리고 웬만한 사용자라면 이들 편의 함수 중 몇 개만 알고 있거나 관심을 둘 것이고 컴파일 의존성을 고민할 이유가 없다는 말이다. 이것들을 나누어 놓는 쉽고 깔끔한 방법은, 즐겨찾기 관련 편의 함수를 하나의 헤더 파일에 몰아서 선언하고, 쿠키 관련 편의 함수는 다른 헤더 파일에 몰아서 선언하고, 인쇄 관련 편의 함수는 제 3의 헤더에 몰아서 선언하는 것이다.
사실 표준 C++ 라이브러리가 이러한 구조로 구성되어 있다. std 네임 스페이스에 속한 모든 것들이 C++StandardLibrary 헤더 같은 것에 모조리 들어가 섞여 있지 않고, 흩어져 선언되어 있다. vector만 필요한 사용자는 굳이 <memory>를 #include할 필요가 없다.
이렇게 하면, 사용자가 실제로 사용하는 구성요소에 대해서만 컴파일 의존성을 고려할 수 있게 된다. 반면 클래스 멤버 함수로 오게 되면 이야기가 암울해진다. 이런 식으로 기능을 쪼개는 것 자체가 불가능하다. 하나의 클래스는 그 전체가 통으로 정의도어야 하고 여러 조각으로 나눌 수가 없기 때문이다.
편의 함수 전체를 여러 개의 헤더 파일에(그러나 하나의 네임스페이스 안에) 나누어 놓으면 편의 함수 집합의 확장(extend)도 손쉬워진다. 해당 네임스페이스에 비멤버 비프렌드 함수를 원하는 만큼 추가해 주기만 하면 그게 확장이다.
이것만은 잊지 말자!
- 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 하자. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어난다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 25: 예외를 던지는 swap에 대한 지원도 생각해 보자 (1) | 2023.10.22 |
---|---|
[Effective C++] 24: 타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (1) | 2023.10.21 |
[Effective C++] 22: 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2023.10.19 |
[Effective C++] 21: 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (1) | 2023.10.18 |
[Effective C++] 20: '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2023.10.17 |