구조체와 열거형의 복잡한 패턴 매칭과 가변 바인딩

구조체(Struct)와 열거형(Enum)의 여러가지 패턴 매칭(Match) 방법과 가변 바인딩(mutable binding)에 대해 알아보겠습니다. 구조체 분해후 바인딩, if let, _, .., | 등의 사용법과 참조와 Shadowing에 대해서도 알아봅니다.

1. 구조체(Struct)에서 복잡한 패턴 매칭과 가변 바인딩

가. 구조체 필드 일부만 매칭하기:

구조체 패턴에서 모든 필드를 반드시 매칭하지 않아도 되며, ..을 써서 나머지 필드는 무시할 수 있습니다.

struct Point {
x: i32,
y: i32,
z: i32,
}

fn main() {
let p = Point { x: 1, y: 2, z: 3 };

match p {
Point { x, .. } => println!("x 값에만 관심 있음: {}", x),
}
}

나. 가변 바인딩 (mutable binding)

기본적으로 Rust의 match에서 바인딩된 변수는 불변입니다. 가변으로 만들려면 mut 키워드를 변수 바인딩 전에 붙입니다.

let mut p = Point { x: 1, y: 2, z: 3 };

match &mut p { // 구조체를 가변 참조로 매칭
Point { x, y, .. } => {
*x += 10; // 가변 참조로 접근 가능
*y += 20;
println!("변경된 x: {}, y: {}", x, y);
}
}
  • 위 예에서 &mut p로 가변 참조를 패턴 매칭했고, 패턴 안의 x와 y는 가변 참조(&mut i32)가 되어 값을 변경할 수 있습니다.

다. 구조체 분해 후 변수 재바인딩

let p = Point { x: 10, y: 20, z: 30 };
let Point { x: a, y: b, z: c } = p;
println!("a: {}, b: {}, c: {}", a, b, c);

a, b, c가 각각 p의필드 값에 바인딩됩니다.

2. 열거형(Enum)에서 복잡한 패턴 매칭과 가변 바인딩

가. 열거형 variant에 포함된 여러 필드 매칭

enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}

fn main() {
let mut msg = Message::Move { x: 10, y: 20 };

match &mut msg {
Message::Move { x, y } => {
*x += 5; // 가변 참조로 내부 값 변경 가능
*y += 5;
println!("이동 좌표 변경: x={}, y={}", x, y);
}
_ => {}
}
}
  • match &mut msg를 통해 열거형 값을 가변 참조로 매칭하면, 내부 필드도 가변 참조로 바인딩되어 값 변경이 가능합니다.

나. 특정 variant만 언패킹하기

if let Message::Write(text) = msg {
println!("메시지: {}", text);
}
  • if let 문법을 활용해 관심 있는 variant 한 가지만 매칭해 변수 바인딩 후 처리할 수 있습니다.

다. 여러 variant를 한 패턴에 묶기

match msg {
Message::Quit | Message::Write(_) => println!("Quit 또는 Write variant"),
_ => println!("다른 variant"),
}
  • 여러 variant를 하나로 합쳐 처리할 수도 있습니다.

3. 패턴 바인딩 시 참조와 가변성

  • 기본적으로 match 패턴 내 변수 바인딩은 값 소유권을 가져오거나 복사하지만, 참조 및 가변 참조로도 바인딩할 수 있습니다.
let p = Point { x: 10, y: 20, z: 0 };

match &p { // 불변 참조 패턴 매칭
Point { x, y, .. } => println!("x={}, y={}", x, y),
}

let mut p2 = Point { x: 0, y: 0, z: 0 };

match &mut p2 { // 가변 참조 패턴 매칭
Point { x, y, .. } => {
*x += 1;
*y += 1;
println!("수정된 값 x={}, y={}", x, y);
}
}
  • 참조에 따라 변수 바인딩 시 가변성 여부가 달라지고, 이를 통해 구조체 또는 열거형 내부 값을 안전하게 변경할 수 있습니다.

4. 변수 이름 재사용(shadowing)과 패턴 매칭

패턴 매칭 시, 이전에 바인딩된 변수 이름과 동일한 이름을 써도 새로운 바인딩이 생성됩니다.

let x = 5;
let p = Point { x: 10, y: 20, z: 30 };

match p {
Point { x, y, .. } => {
// 여기 x는 패턴 내의 새 바인딩, 밖의 x와 다른 변수
println!("패턴 매칭 내의 x: {}, 외부 x: {}", x, 5);
}
}

5. 요약 정리

기능예시 (일부만)설명
구조체 부분 패턴 매칭Point { x, .. }특정 필드만 분해, 나머지는 무시
가변 참조 매칭match &mut p와 Point { x, y, .. }구조체 필드를 가변 참조로 바인딩해 값 수정 가능
열거형 가변 매칭match &mut msg와 Message::Move { x, y }enum 내부 필드를 가변 참조로 바인딩해 값 수정 가능
if let 구문if let Message::Write(text) = msg특정 variant만 골라 간단히 바인딩 후 처리
패턴 내 변수 이름 재사용Point { x, y } 내 x는새로운 바인딩기존 변수와 이름이 동일해도 새 변수로 처리

이상으로 구조체와 열거형에서 변수 바인딩 시 사용하는 다양한 패턴 매칭 기법과 가변 바인딩 방법에 대해 설명드렸습니다.

자바 클래스, 인터페이스와 러스트의 구조체, 메소드, 트레이트 방식 비교

자바(Java)는 모든 메소드를 클래스 내부에 정의합니다. 그러나, 러스트(Rust)는 데이터 구조(Struct)와 메소드(Impl block)가 물리적으로 분리되어 있습니다. 구조체 안에는 데이터만 정의하고, 별도의 impl 블록에서 그 구조체에 대한 메소드를 구현합니다.

Ⅰ. 데이터 구조, 메소드 구현 방식 비교

1. 자바: 클래스 내부에 메소드

자바(Java)는 모든 메소드를 클래스 내부에 정의합니다. 이는 객체 지향 프로그래밍(OOP)의 특징으로, 데이터(필드)와 행동(메소드)이 하나의 단위(클래스)에 밀접하게 결합(cohesion)되어 있습니다.

2. 러스트: 구조체와 메소드의 분리

러스트(Rust)에서는 데이터 구조(Struct)와 메소드(Impl block)가 물리적으로 분리되어 있습니다. 구조체 안에는 데이터만 정의하고, 별도의 impl 블록에서 그 구조체에 대한 메소드를 구현합니다.

3. 장단점

구분자바(메소드=클래스 내부)러스트(메소드=구조체 외부)
응집성데이터와 행동이 하나에 묶임데이터와 행동이 분리되어 명확
확장성상속 등 OOP 구조 확장 용이Trait, 여러 impl로 유연하게 확장
가독성클래스 안에서 한 번에 파악 가능데이터 정의와 메소드 구현 분리
유연성하나의 클래스가 하나의 메소드 집합같은 구조체에 여러 impl, trait 구현 가능
재사용성상속, 인터페이스로 구현Trait 등으로 다양한 방식의 재사용
관리의 용이성대형 클래스에서 복잡해질 수 있음관련 없는 메소드를 구조체와 별도 구현 가능
코드 조직화OOP 방식(클래스-중심)데이터 중심(struct), 행동 분리(impl, trait)
  • 자바는 객체 중심의 응집력, 개발 및 이해 용이성이 강점이나, 큰 클래스가 복잡해지거나 상속 구조의 한계 등이 있습니다.
  • 러스트는 데이터와 행동의 분리로 역할이 명확하며, trait 기반의 확장성과 안전성이 강점이지만, 초보자에겐 코드 연관성 파악이 다소 불편할 수 있습니다.

Ⅱ. 예제를 통한 비교

1. 자바(Java)

자바에서는 데이터(필드)와 메소드가 한 클래스 내부에 정의되어 객체 지향 프로그래밍 패러다임에 맞춰 응집되어 있습니다.

public class Point {
private int x;
private int y;

// 생성자
public Point(int x, int y) {
this.x = x;
this.y = y;
}

// 메소드: 두 점 사이 거리 계산
public double distance(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}

// getter 메소드
public int getX() { return x; }
public int getY() { return y; }
}

// 사용
public class Main {
public static void main(String[] args) {
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
System.out.println(p1.distance(p2)); // 출력: 5.0
}
}
  • 징: Point 클래스 내부에 데이터(x, y)와 동작(거리 계산 메소드)을 모두 포함.
  • 장점: 객체 지향적 응집성(cohesion) 강화, 한 곳에서 모든 관련 기능 파악 가능.
  • 단점: 클래스가 커질수록 복잡성 증가, 상속 구조 제한 등.

2. 러스트(Rust)

러스트는 데이터 구조체(struct)와 메소드 구현부(impl 블록)를 분리해서 작성합니다. 이로써 역할 분리가 명확해지고, 여러 impl 블록과 trait을 통해 유연하게 확장할 수 있습니다.

// 데이터 정의: 구조체는 필드만 가짐
struct Point {
x: i32,
y: i32,
}

