본문 바로가기
IT/Rust 기초 완전 정복

Rust 입출력과 파일 시스템 다루기 (22)

by 지식 발전소 2024. 4. 25.
728x90
반응형
반응형

1

 

표준 입출력

표준 입출력은 프로그래밍 언어에서 가장 기본적인 입출력 방식입니다. 터미널에서 사용자의 입력을 받거나 결과를 출력하는 데 사용됩니다. 다음은 Rust에서 표준 입출력을 다루는 방법입니다.

 

Rust는 std::io 모듈을 통해 표준 입출력 기능을 제공합니다. 주요 구조체로는 Stdin, Stdout, Stderr가 있습니다.

Stdin은 표준 입력 스트림을 다루는 구조체입니다. 이를 통해 사용자의 키보드 입력을 읽어올 수 있습니다. 예를 들어, 다음과 같이 한 줄의 텍스트를 읽어올 수 있습니다.

use std::io;

fn main() {
    let mut input = String::new();
    io::stdin().read_line(&mut input)
        .expect("Failed to read line");
    println!("You entered: {}", input);
}

Stdout Stderr는 각각 표준 출력 스트림과 표준 에러 스트림을 나타냅니다. println! 매크로는 내부적으로 Stdout을 사용하여 출력합니다. 직접 사용하려면 다음과 같이 합니다.

use std::io::{self, Write};

fn main() {
    let stdout = io::stdout();
    let mut handle = stdout.lock();
    
    handle.write(b"Hello, ").unwrap();
    handle.write(b"world!\n").unwrap();
}

Stderr는 에러 메시지를 출력할 때 사용합니다. eprintln! 매크로를 통해 편리하게 사용할 수 있습니다.

eprintln!("An error occurred");

Rust의 표준 입출력은 바이트 스트림을 기반으로 동작하므로, 텍스트 데이터를 처리할 때는 적절한 인코딩을 지정해야 합니다. 이를 위해 std::io::Cursor 구조체를 활용할 수 있습니다.

use std::io::{Cursor, Write};

fn main() {
    let mut cursor = Cursor::new(Vec::new());
    let text = "Hello, world!";
    cursor.write_all(text.as_bytes()).unwrap();
    
    let data = cursor.into_inner();
    println!("{:?}", data); // [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
    
    let text = String::from_utf8(data).unwrap();
    println!("{}", text); // Hello, world!
}

이처럼 Rust의 표준 입출력을 활용하면 간단한 사용자 인터페이스를 구축할 수 있습니다. 하지만 더 복잡한 입출력 작업에는 std::fs 모듈을 활용하는 것이 바람직합니다.

파일 입출력

std::fs 모듈은 파일 시스템과 상호작용할 수 있는 다양한 기능을 제공합니다. 파일을 읽고 쓰는 것은 가장 기본적인 작업 중 하나입니다.

파일을 읽기 위해서는 먼저 File 구조체를 생성해야 합니다. std::fs::File::open 함수를 사용하면 됩니다.

use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() {
    let file = File::open("example.txt").expect("Failed to open file");
    let reader = BufReader::new(file);
    
    for line in reader.lines() {
        let line = line.expect("Failed to read line");
        println!("{}", line);
    }
}

BufReader는 버퍼링된 읽기를 지원하는 래퍼 구조체입니다. 성능 향상을 위해 사용하는 것이 바람직합니다. lines() 메서드는 파일의 각 줄을 String으로 반환하는 반복자를 생성합니다.

파일에 쓰기 위해서는 File::create 함수를 사용하거나 OpenOptions 구조체를 활용할 수 있습니다.

use std::fs::File;
use std::io::prelude::*;

fn main() {
    let mut file = File::create("output.txt").expect("Failed to create file");
    file.write_all(b"Hello, world!").expect("Failed to write to file");
}

OpenOptions를 사용하면 파일 열기 모드와 권한을 세밀하게 제어할 수 있습니다.

use std::fs::OpenOptions;
use std::io::prelude::*;

fn main() {
    let mut file = OpenOptions::new()
        .write(true)
        .append(true)
        .open("output.txt")
        .expect("Failed to open file");
    
    file.write_all(b"Hello, world!\n").expect("Failed to write to file");
}

위 예제에서는 쓰기 모드로 파일을 열고, 기존 내용에 추가하도록 설정했습니다.

