Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-17 00:00
관리 메뉴

nomad-programmer

[Programming/C++] 네임스페이스 (namespace) 본문

Programming/C++

[Programming/C++] 네임스페이스 (namespace)

scii 2023. 1. 24. 23:23

프로젝트의 크기가 커지게 되면 소스 파일의 수도 많아지고, 또 소스 파일들 안에서 정의도니 수많은 변수, 함수, 구조체, 클래스, 열거체 등이 존재하게 된다. 이런 경우에는 관련된 코드별로 그룹을 나누어서 관리하면매우 효율적인데 이때 네임스페이스(namespace)가 관련된 코드를 나누어 담을 수 있는 논리적인 가방의 역할을 한다.

왜 네임 스페이스를 사용해야 할까?

실세 현장에서 개발하는 프로젝트는 수백 개 이상의 소스 파일로 이루어져 있다. 그리고 이 파일들 안에는 수천, 수만 개의 이름이 존재한다. 변수, 함수, 클래스, 구조체, 열거체, 타입 재정의(typedef)를 사용할 때마다 새로운 이름이 생겨나는 것이므로 충분히 수만 개에 달할 수 있다.

중요한 것은 이렇게 정의한 이름들이 모두 한 공간에 위치한다는 점이다. 전역 공간에서 정의한 이름들이라면, 서로 다른 소스 파일에 위치한다고 해도 컴파일러의 입장에서는 모두 한 곳에 정의한 것이나 다름 없다. 예를 들어 서로 다른 두 개의 파일에 a라는 이름의 전역 변수를 각각 정의하더라도 컴파일러는 동일한 이름의 변수를 두 번 정의했다는 에러를 발생시킨다.

이런 상황은 디렉토리 개념이 없는 하드디스크에 비유할 수 있다. 만약 하드디스크에 디렉토리를 만들 수 없다고 가정해보자. 그러면 하드 디스크에 있는 모든 파일이 한 공간에 존재하는 셈이다. 분명 많은 파일들이 서로 이름 충돌을 일으킬 것이고, 혼잡하게 뒤섞인 파일에서 필요한 것을 꺼내오는 것은 힘든 일이 될 것이다.

C++에서는 네임스페이스가 디렉토리와 같은 역할을 해준다. 전역 공간에 존재하는 수많은 코드들을 네임스페이스라는 가상의 디렉토리에 넣어서 보관하게 되면 이름이 충돌할 염려도 없거니와 관련된 코드들을 한 곳에 모을 수 있어서 좋다.

가장 큰 장점은 소스 파일과 코드를 구조적으로 관리할 수 있다는 점에 있다. 

네임스페이스의 기본적인 사용법

일반적으로 전역 공간에 정의할 수 있는 것이라면 무엇이든지 네임스페이스 안에서도 정의할 수 있다. 

namespace Dog {
    struct Info {
        char name[20];
        int age;
    };

    Info dogs[20];
    int count;

    void CreateAll() {};
}

namespace Cat {
    class Info {
    public:
        void Meow();

    protected:
        char name[20];
    };

    Info cats[20];
    int count;

    void CreateAll() {};
}

네임스페이스 안에 이름들을 정의하는 방법은 아주 간단하다. namespace 키워드를 사용해서 네임스페이스의 이름을 적어주고 중괄호로 블럭을 만들면 된다. 그러면 네임스페이스 블럭 안에서 정의한 이름들은 해당 네임스페이스 안에 소속된다.

참고로 네임스페이스 안에서 정의했더라도 전역 변수에 해당한다. 접근 범위와 존속 기간 역시 전역 변수와 동일하다. 네임스페이스 안에서 정의했기 때문에 사용할 때 부가적으로 할 일이 있기는 하지만 여전히 모든 파일에서 접근할 수 있다.

위의 예제에서 살펴볼만한 것은 같은 이들이 여러 번 등장한다는 점이다. Dog, Cat의 네임스페이스에서 Info, cout, CreateAll() 등의 이름이 중복된다. 하지만 예제를 실행해보면 컴파일러는 아무런 오류를 발생시키지 않는다. 서로 다른 네임스페이스라면 얼마든지 동일한 이름을 사용할 수 있기 때문이다. 

int main() {
    Cat::CreateAll();
    Cat::cats[0].Meow();

    Dog::CreateAll();
    int dog_count = Dog::count;

    return 0;
}

위의 코드를 보면 Cat::과 같은 문법이 보인다. 이것은 'Cat 네임스페이스에 소속된...' 이라는 표현이다. 클래스의 멤버 함수를 정의할 때와 같은 표현을 사용한다. 
:: 라는 연산자는 실제로 '영역 지정 연산자(Scope resolution operator)' 라는 이름이 있다. 그리고 이와 같이 클래스의 멤버를 가리키거나 네임스페이스에 소속한 이름을 가리킬 때 사용한다.