// 메소드 구현: impl 블록에서 정의
impl Point {
// 연관 함수(정적 메서드와 유사)
fn new(x: i32, y: i32) -> Point {
Point { x, y }
}

// 메소드: 두 점 사이 거리 계산
fn distance(&self, other: &Point) -> f64 {
let dx = (self.x - other.x) as f64;
let dy = (self.y - other.y) as f64;
(dx.powi(2) + dy.powi(2)).sqrt()
}
}

fn main() {
let p1 = Point::new(0, 0);
let p2 = Point::new(3, 4);
println!("{}", p1.distance(&p2)); // 출력: 5.0
}
  • 특징: struct는 데이터만 선언, 메소드는 별도의 impl 블록에서 구현.
  • 장점: 데이터와 행동이 분리되어 역할 명확, 여러 impl이나 trait로 기능 확장 유리, 컴파일타임 안전성 증대.
  • 단점: 관련 데이터와 메소드가 코드상 분리되어 있어 한눈에 파악하기 어려울 수 있음, 전통적인 OOP 방식과 차이 있음.

3. 비교 표

항목자바(Java)러스트(Rust)
구조클래스 내부에 데이터와 메소드가 함께 있음구조체(데이터)와 impl 블록(메소드)로 분리
작성 방식한 클래스 파일 내에서 모든 정의struct로 데이터 정의, 별도 impl로 메소드 구현
확장성상속과 인터페이스 기반 확장 (단일 상속)여러 impl 블록과 trait 조합으로 유연하고 다중 확장 가능
가독성관련 데이터와 메소드가 한 곳에 있어 파악 용이데이터와 메소드가 분리되어 코드가 흩어질 수 있음
안전성런타임 검사 및 가비지 컬렉션컴파일 타임 소유권 및 빌림 검사로 메모리 안전성 강화
메모리참조 타입 중심, 힙 할당 및 가비지 컬렉션 필요값 타입 중심, 명확한 메모리 제어 및 성능 최적화 가능
객체 지향전통적인 OOP 완전 지원클래스는 없으나 trait로 인터페이스 역할 및 객체지향 유사 기능 제공

러스트의 구조체+impl 방식은 자바와는 다르게 데이터와 메소드가 분리되어 있지만, impl 블록 내에서 메소드를 묶어 객체 지향적 프로그래밍의 많은 특징을 흉내 낼 수 있습니다. trait를 활용하면 인터페이스 역할도 하며, 상속 대신 다중 trait 구현으로 유연하게 기능을 확장할 수 있습니다.

Ⅲ . 자바의 상속과 인터페이스 구조와 러스트의 고급 Trait(트레이트) 및 패턴 비교

자바의 상속과 인터페이스 구조와 러스트의 고급 Trait(트레이트) 및 패턴을 심도 있게 비교 설명하면 다음과 같습니다.

1. 자바의 상속과 인터페이스 구조

가. 상속 (Inheritance)

  • 클래스 간에 “is-a” 관계를 표현하는 가장 기본적인 메커니즘입니다.
  • 한 클래스가 다른 클래스(부모 클래스)를 상속받아 멤버 변수와 메소드를 재사용하거나 오버라이드할 수 있습니다.
  • 단일 상속만 지원하여 다중 상속의 복잡성을 회피합니다.
  • 상속 구조가 깊어지면 유지보수가 어려워지고, 부모 클래스 변경 시 자식 클래스에 의도치 않은 영향이 발생할 수 있음.

나. 인터페이스 (Interface)

  • 여러 클래스가 구현해야 하는 메소드의 명세(계약)를 정의합니다.
  • 자바 8부터는 디폴트 메소드 구현도 지원하여 일부 기능적 확장 가능.
  • 인터페이스의 다중 구현이 가능해 상속 단점을 보완하고 다형성 제공.
  • 인터페이스는 구현 메소드가 없거나 기본 구현만 제공하므로, 설계 시 유연성을 줌.
interface Flyer {
void fly();
}
class Bird implements Flyer {
public void fly() {
System.out.println("Bird is flying");
}
}
class Airplane implements Flyer {
public void fly() {
System.out.println("Airplane is flying");
}
}

2. 러스트의 고급 Trait 및 패턴

가. Trait(트레이트) 개념

  • 러스트의 트레이트는 특정 기능을 구현하도록 강제하는 인터페이스 역할을 합니다.
  • 타입에 따라 여러 트레이트를 다중으로 구현할 수 있어 매우 유연합니다.
  • 트레이트 내에 메소드 기본 구현(default method)을 제공해 부분 구현도 가능.
  • Trait는 다중 상속을 대체하며, 조합(composition) 및 다형성을 지원합니다.

나. 고급 트레이트 특징

(1) 동일 메소드명을 가진 다중 트레이트 구현 (충돌 해결)

  • 예를 들어, 같은 이름 fly() 메소드를 가진 서로 다른 트레이트 Pilot, Wizard를 Human 타입에 모두 구현 가능.
  • 호출 시 Pilot::fly(&human), Wizard::fly(&human)처럼 트레이트 이름을 명시해 충돌 해결.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;

impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}

impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}

impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}

fn main() {
let person = Human;
Pilot::fly(&person); // This is your captain speaking.
Wizard::fly(&person); // Up!
person.fly(); // *waving arms furiously*
}

(2) 슈퍼 트레이트 (Super-traits)

  • 한 트레이트가 다른 트레이트의 구현을 전제로 하는 경우 사용.
  • 예: OutlinePrint 트레이트가 Display 트레이트를 반드시 구현한 경우에만 구현 가능하도록 제한.
use std::fmt::Display;

trait OutlinePrint: Display {
fn outline_print(&self) {
// Display 기능 이용해 출력하는 구현
println!("*{}*", self);
}
}

(3) 제네릭과 트레이트 바운드 (Trait Bounds)

  • 함수, 구조체, 열거형 등에서 트레이트를 타입 매개변수 조건으로 지정하여 유연한 재사용성 제공.
fn print_info<T: Display + Clone>(item: T) {
println!("{}", item);
}

(4) Newtype 패턴

  • 외부 타입에 대해 새로운 타입을 만든 뒤 트레이트를 구현해 동작을 확장하거나 수정하는 패턴.
  • 원래 타입의 구현과 격리돼 안전성과 캡슐화 유지에 도움.

다. 자바 상속-인터페이스와 러스트 Trait 패턴 비교

특징자바 (상속, 인터페이스)러스트 (고급 Trait 패턴)
다중 상속 지원 여부클래스는 단일 상속, 인터페이스 다중 구현 가능다중 트레이트 구현 완전 지원
메소드 충돌 처리인터페이스 디폴트 메소드 충돌은 명시적 오버라이딩 필요트레이트별로 명시적 호출(TraitName::method)로 충돌 처리
구현 분리 여부상속 기반으로 구현 내용 일부 공유, 부모-자식 직접 연관데이터와 행동 완전 분리, 여러 impl 블록과 트레이트로 유연한 구현
확장성 및 유연성인터페이스와 추상 클래스 활용 가능, 런타임 다형성 제공트레이트 조합 및 바운드로 더 강력하고 유연한 제네릭 기반 다형성 가능
안전성런타임에 예외 발생 가능, 컴파일 타임 타입 검사 제한컴파일 타임 엄격한 타입 검사 및 소유권 규칙으로 높은 안전성 보장
메모리 구조클래스는 힙에 할당, 가비지 컬렉션 필요구조체는 값 타입(STACK 기반), 트레이트 객체는 런타임에 크기를 모르는 타입 표현 지원

라. 러스트 Trait를 활용한 고급 설계 패턴 예시

(1) Trait 객체 (Trait Objects)

  • 런타임에 다양한 타입을 동적으로 처리하고자 할 때 사용.
  • Box<dyn Trait> 형태로 포인터에 담아 추상화 제공.
trait Animal {
fn speak(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn make_animal_speak(animal: Box<dyn Animal>) {
animal.speak();
}

fn main() {
let dog = Box::new(Dog);
let cat = Box::new(Cat);

make_animal_speak(dog);
make_animal_speak(cat);
}

(2) 제네릭과 Trait Bound를 이용한 추상화

fn print_animal_speak<T: Animal>(animal: T) {
animal.speak();
}

(3) Newtype 패턴

struct Wrapper(Vec<String>);

impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}

5. 러스트 Trait 객체와 동적 디스패치(Dynamic Dispatch)

가. 기본 개념

  • 러스트의 Trait 객체는 런타임에 다양한 타입들을 추상화하고 동적으로 호출할 수 있게 하는 기능입니다.
  • 구체적인 타입을 명시하지 않고 특정 트레이트를 구현한 여러 객체를 하나의 타입으로 사용할 때 유용합니다.
  • dyn Trait 키워드로 표현하며, 이때 메소드 호출은 컴파일 타임이 아닌 런타임에 결정(동적 디스패치)됩니다.

나. 장점

