콘텐츠로 이동

Rust 환경 변수와 프로세스

모든 프로그램은 어떤 환경(environment) 안에서 실행돼요. 운영체제는 프로그램에 PATH, HOME, PORT 같은 이름이 붙은 텍스트 값들을 건네줘요. 이것을 환경 변수(environment variable)라고 해요. 환경 변수를 쓰면 코드를 고치지 않고도 프로그램 설정을 바꿀 수 있어요. 같은 실행 파일이 내 노트북에서는 8080번 포트를, 서버에서는 80번 포트를 쓸 수 있죠.

프로그램은 다른 프로그램들과 함께 살아가요. 끝날 때 종료 코드(exit code)로 자신을 실행한 쪽에 “잘 됐어요” 또는 “실패했어요”를 알릴 수 있고, 직접 다른 프로그램을 실행할 수도 있어요. Rust에서는 이 모든 도구가 두 표준 모듈 std::env와 std::process에 들어 있어요. 크레이트는 필요 없어요.

std::env::var는 이름을 받아서 Result를 돌려줘요. 변수가 있으면 Ok(String), 없으면(또는 올바른 유니코드가 아니면) Err예요. 변수가 없는 건 흔한 상황이라서, Rust는 그 경우를 꼭 처리하게 해요.

use std::env;
fn main() {
// 어느 컴퓨터에도 거의 확실히 설정돼 있지 않을 이름
match env::var("RUSTTUTORIAL_API_KEY") {
Ok(value) => println!("찾았어요: {value}"),
Err(e) => println!("쓸 수 없어요: {e}"),
}
let port = env::var("RUSTTUTORIAL_PORT").unwrap_or(String::from("8080"));
println!("포트: {port}");
}

실행 결과:

쓸 수 없어요: environment variable not found
포트: 8080
▶ 직접 실행해 보기

environment variable not found는 Rust 표준 라이브러리가 만든 에러 메시지로, “환경 변수를 찾을 수 없어요”라는 뜻이에요. 변수가 없을 때 따로 대응하고 싶다면 match를 쓰세요. 변수가 없으면 그냥 “기본값을 쓰면 된다”는 뜻일 때는 .unwrap_or(기본값)을 쓰세요.

환경 변수는 언제나 텍스트예요. 숫자가 필요하면 파싱해야 하는데, 파싱도 실패할 수 있어요. 자주 쓰는 패턴은 이래요. .ok()로 Result를 Option으로 바꾸고, 파싱을 시도하고, 어디서든 문제가 생기면 기본값으로 돌아가요.

use std::env;
fn main() {
let workers: u32 = env::var("RUSTTUTORIAL_WORKERS")
.ok()
.and_then(|text| text.parse().ok())
.unwrap_or(4);
let debug = env::var("RUSTTUTORIAL_DEBUG").is_ok();
println!("workers = {workers}");
println!("debug = {debug}");
}

실행 결과:

workers = 4
debug = false
▶ 직접 실행해 보기

.is_ok()는 변수를 켜고 끄는 플래그처럼 쓰는 간단한 방법이에요. 값이 뭐든 설정되어 있기만 하면 플래그가 켜진 거예요.

var_os, vars, 그리고 변수 설정하기

섹션 제목: “var_os, vars, 그리고 변수 설정하기”

env::var_os는 Result 대신 Option<OsString>을 돌려줘요. 이상한 바이트가 섞여 있어도 실패하지 않아서 경로를 다룰 때 편해요. env::vars()는 모든 변수를 (이름, 값) 쌍으로 줘요.

env::set_var는 현재 프로세스의 변수를 바꿔요. 주위를 감싼 unsafe 블록을 눈여겨보세요.

use std::env;
fn main() {
unsafe {
env::set_var("MYAPP_COLOR", "blue");
env::set_var("MYAPP_SIZE", "large");
}
println!("{:?}", env::var_os("MYAPP_COLOR"));
println!("{:?}", env::var_os("MYAPP_MISSING"));
let mut mine: Vec<(String, String)> = env::vars()
.filter(|(key, _)| key.starts_with("MYAPP_"))
.collect();
mine.sort(); // env::vars()의 순서는 보장되지 않아요
for (key, value) in &mine {
println!("{key}={value}");
}
}

실행 결과:

Some("blue")
None
MYAPP_COLOR=blue
MYAPP_SIZE=large
▶ 직접 실행해 보기

set_var는 왜 unsafe일까요? 많은 운영체제에서 환경은 프로세스 전체가 함께 쓰는 표 하나예요. 한 스레드가 그걸 바꾸는 동안 다른 스레드(또는 C 라이브러리)가 읽으면 프로그램이 죽을 수 있어요. Rust가 이걸 대신 검사해 줄 수 없어서, 에디션 2024부터 set_var와 remove_var는 unsafe 함수가 됐어요. 에디션 2021에서는 unsafe 없이도 컴파일되지만, 블록을 써 두면 양쪽 모두에서 동작해요. 안전한 규칙은 이거예요. 변수 설정은 main의 맨 처음, 스레드를 시작하기 전에만 하세요. 더 좋은 건 내 프로그램의 환경을 아예 바꾸지 않는 거예요. 자식 프로그램에 변수를 넘기려면 (아래에서 볼) Command::env를 쓰세요. unsafe가 무슨 뜻인지는 unsafe Rust를 참고하세요.

