[Effective C++] 23. 멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자

C++/Effective C++

2020. 8. 29. 21:36

더보기

개인적으로 Effective C++ 보면서 무릎을 탁 쳤던 기법중 하나였다. 세 손가락 안에 들정도로 황홀했다.

 

class WebBrowser{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCokies();
    ...
};
// 한 번에 처리 (멤버 함수)
class WebBrowser(){
public:
    ...
    void clearEveryThing();
    ...
};
// (비멤버 함수)
void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

 

이 항목에서 중요하게 다루어지는 점은 캡슐화이다. 객체 지향 법칙에 관련된 이야기를 찾아보면 데이터와 그 데이터를 기반으로 동작하는 함수는 한 데 묶여 있어야 하며, 멤버 함수가 더 낫다고들 하지만, 불행히도 이 제안은 틀렸다. 위의 clearEveryThing() 함수와 clearBrowser() 함수중에 어느쪽이 캡슐화가 잘 되어 있는가? 바로 clearBrowser()이다.

비멤버 함수가 왜 더 캡슐화가 잘 되어있는지 알아보기 이전에 비멤버 함수가 가져다주는 특성들을 살펴보자.

 

  • 패키징 유연성
  • 컴파일 의존도 감소
  • 더 나은 확장성 제공

먼저 캡슐화에 대해서 이야기를 해야한다. 저번 항목에서 엄청나게 강조했지만 한 문장으로 요약하자면 캡슐화는 이미 있는 코드를 바꾸더라도 제한된 사용자들밖에 영향을 주지 않는 융통성을 확보할 수 있는 수단이다.

 

다시, 위의 예시 코드를 보자. 똑같은 기능을 제공하는데 멤버 함수(그 클래스의 private 데이터 멤버뿐만 아니라 private 멤버로 되어 있는 다른 함수, 나열자, typedef 타입 등등을 모두 접근할 수 있는)를 쓸것이냐, 아니면 비멤버 비프렌드 함수(어느 것도 접근할 수 없는)를 쓸 것이냐를 이제 다시 생각해 봐야한다.

 

당연히 비멤버 비프렌드 함수가 캡슐화 정도가 높을 것이다. 왜냐하면 비멤버 비프렌드 함수는 어떤 클래스의 private 멤버 부분을 접근할 수 있는 함수의 개수를 늘리지 않기 때문이다. (어떤 데이터를 접근하는 함수가 많으면 그 데이터의 캡슐화 정도를 낮다고 말한다.)

 

요약하자면, 클래스 내에서 데이터 멤버와 같은 다양한 정보들이 공유 되지 말아야 할 경우에도 멤버 함수는 접근을 할 수 있기 때문에 캡슐화가 비멤버 비프렌드 함수에 비해 덜하다는 것이다.

 

하지만, 주의해야 할 점은 "함수는 어떤 클래스의 비멤버가 되어야 한다"라는 주장이 "그 함수는 다른 클래스의 멤버가 될 수 없다"라는 의미가 아니라는 것이다. 예를 들어서 clearBrowser 함수가 WebBrowser 클래스의 멤버가 아니기만 하면 된다는 것이다!

 

c++에서는 이를 namespace를 통해서 구현할 수 있다.

namespace WebBrowserStuff{

    class WebBrowser{...};
    void ClearBrowser(WebBrowser& wb);
    
    ...
}

 

이 방식은 자연스러운 구현 이외에도 엄.청.난 효과를 주는데, 네임스페이스를 통해서 클래스와는 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있다는 점이다! ClearBrowser 같은 함수 (편의 함수)는 멤버도 아니고 프렌드도 아니기에, WebBrowser 사용자 수준에서 아무리 애를 써도 얻어낼 수 없는 기능은 이들도 제공할 수가 없다. 예를 들어, clear-Browser가 없다고 해도 사용자는 그냥 clearCache,clearHistory,removeCookies를 알아서 불러주면 되는 것이다.

 

만약 사용자가 즐겨찾기 기능에만 관심이 있다면, 나머지 두 함수에 관심을 가질 필요가 없다. 구태여 다른 함수들에 대한 컴파일 의존성을 고민할 이유가 없다는 것이다. 각 함수들을 나누어 놓는 깔끔한 방법은, 즐겨찾기 관련 편의 함수를 하나의 헤더 파일에 몰아서 선언하고, 쿠키 관련 편의 함수는 다른 헤더 파일에 몰아서 선언하고, 인쇄 관련 편의 함수는 제 3의 헤더에 몰아서 선언하는 것이다.

 

// webbrowser.h 헤더
namespace WebBrowserStuff{
    class WebBrowser{...};
	...
    // 핵심 관련 기능 : 모든 사용자가 써야하는 비멤버 함수 정의
}

// "webbrowserbookmarks.h" 헤더
namespace WebBrowserStuff{
...
}

// "webbrowsercookies.h" 헤더
namespace WebBrowserStuff{
...
}

...

 