  • 다양한 타입에 대해 동일 인터페이스 역할을 수행할 수 있게 하며, 실행 시점에 적절한 메소드가 호출됩니다.
  • 유연한 설계와 다형성 제공.

다. 단점

  • 동적 디스패치로 인해 약간의 성능 오버헤드 발생.
  • 컴파일 타임에 타입 크기를 알 수 없으므로 보통 Box<dyn Trait>, &dyn Trait 형태로 사용.

예제 코드

trait Animal {
fn speak(&self);
}

struct Dog;

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

struct Cat;

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn animal_speak(animal: &dyn Animal) {
animal.speak();
}

fn main() {
let dog = Dog;
let cat = Cat;

animal_speak(&dog); // Woof!
animal_speak(&cat); // Meow!
}
  • animal_speak 함수는 타입이 &dyn Animal로, 호출하는 객체가 Dog이든 Cat이든 런타임에 올바른 speak 메서드를 호출합니다.

요약

  • 자바의 상속과 인터페이스는 클래스 중심의 단일 상속과 다중 인터페이스 구현으로 객체 지향 프로그래밍 패러다임을 완성하며, 런타임 다형성을 제공합니다.
  • 러스트의 트레이트는 다중 상속을 대체하고, 유연한 기능 확장과 컴파일 타임 안전성을 제공하며, 타입별로 여러 트레이트를 조합해 강력한 추상화와 다형성을 구현합니다.
  • 러스트에서 트레이트는 충돌 해결, 슈퍼 트레이트, 제네릭 바운드, 트레이트 객체 등 다양한 고급 패턴으로 복잡한 설계를 수행할 수 있습니다.
  • 두 언어 모두 장단점이 있으므로, 팀의 경험과 프로젝트 요구사항에 따라 적합한 방식을 선택하는 것이 중요합니다.

데이터 타입에 따른 변수 바인딩(Variable Binding)

Rust에서 variable binding(변수 바인딩)은 값을 변수 이름에 연결하는 과정을 의미하며, Rust 프로그래밍에서 변수 선언과 관련된 기본 개념입니다. Rust는 정적 타입 언어이고 기본적으로 변수가 불변(immutable)으로 선언되기에, 가변성을 가지려면 일부러 명시해야 합니다. 이와 함께 Rust의 바인딩은 shadowing(변수 가리기)이나 스코프와 밀접하게 연관되어 있어, 변수 활용 시 중요한 개념입니다.

Ⅰ. 기본 자료형(Primitive Type)

1. 변수 바인딩 기본

Rust에서 변수 바인딩은 let 키워드로 선언합니다. 이때 변수는 기본적으로 불변입니다. 따라서 선언 후 값을 변경하려 하면 컴파일 에러가 발생합니다.

fn main() {
let x = 5;
println!("x의 값: {}", x);
// x = 6; // 오류: 불변 변수에 재할당 불가
}

이 코드는 x를 5로 바인딩했지만, 이후 x = 6과 같이 변경하려 하면 컴파일 에러가 발생합니다. Rust 컴파일러는 이런 불변 변수 변경 시도를 막아 안정성을 보장합니다.

2. 가변 변수 (Mutable Binding)

변수를 변경 가능하게 하려면 mut 키워드를 사용해 가변 변수로 선언해야 합니다.

fn main() {
let mut x = 5;
println!("초기 x: {}", x);
x = 6;
println!("변경된 x: {}", x);
}

이렇게 하면 x에 대한 값 변경이 가능해집니다. 컴파일러가 에러 발생 시 mut를 추가하라고 친절히 안내하는 점도 Rust의 특징입니다.

3. 타입 추론 및 명시적 타입 선언

Rust 컴파일러는 대부분의 경우 변수 선언 시 타입을 자동 추론합니다.

let x = 42;      // i32로 추론
let y = true; // bool로 추론
let z: u32 = 10; // 명시적 타입 선언

필요에 따라 타입을 명시할 수도 있지만, 대부분 타입 추론에 맡깁니다.

4. 변수 가리기 (Shadowing)

Rust의 독특한 특징인 변수 가리기는 같은 이름의 변수를 같은 스코프 내에서 다시 let으로 선언하여 새 변수를 만드는 것입니다. 이때 이전 변수는 새 변수에 의해 가려집니다.

fn main() {
let x = 5;
let x = x + 1; // 이전 x를 가리고 새 x 선언
{
let x = x * 2;
println!("내부 스코프의 x: {}", x); // 12
}
println!("외부 스코프의 x: {}", x); // 6
}

위 예제에서 첫 번째 x는 5, 두 번째는 6, 내부 스코프의 x는 12입니다. 이렇게 하면 가변 변수 없이도 값 변경 효과를 낼 수 있고 타입도 변경할 수 있습니다.

5. 스코프와 변수 유효 범위

변수는 선언된 블록 {} 내에서만 유효하며, 블록을 벗어나면 사라집니다.

fn main() {
let x = 1;
{
let y = 2;
println!("내부 스코프 y: {}", y); // 2
}
// println!("외부 스코프 y: {}", y); // 오류: y는 유효하지 않음
}

또한 내부 스코프에서 같은 이름으로 변수를 다시 선언하면 외부 변수를 가립니다.

6. 실습 예제 종합

fn main() {
// 불변 변수
let a = 10;
println!("a: {}", a);
// a = 20; // 컴파일 에러!

// 가변 변수
let mut b = 10;
println!("b 초기값: {}", b);
b = 20;
println!("b 변경 후: {}", b);

// 변수 가리기 (shadowing)
let c = 5;
let c = c + 10; // 이전 c 가리기, 이제 c = 15
println!("c: {}", c);

{
let c = c * 2; // 내부 스코프 가리기, c = 30
println!("내부 블록의 c: {}", c);
}

println!("외부 블록의 c: {}", c);

// 타입 변경이 가능한 shadowing
let d = "문자열";
println!("d는 문자열: {}", d);
let d = d.len(); // 같은 이름 변수에 정수 대입 (shadowing)
println!("d는 문자열 길이: {}", d);
}

출력 결과:

a: 10
b 초기값: 10
b 변경 후: 20
c: 15
내부 블록의 c: 30
외부 블록의 c: 15
d는 문자열: 문자열
d는 문자열 길이: 6

Ⅱ. 벡터(Vector)와 튜플(Tuple) 타입 변수 바인딩

1. 벡터(Vector) 타입 변수 바인딩

벡터는 같은 타입 요소들을 동적 크기로 저장하는 컬렉션입니다. Rust에서 벡터는 Vec<T> 타입으로 표현됩니다.

  • 벡터 바인딩 기본 예:
fn main() {
// 빈 vector 생성 (타입 명시 필요)
let mut v: Vec<i32> = Vec::new();
// 값 추가 가능하려면 mut 필요
v.push(1);
v.push(2);
println!("{:?}", v); // 출력: [1, 2]

// 초기값과 함께 벡터 생성 (타입 추론 가능)
let v2 = vec![10, 20, 30];
println!("{:?}", v2); // 출력: [10, 20, 30]
}
  • 벡터의 요소 접근:
let first = v2[0];       // 인덱스를 통한 접근 (주의: 범위 초과 시 패닉)
let maybe_first = v2.get(0); // Option 타입 반환 (안전 접근)
  • 가변 벡터 요소 수정도 mut으로 가능:
let mut v3 = vec![1, 2, 3];
v3[1] = 5; // 두 번째 요소를 5로 변경

벡터는 가변성을 가지고 가리키는 데이터가 동적으로 바뀔 수 있으므로 보통 mut 바인딩과 함께 선언합니다.

2. 3차원 벡터

1) 가변 크기: Vec<Vec<Vec<T>>>

// 2 x 3 x 4 크기의 0으로 채워진 3차원 벡터
let x = 2;
let y = 3;
let z = 4;
let mut v: Vec<Vec<Vec<i32>>> = vec![vec![vec![0; z]; y]; x];

// 읽기
let a = v[1][2][3];

// 쓰기
v[0][1][2] = 42;

헬퍼 함수를 하나 두면 더 편합니다:

fn new_3d<T: Clone>(x: usize, y: usize, z: usize, value: T) -> Vec<Vec<Vec<T>>> {
vec![vec![vec![value.clone(); z]; y]; x]
}

let mut v = new_3d(2, 3, 4, 0i32);

2) 고정 크기: 중첩 배열(스택 또는 Box로 힙에)

크기가 컴파일 타임에 고정되어 있다면 배열을 사용할 수 있습니다.

const X: usize = 2;
const Y: usize = 3;
const Z: usize = 4;

let mut v: [[[i32; Z]; Y]; X] = [[[0; Z]; Y]; X];

v[1][2][3] = 7;

크기가 커서 스택에 올리기 부담스럽다면 Box로 감싸 힙에 둘 수도있습니다.

let mut v: Box<[[[i32; Z]; Y]; X]> = Box::new([[[0; Z]; Y]; X]);

3) 평탄화(flat)한 1차원 Vec<T> + 인덱스 계산

성능/메모리 지역성을 위해 1차원 벡터에 직접 담고 인덱스를 수식으로 계산하는 패턴도 자주 씁니다.