env::current_dir()는 프로그램을 시작한 폴더를 PathBuf로 돌려줘요. "config.toml" 같은 상대 경로는 이 폴더를 기준으로 해석돼요. 실제 경로는 컴퓨터마다 다르기 때문에, 이 예제는 어디서나 참인 사실만 출력해요.

use std::env;
fn main() {
let dir = env::current_dir().expect("현재 디렉터리를 읽을 수 없어요");
println!("절대 경로인가요? {}", dir.is_absolute());
let config = dir.join("config.toml");
println!("config.toml로 끝나나요? {}", config.ends_with("config.toml"));
}

실행 결과:

절대 경로인가요? true
config.toml로 끝나나요? true
▶ 직접 실행해 보기

내 컴퓨터에서 dir.display()를 출력해 보면 전체 경로를 볼 수 있어요. 명령줄 인자도 std::env에 들어 있는데, 따로 페이지가 있어요. 명령줄 인자를 보세요.

프로그램이 끝나면, 자신을 실행한 쪽에 숫자 하나를 돌려줘요. 0은 성공, 그 밖의 숫자는 실패를 뜻해요. 셸 스크립트나 CI 시스템이 이 숫자를 확인해요. std::process::exit(code)는 그 코드로 프로그램을 즉시 멈춰요.

use std::env;
use std::process;
fn main() {
println!("시작하는 중...");
let token = match env::var("RUSTTUTORIAL_TOKEN") {
Ok(t) => t,
Err(_) => {
eprintln!("에러: RUSTTUTORIAL_TOKEN이 설정되지 않았어요");
process::exit(1);
}
};
println!("토큰 {token}을 사용해요");
}

실행 결과:

시작하는 중...
▶ 직접 실행해 보기

에러 줄은 eprintln!으로 출력하기 때문에 stdout이 아니라 stderr로 가요. Playground에서는 종료 상태와 함께 별도의 영역에 보여 줘요.

알아 둘 게 두 가지 있어요. 첫째, process::exit는 프로그램을 즉시 끝내기 때문에, 아직 살아 있는 값의 소멸자(Drop)가 실행되지 않아요. 둘째, 꼭 이걸 써야 하는 경우는 많지 않아요. main이 Result<(), E>를 돌려주게 하고 Err를 반환하면, Rust가 에러를 출력하고 코드 1로 종료해 줘요.

Command로 다른 프로그램 실행하기

섹션 제목: “Command로 다른 프로그램 실행하기”

std::process::Command는 다른 프로그램을 시작해요. 한 단계씩 만들어 가요. 먼저 프로그램 이름, 그다음 .arg()나 .args()로 인자를 하나씩 붙여요. 그리고 어떻게 실행할지 골라요.

  • .output()은 프로그램이 끝날 때까지 기다리고, stdout과 stderr를 잡아 둬요.
  • .status()도 끝날 때까지 기다리지만, 출력은 그대로 내 터미널로 보내요.
use std::process::Command;
fn main() {
let output = Command::new("echo")
.arg("안녕하세요")
.arg("자식 프로세스에서 보냈어요")
.output()
.expect("echo를 실행하지 못했어요");
let text = String::from_utf8_lossy(&output.stdout);
println!("성공: {}", output.status.success());
println!("stdout: {}", text.trim());
}

실행 결과:

성공: true
stdout: 안녕하세요 자식 프로세스에서 보냈어요
▶ 직접 실행해 보기

프로그램은 무엇이든 출력할 수 있기 때문에 output.stdout은 날것의 바이트(Vec<u8>)예요. String::from_utf8_lossy로 텍스트로 바꿔요.

다음은 .status()로 종료 코드를 읽고, .env()로 자식 프로그램에게만 변수를 넘기는 예제예요.

use std::process::Command;
fn main() {
let status = Command::new("sh")
.args(["-c", "exit 3"])
.status()
.expect("sh를 실행하지 못했어요");
println!("성공: {}", status.success());
println!("코드: {:?}", status.code());
let output = Command::new("sh")
.args(["-c", "echo \"$GREETING, $NAME\""])
.env("GREETING", "안녕")
.env("NAME", "Ferris")
.output()
.expect("sh를 실행하지 못했어요");
print!("{}", String::from_utf8_lossy(&output.stdout));
}

실행 결과:

성공: false
코드: Some(3)
안녕, Ferris
▶ 직접 실행해 보기

Windows에서는: echo가 별도의 프로그램이 아니라 cmd 셸에 내장된 명령이고, sh는 보통 없어요. 같은 일을 하려면 Command::new("cmd").args(["/C", "echo Hello"])처럼 써요. 위의 예제들은 Linux, macOS, Rust Playground에서 동작해요.

