Rust와 Axum을 활용한 웹 백엔드 개발 - 기본 문법: 변수, 함수, 소유권
추천 실습자료 : Rust Exercise
Rust는 안전성, 성능, 병행성을 강조하는 시스템 프로그래밍 언어입니다. 다른 언어와 구별되는 Rust만의 독특한 문법과 개념들은 처음에는 다소 복잡하게 느껴질 수 있지만, 이를 이해하면 강력하고 안정적인 코드를 작성할 수 있습니다. 이번 글에서는 Rust의 핵심 문법인 변수, 데이터 타입, 함수, 제어 흐름, 소유권, 구조체와 열거형 등을 자세히 살펴보겠습니다.
1. 변수와 가변성
1.1 변수 선언과 불변성
Rust에서 변수는 기본적으로 불변성(immutable)입니다. 즉, 변수의 값을 변경할 수 없습니다.
fn main() {
let x = 5;
println!("x의 값은 {}입니다.", x);
// x = 6; // 컴파일 에러 발생: 불변 변수의 값을 변경할 수 없습니다.
}
위 코드에서 x = 6;
을 시도하면 컴파일 에러가 발생합니다.
1.2 가변 변수 선언 (mut
키워드)
변수의 값을 변경하고 싶다면 mut
키워드를 사용하여 가변성(mutable)을 선언해야 합니다.
fn main() {
let mut x = 5;
println!("x의 값은 {}입니다.", x);
x = 6;
println!("변경된 x의 값은 {}입니다.", x);
}
출력:
x의 값은 5입니다.
변경된 x의 값은 6입니다.
1.3 변수 섀도잉(Shadowing)
같은 이름의 변수를 다시 선언하여 이전 변수를 섀도잉(가리기)할 수 있습니다.
fn main() {
let x = 5;
let x = x + 1; // 새로운 x 변수 선언
println!("x의 값은 {}입니다.", x);
}
출력:
x의 값은 6입니다.
이때, 새로운 x
는 이전 x
를 섀도잉하며, 가변성을 변경할 수 있습니다.
2. 데이터 타입
Rust는 강타입 언어로, 모든 변수는 명확한 타입을 가져야 합니다.
추가 참고 사항
- 정적 타입(static typing): 변수와 표현식의 타입이 컴파일 시점에 결정되는 것.
- 동적 타입(dynamic typing): 변수의 타입이 런타임 시점에 결정되는 것.
- 강한 타입 검증(strong type checking): 타입 간의 불일치나 암시적 변환을 엄격하게 제한하는 것.
- 약한 타입 검증(weak type checking): 타입 간의 암시적 변환을 허용하여 유연하게 연산을 수행하는 것.
2.1 스칼라 타입
정수형 (integer)
i8
,i16
,i32
,i64
,i128
,isize
(부호 있는 정수)u8
,u16
,u32
,u64
,u128
,usize
(부호 없는 정수)
let a: i32 = -10;
let b: u32 = 10;
실수형 (부동소수점, float)
f32
,f64
let x: f64 = 3.14;
불리언 (참, 거짓)
let is_active: bool = true;
문자 (char)
- 유니코드 스칼라 값을 표현하며, 작은따옴표 사용
let c: char = 'A';
2.2 복합 타입
튜플(Tuple)
여러 타입의 값을 하나로 묶을 수 있습니다.
let tup: (i32, f64, char) = (500, 6.4, 'Z');
let (x, y, z) = tup; // 구조 분해
println!("y의 값은 {}입니다.", y);
배열(Array)
같은 타입의 값들을 고정된 길이로 저장합니다.
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("첫 번째 원소는 {}입니다.", arr[0]);
3. 함수
3.1 함수 정의와 호출
Rust에서 함수는 fn
키워드로 정의합니다.
fn main() {
hello_world();
}
fn hello_world() {
println!("Hello, world!");
}
3.2 매개변수와 반환값
함수는 매개변수와 반환값을 가질 수 있습니다.
fn main() {
let result = add(5, 3);
println!("결과는 {}입니다.", result);
}
fn add(x: i32, y: i32) -> i32 {
x + y // 세미콜론이 없으므로 표현식이며, 반환값이 됩니다.
}
3.3 표현식과 문장
- 문장(Statement): 어떤 동작을 수행하지만 값을 반환하지 않습니다.
- 표현식(Expression): 값을 계산하고 반환합니다.
fn main() {
let y = { // 표현식 블록
let x = 3;
x + 1 // 세미콜론이 없으므로 이 값이 y에 할당됩니다.
};
println!("y의 값은 {}입니다.", y);
}
4. 제어 흐름
4.1 if
문
fn main() {
let number = 7;
if number < 5 {
println!("조건이 참입니다.");
} else {
println!("조건이 거짓입니다.");
}
}
4.2 if
표현식
if
는 표현식으로 사용할 수 있습니다.
let condition = true;
let number = if condition { 5 } else { 6 };
println!("number의 값은 {}입니다.", number);
4.3 반복문
loop
무한 루프를 생성합니다.
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 값을 반환하며 루프 종료
}
};
println!("결과는 {}입니다.", result);
}
while
조건이 참인 동안 반복합니다.
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("발사!");
}
for
컬렉션의 각 요소에 대해 반복합니다.
fn main() {
let arr = [10, 20, 30, 40, 50];
for element in arr.iter() {
println!("값은 {}입니다.", element);
}
}
4.4 match
를 통한 패턴 매칭
fn main() {
let number = 3;
match number {
1 => println!("하나"),
2 => println!("둘"),
3 => println!("셋"),
_ => println!("다른 숫자"),
}
}
5. 소유권과 빌림
Rust의 가장 독특한 개념으로, 메모리 안전성을 보장합니다.
5.1 소유권(Ownership) 개념
- 각 값은 단 하나의 소유자(owner)를 가집니다.
- 소유자가 범위를 벗어나면 값은 드롭됩니다.
fn main() {
let s = String::from("hello"); // s가 소유권을 가짐
takes_ownership(s);
// s는 더 이상 유효하지 않음
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
5.2 참조자와 빌림(Borrowing)
값의 소유권을 이동하지 않고 참조를 통해 접근합니다.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // s1의 참조를 전달
println!("'{}'의 길이는 {}입니다.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
5.3 가변 참조자
가변 참조자를 통해 값을 변경할 수 있습니다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
주의: 한 번에 하나의 가변 참조자만 가질 수 있습니다.
5.4 슬라이스(Slice) 타입
컬렉션의 일부를 참조합니다.
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 또는 &s[..5]
let world = &s[6..11]; // 또는 &s[6..]
println!("{} {}", hello, world);
}
6. 구조체와 열거형
6.1 구조체(Struct)
구조체 정의 및 인스턴스 생성
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("username123"),
active: true,
sign_in_count: 1,
};
}
메서드(Method) 작성
impl 문 안에서 함수를 정의합니다.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("사각형의 면적은 {} 제곱 픽셀입니다.", rect.area());
}
6.2 열거형(Enum)
열거형 정의 및 사용
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
}
값이 있는 열거형
enum IpAddr {
V4(String),
V6(String),
}
fn main() {
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
6.3 표준 라이브러리의 Option
과 Result
열거형
Option
타입
값이 있거나 없을 수 있는 상황을 처리합니다.
fn main() {
let some_number = Some(5);
let absent_number: Option<i32> = None;
}
Result
타입
에러 처리를 위한 열거형입니다.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("파일 생성 오류: {:?}", e),
},
other_error => panic!("파일 열기 오류: {:?}", other_error),
},
};
}
7. 컬렉션
7.1 벡터(Vector)
동일한 타입의 가변 길이의 값들을 저장합니다.
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
println!("{:?}", v);
}
7.2 문자열(String)
UTF-8로 인코딩된 가변 길이의 문자열입니다.
fn main() {
let mut s = String::from("hello");
s.push_str(" world");
println!("{}", s);
}
7.3 해시 맵(HashMap)
키와 값의 쌍을 저장합니다.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("블루"), 10);
scores.insert(String::from("옐로우"), 50);
let team_name = String::from("블루");
let score = scores.get(&team_name);
println!("{:?}", score);
}
8. 에러 처리
8.1 Result
타입을 활용한 에러 처리
Result<T, E>
는 작업의 성공과 실패를 표현합니다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => panic!("파일 열기 오류: {:?}", error),
};
}
8.2 ?
연산자를 통한 에러 전파
함수에서 에러를 호출자에게 전달합니다.
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?; // 에러 발생 시 즉시 반환
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
8.3 커스텀 에러 타입 정의 기초
use std::fmt;
#[derive(Debug)]
struct CustomError;
impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "커스텀 에러 발생")
}
}
impl std::error::Error for CustomError {}
fn main() -> Result<(), CustomError> {
Err(CustomError)
}
9. 모듈과 크레이트
9.1 모듈(Module)을 통한 코드 조직화
모듈을 사용하여 코드를 구조화합니다.
mod network {
pub fn connect() {
println!("네트워크에 연결합니다.");
}
}
fn main() {
network::connect();
}
9.2 외부 크레이트(Crate) 사용 방법
외부 라이브러리를 프로젝트에 추가하고 사용하는 방법입니다.
Cargo.toml
에 의존성 추가[dependencies] rand = "0.8.5"
- 코드에서 사용하기
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen();
println!("생성된 숫자: {}", n);
}
9.3 Cargo를 통한 의존성 관리
- 의존성 버전 지정
rand = "0.8" # 최소 0.8.0, 0.9.0 미만
rand = ">=0.8" # 0.8.0 이상
rand = "=0.8.5" # 정확히 0.8.5 버전- 의존성 업데이트
cargo update
연습 과제:
- 간단한 계산기를 만들어보세요. 사용자로부터 두 숫자와 연산자를 입력받아 결과를 출력합니다.
- 구조체와 메서드를 활용하여 직사각형의 넓이와 둘레를 계산하는 프로그램을 작성해보세요.
Result
타입과?
연산자를 활용하여 파일 읽기 및 쓰기 프로그램을 만들어보세요.
참고 자료