struct Vec3D<T> {
data: Vec<T>,
x: usize,
y: usize,
z: usize,
}

impl<T: Clone> Vec3D<T> {
fn new(x: usize, y: usize, z: usize, value: T) -> Self {
Self {
data: vec![value; x * y * z],
x, y, z,
}
}

#[inline]
fn idx(&self, i: usize, j: usize, k: usize) -> usize {
// (i, j, k) -> linear index
i * self.y * self.z + j * self.z + k
}

fn get(&self, i: usize, j: usize, k: usize) -> &T {
&self.data[self.idx(i, j, k)]
}

fn get_mut(&mut self, i: usize, j: usize, k: usize) -> &mut T {
let idx = self.idx(i, j, k);
&mut self.data[idx]
}
}

let mut v = Vec3D::new(2, 3, 4, 0i32);
*v.get_mut(1, 2, 3) = 5;

2. 튜플(Tuple) 타입 변수 바인딩

튜플은 여러 개의 서로 다른 타입 값을 하나로 묶는 복합 타입입니다. 고정 길이이며 각각 요소의 타입은 다를 수 있습니다.

  • 튜플 선언과 바인딩:
let tup: (i32, f64, char) = (500, 6.4, 'z');
  • 튜플의 요소에 접근하려면 패턴 매칭 또는 점(.) 표기법 사용:
let (x, y, z) = tup;  // 구조 분해 (destructuring)
println!("x: {}, y: {}, z: {}", x, y, z);

let five_hundred = tup.0;
let six_point_four = tup.1;
let z_char = tup.2;

튜플은 기본적으로 불변이며, 가변 선언 시 요소 변경 가능:

let mut tup2 = (1, 2);
tup2.0 = 5; // 첫 번째 요소 변경 가능

4. 스코프와 바인딩의 변수 가리기

벡터, 튜플 등도 기본 자료형과 마찬가지로 스코프 내에서 변수 가리기(shadowing)가 가능합니다:

let v = vec![1];
let v = vec![2, 3]; // 이전 v 가려짐
println!("{:?}", v); // [2, 3]

Ⅲ. 배열

1. 1차원 배열 정의 방법

let arr = [1, 2, 3];  // 길이 3의 1차원 배열

2. 2차원 배열

let arr = [[1, 2, 3], [4, 5, 6]];  // 2x3 배열

3. 3차원 배열

let arr = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
];

Ⅳ. 해시맵

Rust에서 해시맵(HashMap)의 변수 바인딩도 기본적으로 let 키워드를 사용해 이름을 해시맵 값에 바인딩하는 방식으로 이루어집니다. 즉, 해시맵도 다른 변수처럼 데이터 구조 전체를 하나의 변수 이름에 연결하는 행위입니다.

  • 해시맵을 선언할 때는 use std::collections::HashMap;를 임포트하고,
  • 보통 HashMap::new()를 호출하여 빈 해시맵을 만들고,
  • let mut를 붙여 가변 바인딩을 해야 해시맵에 키-값 쌍을 추가하거나 수정할 수 있습니다.

예를 들어:

use std::collections::HashMap;

fn main() {
// 가변 해시맵 바인딩
let mut scores = HashMap::new();

// 값 삽입
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// 값 접근
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);

println!("Blue 팀 점수: {}", score);

// 해시맵의 모든 키-값 쌍 출력
for (key, value) in &scores {
println!("{key}: {value}");
}
}
  • 여기서 scores 변수에 해시맵 데이터 구조가 바인딩되었고,
  • mut 키워드 때문에 값 삽입과 변경이 가능합니다.
  • .insert() 메서드로 키-값 쌍을 추가하거나 갱신하며,
  • .get() 메서드로 해당 키에 연관된 값을 읽을 수 있습니다.

요약하면, 해시맵도 배열, 벡터, 튜플, 구조체와 같이 Rust에서 변수 바인딩 개념이 동일하게 적용됩니다. let으로 이름을 데이터에 바인딩하고, 변경하려면 mut가 필수입니다. 해시맵은 키-값 쌍 컬렉션이라는 점에서 특수하지만, 바인딩이라는 개념 면에서는 일반 변수와 차이가 없습니다.

추가적으로 해시맵 내부에 구조체 같은 복합 데이터 타입을 저장해도, 변수 바인딩과 관련된 기본 원칙은 같습니다.

Ⅴ. 사용자 정의 타입

Rust에서 사용자 정의 타입(예: 구조체와 열거형)에 대한 변수 바인딩은 기본 타입이나 벡터, 튜플 등과 동일하게 let 키워드를 사용하여 값을 변수 이름에 연결하는 것을 의미합니다. 하지만 구조체와 열거형은 내부 필드나 variant에 데이터를 포함할 수 있어서, 변수 바인딩과 활용이 더 다양한 형태로 나타납니다.

아래에서 구조체와 열거형을 사용자 정의 타입의 변수 바인딩 예제로 구체적으로 설명합니다.

1. 구조체(Struct) 변수 바인딩 예제

// 구조체 정의
struct Point {
x: i32,
y: i32,
}

fn main() {
// 구조체 인스턴스를 생성하고 변수 p에 바인딩
let p = Point { x: 10, y: 20 };

// 구조체 필드에 접근
println!("x: {}, y: {}", p.x, p.y);

// 가변 바인딩 시 필드 값 변경 가능
let mut p_mut = Point { x: 5, y: 5 };
p_mut.x = 15;
println!("변경된 x: {}", p_mut.x);

// 구조체를 분해하여 필드 값을 각각 변수에 바인딩
let Point { x: a, y: b } = p;
println!("분해된 x: {}, y: {}", a, b);
}
  • let p = Point { … };에서 p는 Point 타입 값에 변수 바인딩입니다.
  • 패턴 매칭처럼 let Point { x: a, y: b } = p; 구문으로 구조체 필드를 변수 a, b에 바인딩할 수도 있습니다.
  • mut 키워드로 가변 바인딩을 선언하면 구조체 필드를 수정할 수 있습니다.

2. 열거형(Enum) 변수 바인딩 예제

// 열거형 정의
enum Message {
Quit, // 데이터 없는 variant
Move { x: i32, y: i32 }, // 필드가 있는 variant (struct-like)
Write(String), // 튜플 형태 variant
ChangeColor(i32, i32, i32), // 여러 필드를 가진 튜플 variant
}

fn main() {
// enum 값에 변수 바인딩
let msg = Message::Move { x: 10, y: 20 };

// match로 variant별 데이터 바인딩과 처리
match msg {
Message::Quit => println!("Quit variant"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to {}, {}, {}", r, g, b),
}
}
  • let msg = Message::Move { x: 10, y: 20 };에서 msg가 Message enum의 Move variant 값에 바인딩되었습니다.
  • match 문에서는 각 variant의 내부 데이터를 별도의 변수(x, y, text, r, g, b)에 패턴 매칭을 사용한 변수 바인딩으로 추출합니다.
  • 열거형 값 하나가 여러 variant 중 하나로만 존재할 수 있기에, match를 통해안전하게 처리합니다.

요약

구분변수 바인딩 방식설명
기본 타입let x = 5;변수 이름이 단순 값과 바인딩됨
구조체let p = Point { … };변수 이름이 구조체 인스턴스에 바인딩
let Point { x: a, y: b } = p;구조체 필드를 변수에 분해하여 바인딩
열거형let msg = Message::Write(“Hi”.to_string());변수에 enum variant 값 바인딩
match msg { Message::Write(text) => { … } }match 내 패턴 매칭으로 variant 내 데이터에 변수 바인딩

이처럼 사용자 정의 타입에서도 변수 바인딩은 let을 사용해 값을 변수 이름에 연결하는 동일한 개념이며, 구조체는 필드별 분해 바인딩이 가능하고, 열거형은 variant별 패턴 매칭을 통해 내부 데이터를 변수에 바인딩하는 방식으로 동작합니다.

구조체와 튜플을 조합한 데이터 모델링

Rust에서 구조체(struct)와 튜플(tuple)을 조합해 복잡한 데이터 모델링을 하는 방법은, 각 자료구조의 장점을 살려 중첩(nesting)하거나, 서로 포함시켜 계층적인 구조를 만드는 것입니다. 이렇게 하면 의미 있는 필드(구조체)와 위치 기반 데이터(튜플)를 효과적으로 결합할 수 있습니다.

실제 프로젝트에서는 구조체로 주요 엔티티(예: 사용자, 상품, 센서 등)를 정의하고,
구조체의 일부 필드를 튜플로 선언해 위치, 좌표, 설정값 등 간단한 데이터를 묶어 표현하거나, 구조체로 선언해서 의미를 명확히 합니다.

1. 구조체 안에 튜플을 포함하는 예시

struct Employee {
name: String,
age: u32,
// (연, 월, 일) 생년월일을 튜플로 표현
birth_date: (u16, u8, u8),
}

fn main() {
let emp = Employee {
name: String::from("Kim"),
age: 28,
birth_date: (1997, 5, 14),
};
println!("{}의 생년월일: {}-{}-{}", emp.name, emp.birth_date.0, emp.birth_date.1, emp.birth_date.2);
}
  • 구조체는 필드의 의미를 명확히하고, 튜플은 간단한 데이터 묶음에 적합합니다.
  • Emplyee라는 구조체를 선언(정의)하면서 필드명과 형식을 지정하는데, birthdate는 생년월일의 연,월,일을 튜플 형식으로 지정한 것입니다.
  • let 문을 이용해서 Employee의 instance를 생성하고, 여기서는 emp, 출력할 때는 emp를 이용해서 emp.필드명 식으로 하면 되는데, 튜플 타입은 emp.필드명 다음에 튜플이므로 index를 붙여서 emp.birth_date.0, .1, .2식으로 표현합니다.
  • 출력값은 Kim의 생년월일: 1997-5-14입니다. 두 자릿수로 출력하려면 {:02}로 수정하면 됩니다. 두 자릿수로 출력하는데, 부족하면 0으로 채우라는 의미입니다.

2. 튜플 안에 구조체를 포함하는 예시

struct Product {
id: u32,
name: String,
}

fn main() {
// (상품, 수량) 형태로 장바구니 항목 표현
let cart_item: (Product, u32) = (
Product { id: 1, name: String::from("Book") },
3,
);
println!("{}: {}개", cart_item.0.name, cart_item.1);
}
  • 튜플로 여러 정보를 임시로 묶되, 각 요소가 구조체라면 의미를 명확히 할 수 있습니다.
  • cart_item을 튜플 형식으로 지정해서 Product와 수량을 받는데, Product를 구조체와 연결해서 id와 name으로 의미를 명확히하는 것입니다.
  • 튜플 속에 구조체가 들어있으므로 출력할 때 cart_item 다음에 인덱스를 적고, 구조체의 필드명을 적어서 표시합니다. 예) cart_item.0.id, cart_item.0.name, cart_item.1
  • 출력 결과는 ‘Book: 3개’입니다.

