접근성 제어 (Visibility Control)

이 포스트는 Rust를 처음 공부하면서 정리한 내용입니다. 해당 포스트는 개인적으로 기억하기 위한 메모 성격에 가깝습니다. “러스트 프로그래밍 공식 가이드 (2018, 제이펍)” 서적을 참고하였으며, 러스트 공식 사이트 에서 책과 동일한 내용을 찾을 수 있습니다. 따라서 더 자세한 내용을 찾으시면 위의 링크를 참고하시면 좋습니다.

C++ 에서 클래스 내의 멤버 변수와 메서드는 private, protected, public 의 세 키워드로 가시성을 설정하였습니다. Rust에서도 이와 마찬가지로 한 모듈 내에서의 가시성을 설정할 수 있습니다. C++에서는 각 분류마다 접근성이 달라졌지만 Rust 에서는 pub 키워드 하나만으로 접근성을 제어합니다.

추가로, Rust 2018 버전으로 설명합니다. 이전 버전과는 조금 다른 점들이 보이더라구요. 이는 추후 포스트에서 자세히 다루어 볼 예정입니다. 이전 버전에 대한 언급은 따로 표기해두겠습니다.

또한 여기의 예제에서는 함수를 본문은 구현하지 않습니다.

모듈의 범위

앞선 포스트에서 언급했듯, 모듈(module)은 크레이트의 코드를 그룹화해서 가독성과 재사용성을 향상시키는 방법입니다. 이때 아이템을 외부의 코드가 사용할 수 있는지 공개하거나 비공개 할수 있는 구현 방법을 제공하는데, 이러한 공개/비공개의 여부를 아이템의 접근성(privacy)라고 합니다.

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}

pub

기본적으로 Rust에서는 모든 아이템(item, 공식 문서에서도 아이템이라는 단어를 사용하더라구요)은 비공개입니다. 또한 부모 모듈의 아이템들은 자식 모듈 안의 비공개 아이템을 사용할 수는 없으나 자식 모듈의 아이템은 부모 모듈의 아이템을 사용할 수 있습니다. 이는 자식 모듈은 부모 모듈 아이템이 정의된 컨텍스트(context)를 볼 수 있기 때문입니다. 따라서 기본적으로 상세 구현 내용은 기본적으로 비공개 처리 됩니다.

pub 키워드는 자식 모듈의 일부분을 외부의 부모 모듈에게 공개할 수 있습니다.

공개할 수 있는 대상은 하위 모듈, 함수, 구조체와 열거자가 있습니다.

구조체 공개

pub 키워드는 구조체 자체를 공개할 때도 사용할 수 있지만 필드를 공개하는 경우에도 사용됩니다. 구조체의 필드는 기본적으로 비공개로 설정됩니다. 따라서 공개하고자 하는 필드는 pub로 공개해주어야 합니다.

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }
}

위의 예제에서 toast는 마침표(.)를 이용하여 외부에서도 접근할 수 있지만, seasonal_fruit는 접근할 수 없습니다. 따라서 이러한 경우에는 seasonal_fruit을 접근하며 인스턴스를 생성할 수 있는 공개 연관 함수를 만들어주어야 합니다.

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    // Breakfast 인스턴스를 생성하는 연관함수
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("Apple")
            }
        }
    }
}

열거자 공개

열거자의 경우는 구조체와 반대로 모든 열것값이 공개됩니다. 따라서 enum 키워드 앞에 pub 키워드만 추가하면 됩니다.

mod back_of_house {
    pub enum Appetizer {
        Soup, 
        Salad
    }
}

경로 (Path)

경로는 두 가지의 종류가 있습니다.

  1. 절대 경로 (Absolute Path): 크레이트 이름이나 crate 리터럴을 이용해 크레이트 루트로부터 시작하는 경로입니다.
  2. 상대 경로 (Relative Path): 현재 모듈로 부터 시작하여 self, super 혹은 현재 모듈의 식별자를 이용하여 명시합니다.

그러면 위의 함수를 호출할 경우에는 아래와 같이 사용합니다.

// src/lib.rs
mod front_of_house {
    mod hosting { // error
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 절대경로
    crate::front_of_house::hosting::add_to_waitlist();