실제 프로그램은 보통 시작할 때 한 번, 모든 설정을 구조체로 읽어 들여요. 설정마다 적당한 기본값이 있고, 잘못된 값이 들어오면 프로그램이 죽는 대신 알기 쉬운 메시지를 보여 줘요.

use std::env;
#[derive(Debug)]
struct Config {
host: String,
port: u16,
verbose: bool,
}
impl Config {
fn from_env() -> Config {
let host = env::var("APP_HOST").unwrap_or_else(|_| String::from("127.0.0.1"));
let port = match env::var("APP_PORT") {
Ok(text) => text.parse().unwrap_or_else(|_| {
println!("APP_PORT={text:?}은(는) 숫자가 아니라서 3000을 써요");
3000
}),
Err(_) => 3000,
};
let verbose = matches!(env::var("APP_VERBOSE").as_deref(), Ok("1") | Ok("true"));
Config { host, port, verbose }
}
}
fn main() {
// 사용자가 프로그램을 실행하기 전에 설정했을 법한 값을 흉내 내요
unsafe {
env::set_var("APP_PORT", "eighty");
env::set_var("APP_VERBOSE", "true");
}
let config = Config::from_env();
println!("{config:?}");
}

실행 결과:

APP_PORT="eighty"은(는) 숫자가 아니라서 3000을 써요
Config { host: "127.0.0.1", port: 3000, verbose: true }
▶ 직접 실행해 보기

실제로 쓸 때는 set_var 줄을 지우고, 대신 셸에서 변수를 설정하세요.

Terminal window
APP_PORT=9000 APP_VERBOSE=1 cargo run

(Windows PowerShell에서는 $env:APP_PORT="9000"; cargo run이에요.)

env::var가 String을 돌려준다고 생각하기. 변수가 없을 수도 있으니 Result를 돌려줘요.

let port: String = env::var("PORT");
error[E0308]: mismatched types
--> src/main.rs:4:24
|
4 | let port: String = env::var("PORT");
| ------ ^^^^^^^^^^^^^^^^ expected `String`, found `Result<String, VarError>`
| |
| expected due to this

“타입이 맞지 않아요. String을 기대했는데 Result<String, VarError>가 왔어요”라는 뜻이에요. 해결 방법: 변수가 없을 때 어떻게 할지 정하세요. 기본값을 쓰려면 env::var("PORT").unwrap_or(String::from("8080")), 알려 주려면 match를 쓰세요.

에디션 2024에서 unsafe 없이 set_var 호출하기. cargo new로 만든 새 프로젝트는 에디션 2024를 쓰기 때문에, 이렇게 하면 실패해요.

error[E0133]: call to unsafe function `set_var` is unsafe and requires unsafe block
--> src/main.rs:2:5
|
2 | std::env::set_var("MODE", "fast");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call to unsafe function

“unsafe 함수 set_var를 호출하려면 unsafe 블록이 필요해요”라는 뜻이에요. 해결 방법: unsafe { ... }로 감싸고 스레드를 시작하기 전에만 호출하세요. 자식 프로그램에 넘기는 거라면 Command::env를 쓰세요.

명령줄 전체를 Command::new에 넣기. Command::new("echo hello")는 이름이 말 그대로 echo hello인 프로그램을 찾는데, 그런 프로그램은 없어요. .output()은 Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })(그런 파일이나 디렉터리가 없음)를 돌려줘요. 해결 방법: 프로그램 이름은 new에, 인자는 하나하나 .arg()나 .args()에 넣으세요. Command::new("echo").arg("hello")처럼요.

  • env::var("NAME")은 Result<String, VarError>를 돌려줘요. 변수가 없는 경우는 match, .unwrap_or(), .ok()로 처리하세요.
  • 환경 변수의 값은 텍스트예요. 숫자는 직접 파싱하고, 파싱이 실패하면 기본값으로 돌아가세요.
  • 환경은 모든 스레드가 함께 쓰기 때문에 에디션 2024에서 env::set_var는 unsafe예요. 자식 프로그램에는 Command::env를 쓰는 편이 좋아요.
  • process::exit(code)는 종료 코드와 함께 프로그램을 즉시 끝내요. 0은 성공, 그 밖의 숫자는 실패예요.
  • Command::new("program").args([...])는 다른 프로그램을 실행해요. .output()은 그 프로그램이 출력한 내용을 잡아 두고, .status()는 종료 코드를 줘요.

이해도 점검

1. PORT가 설정되어 있지 않을 때 env::var("PORT")는 무엇을 돌려줄까요?

2. 에디션 2024에서 env::set_var가 unsafe 함수인 이유는 무엇일까요?

3. 전통적으로 "성공"을 뜻하는 종료 코드는 무엇일까요?

4. Command로 ls -l을 실행하려면 어떻게 할까요?

점수: 0 / 4