3. 중첩 구조체와 튜플을 활용한 복합 모델

struct Address {
city: String,
zip: String,
}

// (위도, 경도) 위치 정보를 튜플로 표현
struct Store {
name: String,
address: Address,
location: (f64, f64),
}

fn main() {
let store = Store {
name: "Rust Mart".to_string(),
address: Address {
city: "Seoul".to_string(),
zip: "12345".to_string(),
},
location: (37.5665, 126.9780),
};
println!("{} ({}, {}) - 위치: ({}, {})",
store.name, store.address.city, store.address.zip, store.location.0, store.location.1
);
}
  • Address 구조체를 정의한 다음 Address 구조체를 Store의 address 필드의 type으로 사용하고, Store의 location은 위도와 경도를 튜플 형식으로 정의했습니다.
  • 따라서, Store 구조체의 인스턴스를 만들 때도 address를 Address 구조체로 입력하고, location은 위도와 경도를 튜플 형식으로 입력했습니다.
  • 그리고, 출력할 때는 인스턴스명.필드명인데, address는 구조체이므로 다시 한번 더 필드명을 적어주었고, tuple 타입은 필드명 다음에 인덱스를 추가했습니다.
  • 출력 결과는 ‘Rust Mart (Seoul, 12345) – 위치: (37.5665, 126.978)’입니다.

4. 튜플 구조체와 일반 구조체 조합

struct Point(i32, i32, i32);

struct Sensor {
id: u32,
position: Point,
}

fn main() {
let sensor = Sensor { id: 101, position: Point(10, 20, 30) };
println!("센서 {} 위치: ({}, {}, {})", sensor.id, sensor.position.0, sensor.position.1, sensor.position.2);
}
  • 이번에는 튜플 구조체를 정의한 다음, 일반 구조체의 타입으로 사용한 예입니다.
  • 일반 구조체의 타입이 튜플이냐 아니냐만 다를 뿐 표현하는 방식은 위와 동일합니다.

이처럼 구조체와 튜플을 조합하면 복잡한 데이터도 명확하고 효율적으로 모델링할 수 있습니다.

  • 구조체는 필드의 의미와 계층 구조를,
  • 튜플은 간단한 값 묶음이나 위치 기반 데이터를 담당하게 하여,
  • 코드의 가독성과 확장성을 모두 높일 수 있습니다

5. 튜플 구조체로 타입 구분

struct Point(i32, i32, i32);
struct Color(i32, i32, i32);

fn draw_sphere(center: Point, color: Color) {
// center와 color가 같은 (i32, i32, i32) 구조지만, 타입이 달라 혼동 방지
// This function would contain logic to draw a sphere at the given center
// with the specified color.

println!("Drawing sphere at center: ({}, {}, {}) with color: ({}, {}, {})",
center.0, center.1, center.2,
color.0, color.1, color.2);
}

fn main() {
let center = Point(0, 0, 0);
let color = Color(255, 0, 0); // Red color

draw_sphere(center, color);
}
  • 위와 같이 구조체를 튜플 형식으로 지정하면 draw_sphere 함수에서 입력 타입이 구조체 형식과 맞는지 체크하는데,
  • 아래와 같이 함수의 인수를 튜플 형식으로 지정하면 둘 다 튜플 형식이기 때문에 center 자리에 Point 구조체 타입이 아닌 color 튜플을 넣어도 맞는 타입인지 체크를 못합니다.
  • 튜플 구조체(예: struct Point(i32, i32, i32);)를사용하면,
    동일한 데이터 구조라도 타입별로 구분할 수 있어 실수 방지 및 타입 안전성을 높입니다.
fn draw_sphere(center: (i32, i32, i32), color: (i32, i32, i32)) {
...
}

fn main() {
let center = (0, 0, 0);
let color = (255, 0, 0); // Red color

draw_sphere(color,center);
}

6. 함수 반환값 및 임시 데이터

함수에서 여러 값을 반환할 때 튜플을 사용하고,
이 반환값을 구조체의 필드로 저장하거나, 여러 구조체 인스턴스를 튜플로 묶어 일시적으로 처리할 수 있습니다.

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}

struct Stats {
min: i32,
max: i32,
}

fn main() {
let numbers = [3, 7, 2, 9, 4];
let (min, max) = min_max(&numbers);

let stats = Stats { min, max };
println!("최솟값: {}, 최댓값: {}", stats.min, stats.max);
}
  • let (min, max) = min_max(&numbers);
    => numbers 배열을 참조로 가져와서 min_max 함수를 처리한 다음 결괏값을 min, max 튜플에 넣고,
  • let stats = Stats { min, max };
    => min과 max를 Stats 구조체에 넣어 stats 인스턴스(또는 변수)를 만듭니다.
    min: min, max: max라고 쓰는 것이 정석이지만 필드명과 변수명이 같기 때문에 필드명만 적으면 됩니다.
  • 그리고, 인스턴스의 min과 max를 출력하는 것입니다.

7. 설정값, 좌표, 범위 등 불변 데이터 관리

고정된 설정값이나 좌표와 같이, 변경되지 않는 데이터는 튜플로 관리하고,
이 값을 구조체의 일부로 포함시켜 사용합니다.

struct DbConfig { 
host: String,
port: u16,
credentials: (String, String), // (username, password)
}

fn main() {
let db_config = DbConfig {
host: String::from("localhost"),
port: 5432,
credentials: (String::from("user"), String::from("password")),
};

println!("DB 호스트: {}", db_config.host);
println!("DB 포트: {}", db_config.port);
println!("DB 사용자명: {}", db_config.credentials.0);
println!("DB 비밀번호: {}", db_config.credentials.1);
}

8. 튜플과 달리 Struct는 메서드와 함께 활용

구조체에 메서드를 구현하여 데이터와 동작을 결합할 수 있습니다.
예를 들어, 2차원 평면상의 점(Point)에 대해 특정 축 위에 있는지 판별하는 메서드를 추가할 수 있습니다.

struct Point(f32, f32);

impl Point {
fn on_x_axis(&self) -> bool {
self.0 == 0.0
}
fn on_y_axis(&self) -> bool {
self.1 == 0.0
}
}

fn main() {
let point = Point(0.0, 0.0);
if point.on_x_axis() && point.on_y_axis() {
println!("원점에 있습니다.");
}
}
  • 구조체에 메서드를 추가해 객체 지향적으로 사용할 수 있습니다.
  • 구조체의 메서드를 만들려면 impl 구조체라고 명명하고, 그 안에서 함수(fn, 메소드)를 작성하는데, 첫번째 인수는 &self, 구조체 자체입니다.
  • fn on_x_axis(&self) -> bool은 구조체를 인수로 받아 bool 형식인 True, False를 반환합니다.
  • self.0 == 0.0
    => 세미콜론으로 끝나지 않으므로 반환값인 표현식으로 첫번째 튜플 값이 0.0인지 비교해서 같다면 True를 반환하고, 아니면 False를 반환하는 것입니다.
  • self.1 == 0.0는 튜플의 두번째 값이 0.0인지 비교하는 것입니다.
  • 출력값은 튜플의 값이 모두 0.0이므로 ‘원점에 있습니다.’입니다.

