웹브라우저를 나타내는 클래스가 하나 있다고 가정합시다. 웹브라우저 클래스라면 이런 저런 함수를 통해 제공하는 기능이 많을 것입니다. 웹브라우저로 다운로드한 파일들을 임시 저장한 캐시를 비우는 함수, 방문한 URL의 기록을 없애는 함수, 시스템이 갖고 있는 쿠키를 전부 제거하는 함수도 그 중에 속해 있겠지요.
1 2 3 4 5 6 7 8 | class WebBrowser { public: ... void clearCache(); void clearHistory(); void removeCookies(); ... }; | cs |
하지만 사용자 중에는 이 세 동작을 한 번에 하고 싶은 분들도 상당수 있기 때문에, 세 함수를 모아서 불러주는 함수도 준비해 둘 수 있을 것입니다.
1 2 3 4 5 6 | class WebBrowser { public: ... void clearEverything(); ... }; | cs |
물론 이 기능은 비멤버 함수로 제공해도 됩니다. 웹브라우저 객체의 멤버 함수를 순서대로 불러주기만 하면 되는 거죠.
1 2 3 4 5 6 | void clearBrowser(WebBrowser& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); } | cs |
어느 쪽이 더 괜찮을까요? 멤버 버전인 clearEverything일까요, 아니면 비멤버 버전인 clearBrowser일까요? 객체 지향 법칙에 관련된 이야기를 찾아보면 데이터와 그 데이터를 기반으로 동작하는 함수는 한 데 묶여 있어야 하며, 멤버 함수가 더 낫다고들 합니다. 하지만 불행히도 이 제안은 틀렸습니다. 분명히 객체 지향 법칙은 할 수 있는 만큼 데이터를 캡슐화하라고 주장하고 있지요. 그러나 멤버 버전인 clearEverything은 비멤버 버전인 clearBrowser보다 캡슐화 정도에서 오히려 형편없습니다. 이것 말고도, 비멤버 함수를 사용하면 WebBrowser 관련 기능을 구성하는 데 있어서 패키징 유연성(packaging flexibility)이 높아지는 장점이 있는데다가, 이로 인해 얻게 되는 추가적인 이점으로 컴파일 의존도를 낮추고 WebBrowser의 확장성도 높일 수 있습니다.
어떤 것을 캡슐화하면, 우선 외부에서 이것을 볼 수 없게 됩니다. 캡슐화하는 것이 늘어나면 그만큼 밖에서 볼 수 있는 것들이 줄어듭니다. 밖에서 볼 수 있는 것들이 줄어들면, 그것들을 바꿀 때 필요한 유연성이 커집니다. 변경 자체가 영향을 줄 수 있는 범위가 '변경된 것을 볼 수 있는 것들'로 한정되기 때문입니다. 그러니까 캡슐화되는 것들이 많아지면, 그것들을 변경할 수 있는 여유도 많아집니다. 바로 이것 때문에 우선 무엇보다도 캡슐화에 가치를 두는 것이지요.
어떤 객체의 모습을 그 객체의 데이터로 설명할 수 있다고 생각해 봅시다. 이 데이터를 직접 볼 수 있는(다시 말해 접근할 수 있는) 코드가 적으면 적을수록 그 데이터는 많이 캡슐화된 것이고, 그 객체가 가진 데이터의 특징을 바꿀 수 있는 자유도가 그만큼 높은 것입니다. 똑같은 기능을 제공하는데 멤버 함수(그 클래스의 private 멤버뿐만 아니라 private 멤버로 되어 있는 다른 함수, 나열자, typedef 타입 등등을 모두 접근할 수 있는)를 쓸 것이냐, 아니면 비멤버 비프렌드 함수(어느 것도 접근할 수 없는)를 쓸 것이냐를 다시 생각해 보면, 단연 캡슐화 정도가 더 높은 후자입니다. 비멤버 비프렌드 함수는 어떤 클래스가 private 멤버 부분을 접근할 수 있는 함수의 개수를 늘리지 않으니까요. 따라서 clearBrowser(비멤버 비프렌드 함수)가 clearEverything(멤버 함수)보다 클래스에 대한 캡슐화 정도가 더 높기에 바람직합니다.
여기서 주의해야 할 부분 두 가지가 있습니다. 첫째, 이 이야기는 비멤버 비프렌드(non-friend) 함수에만 적용된다는 것입니다. 프렌드 함수는 private 멤버에 대한 접근권한이 해당 클래스의 멤버 함수가 가진 접근권한과 똑같기 때문에, 캡슐화에 대한 영향 역시 같습니다. 캡슐화라는 관점에서 보았을 때, 위의 선택은 멤버 함수와 비멤버 한수 사이의 선택이 아닌, 멤버 함수와 비멤버 '비프렌드' 함수 사이의 선택입니다.
주의해야 할 점 두 번째는, 캡슐화에 대한 이런저런 이야기 때문에 "함수는 어떤 클래스의 비멤버가 되어야 한다"라는 주장이 "그 함수는 다른 클래스의 멤버가 될 수 없다"라는 의미가 아니라는 것입니다. 이를테면, clearBrowser 함수를 다른 유틸리티 클래스 같은 데의 정적 멤버 함수로 만들어도 된다는 이야기입니다. 어쨌든 이 함수가 WebBrowser 클래스의 멤버(혹은 프렌드)가 아니기만 하면 됩니다. WebBrowser가 가진 private 멤버의 캡슐화에 영향을 주지 않는다는 점이 중요하니까요.
C++로는 더 자연스런 방법을 구사할 수 있습니다. clearBrowser를 비멤버 함수로 두되, WebBrowserStuff와 같은 네임스페이스 안에 두는 것입니다.
1 2 3 4 5 | namespace WebBrowserStuff { class WebBrowser { ... }; void clearBrowser(WebBrowser& wb); ... } | cs |
사실 이건 자연스러움보다 몇 걸음 더 나아간 방법이라고 볼 수 있습니다. 왜냐하면 네임스페이스는 클래스와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있기 때문입니다. 지금 이 부분은 굉장히 중요한데, clearBrowser 같은 함수들은 편의상 준비한 함수들이기 때문입니다. 멤버도 아니고 프렌드도 아니기에, WebBrowser 사용자 수준에서 Browser가 없다고 해도 사용자는 그냥 clearCache, clearHistory, removeCookies를 알아서 불러주면 되는 것입니다.
WebBrowser처럼 응용도가 높은 클래스는 이런 종류의 편의 함수가 꽤 많이 생길 수 있습니다. 즐겨찾기(bookmark)에 관련한 함수라든지, 인쇄에 관련된 함수도 있을 수 있고, 쿠키 관리용 함수도 충분히 가능하지요. 일반적인 경우입니다만, 웬만한 사용자라면 이들 편의 함수들 중 몇 개만 알고 있거나 관심을 둘 것입니다. 즐겨찾기 기능에만 관심 있는 사용자가 구태여 다른 함수들, 뭐 이를테면 쿠키 관련 편의 함수에 대한 컴파일 의존성을 고민할 이유가 없다는 말이죠. 이것들을 나누어 놓는 쉽고 깔끔한 방법은, 즐겨찾기 관련 편의 함수를 하나의 헤더 파일에 몰아서 선언하고, 쿠키 관련 편의 함수는 다른 헤더 파일에 몰아서 선언하고, 인쇄 관련 편의 함수는 제3의 헤더에 몰아서 선언하는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // "webbrowser.h" 헤더 - WebBrowser 클래스 자체에 대한 헤더 // 그리고 WebBrowser에 관련된 '핵심' 기능들이 선언되어 있음 namespace WebBrowserStuff { clear WebBrowser { ... }; ... // '핵심' 관련 기능, 이를테면 거의 모든 사용자가 // 써야 하는 비멤버 함수들이 여기에 들어갑니다. } // "webbrowserbookmarks.h" 헤더 namespace WebBrowserStuff { ... // 즐겨찾기 관련 편의 함수들이 여기에 들어갑니다. } // "webbrowsercookies.h" 해더 namespace WebBrowserStuff { ... // 쿠키 관련 편의 함수들이 여기에 들어갑니다. } | cs |
표준 C++ 라이브러리가 이러한 구조로 구성되어 있습니다. std 네임스페이스에 속한 모든 것들이 <C++StandardLibrary> 헤더 같은 것에 모조리 들어가 한 통으로 섞여 있지 않고, 몇 개의 기능과 관련된 함수들이 수십 개의 헤더(<vector>. <algorithm>, <memory> 등)에 흩어져 선언되어 있습니다. vector 기능만 필요한 사용자는 굳이 <memory>를 #include할 필요가 없으며, list를 사용하지 않는 사용자는 <list>를 #include하지 않아도 됩니다. 이렇게 하면, 사용자가 실제로 사용하는 구성요소에 대해서만 컴파일 의존성을 고려할 수 있게 되는 거죠. 반면 클래스 멤버 함수로 오게 되면 하나의 클래스는 그 전체가 통으로 정의되어야 하기에 여러 조각으로 나누지 못하여 앞과 같은 방식으로 기능을 쪼개는 것이 불가능합니다.
편의 함수 전체를 여러 개의 헤더 파일에(그러나 하나의 네임스페이스에) 나누어 놓으면 편의 함수 집합의 확장(extend)도 손쉬워집니다. 해당 네임스페이스에 비멤버 비프렌드 함수를 원하는 만큼 추가해 주기만 하면 그게 확장입니다. 클래스는 역시 이런 부분은 제공이 불가능합니다. 클래스 정의 자체를 사용자가 확장할 수는 없으니까요. 물론 새로운 클래스를 파생시킬 수 있기는 하지만, 파생 클래스는 기본 클래스 안에 캡슐화된(다시 말해 private) 멤버에 대한 접근권한이 없기 때문에, 이런 식의 '확장 기능'은 약간 아쉽습니다.
멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 합시다. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어납니다.