    // 상대경로
    front_of_house::hosting::add_to_waitlist();
}

add_to_waitlist 함수는 처음 절대경로를 통하여 호출될 때 crate:: 에서 부터 시작하고 있습니다. 위의 코드는 아래와 같은 트리 구조를 하고 있습니다.

crate
└── front_of_house
    └── hosting
        └── add_to_waitlist

앞서 src/main.rs 와 src/lib.rs 파일을 크레이트 루트라고 부른다고 언급했습니다. 그 이유는 두 파일의 콘텐츠는 crate라는 이름의 모듈로 구성되고, 이 모듈은 module tree라고 부르는 크레이트 모듈 구조에서 루트 역할을 하기 때문입니다. 따라서 위의 예제에서와 같이 함수를 호출할 때 파일 탐색기에서 파일을 찾는 것과 같이 경로를 명시해야 합니다.

따라서 위의 코드는 경로를 찾을 수 없다는 에러를 내뿜게 됩니다. 우리는 hosting 모듈과 function을 공개해서 eat_at_restaurant 함수가 add_to_waitlist 함수에 접근이 가능하도록 해야 합니다.

// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 절대경로
    crate::front_of_house::hosting::add_to_waitlist();

    // 상대경로
    front_of_house::hosting::add_to_waitlist();
}

트리 구조에서 알 수 있듯이, eat_at_restaurant와 front_of_house 모듈은 형제 관계입니다. 따라서 eat_at_restaurant에서 front_of_house 모듈을 접근할 수 있으므로 pub 키워드는 front_of_house에 붙이지 않아도 무방합니다.

경로에 관한 더 자세한 내용은 이 곳에서 확인할 수 있습니다.

만일 다른 파일로 분리한다면 아래와 같이 정리할 수 있습니다.

Module Path Filesystem Path File Contents
crate src/lib.rs mod front_of_house;
crate::front_of_house src/front_of_house.rs mod hosting;
crate::front_of_house::hosting src/front_of_house/hosting.rs  

앞의 포스트에서 mod.rs로도 분리가 가능하지만 공식 문서에서는 이 방법을 권장하고 있지는 않습니다. 직관적인 naming convention을 사용하도록 권장하고 있습니다.

상대 경로: super와 self

super 키워드는 리눅스의 상위 폴더로 이동할 때 사용하는 .. 문법과 같습니다.

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order(); // 여기 상대경로
    }
}

super를 이용한 상대경로 명시는 나중에 크레이트 모듈의 트리가 재구성되는 경우에도 수정해야 할 코드를 줄일 수 있다는 장점이 있습니다.

반대로 self 키워드는 리눅스 현재 폴더의 위치를 나타내는 . 문법과 동일합니다.

경로 가져오기: use

지금까지는 함수를 불러우기 위해 경로를 전부 작성했지만 C++에서 using namespace std;와 같이 쉽게 가져오는 방법이 있습니다. Rust에서도 그러한 방법을 지원하며 use 키워드를 사용하면 됩니다. use를 사용하면 마치 경로의 아이템이 현재 범위 내에 있는 것처럼 호출이 가능합니다.

// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;
// 마치 hosting 모듈이 root에 있는 것과 같습니다.
// mod hosting { }