Rust는 파일 시스템 작업을 위한 다양한 편의 함수도 제공합니다. std::fs::read_to_string std::fs::write 함수를 사용하면 코드를 좀 더 간결하게 작성할 수 있습니다.

use std::fs;

fn main() {
    let contents = fs::read_to_string("example.txt")
        .expect("Failed to read file");
    println!("{}", contents);
    
    fs::write("output.txt", contents)
        .expect("Failed to write file");
}

파일 경로를 다룰 때는 std::path 모듈을 활용하면 편리합니다. Path 구조체와 PathBuf 구조체를 통해 플랫폼 독립적인 방식으로 경로를 처리할 수 있습니다.

use std::path::Path;

fn main() {
    let path = Path::new("./data/input.txt");
    let parent = path.parent().unwrap();
    let file_stem = path.file_stem().unwrap();
    
    println!("Parent directory: {}", parent.display());
    println!("File stem: {}", file_stem.to_str().unwrap());
}

이처럼 Rust의 std::fs 모듈은 파일 입출력뿐만 아니라 파일 시스템 작업을 위한 다양한 기능을 제공합니다. 파일 복사, 이동, 삭제 등의 작업도 가능합니다. 또한 디렉터리 생성, 삭제, 탐색 등의 기능도 지원합니다.

버퍼링과 스트림

입출력 작업에서 성능 향상을 위해 버퍼링 기법이 자주 사용됩니다. Rust는 std::io::BufReader std::io::BufWriter 구조체를 통해 버퍼링 기능을 제공합니다.

use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() {
    let file = File::open("example.txt").expect("Failed to open file");
    let reader = BufReader::new(file);
    
    for line in reader.lines() {
        let line = line.expect("Failed to read line");
        println!("{}", line);
    }
}

BufReader는 읽기 전용 버퍼를 구현한 구조체입니다. 읽기 작업에서 효율성을 높일 수 있습니다. 반대로 BufWriter는 쓰기 전용 버퍼를 제공합니다.

use std::fs::File;
use std::io::BufWriter;

fn main() {
    let file = File::create("output.txt").expect("Failed to create file");
    let mut writer = BufWriter::new(file);
    
    writer.write_all(b"Hello, ").expect("Failed to write to buffer");
    writer.write_all(b"world!").expect("Failed to write to buffer");
    writer.flush().expect("Failed to flush buffer");
}

flush() 메서드를 호출하여 버퍼의 내용을 파일에 기록할 수 있습니다.

Rust는 std::io::Read std::io::Write 트레이트를 통해 스트림 기반의 입출력을 지원합니다. 이를 통해 다양한 소스로부터 데이터를 읽거나 쓸 수 있습니다.

use std::io::{Read, Write};

fn main() {
    let mut buffer = Vec::new();
    let mut stdin = std::io::stdin();
    
    stdin.read_to_end(&mut buffer).expect("Failed to read from stdin");
    
    let mut stdout = std::io::stdout();
    stdout.write_all(&buffer).expect("Failed to write to stdout");
}

위 예제에서는 표준 입력으로부터 데이터를 읽어 버퍼에 저장한 다음, 표준 출력에 쓰고 있습니다.

std::io::Cursor 구조체를 활용하면 메모리 버퍼를 스트림처럼 다룰 수 있습니다. 이는 입출력 테스트에 유용합니다.

use std::io::{Cursor, Read, Seek, SeekFrom};

fn main() {
    let data = Cursor::new(vec![1, 2, 3, 4]);
    let mut buffer = vec![0; 4];
    
    data.read_exact(&mut buffer).expect("Failed to read from cursor");
    assert_eq!(buffer, vec![1, 2, 3, 4]);
    
    data.seek(SeekFrom::Start(2)).expect("Failed to seek cursor");
    data.read_exact(&mut buffer).expect("Failed to read from cursor");
    assert_eq!(buffer, vec![3, 4, 3, 4]);
}

Cursor Read, Write, Seek 트레이트를 구현하고 있어 다양한 작업이 가능합니다.

Rust의 입출력 시스템은 효율성과 유연성을 모두 제공합니다. 표준 입출력부터 파일 시스템 작업, 버퍼링 및 스트림 처리까지 다양한 기능을 지원합니다. 이를 활용하여 성능 최적화된 입출력 코드를 작성할 수 있습니다.

비동기 입출력

비동기 입출력은 프로그램이 입출력 작업을 대기하는 동안 다른 작업을 수행할 수 있게 해줍니다. 이를 통해 프로그램의 응답성과 효율성을 높일 수 있습니다.

Rust는 std::io::Read std::io::Write 트레이트에 비동기 메서드를 제공하지 않습니다. 대신 tokio 크레이트와 async/await 구문을 활용하여 비동기 입출력을 구현할 수 있습니다.

tokio는 비동기 프로그래밍을 위한 런타임 및 다양한 유틸리티를 제공합니다. 다음은 tokio를 사용하여 파일을 비동기적으로 읽는 예제입니다.

use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut file = File::open("example.txt").await?;
    let mut contents = Vec::new();
    
    file.read_to_end(&mut contents).await?;
    
    println!("{}", String::from_utf8_lossy(&contents));
    
    Ok(())
}

async fn await 키워드를 사용하여 비동기 코드를 작성할 수 있습니다. tokio::main 어트리뷰트를 통해 비동기 메인 함수를 실행할 수 있습니다.

tokio::fs::File std::fs::File과 유사한 인터페이스를 제공하지만, 비동기 메서드를 지원합니다. read_to_end 메서드는 파일의 모든 내용을 읽어와 벡터에 저장합니다.

파일 쓰기도 비슷한 방식으로 구현할 수 있습니다.

use tokio::fs::File;
use tokio::io::{self, AsyncWriteExt};

#[tokio::main]
async fn main() -> io::Result<()> {
    let mut file = File::create("output.txt").await?;
    let data = "Hello, world!";
    
    file.write_all(data.as_bytes()).await?;
    file.sync_all().await?;
    
    Ok(())
}

sync_all 메서드를 호출하여 버퍼의 내용을 디스크에 기록합니다.

tokio는 TCP 소켓, UDP 소켓, Unix 도메인 소켓 등과 같은 다양한 입출력 소스에 대한 비동기 지원을 제공합니다. 이를 활용하면 네트워크 프로그래밍에서## 네트워크 입출력

tokio 크레이트는 TCP, UDP 및 Unix 도메인 소켓과 같은 네트워크 리소스에 대한 비동기 입출력 기능을 제공합니다. 이를 통해 고성능 네트워크 애플리케이션을 구축할 수 있습니다.

TCP 서버/클라이언트

TCP 서버를 구축하려면 tokio::net::TcpListener를 사용하여 연결 요청을 수신해야 합니다.

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (mut socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            loop {
                let n = match socket.read(&mut buf).await {
                    Ok(n) if n == 0 => return,
                    Ok(n) => n,
                    Err(e) => {
                        eprintln!("failed to read from socket; err = {:?}", e);
                        return;
                    }
                };

                if let Err(e) = socket.write_all(&buf[0..n]).await {
                    eprintln!("failed to write to socket; err = {:?}", e);
                    return;
                }
            }
        });
    }
}

TcpListener::bind를 통해 지정된 주소와 포트에서 수신 대기 상태가 됩니다. accept 메서드는 새로운 연결이 수신되었을 때 TcpStream을 반환합니다.

tokio::spawn은 비동기 작업을 생성하고 실행합니다. 새로운 연결에 대해 별도의 작업을 실행하여 동시에 여러 클라이언트를 처리할 수 있습니다.

클라이언트 측에서는 tokio::net::TcpStream을 사용하여 서버에 연결합니다.

use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
    let mut buf = [0; 1024];

    stream.write_all(b"Hello, world!").await?;

    let n = stream.read(&mut buf).await?;
    println!("Received: {}", String::from_utf8_lossy(&buf[..n]));

    Ok(())
}

TcpStream::connect를 통해 서버에 연결하고, 데이터를 송수신할 수 있습니다.

UDP

UDP 소켓을 사용하려면 tokio::net::UdpSocket을 활용합니다.

use tokio::net::UdpSocket;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let socket = UdpSocket::bind("127.0.0.1:8080").await?;
    let mut buf = [0; 1024];

    loop {
        let (n, addr) = socket.recv_from(&mut buf).await?;
        println!("Received {:?} from {:?}", &buf[..n], addr);

        socket.send_to(&buf[..n], &addr).await?;
    }
}

UdpSocket::bind를 호출하여 UDP 소켓을 생성하고, recv_from send_to를 통해 데이터를 송수신할 수 있습니다.

Unix 도메인 소켓

Unix 도메인 소켓은 동일한 시스템 내에서 프로세스 간 통신을 위해 사용됩니다. tokio::net::UnixListener tokio::net::UnixStream을 사용하여 구현할 수 있습니다.

use tokio::net::{UnixListener, UnixStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = UnixListener::bind("/tmp/mysocket")?;

    loop {
        let (stream, _) = listener.accept().await?;
        tokio::spawn(async move {
            handle_connection(stream).await;
        });
    }
}

async fn handle_connection(mut stream: UnixStream) {
    let mut buf = [0; 1024];

    loop {
        let n = match stream.read(&mut buf).await {
            Ok(n) if n == 0 => return,
            Ok(n) => n,
            Err(e) => {
                eprintln!("failed to read from socket; err = {:?}", e);
                return;
            }
        };

        if let Err(e) = stream.write_all(&buf[0..n]).await {
            eprintln!("failed to write to socket; err = {:?}", e);
            return;
        }
    }
}

UnixListener::bind를 통해 Unix 도메인 소켓을 생성하고, accept를 호출하여 클라이언트 연결을 수신합니다. 연결이 수신되면 handle_connection 함수에서 데이터를 송수신합니다.

이처럼 tokio 크레이트는 다양한 네트워크 프로토콜에 대한 비동기 입출력 기능을 제공합니다. 이를 활용하면 고성능의 네트워크 서버와 클라이언트를 구축할 수 있습니다.

파일 모니터링

std::fs 모듈은 파일 시스템 변경 사항을 모니터링할 수 있는 기능도 제공합니다. 이를 통해 파일 변경 시 특정 작업을 수행할 수 있습니다.

std::fs::Metadata 구조체는 파일 또는 디렉터리의 메타데이터를 나타냅니다. std::fs::metadata 함수를 호출하여 메타데이터를 가져올 수 있습니다.

use std::fs;
use std::time::SystemTime;

fn main() {
    let metadata = fs::metadata("example.txt").expect("Failed to get metadata");
    let modified = metadata.modified().expect("Failed to get modification time");
    println!("Last modified: {}", modified);
}

modified 메서드를 통해 파일의 최종 수정 시간을 확인할 수 있습니다.

하지만 파일 변경 사항을 지속적으로 모니터링하려면 std::fs::Watcher 구조체를 사용해야 합니다. 이 구조체는 파일 시스템 이벤트를 생성하고 반복자를 통해 이벤트를 소비할 수 있게 해줍니다.

use std::fs::{self, Watcher};
use std::path::Path;
use std::sync::mpsc::channel;

fn main() {
    let (tx, rx) = channel();

    let mut watcher = Watcher::new(tx).expect("Failed to create watcher");
    watcher.watch("./", RecursiveMode::Recursive).expect("Failed to watch directory");

    loop {
        match rx.recv() {
            Ok(event) => {
                match event {
                    DirEntry::Create(entry) => {
                        let path = entry.path();
                        if path.is_file() {
                            println!("File created: {}", path.display());
                        } else {
                            println!("Directory created: {}", path.display());
                        }
                    }
                    DirEntry::Modify(entry) => {
                        let path = entry.path();
                        if path.is_file() {
                            println!("File modified: {}", path.display());
                        } else {
                            println!("Directory modified: {}", path.display());
                        }
                    }
                    DirEntry::Remove(entry) => {
                        let path = entry.path();
                        if path.is_file() {
                            println!("File removed: {}", path.display());
                        } else {
                            println!("Directory removed: {}", path.display());
                        }
                    }
                    _ => {}
                }
            }
            Err(e) => println!("Watcher error: {:?}", e),
        }
    }
}

위 코드에서는 Watcher::new 함수를 호출하여 파일 시스템 이벤트 감시기를 생성합니다. watch 메서드를 통해 모니터링할 디렉터리를 지정할 수 있습니다.

RecursiveMode::Recursive 옵션을 전달하면 하위 디렉터리의 변경 사항도 모니터링합니다.

이벤트는 DirEntry 열거형으로 표현됩니다. Create, Modify, Remove 등의 이벤트를 처리할 수 있습니다.

이 예제에서는 간단히 이벤트 종류와 경로를 출력하고 있지만, 실제로는 이벤트에 따라 원하는 작업을 수행할 수 있습니다.

파일 모니터링 기능은 버전 관리 시스템, 빌드 도구, IDE 등 다양한 분야에서 활용될 수 있습니다. 파일 변경 시 자동으로 특정 작업을 수행하게 하여 개발 생산성을 높일 수 있습니다.

직렬화와 역직렬화

데이터를 파일이나 네트워크를 통해 전송하려면 직렬화가 필요합니다. 직렬화란 데이터 구조를 바이트 스트림으로 변환하는 과정을 말합니다. 역직렬화는 그 반대 과정입니다.

Rust에는 serde 크레이트가 직렬화와 역직렬화를 위한 강력한 기능을 제공합니다. serde는 데이터 형식에 상관없이 일관된 인터페이스를 제공하며, 다양한 데이터 형식을 지원합니다.

JSON 직렬화/역직렬화

JSON은 가장 널리 사용되는 데이터 교환 형식 중 하나입니다. serde_json 크레이트를 사용하면 Rust의 데이터 구조를 JSON으로 직렬화하고 역직렬화할 수 있습니다.

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u32,
    email: String,
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
        email: "alice@example.com".to_string(),
    };

    let json_str = serde_json::to_string(&person).unwrap();
    println!("{}", json_str);

    let deserialized: Person = serde_json::from_str(&json_str).unwrap();
    println!("{:?}", deserialized);
}

#[derive(Serialize, Deserialize)] 어트리뷰트를 추가하면 구조체를 자동으로 JSON으로 직렬화하고 역직렬화할 수 있습니다.

serde_json::to_string을 통해 JSON 문자열을 생성하고, serde_json::from_str을 통해 JSON 문자열에서 구조체를 복원할 수 있습니다.

바이너리 직렬화/역직렬화

JSON은 가독성이 좋지만 공백 문자가 많아 효율적이지 않을 수 있습니다. 더 효율적인 바이너리 직렬화를 위해 bincode 크레이트를 사용할 수 있습니다.

use bincode::{serialize, deserialize};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u32,
    email: String,
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
        email: "alice@example.com".to_string(),
    };

    let encoded: Vec<u8> = serialize(&person).unwrap();
    println!("{:?}", encoded);

    let decoded: Person = deserialize(&encoded[..]).unwrap();
    println!("{:?}", decoded);
}

bincode::serialize를 통해 구조체를 바이트 벡터로 직렬화할 수 있습니다. bincode::deserialize는 바이트 벡터에서 구조체를 복원합니다.

바이너리 직렬화는 JSON보다 더 작은 크기와 빠른 속도를 제공합니다. 하지만 가독성이 낮기 때문에 주로 네트워크 전송이나 파일 저장에 사용됩니다.

커스텀 직렬화/역직렬화

serde 크레이트는 사용자 정의 직렬화와 역직렬화도 지원합니다. 이를 통해 기존 포맷에 맞추거나 특수한 요구사항을 처리할 수 있습니다.

use serde::{Serialize, Deserialize, Serializer, Deserializer};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    #[serde(serialize_with = "serialize_name")]
    #[serde(deserialize_with = "deserialize_name")]
    name: String,
    age: u32,
    email: String,
}

fn serialize_name<S>(name: &str, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    serializer.serialize_str(&format!("Name: {}", name))
}

fn deserialize_name<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let s: String = Deserialize::deserialize(deserializer)?;
    Ok(s.replacen("Name: ", "", 1))
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
        email: "alice@example.com".to_string(),
    };

    let json_str = serde_json::to_string(&person).unwrap();
    println!("{}", json_str);

    let deserialized: Person = serde_json::from_str(&json_str).unwrap();
    println!("{:?}", deserialized);
}

#[serde(serialize_with = "...")] 어트리뷰트를 사용하여 직렬화 함수를 지정할 수 있습니다. #[serde(deserialize_with = "...")]는 역직렬화 함수를 지정합니다.

이 예제에서는 name 필드에 대해 커스텀 직렬화/역직렬화 함수를 정의했습니다. 직렬화 시에는 "Name: " 접두사를 추가하고, 역직렬화 시에는 접두사를 제거합니다.

커스텀 직렬화/역직렬화는 데이터 형식을 변환하거나 특정 포맷에 맞추는 등 다양한 용도로 사용될 수 있습니다.

참고 자료

  1. The Rust Programming Language by Steve Klabnik and Carol Nichols
  2. Rust by Example
  3. Rust Standard Library Documentation
  4. Tokio Documentation
  5. Serde Documentation
  6. Bincode Documentation

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형

댓글