9. 요약

  • 튜플: 간단한 값 묶음, 여러 값 반환, 임시 데이터에 적합
  • 구조체: 명확한 의미의 데이터 구조, 필드 이름, 가독성·유지보수성 강조
  • 튜플 구조체: 같은 구조이지만 다른 의미의 타입 구분으로 타입 안전성을 강화

fmt::Display trait

Debug는 구조체 정보를 그대로 출력하고, fmt::Display는 원하는 방식으로 표현을 커스터마이징할 수 있게 해줍니다. Debug는 #[derive(Debug)]로 자동 구현이 되고, fmt::Display는 impl fmt::Display를 통해 수동 구현이 필요합니다.

구분DebugDisplay
목적디버깅용 출력사용자 친화적 출력
매크로println!(“{:?}”, x)println!(“{}”, x)
구현 방법#[derive(Debug)]로 자동 구현수동 구현 필요 (impl fmt::Display)
포맷 예시Matrix(1,0, 2.0, 3.0, 4.0)(1.0, 2.0)
(3.0, 4.0)
문자열화format!(“{:?}”, x)x.to_string() 또는
format!(“{}”, x)

1. tuple 구조체

가. 선언

#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);
  • [derive(Debug)]는 Rust에서 자동으로 Debug 트레이트를 구현해 달라는 뜻입니다. 이를 통해 해당 구조체(또는 열거형 등)를 {:?} 형식으로 출력할 수 있게 됩니다.
  • Matrix 구조체는 f32 타입의 값 4개를 가지며, 필드에 이름이 없어서 튜플처럼 순서로 접근 합니다.

나. instance 생성 및 Debug 포맷 출력

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
}
  • Matrix라는 튜플 구조체의 새 인스턴스(instance)를 생성하는 코드입니다.
  • (1.1, 1.2, 2.1, 2.2) 라는 4개의 f32 값을 순서대로 Matrix 구조체에 넣어 새 변수 matrix에 저장합니다.
  • println!(“{:?}”, matrix);는 Debug 트레잇을 이용해 구조체 내부 값을 출력하는 것입니다.
    출력값은 Matrix(1.1, 1.2, 2.1, 2.2)입니다.

2. fmt::Display 트레잇 구현

위는 Debug 트레잇으로 출력하는 것이고, fmt::Display 트레잇을 통해 다양한 포맷으로 출력할 수 있습니다.

use std::fmt;

impl fmt::Display for Matrix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // 원하는 포맷으로 출력 구성
        write!(
            f,
            "( {} {} )\n( {} {} )",
            self.0,self.1, self.2,self.3
        )
    }
}
  • use std::fmt;
    => Rust 표준 라이브러리(std) 안에 있는 fmt 모듈을 현재 스코프(scope)로 가져온다는 뜻입니다.
    fmt는 그 안에 문자열 포맷(formatting) 관련 함수와 트레이트들이 모여있는 모듈(module)입니다.
    use std::fmt;를 하면, 나중에 fmt::Display나 fmt::Formatter 같은 타입이나 트레이트를 쓸 때, std::fmt 전체를 계속 적지 않고 그냥 fmt만 써도 됩니다.

  • impl fmt::Display for Matrix
    => Matrix에 대해 Display 트레이트를 수동으로 구현합니다.
  • fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    => 출력 형식을 어떻게 할지를 정의하는 함수입니다.
    메소드명은 fmt이고, 인수로 자기 자신인 &self와 가변 참조 형식의 fmt::Fomatter 형식(구조체)의 f를 받고, 반환 형식은 fmt::Result입니다.

    fmt::Result는 Result<(), fmt::Error>의 별칭으로,
    성공하면 Ok(()), 실패하면 Err(fmt::Error)를 반환합니다.
  • write! 매크로의 입력 포맷은 write!(f, “{}”, value)입니다. “{}”라는 포맷에 value를 넣고, 그 결과를 f에 기록합니다.
  • “( {} {} )\n( {} {} )”는 괄호안에 값 2개씩을 넣어 출력하는데, \n은 줄 바꿈을 의미합니다.
  • self.0,self.1, self.2,self.3는 포맷에 들어갈 값을 순서대로 나열한 것입니다.

3. main 함수

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);

    // Debug 포맷
    println!("{:?}", matrix);

    // Display 포맷
    println!("{}", matrix);
}

위에서 첫번째 println! 매크로는 Debug 트레잇으로 출력하는 것으로 위에서 봤고,

두번째 println! 매크로는 fmt::Display 트레잇으로 출력하는 것으로 2번에서 구현한 바 있습니다.

출력하면 아래와 같이 튜플 구조체의 값이 두 개씩 괄호안에 싸여 두 줄로 표시됩니다.

( 1.1 1.2 )
( 2.1 2.2 )

4. fmt::Formatter의 주요 메서드

가. width(): Option<usize>

  • 출력 시 최소 너비를 설정하는 값입니다.
  • Some(n)이면, 출력 문자열이 최소 n칸은 차지해야 함을 의미합니다.
  • 예: println!(“{:5}”, “hi”) → ” hi” (공백 3개 + hi)

사용 예:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let width = f.width().unwrap_or(0);
write!(f, "{:width$}", "hi", width = width)
}

나. precision(): Option<usize>

  • 소수점 이하 자리수 또는 최대 출력 길이를 설정합니다.
  • Some(n)이면, 최대 n자리 또는 n글자까지만 출력합니다.

사용 예:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let prec = f.precision().unwrap_or(2);
write!(f, "{:.prec$}", 3.14159, prec = prec)
}
// precision이 2이면 → 3.14

또는 문자열에 적용하면:

write!(f, "{:.5}", "Hello, world!") // → Hello

다.align(): Option<fmt::Alignment>

  • 정렬 방식 설정입니다. 리턴값은 Option이고, Alignment는 열거형입니다.
의미
Some(Left)왼쪽 정렬 (:<)
Some(Right)오른쪽 정렬 (:>)
Some(Center)가운데 정렬 (:^)
None정렬 지정 없음

사용 예:

use std::fmt::{self, Alignment};

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match f.align() {
Some(Alignment::Left) => write!(f, "왼쪽 정렬됨"),
Some(Alignment::Right) => write!(f, "오른쪽 정렬됨"),
Some(Alignment::Center) => write!(f, "가운데 정렬됨"),
None => write!(f, "기본 정렬"),
}
}

라. Matrix 구조체에 width, precision 적용하기

impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let w = f.width().unwrap_or(5);
let p = f.precision().unwrap_or(2);
write!(
f,
"( {:>w$.p$} {:>w$.p$} )\n( {:>w$.p$} {:>w$.p$} )",
self.0, self.1, self.2, self.3,
w = w, p = p
)
}
}

사용:

let m = Matrix(1.2345, 12.3456, 123.4567, 1234.5678);
println!("{:8.1}", m); // 최소 너비 8칸, 소수점 아래 1자

출력:

(     1.2    12.3 )
( 123.5 1234.6 )

바. format!와 write!

방식장점
format!() 사용문자열을 먼저 만들어 두기 편함
write!() 직접 사용메모리 절약, 포맷 버퍼 덜 생성함

Rust에서는 둘 다 가능하지만, 성능이나 메모리를 조금 더 아끼고 싶다면 write!()를 직접 쓰는 방식이 더 낫습니다.

5. 튜플 위치 변경(reverse)

// Tuples can be used as function arguments and as return values.
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // `let` can be used to bind the members of a tuple to variables.
    let (int_param, bool_param) = pair;

    (bool_param, int_param)
}

fn main() {
    let pair = (1, true);
    println!("Pair is {:?}", pair);

    println!("The reversed pair is {:?}", reverse(pair));
}
  • fn reverse(pair: (i32, bool)) -> (bool, i32) {
    => reverse 함수는 pair라는 인수를 받는데, i32와 bool 타입의 튜플이며, 반환 형식은 bool, i32 타입의 튜플입니다.
  • let (int_param, bool_param) = pair;
    => pair라는 튜플에서 값을 튜플 형식으로 받는데 int_param과 bool_param은 튜플의 멤버와 묶입니다.
  • (bool_param, int_param)은 표현식으로 reverse 함수의 return 값입니다. 인수는 i32, bool 타입이었는데, 순서가 바뀌어서 bool, i32 타입으로 반환됩니다.
  • let pair = (1, true);
    => pair란 변수에 1, true를 멤버로 하는 튜플을 대입합니다.
  • println!(“Pair is {:?}”, pair);
    => pair란 튜플을 debug 포맷으로 출력합니다.
  • println!(“The reversed pair is {:?}”, reverse(pair));
    => reverse 함수를 이용해 pair 튜플의 순서를 바꿔서 출력합니다.
  • cargo run으로 실행하면
    입력한 값대로 (1, true)로 출력되고,
    그 다음은 reverse 함수가 적용되어 순서가 바뀌어 (true, 1)로 출력됩니다.
Tuple 필드 reverse

6. 구조체 위치 변경(transpose)

use std::fmt;

#[derive(Debug)]
struct Matrix (f32, f32, f32, f32);

impl fmt::Display for Matrix {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "( {} {} )\n( {} {} )",
            self.0,self.1, self.2,self.3
        )
    }
}

fn transpose(matrix: Matrix) -> Matrix {
    Matrix(matrix.0, matrix.2, matrix.1, matrix.3)
}

fn main() {
    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
    println!("Matrix:\n{}", matrix);
    println!("Transpose:\n{}", transpose(matrix));
}
fn transpose(matrix: Matrix) -> Matrix {
Matrix(matrix.0, matrix.2, matrix.1, matrix.3)
}
  • fn transpose(matrix: Matrix) -> Matrix
    => 이 부분을 함수 signature라고 부르며,
    의미는 transpose 함수를 만드는데, 인수는 matrix이고, matrix의 타입은 Matrix 구조체입니다. 또한 반환 형식도 Matrix 구조체입니다.
  • { Matrix(matrix.0, matrix.2, matrix.1, matrix.3) }
    => 중괄호 안에 있는 것은 함수 본문으로 세미콜론이 없기 때문에 반환값을 나타내는 표현식입니다.

    Matrix(matrix.0, matrix.2, matrix.1, matrix.3)는 matrix라는 인수의 필드에 접근하는데, 인덱스가 0, 1, 2, 3이지만 순서를 바꾸기 위해 0, 2, 1, 3으로 표시했습니다. 이들 필드를 결합해서 Matrix 구조체를 반환합니다.
    println!("Matrix:\n{}", matrix);
println!("Transpose:\n{}", transpose(matrix));

fmt::Display trait을 이용해 matrix와 transpose(matrix)를 출력합니다.

출력 결과는
Matrix:
( 1.1 1.2 )
( 2.1 2.2 )
Transpose:
( 1.1 2.1 )
( 1.2 2.2 )
입니다.

1.2와 2.1의 위치가 달라졌습니다.

외부 라이브러리 사용법 (Crate 활용)

Rust는 Crate(크레이트) 단위로 코드와 라이브러리를 구성합니다.
크레이트는 Rust의 패키지 시스템에서 가장 작은 단위로, 우리가 Cargo.toml에 추가해서 사용하는 외부 라이브러리들도 모두 크레이트입니다.

크레이트의 기본 개념과 사용법, 외부 라이브러리를 프로젝트에 추가하고 사용하는 법, 그리고 버전 관리와 의존성에 대해 알아보겠습니다.


1. Crate란?

  • 크레이트(Crate)는 하나의 컴파일 단위입니다.
  • 두 가지로 나뉩니다:
    Binary Crate: main() 함수를 포함하고, 실행 가능한 프로그램이 됨
    Library Crate: 다른 크레이트에서 가져와 사용할 수 있는 재사용 가능한 코드 집합

예를 들어 우리가 사용하는 tokio, serde, rand 등은 모두 라이브러리 크레이트입니다.


2. Cargo로 외부 크레이트 사용하기

Rust 프로젝트는 Cargo라는 빌드 도구를 중심으로 관리됩니다.

가. Cargo.toml에 의존성(dependency) 추가

예를 들어 난수를 생성하는 rand 크레이트를 사용하려면, 프로젝트 루트의 Cargo.toml 파일의 dependencies 절에 다음과 같이 작성합니다.

처음에 버전을 0.8.0을 적었더니 버전이 0.9.1까지 있으니 여기서 선택하라고 화면이 표시됩니다.

dependency  더 높은 버전 제시
[dependencies]
rand = "0.9.1"

이제 cargo build 또는 cargo run을 실행하면 자동으로 해당 크레이트가 다운로드되고 프로젝트에 포함됩니다.

나. 코드에서 사용하기

use rand::Rng;

fn main() {
let mut rng = rand::rng();
let n: u8 = rng.random_range(1..=10);
println!("1부터 10 사이의 무작위 수: {}", n);
}
use rand::Rng;
  • Rng는 random_range() 같은 메서드를 제공하는 트레잇(trait)입니다.
  • 이걸 use해야 random_range(…) 같은 메서드를 쓸 수 있습니다.

let mut rng = rand::rng();
  • 최신 rand에서는 rand::rng()로 난수 생성기를 가져옵니다.
  • 이전에는 rand::thread_rng()를 사용했지만, 그건 deprecated 되었고 이제는 rng()로 대체됩니다. rand 버전을 0.8로 하면 아래와 같은 에러가 발생합니다.
crate 버전이 낮아 오류 발생
  • 반환 타입은 여전히 내부적으로 ThreadRng입니다.

📝 의미:

“현재 스레드에서 사용할 난수 생성기를 가져와서 rng에 저장하라”


let n: u8 = rng.random_range(1..=10);
  • random_range(1..=10)은 1부터 10까지의 정수 중 무작위 값을 생성합니다.
  • inclusive range (..=)를 사용했으므로, 10도 포함됩니다.
  • 반환되는 값은 u8 타입으로 명시적으로 지정했습니다.
  • random_range는 gen_range() 대신 사용되는 최신 방식입니다.

println!(“1부터 10 사이의 무작위 수: {}”, n);
  • 생성된 난수 n을 콘솔에 출력합니다.
  • 실행할 때마다 1~10 사이의 숫자가 무작위로 나옵니다.


3. 크레이트 문서 확인하기

모든 주요 크레이트는 문서를 잘 갖추고 있습니다.
https://docs.rs에서 크레이트 이름으로 검색하면 API 문서를 확인할 수 있습니다.

예: https://docs.rs/rand

문서에는 모듈 구조, 사용 예제, Trait 설명 등이 포함되어 있어 매우 유용합니다.


4. 크레이트 버전 지정

Cargo는 Semantic Versioning을 따릅니다.

  • rand = “0.9” → 0.9.x까지 자동 업데이트 (1.0은 제외)
  • rand = “=0.9.1” → 정확한 버전
  • rand = “>=0.9, <10.0” → 범위 지정

보통은 “0.9”와 같이 호환 가능한 최신 버전으로 표시하는 것이 일반적입니다.


5. 여러 크레이트 함께 사용하기

Rust 프로젝트는 여러 외부 크레이트를 함께 사용할 수 있습니다.

예:serde와 serde_json을 함께 사용하여 JSON을 파싱:

[Cargo.toml]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[main.rs]

use serde::{Deserialize, Serialize};
use serde_json::Result;

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

fn main() -> Result<()> {
let data = r#"{"name": "홍길동", "age": 30}"#;
let p: Person = serde_json::from_str(data)?;
println!("{:?}", p);
Ok(())
}

가. use 키워드

use serde::{Deserialize, Serialize};
use serde_json::Result;
  • Rust의 use 키워드는 코드에서 다른 모듈이나 라이브러리의 특정 항목(함수, 구조체, 열거형 등)을 현재 스코프(범위)로 가져와 사용하기 위해 사용됩니다. 이를 통해 코드를 더 간결하고 읽기 쉽게 만들 수 있습니다. 
  • serde::{Deserialize, Serialize}: 구조체를 직렬화(Serialize) 또는 역직렬화(Deserialize) 하려면 이 트레잇이 필요합니다.
  • serde_json::Result: serde_json이 제공하는 Result 타입(Enum)을 사용합니다. 에러 처리를 쉽게 하기 위해 사용됩니다.

나. 구조체 정의

#[derive(Serialize, Deserialize, Debug)]
struct Person {
name: String,
age: u8,
}
  • [derive(…)]: 구조체에 자동으로 트레잇 구현을 추가합니다.
    – Serialize: 구조체를 JSON으로 변환할 수 있게 함.
    – Deserialize: JSON을 구조체로 변환할 수 있게 함.
    – Debug: println!(“{:?}”, …)으로 구조체 내용을 출력할 수 있게 함.
  • Person 구조체 정의
    – name: 문자열 타입
    – age: u8(0~255 정수 타입)

다. main 함수