pub fn eat_at_restaurant() {

    // hosting 이름으로만 경로 가져오기
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

예시에서 use crate::front_of_house::hosting; 구문을 추가하는 행위는 파일 시스템에서 심볼릭 링크를 만드는 것과 비슷합니다. 이때 crate::front_of_house::hosting 구문은 root 범위에서 추가되었기 때문에 hosting 모듈은 root에 정의한 것처럼 root 범위에서 유효한 이름이 됩니다.

여기에서 주의할 점은 use 키워드를 이용하여 상대경로를 지정하는 경우에는 현재 범위 이름을 직접 지정하여 명시하는 것이 아닌 self 키워드를 이용한 상대경로를 작성해야 합니다.

// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use self::front_of_house::hosting; // 상대경로

pub fn eat_at_restaurant() {

    // hosting 이름으로만 경로 가져오기
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

새로운 이름 부여하기 (Renaming): as

파이썬을 사용하다 보면 아래와 같이 관용적으로 쓰는 문구가 있습니다.

import numpy as np

a = np.array([1, 2, 3])

numpy 라이브러리를 np 라는 이름으로 가져오게 되면 그 이름으로 하위 함수나 변수 등을 사용할 수 있습니다.

Rust에서도 마찬가지로 새로운 이름을 부여하면서 경로를 가져올 수 있습니다.

use std::fmt::Result;
use std::io::Result as IoResult;

fn Function1() -> Result {}
fn Function2() -> IoResult<()> {}
// 함수의 본문은 생략합니다.

Rust에서 as 키워드는 특히 동일한 이름을 가진 두 타입을 현재 범위로 가져오기 위해 사용하기도 합니다. 따라서 이름 충돌 없이 Function1Function2를 작성할 수 있습니다.

중첩되는 경로

경로를 가져오다 보면 많은 아이템을 가져오게 되는 경우 동일한 코드가 반복되기 때문에 많은 줄 수를 차지하게 됩니다. 이렇게 중첩되는 경로는 아래와 같이 포함시킬 수 있습니다.

use std::io;
use std::cmp::Ordering;

// 위는 아래와 동일한 효과를 냅니다.
use std::{io, cmp::Ordering};

이는 마치 파이썬에서 여러 개의 모듈을 가져오는 것과 비슷합니다.

from torch import nn, Tensor

또는 아래와 같이 포함시킬 수도 있습니다.

use std::io;
use std::io::Write;

// 위와 동일합니다.
use std::io{self, Write};

파이썬과 마찬가지로 어떠한 경로의 공개 아이템을 모두 현재 범위로 가져오려면 글롭 연산자 *를 사용합니다.

use std::collections::*;

이름 다시 내보내기 (re-exporting): pub use

use 키워드를 사용하여 범위로 이름을 가져오면 이 이름은 새 범위에서 비공개 이름이 됩니다. 따라서 자신이 작성한 코드를 호출하는 코드가 다른 범위에서 가져온 이름도 현재 범위에 정의된 것처럼 접근할 수 있도록 하고자 한다면 use를 public으로 만들어 버리면 됩니다. 즉, pub use를 사용하면 됩니다.

즉, Re-exporting 이란 아이템을 현재 범위로 가져옴과 동시에 다른 범위로 내보내는 방법입니다.

예를 들어, 아래의 코드가 있다면

// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting; // 상대경로

pub fn eat_at_restaurant() {

    // hosting 이름으로만 경로 가져오기
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

pub use로 인해 외부에서도 crate::front_of_house::hosting::add_to_waitlist 함수를 hosting::add_to_waitlist 으로 호출할 수 있습니다. 즉, 현재 범위에서 eat_at_restauranthosting::add_to_waitlist 으로 호출하는 것 처럼 외부의 코드도 hosting::add_to_waitlist 으로 호출할 수 있습니다.

다른 예시로 보겠습니다.

mod quux {
    pub use self::foo::{bar, baz};
    pub mod foo {
        pub fn bar() {}
        pub fn baz() {}
    }
}

fn main() {
    quux::bar();
    quux::baz();
}

main 함수는 quux 와 동일한 root 경로에 있는 함수입니다. 이때 정상적으로 bar, baz 함수를 호출하기 위해서는 quux::foo::bar과 같이 사용해야 합니다.

우선 quux 내부에서 use::self::foo::{bar, baz}를 선언하고 있습니다. 따라서 quux 내부의 아이템은 foo::bar(); foo::baz();로 함수를 호출하는 것이 아니라 bar(), baz()의 형태로 호출이 가능합니다. 동시에 use가 공개되었기 때문에 quux 외부, 즉 main 함수에서도 quux::bar()과 같은 형태로 호출하고 있습니다.

외부 패키지 사용하기

외부 패키지를 사용하기 위해서는 Cargo.toml 파일에 의존성을 추가합니다.

[dependencies]
rand = "0.5.5"

Cargo.toml 파일에 rand를 의존성으로 추가하였기 때문에 cargo는 이 패키지와 rand 패키지를 사용하기 위한 모든 의존성 패키지를 https://crates.io 에서 내려받습니다.

extern crate (2018 이전)

가끔 인터넷에 돌아다니는 코드를 구경하다 보면 외부 크레이트를 사용하는 경우 extern crate 라는 키워드를 확인할 수 있습니다. Rust는 2018 버전 이후부터 외부 의존성 명시를 위한 extern crate를 사용하지 않아도 됩니다. Cargo.toml 파일에 edition = “2018”으로 설정하면 extern crate는 없어도 됩니다.

예를 들어 아래와 같은 파일 구조를 가지고 있다고 가정하겠습니다.

├── src
│   ├── client.rs
│   ├── lib.rs
│   └── network
│       ├── mod.rs
│       └── server.rs

그러면 외부의 main 함수에서 client 모듈 내의 함수 connect를 사용하기 위해서는 extern crate 키워드로 communicator 크레이트를 가져와야 합니다.

extern crate communicator;

fn main() {
    communicator::client::connect();
}

태그:

카테고리:

업데이트: