본문 바로가기

러스트의 모듈 시스템과 접근 제어

기술적인 이야기/기타 개발 2020. 7. 29.
반응형

모듈화는 프로그래밍에 있어 정말 중요한 요소입니다. 코드의 가독성과 함께 유지보수의 편리성에도 영향을 끼치지만, 그 자체로 설계가 되어 기능과 성능 자체에도 영향을 끼치지요. 러스트도 그래서 당연히 모듈화를 지원합니다.

다만 러스트의 모듈 시스템은 좀 독특한 편입니다. 몇 가지 시험을 해보면서 알아낸 내용을 정리해 보겠습니다.

러스트 모듈 시스템의 기초

러스트에서 모듈은 mod라는 키워드를 이용해 코드 어디서든 정의할 수 있습니다.

mod foo {
    pub fn public_foo() {
        println!("foo::public_foo!")
    }

    fn private_foo() {
        println!("foo::private_foo!")
    }
}

fn main() {
    foo::public_foo();
}

위 예제는 main 함수가 있는 main.rs 파일에 모듈을 정의했다고 가정합니다.

잉 이게 모듈이야? 라고 생각할 수도 있는데, 러스트는 모듈은 기본적으로 네임스페이스(namespaces)를 분리시키는 개념에 가깝습니다. 그리고 분리된 네임스페이스를 :: 오퍼레이터를 이용해 구분합니다. 따라서 foo::public_foo() 명령은 foo 네임스페이에 정의된 public_foo() 심볼을 실행시키라는 의미가 됩니다.

여기서 pub라는 키워드가 보입니다. 이 키워드는 public 즉 외부에 이름을 공개하는 기능입니다. 다른 언어에서도 흔히 볼 수 있는 접근 제어(Access Control)와 관련된 기능으로 볼 수 있습니다. 러스트의 모듈 시스템은 이 접근 제어 시스템도 함께 알아야 설명이 가능할 것 같습니다만 사실 이게 전부라 어려울 건 없을 것 같습니다.

어쨌든 모듈 외부에서 사용이 가능한 함수나 서브 모듈은 모두 이 pub 키워드를 명시해야 하며, 그 외의 함수나 모듈은 모두 private로 인식되어 외부에서는 사용할 수 없는 기능이 됩니다.

다만 동일 파일에 존재하는 최상위 모듈은 동일한 네임스페이스에 있기 때문에 위처럼 pub 표기가 되어있지 않아도 사용할 수 있다는 것을 알 수 있습니다.

 

모듈 계층 구조

디렉터리 구조처럼 러스트의 모듈 시스템도 계층(hierarchy) 구조를 지원합니다. 즉 모듈 안에 모듈을 선언할 수 있습니다.

mod foo {
    pub fn public_foo() {
        println!("foo::public_foo!")
    }

    fn private_foo() {
        println!("foo::private_foo!")
    }

    mod private_bar {
        pub fn public_bar() {
            println!("foo::private_bar::public_bar!")
        }
    }

    pub mod bar {
        pub fn public_bar() {
            println!("foo::bar::public_bar!")
        }
    }
}

fn main() {
    foo::public_foo();
    foo::bar::public_bar();
}

위 예에서 mod로 분리된 모듈 내부에서 또 mod를 선언하는 모습을 볼 수 있습니다. 따라서 깊이에 상관없이 복잡한 모듈 구조를 마음껏 디자인할 수 있다는 것을 알 수 있었습니다. foo::bar::public_bar() 코드처럼 깊은(?) 모듈 안의 함수 호출 방법도 크게 차이가 없는 모습을 볼 수 있습니다.

이번에도 같은 파일에 있는 모듈이지만, 모듈 안의 모듈은 기본적으로 private가 되기 때문에 현재의 main 함수에선 pub가 붙어있지 않은 foo::private_bar 모듈에는 접근할 수 없다는 것도 확인할 수 있었습니다.

 

외부 파일로 모듈 분리하기

사실 한 파일 안에 모듈을 같이 선언하는 것은 좀 무의미하고 무책임한(?) 행위입니다. 모듈은 애초에 코드를 용도에 맞게 분리하고 정리하려는 목적으로 사용하니 말이죠.

이제 위에서 선언한 모듈을 별도의 파일로 분리해 봅시다. 우선 foo 모듈을 foo.rs라는 별도의 파일로 떼어낼 것인데, 내용은 아래와 같이 약간 바뀌었습니다.

pub fn public_foo() {
    println!("foo::public_foo!")
}

fn private_foo() {
    println!("foo::private_foo!")
}

mod private_bar {
    pub fn public_bar() {
        println!("foo::private_bar::public_bar!")
    }
}

pub mod bar {
    pub fn public_bar() {
        println!("foo::bar::public_bar!")
    }
}

바뀐 점은 제일 바깥쪽의 mod가 빠졌다는 점이지요. 약간 의아하죠?

이제 main.rs 파일을 수정해 봅시다.

mod foo;

fn main() {
    foo::public_foo();
    foo::bar::public_bar();
}

기존의 foo 모듈 구현이 생략되고 바로 세미콜론이 찍혀있습니다. 즉 modmain.rs 에는 그대로 남아있다는 것을 알 수 있습니다. 이런 식으로 고치면 이전과 동일하게 동작하는 것을 확인했습니다.

결과적으로 mod 키워드는 다른 일반(?) 러스트 파일을 모듈로 묶어주는 기능을 제공한다고 볼 수 있을 것 같습니다. 어쩌면 mod의 역할이 C 언어의 #include와 비슷한 목적인가라는 생각이 들기도 했었네요.

 