fn main() -> Result<()> {
  • 반환 타입이Result<()>인 이유는 serde_json::from_str가 실패할 수 있기 때문입니다. 실패하면 에러를 리턴하고, 성공하면 Ok(())를 반환합니다.

라. JSON 문자열 → 구조체

let data = r#"{"name": "홍길동", "age": 30}"#;
  • r#”…#”는 raw string(원시 문자열) 문법입니다. 문자열 내부에 따옴표가 있어도 별도로 이스케이프할 필요가 없어 편리합니다.
  • {“name”: “홍길동”, “age”: 30}는 JSON 포맷 문자열입니다.
let p: Person = serde_json::from_str(data)?;
  • serde_json::from_str는 JSON 문자열을 파싱해서 Person 구조체로 변환합니다.
  • ? 연산자는 실패 시 에러를 리턴하고, 성공 시 결과값을 반환합니다.

마. 구조체 출력

println!("{:?}", p);
  • 구조체 p를 Debug 포맷으로 출력합니다. 결과는 예를 들어 다음과 같이 나옵니다.
    Person { name: “홍길동”, age: 30 }

6. Crate.io에서 크레이트 찾기

외부 라이브러리는 모두 https://crates.io에서 검색할 수 있습니다.

  • 인기 순, 다운로드 수, 최근 업데이트 순으로 정렬 가능
  • 사용자 리뷰, 문서 링크, GitHub 코드도 확인 가능

예:

  • 웹 요청 라이브러리: reqwest
  • 비동기 실행기: tokio
  • CLI 도구 만들기: clap

7. 크레이트 네임스페이스(namespace)와 모듈(module)

크레이트를 임포트할 때는 보통 최상위 네임스페이스부터 시작합니다.

use regex::Regex; // regex 크레이트 내 Regex 타입 사용

내부 모듈에 접근할 땐 점(::)을 따라 구조를 확인합니다.

예:

use tokio::time::{sleep, Duration};

8. 주의사항

  • 크레이트마다 버전이 다르면 충돌이 생길 수 있음 → Cargo가 자동으로 중복 조율
  • 사용하지 않는 크레이트는 지워주는 게 좋음
  • 일부 크레이트는 feature flag를 통해 기능을 선택적으로 활성화함

9. 정리

항목설명
CrateRust 코드의 재사용 단위, 외부 라이브러리
Cargo.toml의존성을 정의하는 설정 파일
crates.io외부 크레이트 검색/다운로드 플랫폼
docs.rs모든 크레이트의 공식 문서 모음 사이트
use크레이트 또는 모듈에서 항목을 가져오는(import) 키워드

10. 마무리

Rust의 크레이트 생태계는 매우 강력하고 체계적입니다.
외부 라이브러리를 잘 활용하면, 코드의 양을 줄이고 품질을 높일 수 있습니다.
이제는 필요한 기능이 있다면 직접 구현하기보다 crates.io에서 검색해보는 것이 먼저입니다.

구조체 (Structs)

Rust에서 복잡한 데이터를 다루기 위해 사용하는 기본 단위가 바로 구조체(struct)입니다. 구조체는 여러 개의 관련된 데이터를 하나의 타입으로 묶어 표현할 수 있도록 해줍니다. Rust는 구조체와 메서드(impl 블록)를 통해 모듈화, 캡슐화, 데이터 모델링이 가능합니다.


1. 기본 구조체 정의 및 사용

가장 기본적인 구조체는 struct 다음에 구조체 이름을 쓰고, 중괄호 안에 필드의 이름과 타입을 :으로 연결해 선언합니다.

Rust의 구조체 이름 규칙은 대문자 카멜 케이스(Camel case)입니다. 예를 들어 User, MySruct와 같이 단어 시작을 대문자로 하고, _를 사용할 수 있으며 숫자로 시작할 수 없고, 공백이 포함되면 안됩니다.

struct User {
    username: String,
    email: String,
    active: bool,
}

사용할땐 struct의 인스턴스를 생성합니다.

일반 변수 선언할 때와 마찬가지로 let 키워드를 사용하고, 그 다음에 인스턴스 이름을 적고, = 구조체 이름을 적은 다음 중괄호안에 필드의 이름과 값을, key: value 쌍으로 아래와 같이 입력합니다. 구조체는 모든 필드의 타입이 명확해야 합니다.

fn main() {
    let user1 = User {
        username: String::from("alice"),
        email: String::from("alice@example.com"),
        active: true,
    };

    println!("username: {}", user1.username);
}
  • 문자열(String)은 “alice”와 같이 큰따옴표 안에 입력한다고 되는 것이 아니며, String::from(“alice”)라고 하거나, “alice”.to_string()으로 입력해야 합니다.
  • bool(논리값)도 true, false와 같이 모두 소문자로 표기합니다.
  • 필드 값을 끝까지 입력하고, 쉼표가 있어도 문제가 없습니다.
  • 구조체 인스턴스는 tuple과 마찬가지로 . 연산자(notation)로 접근할 수 있습니다.
  • Rust는 사용하는 기호도 여러가지가 사용돼서 복잡합니다.

지금까지 나온 것이
변수의 형식은 : 다음에 표시하고,
println다음에 !를 붙여야 하며,
match 패턴의 경우 => 을 사용해서 실행 코드를 지정하고, else를 _로 표시하며,
숫자 입력시 천단위 구분 기호로 _를 사용하고,
char를 입력할 때는 작은 따옴표, String을 입력할 때는 큰따옴표,
반환 값의 타입을 지정할 때는 ->,
loop label은 ‘로 시작하며,
참조를 표시할 때는 &를 사용하고,
튜플과 구조체의 값을 지정할 때는 .을 사용합니다.


2. 구조체는 소유권을 가진다

Rust에서 구조체는 일반 변수처럼 소유권을 가집니다. 즉, 구조체를 다른 변수로 이동시키면 원래 변수는 더 이상 사용할 수 없습니다.

let user2 = user1; // user1의 소유권이 user2로 이동
// println!("{}", user1.email); // 오류!

필드 하나만 이동하는 경우도 마찬가지입니다.

    let username = user1.username; // 소유권 이동 (user1.username에 대한 소유권은 종료됨)

    // user1.username은 더 이상 유효하지 않음, username 변수가 소유권을 갖게 됨

    println!("username: {}", username);

일부 필드를 참조로 처리하거나 클론(clone)을 사용해야 합니다.

let username = &user1.username;
또는
let username = user1.username.clone();

3. 기존 구조체 인스턴스로 새 구조체 인스턴스 생성하기

구조체 인스턴스를 만들 때 기존 구조체를 기반으로 일부 필드만 바꾸고 싶은 경우, 다음과 같이 .. 문법을 사용하여 나머지는 (user2와) 동일하다고 할 수 있습니다:

let user3 = User {
    email: String::from("bob@example.com"),
    ..user2
};

단, user2는 이후 더 이상 사용할 수 없습니다. 그 이유는 username, email과 active 필드의 소유권이 user3에게로 넘어갔기 때문입니다.

또한 ..user2라고 나머지 필드는 똑같다고 할 때 맨 뒤에 ,를 붙이면 안됩니다. 구조체 정의할 때는 ,로 끝나도 되는 것과 구분됩니다.


4. 튜플 구조체 (Tuple Struct)

필드의 이름이 없고 형식만 있는 구조체도 정의할 수 있습니다. 이를 튜플 구조체라고 하며, 단순한 데이터 묶음에 유용합니다. 구조체 이름 다음이 중괄호가 아니라 소괄호인 것도 다릅니다.

struct Color(i32, i32, i32);

fn main() {
    let red = Color(255, 0, 0);
    println!("Red: {}, {}, {}", red.0, red.1, red.2);
}


5. 유사 유닛 구조체 (Unit-like Struct)

필드가 없는 구조체도 정의할 수 있습니다. 이를 유닛 구조체라고 하며, 마치 빈 enum처럼 동작하거나 타입 태깅 등에 사용됩니다.

struct Marker;

fn main() {
    let _m = Marker;
}

이런 구조체는 메모리를 차지하지 않으며, 값 자체보다 타입에 의미를 둘 때 사용됩니다.


6. 구조체에 메서드 구현

Rust는 구조체에 메서드(method)를 추가할 수 있습니다. impl 블록을 통해 구조체에 동작을 부여할 수 있습니다.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

&self는 해당 메서드가 구조체 인스턴스를 참조로 빌려서 사용한다는 뜻입니다.

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("면적: {}", rect.area());
}

impl 블록 안에는 여러 메서드(함수)를 정의할 수 있으며, 정적 메서드(fn new, 생성자 역할)는 다음처럼 작성합니다:

impl Rectangle {
    fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }
}

위와 같이 생성자를 선언한 경우 아래와 같이 Rectangle::new 다음의 괄호 안에 필드 이름을 입력할 필요 없이 너비와 높이만을 입력해서 인스턴스를 만들 수 있으며 , 면적을 계산하는 것은 같습니다.

    let rect1 = Rectangle::new(10, 20);
    println!("rect1 면적: {}", rect1.area());

7. 디버깅을위한 #[derive(Debug)]

구조체를 println!으로 출력하려면 Debug 트레이트를 구현해야 합니다.

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 3, y: 7 };
    println!("{:?}", p);
}

위에서 {:?} 포맷은 Debug 형식 출력을 의미하며, 결과는 Point { x: 3, y: 7 }처럼 구조체의 필드 이름과 값을 포함한 형태로 출력됩니다.

그러나, 아래와 같이 #[derive(Debug)]를 주석 처리하고 실행하면 “Point가 {:?}를 사용해서 포맷될 수 없다”는 에러 메시지가 표시됩니다.


마무리

Rust의 구조체는 단순한 데이터 묶음을 넘어서, 로직과 상태를 함께 표현할 수 있는 강력한 도구입니다. 구조체를 메서드와 함께 사용하면 객체지향적 모델도 자연스럽게 구현할 수 있으며, 안전하고 구조화된 데이터 설계가 가능합니다.