using namespace Cat;

위의 코드를 사용하면 앞으로 말하는 이름들은 Cat 네임스페이스에 소속한 이름들이다. 라고 알려주는 것이다. 그러므로 아래의 코드처럼 사용할 수 있다.

int main() {
    CreateAll();
    cats[0].Meow();

    Dog::CreateAll();
    int dog_count = Dog::count;

    return 0;
}

그렇기 때문에 그냥 CreateAll()이라고만 적어주어도 Cat::CreateAll()을 의미하게 된다.

* 참고로 using 키워드를 사용해서 네임스페이스를 지정한 경우라도 Cat::CreateAll() 과 같은 표현을 사용할 수 있다.

using Cat::CreateAll;

int main()
{
    CreateAll();
}

위와 같이 using을 선언한다면 Cat 네임스페이스에 소속한 CreateAll을 의미한다고 알려주는 것이다. 따라서 CreateAll()이라고만 적어주어도 컴파일러는 혼동하지 않고 Cat::CreateAll() 를 호출하게 된다.


전역 공간에서 정의한 변수를 직접 사용하는 방법

int foo = 10;

int main() {
    int foo = 55;

    cout << "local variable: " << foo << endl;
    cout << "global variable: " << ::foo << endl;

    return 0;
}


// 결과
local variable: 55
global variable: 10

::count 처럼 영역 지정 연산자(::) 앞에 아무런 이름도 적어주지 않으면 전역 공간에서 정의한 변수를 의미하게 된다. 
가장 최근에 정의한 변수가 이전에 정의한 변수를 숨기는 것이 C++의 규칙이다. 하지만 위와 같이(::count) 사용한다면, 같은 이름의 지역 변수가 있는 경우라도 직접적으로 전역 변수를 사용할 수 있게 된다.


C++ 표준 라이브러리와 std 네임스페이스

cout 객체 사용법 분석

cout 객체를 사용할 때마다 <iostream> 헤더 파일을 포함시키는 것뿐만 아니라 다름과 같은 코드도 적어주었다.

using namespace std;

위의 코드는 "이제부터 std 네임스페이스에 있는 이름들을 그냥 사용하겠다" 라는 뜻이 된다. 여기서 짐작할 수 있는 것은 cout, cin을 비롯한 C++ 표준 라이브러리와 관련된 코드들은 std라는 이름의 네임스페이스 안에서 정의되어 있다는 점이다. 

cout 객체가 std에 속해 있다는 사실을 모를 사람은 없겠지만, std 네임스페이스 전체를 지정하는 것보다는 std:: 를 붙여서 사용하는 방법이 보다 명확하고 불필요한 이름 충돌을 막을 수 있다.


네임스페이스와 소스 파일의 관계

네임 스페이스와 소스 파일 중에서 어느 것이 더 큰 범주에 속할까? 다시 말해 한 네임스페이스 안에 여러 파일이 포함될 수 있을까, 아니면 한 파일 안에 여러 네임스페이스가 포함될 수 있을까?

정답은 둘 다 아니다. 네임스페이스와 소스 파일은 독립적인 관계다. 하나의 네임스페이스가 여러 파일에 걸쳐서 존재할 수도 있고, 한 파일 안에 여러 네임스페이스가 존재할 수도 있다. 이 관계를 이해해두는 것이 실제로 네임스페이스를 사용할 때 도움이 되므로 아래 그림을 통해 정리해보자.

하나의 파일에 존재하는 여러 개의 네임 스페이스

이번에는 C++의 표준 라이브러리를 예로 들어보자. 표준 라이브러리와 관련된 코드들은 모두 std 네임스페이스 안에 소속되어 있지만 이 코드들은 수백 개의 소스 파일에 나뉘어서 존재한다. 그림으로 표현하면 다음과 같다.

하나의 네임스페이스 안에 속하는 여러 소스 파일들

실제로 네임스페이스를 사용할 때는 백이면 백 후자와 같은 방식을 사용한다. 왜냐하면 네임스페이스의 목적이 수 많은 파일과 코드를 관련된 것끼리 모으는 것이기 때문이다.
그렇기 때문에 '네임스페이스가 여러 소스 파일을 담는 논리적인 가방' 이라고 생각하면 된다.

// cat.h

#pragma once

namespace Cat {
    class Info {
    public:
        void Meow();

    protected:
        char name[20];
    };

    extern Info cats[20];
    extern int count;

    void CreateAll();
}
// cat.cpp

#include "cat.h"

namespace Cat {
    Info cats[20];
    int count;

    void Info::Meow() { }

    void CreateAll() { }
}
// main.cpp

#include "cat.h"
#include <iostream>

using namespace std;

int main() {
    Cat::CreateAll();

    return 0;
}

