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

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. 요약

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

변수, 불변성, 데이터 타입

Rust는 안전성을 위해 변수는 기본적으로 불변(immutable) 입니다. 따라서, 필요한 경우 변수의 값을 변경하려면 명시적으로 가변(mut)으로 선언해야 합니다. 변수 선언 시 let을 사용하고, 스칼라 타입은 숫자, 불리언, 문자가 있으며, 타입을 지정해야 하는 정적 타입 언어이나 타입 추론이 가능합니다.


🔹 준비

Code의 터미널 창에서 D:\rust-practice 폴더로 이동한 다음
cargo new day2라고 입력해서 day2 패키지를 만들고
cd day2라고 입력해서 day2 폴더로 이동합니다.

main.rs는 day2 아래 src 폴더에 있습니다.

🔹 변수 선언

fn main() {
let x = 5;
println!("x의 값은: {}", x);
}
  • let 키워드는 변수를 선언합니다.
  • x는 기본적으로 불변 변수입니다. 변수인데 불변이라고 하니 Rust는 참 독특한 개념의 언어입니다.
  • {}는 print, 다시 말해 출력 시 변수가 위치할 자리(placeholder)입니다.

실행

위 코드를 복사해서 에디터의 main.rs의 내용을 덮어쓰고 저장한 다음
cargo run을 실행하면
출력되는 값은 “x의 값은: 5″입니다.

출력 구문 변경

중괄호안에 변수를 넣어
println!(“x의 값은: {x}”);
과 같이 구문을 바꿀 수 있습니다.

main.rs 위의 run 실행

Code를 닫았다가 열면 rust-analyzer가 다시 실행되면서 아래와 같이 main.rs위에 Run|Debug가 생기므로

main.rs 위에 run 명령어가 보이고, 이를 눌러 rust 패키지를 실행할 수 있다.
흰색 동그라미는 파일이 저장되지 않았다는 의미

run을 눌러 실행할 수 있습니다.

cargo run을 할 때는 먼저 파일을 저장해야 하는데 위 run을 누르면 자동으로 파일을 저장하고, cargo run을 실행하므로 너무 편리합니다.

🔁 값을 바꾸려면?

fn main() {
let x = 5;
x = 6; // 에러! 기본 변수는 값 변경 불가
}

오류 메시지: cannot assign twice to immutable variable x
(불편 변수인 x에 값을 두 번 할당할 수 없다)

//는 주석 표시입니다. 다시 말해 // 다음의 문장은 프로그램 실행 시 제외됩니다.

기존 구문을 주석 처리하고 위 구문을 추가할 때
해당하는 2줄을 마우스로 끌어서 선택하고 Ctrl + /키를 누르면 맨 앞에 // 표시가 추가됩니다.

fn main() {
    // let x = 5;
    // println!("x의 값은: {x}");

    let x = 5;
    x = 6; // 에러! 기본 변수는 값 변경 불가    
}

🔧 가변 변수 (mutable)

fn main() {
let mut x = 5;
println!("처음 x: {x}");
x = 6;
println!("바뀐 x: {x}");
}
  • mut 키워드를 사용하면 값 변경이 가능합니다.

실행

실행하면 화면에 아래와 같이 출력됩니다.
처음 x: 5
바뀐 x: 6


🧮 스칼라 타입(Scalar Types)

스칼라(scalar)는 하나의 값으로 표현되는 타입입니다. Rust는 정수(integer), 부동소수형 숫자(floating-point numbers), boolean(논리형), 그리고 문자(char)라는 네 가지 타입을 보유하고 있습니다. Rust는 정적 타입 언어이며, 대부분 타입은 명시하지 않아도 추론됩니다.

정적 타입 언어는 컴파일 시 변수 타입이 결정되는 언어이고, 동적 타입 언어는 런타임 시 변수 타입이 결정되는 언어, 다시 말해, 코드를 실행할 때 알아서 변수 타입을 판단해주는 언어입니다. 그러나 Rust는 정적 타입 언어임에도 변수 타입을 프로그램에서 지정하지 않더라도 변수 타입을 추론한다는 것이 다른 점입니다.

숫자형

정수형은 소수점이 없는 숫자이고, 부동소수형은 소수점이 있는 숫자입니다. 3을 부동소수형으로 표현할 경우 소수점이하가 0이더라도 3.0이라고 써야지 3으로 쓰면 안되며, 최소한 3.이라고 소수점을 찍어야 합니다.

위 코드에서 let a = 10;이라고 변수에 정수를 대입하면 : i32를 입력하지 않아도 rust analyzer가 : i32를 알아서 추가해서 표시합니다. 3.14도 마찬가지로 : f64라는 타입을 자동으로 표시합니다. 이것이 추론입니다.

: i32 또는 : f64가 없어도 프로그램 실행에 전혀 문제가 없으며, 직접 입력하려면 : i32라고 입력해야 합니다. 아래와 같이 Code에서 타입을 입력한 것은 녹색으로 보이고, 추론한 것은 회색으로 보입니다.

숫자형의 종류

불리언 & 문자

let t: bool = true;
let c: char = 'R'; // 유니코드 문자

마찬가지로 : bool을 입려하지 않아도 값이 true이므로 boolean으로 추론되고, 문자 하나를 입력했으므로 : char로 자동으로 표시됩니다. 이때 문자는 반드시 작은따옴표 안에 입력해야 하며, “R”로 바꾸면 에러가 발생합니다.

파이썬은 자유롭게 작은따옴표 또는 큰따옴표를 사용할 수 있는 것과 다릅니다.


※ 문자 한 개(char)가 아니라 “I am a boy”과 같은 문자열은 String 타입으로 구분되므로 나중에 별도로 다루도록 하겠습니다.


📌 변수 섀도잉(Shadowing)

Rust에서는 같은 이름의 변수로 새로운 값을 선언할 수 있습니다.

fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("x의 값: {x}"); // 12
}
  • let을 반복 사용하면 기존 변수를 덮어쓰는(shadowing) 것처럼 새 변수 선언이 가능합니다.
  • 타입을 변경하는 것도 허용됩니다. 아래는 x가 i32 형식이었다가 char 형식으로 변경된 것을 보여줍니다.

‘R’; 다음의 주석 “// 변수 재정의 가능”도 입력한 것이 아니고 rust analyzer가 알아서 입력한 것입니다. 참 똑똑합니다.

위 코드의 출력 값은
x의 값: 12 (x는 5 -> 6 -> 12가 됨)


🧠 정리

  • 변수는 기본 불변 → 변경하려면 mut 사용
  • 정적 타입 → 타입을 명시하는 것이 원칙이지만 타입 추론 가능
  • Shadowing으로 가변처럼 활용 가능하나, 메모리 안정성을 유의해서 사용하지 않는 것이 바람직함