이렇게 다른 헤더 파일에 함수들을 흩뿌려놓으면 특정 기능만을 필요로하는 사용자가 컴파일 의존성을 고려할 수 있게 된다.

 

더보기

헤더 파일에 다른 헤더 파일들을 include 시키다 보면 include 한 파일들에서 변경 사항이 생길 때마다 다른 모든 파일들이 재컴파일 된다. 따라서, 하나의 파일에 다른 파일이 include 된 정도를 컴파일 의존성이라고 한다.

 

편의 함수 전체를 여러 개의 헤더 파일에 나누어 놓으면 편의 함수 집합의 확장도 손쉬워진다. 해당 네임스페이스에 비멤버 비프렌드 함수를 원하는 만큼 추가해 주기만 하면 그게 확장이다.

 

이것만은 잊지 말자!

  • 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 합시다. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어납니다.

 

이번 장 예시

// Test.h
#pragma once

namespace cppTest {
	class Test
	{
	public:
		int GetX()const { return x; };
		int GetY()const { return y; }
		Test() { x = 1; y = 1; }
	private:
		int x, y;
	};
}


// Test.cpp
#include "Test.h"
namespace cppTest {
	int GetNextCoordX(const Test& test) {
		return test.GetX() + 1;
	}
}

// Coord.h
#pragma once
namespace cppTest {
	class Test;
	int GetNextCoordX(const Test& test);
}

// main.c
// ConsoleApplication3.cpp : 이 파일에는 'main' 함수가 포함됩니다. 거기서 프로그램 실행이 시작되고 종료됩니다.
//

#include <iostream>
#include "Test.h"
#include "Coord.h"
using namespace std;
using namespace cppTest;
int main()
{
	Test* t = new Test();
	cout << t->GetX() << "," << t->GetY() << endl;
	cout << GetNextCoordX(*t);
}

 

생각보다 꽤 헤맸다. 일단 이런식의 구현이 이번 항목에서 말하는 바가 맞는지조차 모르겠다. (아시는분 댓글좀 ㅠ)

결론은 선언은 다른 헤더 파일에서 하고 정의는 기본이 되는 Test라는 cpp 파일에서 했다. 구현 자체를 Coord 헤더파일에서 하려다가 순간, "이거 상호참조아닌가?" 해서 컴파일 의존성을 줄이는 방법인 전방 선언 (네임스페이스 밖에다 하면 메인 함수에서 모호하다고 한다 ㅠ 이것 때문에도 헤맴.)을 해줬다. 음.. 근데 지금 생각해보니 coord cpp 파일을 만들면 그만 아닌가? 어이없는 짓을 하고 만 것같다.. 그냥 구현은 coord.cpp 를 만들고 해주면 되는 것을 ㅡㅡ;

 

책에서 std 방식이 비멤버 비프렌드 함수 형태로 이루어져있다고 하였는데, 메인 함수 (사용자) 기준에서 필요한 함수 (GetNextCoordX)를 사용하기 위해서는 해당 헤더 파일을 include 해야 하고, 필요 없다면 include 해주지 않으면 된다. 여기까지 보면 대강 맞는 것 같다. 책에서는 이 이상의 구현 (위의 코드 정도도)을 해 주지 않아서 확신이 서지는 않지만 

캡슐화,파일 분리, 컴파일 의존성 제거등이 적용된 것을 보면 그대로 사용해도 무리가 없을 것 같다!

 

그리고, 삽질하다가 링크오류가 나서 무슨 삽질을 한거지 했는데.. 이것 저것 만지다 선언은 const를 안붙이고 정의에는 const를 붙이는 바람에 오류가 난 것.. 이런 오류는 발견하기 어렵다. 

https://sanghun219.tistory.com/132?category=930812 여기서 const로 오버로딩이 가능하다는 얘기를 했는데, 아마 뒷장 어디선가 const와 관련된 구현 문제를 다루었던 것 같다. 그 때 다시 포스팅하기로 하겠다.

 

이번 항목은 굉장히 신중히 읽고, 코드도 짜보았는데 그 이유는 서버 코드를 짤 때 참고했던 현직 서버 개발자분께서 실제로 이런 방식의 코드를 작성하셨기 때문이다. 저번 컴투스 인턴 제출용 포폴에서도 사용하였다. 기억이 맞다면 해시 맵의 키에 따른 함수포인터로 설정된 함수들을 namespace와 파일을 통해 분리 시켰었다. 지금 생각해 보면 헤더 파일의 묶음 단위가 별로였던 것 같다.

 

아무튼 내가 생각하는 effective c++ 항목중 세 손가락 안에 드는 좋은 기법이었다. 

하나는 상수 객체 반환에 대한 기법이었고, 나머지 하나는 아직 포스팅하지 않은 상속 관련 기법이었다.

상수 객체 반환이야 늘 쓰고는 있었지만 정확한 이유를 알지 못했기에 순위에 넣었고 상속은 정말 입이 딱 벌어지는 기법인 것으로 기억한다.. 조만간에 해당 카테고리 포스팅이 요란하게 느껴진다면 이 기법임을 알면 되겠다.