디렉터리로 모듈 분리하기

이렇게 파일 단위로 모듈을 분리할 수도 있겠지만, 좀 더 깔끔한 계층별 모듈 구조를 디자인하기 위해 아예 모듈을 별도의 디렉터리로 분리할 수도 있습니다. 이번엔 새로운 모듈 quu를 구현하면서 아예 하위 디렉터리에 만들어 보겠습니다.

우선 quu라는 디렉터리를 만들고 quu/mod.rs 파일을 아래와 같은 내용으로 만들었습니다.

pub fn public_quu() {
    println!("public_quu!")
}

파일 이름이 mod.rs라는 점만 제외하면 별 차이는 없습니다.

이제 새로 만든 quu 모듈을 main.rs에서 사용해봅시다.

mod quu;

fn main() {
    quu::public_quu();
}

별도의 파일이던 것과 거의 동일합니다.

결과적으로 별도의 디렉터리로 분리했을 때 대표 파일의 이름mod.rs여야 한다는 점이 규칙이며, 그 외에는 자유로운 것 같습니다. 예를 들자면 quu 디렉터리에 다른 러스트 파일을 구현하고 mod.rs 파일에서 mod로 묶어주면 동일한 레벨의 모듈 구조가 완성되겠지요.

 

모듈을 좀 더 간단하게 사용하기

제목을 짓기가 좀 힘들었는데, 이번에는 use 커맨드를 살펴봅시다. 이미 공부를 해봤다면 알 수 있겠지만 use는 라이브러리나 외부 패키지를 로드할 때 사용하는 명령입니다. 이를 프로젝트 모듈에서도 사용할 수 있습니다.

앞서 언급된 예제의 일부를 다시 이용해 봅시다.

mod foo;

fn main() {
    foo::bar::public_bar();
}

여기서 foo::bar::public_bar()라는 함수를 좀 더 짧게 호출하기 위해 아래와 같이 use 명령을 사용할 수 있습니다.

mod foo;
use foo::bar;

fn main() {
    bar::public_bar();
}

use foo::bar 라는 명령의 의미는 foo 안의 bar 라는 모듈을 현재 네임스페이스에 병합하겠다는 의미가 됩니다. 따라서 bar 모듈을 바로 액세스 하는 것이 가능해집니다.

물론 아래와 같이 최대한 줄이는 것도 당연히 가능합니다.

mod foo;
use foo::bar::public_bar;

fn main() {
    public_bar();
}

아예 bar 모듈의 특정 함수를 가져와 버렸습니다. 당연하게도 이렇게 해서 이름이 겹치지 않는다면 이렇게 쓰는 것이 코드를 줄일 수 있다는 점에서 좋을 수도 있겠지요. 물론 어느 정도 구분될 수 있는 수준으로 축약시키는 것이 권장되겠지만요.

만약 가져올 함수가 많다면 아래처럼 와일드카드(?)를 동원할 수도 있습니다.

use foo::bar::*;

이렇게 하면 foo 모듈 안의 bar 모듈 안의 모든 공개된 함수들을 현재 네임스페이스에서 쓸 수 있게 됩니다.

다만 이렇게 모든 이름을 가져와버리면 아무래도 현재 네임스페이스에서 이름이 겹쳐지는 사태가 발생할 확률이 커지므로 빌드 오류가 발생할 여지도 높아집니다. 따라서 상황에 맞게 잘 활용해야겠지요.

 

부모 디렉터리 모듈에 접근하기

현재 모듈의 구현에서 부모 모듈의 함수를 호출하는 경우가 있을 수도 있습니다. 물론 그다지 좋은 설계는 아니겠지만 이런 상황을 완벽하게 피할 수 있다는 보장은 없습니다.

하여간 이럴 때는 super 를 통해 접근하는 방법이 있습니다.

그전에 지금까지의 프로젝트 구조가 어떻게 되어있는지 다시 확인해 봅시다.

src/
    main.rs
    foo.rs
    quu/
        mod.rs

이제 quu 모듈에서 foo 안의 함수에 접근해 봅시다. 아래는 quu 모듈에 새 함수 public_deep_quu 함수를 구현하면서 앞서 구현한 부모 디렉터리에 위치하는 foo 모듈의 함수를 사용하는 예제입니다.

pub fn public_quu() {
    println!("public_quu!")
}

use super::foo;
pub fn public_deep_quu() {
    foo::public_foo();
}

이렇게 super 지시어를 이용해서 부모 모듈의 네임스페이스 또한 use를 통해서 사용하는 것이 가능해집니다.

이미 언급했지만 이런 식으로 쓸 수 있다는 것이지 그다지 권장할 만한 기능은 아닐지도 모르겠네요. 하지만 세상사가 그렇게 생각하는 대로 흘러가기만 하는 것은 아니라는 것도 인정해야 할 것 같습니다.

 

맺음말

지금까지 러스트 특유의 모듈 시스템과 접근 제어 규칙에 관해 간략히 살펴봤습니다. 사실 개인적으로 궁금했던 사항 위주로 정리한 것이라 편파적(?)인 글일지도 모르겠지만, 혹시나 러스트를 처음 접하는 분들에게 도움이 될 수 있었으면 좋겠습니다.

다음 글은 러스트의 기본 타입들에 대한 내용으로 이어집니다.

728x90
반응형

댓글