Introduction
C++을 학부 수업에서 처음 접할 때 아마 모두가 같은 코드를 적을 것입니다. 차이가 있더라도 굉장히 미미할 것이라고 생각되는데요. 아니라면 세대 차이에 충격받을… 일단 코드부터 보시죠.
#include <iostream>
using namespace std;
int main()
{
cout << “Hello world!” << endl;
return 0;
}
<iostream>
의 경우 라이브러리임을 알 수 있고, main
함수 내부에는 무언가 출력문이 있구나 유추할 수 있습니다. 대부분 다른 프로그래밍 언어에서도 같은 동작을 배우기에 이제는 꾼들이 되었잖아요? 그런데 한 가지 알 수 없는 구문이 있는데요. using namespace std
의 경우 조금 낯설 수 있습니다. 오늘은 이 표현이 나타내는 이름 공간(namespace)이 무엇인지 살펴보도록 하겠습니다.
이름 공간 (namespace)
우리의 친절한 이웃 위키피디아에 이름 공간을 검색해보면 아래와 같이 알려주고 있습니다.
이름공간 또는 네임스페이스(namespace)는 개체를 구분할 수 있는 범위를 나타내는 말로 일반적으로 하나의 이름 공간에서는 하나의 이름이 단 하나의 개체만을 가리키게 된다.
이런 게 왜 필요한 걸까요? 사실 학부 수준의 프로젝트나 이후에도 개인이 진행하는 토이 프로젝트에서는 네임 스페이스는 사실 필요성이 떨어집니다. 좀 과격하게 말하면 몰라도 됩니다. 그러나 오픈 소스 라이브러리를 활용하거나, 다른 사람들과의 협업이 필요한 경우 네임 스페이스 관리는 필수적인데요. 그 이유에 대한 예시를 보기 전에 using namespace std
해석을 통해 네임 스페이스 해석 관련해 잠시 짚어보도록 하겠습니다.
using namespace std;
std
는 C++ 표준 라이브러리의 모든 함수, 객체가 정의된 자체 네임 스페이스입니다. 프로그래머의 편의를 위해 C++ 자체적으로 제공하는 여러 라이브러리들의 함수들을 모두 포함하는 네임 스페이스입니다.
using
은 그러면 어떤 기능을 제공할까요? 정답은 어떠한 네임 스페이스, 혹은 그 안에 존재하는 함수를 지정해 이 소스 코드에서는 사용하겠다고 당당하게 선언하는 문법입니다. 이제 컴파일러는 std
네임 스페이스에 속한 모든 함수와 변수의 이름을 갖는 코드들은 모두 해당 네임 스페이스의 그것들로 인식을 하게 됩니다. 그러면 네임 스페이스 지정을 하지 않는다면, 위 예제 코드는 어떻게 작성할 수 있을까요?
using
미사용
#include <iostream>
int main()
{
std::cout << “Hello world!” << std::endl;
return 0;
}
using
사용,namespace
미지정
#include <iostream>
using std::cout, std::endl;
int main()
{
cout << “Hello world!” << endl;
return 0;
}
첫 예제와 바로 위의 두 예제는 모두 같은 결과가 나오게 됩니다. 전체 네임 스페이스 지정 대신 이미 알고 있는 네임 스페이스라면 개별적으로 지정해 줄 수 있으며, 혹은 네임 스페이스 내 몇 개의 함수들만 사용하도록 선언할 수 있습니다. 이 경우 ::
(스코프 지정 연산자)를 이용해 네임 스페이스 + 함수 이름 형태로 호출할 수 있습니다.
이름 공간은 왜 사용하는데?
위에서도 짧게 언급했는데요. 네임 스페이스는 학부, 개인 수준의 프로젝트에서는 필수적이지 않습니다. 오히려 설계할 때 고려할게 불필요하게 추가되어 머리만 아플 수 있습니다. 반면에 여럿이 같이 프로젝트를 진행하는 경우 함수나 변수 이름이 서로 충돌할 수 있습니다. 엄격한 코딩 컨벤션을 합의한 바람직한 조직일수록 프로그래머들의 빈약한 상상력으로 인해 이름이 겹치는 사고가 많이 발생할 수 있습니다.
이럴 때 개발자마다 네임 스페이스를 포함해 자신의 코드를 작성하면 병합 과정에 하나 하나 수정하는 불상사를 피할 수 있는데요. 아래 예시와 함께 살펴보도록 하겠습니다.
개발자 A와 B는 공동으로 어떠한 게임을 개발 중인데요. A는 플레이어 관련 기능들을, B는 시스템 관련 기능들을 구현 중입니다. 이제 개발한 코드를 병합해 테스트를 하려 하는데 아래와 같이 함수명이 겹쳐버렸습니다.
#include <iostream>
#include <string>
using std::cout, std::endl;
void resetToZero(int &hp) { hp = 0; }
void resetToZero(int &play_time) { play_time = 0; }
int main()
{
int hp = 10, play_time = 100;
resetToZero(hp);
resetToZero(player);
cout << hp << endl
<< play_time << endl;
return 0;
}
C++ 이 기본적으로 함수 오버로딩을 지원하지만, 파라미터 개수와 그 자료형마저 같은 경우 컴파일러가 구분할 방법이 없는데요. 여기서 함수 오버로딩은 같은 이름의 함수에 파라미터를 다르게 사용해 다양한 입력에 대해 동일한 기능을 지원하는 방식입니다. 이에 대해서는 후에 자세히 다룰 예정이라 지금은 이 정도로만 언급하고 넘어가겠습니다.
다시 본론으로 돌아와서 이런 경우 어떻게 해야 할까요? 사실 위 예시뿐 아니라 다양한 함수의 경우 플레이어와 시스템은 같은 기능이 필요한 경우가 많습니다. 단적인 예시로 initialize
, addItem
, saveCurrentStatus
등 많은 기능들이 세부적으로는 다르지만 동작 관점에서는 크게 다르지 않죠.
이럴 때 네임 스페이스의 효용성은 극대화 됩니다. 각 개발자들이 자신이 설계하는 영역의 네임 스페이스만 지정해 준다면, 세부적인 사항은 더 이상 고려할 필요가 없게 됩니다. 운이 없게 네임 스페이스가 이름이 같은 상황이 발생하더라도, 함수 하나하나 수정하는 것보다 네임 스페이스 이름만 변경해 주는 것이 훨씬 품이 적게 듭니다. 아래는 네임 스페이스를 적용한 예시입니다.
#include <iostream>
#include <string>
using std::cout, std::endl;
namespace Player
{
void resetToZero(int &hp) { hp = 0; }
}
namespace System
{
void resetToZero(int &play_time) { play_time = 0; }
}
int main()
{
int hp = 10, play_time = 100;
Player::resetToZero(hp);
System::resetToZero(play_time);
cout << hp << endl
<< play_time << endl;
return 0;
}
중첩된 이름 공간
C++17부터는 네임 스페이스가 중첩된 케이스(nested namespace)의 가독성을 많이 향상시켰는데요. 사실 이 중첩된 네임 스페이스를 개인적으로 사용한 경험은 없습니다. 네임 스페이스를 사용할 일이 생기면 최대한 쪼개서 사용하는 걸 선호하기 때문인데요. 그래도 알아두면 좋으니 간단하게만 소개하려고 합니다.
우선 C++17 이전에 중첩된 네임 스페이스를 사용하려면 아래와 같이 적어줬어야 합니다.
namespace Game {
namespace System {
namespace Map {
// Code
}
}
}
코드가 매우 길어짐과 동시에 저처럼 nested syntax에 알러지가 있는 사람은 소스 코드를 닫아버리고 싶은 충동이 들기 마련입니다. 다행히 이러한 점이 많은 개발자들의 공통적인 불만이었는지 C++17부터는 아래와 같이 ::
(scope resolution operator)를 이용해 편리하게 표현할 수 있게 되었습니다.
namespace Game::System::Map {
// Code
}
또한, namespace alias를 사용하면 Python에서 import <library> as <alias>
를 사용해 라이브러리를 편리하게 임포트하는 것처럼 네임 스페이스를 더 짧고 편하게 호출할 수 있습니다.
namespace GameMap = Game::System::Map;
이름 공간 사용 시 주의 사항
네임 스페이스 사용 시 몇 가지 주의할 점이 있는데요. 안 지킨다고 보드가 타거나 런타임 에러가 발생하지는 않지만, 원인을 찾기 어려운 경우도 간혹 있어 코드를 작성하기 전에 한 번은 리마인드 하시면 좋을 것 같습니다.
- 헤더 파일 안에서는
using
문 사용을 기피해야 한다
헤더 파일 내부에서using
을 사용하게 되면 해당 헤더 파일을 인클루드하는 모든 파일에서 헤더 파일에 적용된 네임 스페이스를 계승하게 됩니다. 그러면 원치 않는 네임 스페이스 사용이 자기도 모르는 사이에 적용될 수도 있고, 이 경우 사용자가 생성한 함수와 네임 스페이스 간에 충돌이 발생할 수 있습니다. 헤더 파일에using
을 사용하는 것은 부비 트랩이 설치된 선물을 전달하는 것과 같습니다.
using namespace <namespace>
사용은 기피해야 한다
네임 스페이스 전체를using
문으로 호출하는 행위는 기피하는게 좋은데요. 대표적으로 스탠더드 네임 스페이스가 그렇습니다.std
네임 스페이스에는 STL에서 사용하는 모든 함수들이 정의되어 있는데요. 이는 우리가 상상하는 것 이상으로 방대합니다. 따라서, 해당 구문을 사용하면 우리는 그 모든 함수 이름을 배제하고 새로운 함수 이름을 지어야 하는 거죠. 사실 더 큰 문제는 어떤 함수들이 들어있는지조차 가늠이 안 된다는 것입니다. 그렇기에 네임 스페이스를 호출하기보다는 해당 네임 스페이스 내의 특정 함수를::
연산자로 선언하기를 권장합니다.