이 예제에서 가장 중요한 부분은 cat.h와 cat.cpp 모두에서 네임스페이스 블럭을 만들어주었다는 점이다. 서로 다른 파일에서 만든 Cat 네임스페이스지만 컴파일러 입장에서는 모두 동일한 Cat 네임스페이스가 된다. 이런 방식을 사용해서 수 많은 파일에 걸쳐서 존재하는 코드들을 하나의 네임스페이스 안에 담을 수 있는 것이다.

cat.h에 있는 변수에 extern 키워드를 붙인 이유

extern 키워드를 붙이는 것의 의미는 다른 파일에서 정의한 변수를 사용하겠다고 선언하는 의미를 가지고 있다. cat.h에서 extern 키워드를 붙인 이유도 전혀 다를 것이 없다.
다른 모든 헤어 파일이 그렇듯이 cat.h 파일의 용도는 main.cpp와 같은 파일에 의해서 포함되기 위한 것이다. 즉, main.cpp에서 cat.cpp에 정의된 cats나 count 변수에 접근할 수 있도록 만드는 역할을 한다.


네임스페이스가 가진 그 밖의 기능

이름 없는 네임스페이스

네임스페이스 블럭을 만들 때 네임스페이스의 이름을 적지 않는 것도 가능하다. 이렇게 하면 네임스페이스에 소속된 이름들을 다른 파일에 숨기는 결과를 가져온다. 다음 예제를 보도록 하자.

// test.cpp

namespace {
    int a;
}

void Func() {
    a = 555;    // 성공
}
// main.cpp

// test.cpp에서 정의한 a를 사용하기 위한 준비
extern int a;

int main() {
    a = 555;    // 실패
    
    return 0;
}

test.cpp를 보면 변수 a가 이름 없는 네임스페이스 안에서 정의되었다. 네임스페이스 안에서 정의되었지만 이름이 없는 네임스페이스기 때문에 test.cpp에서의 'a = 555;' 처럼 사용하는 것이 가능하다. 하지만 이는 어디까지나 같은 파일의 경우고 다른 파일의 경우에는 사정이 다르다.

main.cpp를 보면 다른 파일에 정의된 변수 a를 사용하기 위해서 extern 키워드를 사용하고 있다. 하지만 정작 'a = 555;' 처럼 a변수를 사용하려고 하면 그런 변수가 없다는 오류가 발생한다. 

그리고 이것이 바로 이름 없는 네임스페이스의 용도가 된다. 이름 없는 네임스페이스를 사용하면 다른 파일에서 접근할 수 없게 만들 수 있다.

static 키워드를 사용해서 전역 변수나 함수를 정의한 경우에도 동일한 효과를 얻을 수 있다. 실제로 두 가지 방법 모두 가능한데 현재로서는 static 키워드를 사용하는 개발자들이 대부분이다. C언어에서부터 사용하던 방법이기 때문이다. 하지만 C++ 표준 문서에서는 이름 없는 네임스페이스를 사용할 것은 권장하고 있기 때문에 "이름 없는 네임스페이스의 사용을 추천" 하고 싶다.


중첩된 네임스페이스

네임스페이스 안에 또 다른 네임스페이스를 만드는 것이 가능하다.

#include <iostream>

using namespace std;

namespace Data {
    namespace User {
        int number;
    }
}

int main() {
    int user_num = Data::User::number;

    return 0;
}

중첩된 네임스페이스에 접근하기 위해서는 반복적으로 영역 지정 연산자를 사용하면 된다. Data::User::number 라고 적어주면 'Data 네임스페이스 안에 User 네임스페이스 안에 정의한 변수 number' 를 의미하게 된다.


네임스페이스를 별명으로 부르기

네임스페이스의 이름이 너무 긴 경우에는 간단한 별명을 붙여준 후, 그 별명을 대신 사용할 수 있다.

#include <iostream>

using namespace std;

namespace aaa_bbb_ccc_ddd_eee_fff_ggg {
    int n;
}

// 별명을 붙여준다.
namespace abcdefg = aaa_bbb_ccc_ddd_eee_fff_ggg;

int main() {
    abcdefg::n = 55;

    return 0;
}

aaa_bbb_ccc_ddd_eee_fff_ggg 라는 긴 이름의 네임스페이스에 abcdefg라는 짧은 별명을 붙여주고 있다. 이제 이 네임스페이스는 abcdefg라는 별명으로 부를 수 있게 된다.

긴 이름의 네임스페이스 뿐만 아니라 여러 번 중첩된 네임스페이스에도 별명을 붙여줄 수 있다. 예를 들어 Data::User::Free와 같이 접근해야 하는 네임스페이스가 있다고 생각해보자. 이때는 다음과 같은 방식으로 별명을 붙여서 사용할 수 있다.

namespace foo = Data::User::Free;

네임스페이스는 사용법이 아주 간단하면서도 소스 코드를 구조적으로 만드는 데 많은 도움을 주는 기능이다. 

Comments