만자의 개발일지

[Rust] 제네릭 본문

Rust

[Rust] 제네릭

박만자 2025. 7. 29. 22:37

지난 포스팅에서는 참조와 대여에 대해 알아보았습니다. 이번 포스팅에서는 제네릭에 대해 알아보도록 하겠습니다.

제네릭(Generics)

제네릭은 함수, 구조체, 열거형, 메서드 등에서 특정 타입을 직접 명시하는 대신 타입 매개변수(type parameter)를 사용하여 추상적인 타입을 정의하는 방식입니다. 제네릭을 사용하면 하나의 코드 정의로 여러 다른 타입에 대해 동작하는 코드를 작성할 수 있고, 이를 통해 중복되는 코드를 줄일 수 있습니다. Rust의 제네릭은 컴파일 타임에 단형성화(monomorphization)되어 구체 타입으로 변환되기 때문에 런타임 성능에 영향을 미치지 않는다는 장점이 있습니다.

 

제네릭은 타입 별칭, 구조체, 열거형, 함수, 메서드, 트레이트 등 다양한 곳에서 사용됩니다.

제네릭은 기본적으로 꺽쇠괄호(<>) 안에 타입 매개변수를 선언하여 사용합니다.

 

함수에서의 제네릭

함수에서 제네릭을 사용할 때는 함수 이름뒤에 타입 매개변수를 선언합니다.

fn create_tuple<T>(a: T, b: T, c: T) -> (T, T, T) {
    (a, b, c)
}

fn main() {
    let tup1 = create_tuple(10, 20, 30);
    println!("{:?}", tup1);
    
    let tup2 = create_tuple(1.5, 2.5, 3.5);
    println!("{:?}", tup2);
}

위 예시에서 <T>T라는 타입 매개변수를 선언한 것입니다. 이 T는 함수 본문 내에서 구체적인 타입(i32, f64 등)을 대체하게 됩니다.

 

구조체에서의 제네릭

구조체에서 제네릭을 사용할 때는 구조체 이름뒤에 타입 매개변수를 선언합니다.

struct Point<T> {
    x: T,
    y: T,
}

struct MultiPoint<T, U> {
    x: T,
    y: U,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
    let mixed_point = MultiPoint { x: 5, y: 4.0 };
}

Pointxy 필드가 모두 같은 타입 T를 가지도록 정의되었습니다. 만약 다른 타입을 가지게 하고 싶다면 MultiPoint 와 같이 여러 개의 타입 매개변수를 선언하여 사용할 수 있습니다.

 

열거형에서의 제네릭

열거형에서 제네릭을 사용할 때는 열거형 이름뒤에 타입 매개변수를 선언합니다.

enum Option<T> {
    Some(T),
    None,
}

제네릭이 사용된 열거형 중에 가장 자주 사용되는 Option<T> 열거형입니다. Some variant는 타입 매개변수를 통해 어떤 타입의 데이터든 포함할 수 있습니다.

 

메서드에서의 제네릭

메서드에서 제네릭을 사용할 때는 크게 두 가지 방식이 있습니다.

 

첫번째는 구조체나 열거형이 타입 매개변수를 가지고 있는 경우 impl 키워드 뒤에 타입 매개변수를 선언하여 해당 타입을 사용하는 방식입니다.

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }

    fn get_x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p1 = Point::new(10, 20);
    println!("p1.x: {}", p1.get_x());

    let p2 = Point::new(10.5, 20.5);
    println!("p2.x: {}", p2.get_x());
}

여기서 newget_x 메서드에서 사용되는 타입 매개변수는 Point 구조체가 사용하는 T 타입을 그대로 사용합니다.

 

두번째는 특정 메서드에서만 추가적인 타입 매개변수를 사용하고 싶은 경우 메서드 이름뒤에 타입 매개변수를 선언하여 사용하는 방식입니다.

struct Example;

impl Example {
    fn example_func<U>(value: U) -> U{
        value
    } 
}

fn main() {
    let a = Example::example_func(10);
    
    println!("a: {a}");
}

example_func 메서드는 U라는 자체적인 타입 매개변수를 가집니다. 이 U는 메서드 호출 시점에 결정되기 때문에 구조체 내에 다른 부분에는 영향을 끼치지 않습니다.

 

Const Generics

상수 값을 인자로 받는 타입 파라미터도 선언할 수 있습니다. 타입 파라미터 앞에 const 키워드를 사용하여 선언합니다.

단, const 타입 파라미터는 u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char, bool 타입만 가능합니다.

fn double<const N: i32>() {
    println!("doubled: {}", N * 2);
}

const SOME_CONST: i32 = 12;

fn main() {
    double::<9>();
    double::<-123>();
    double::<{7 + 8}>();
    double::<SOME_CONST>();
    double::<{ SOME_CONST + 5 }>();
}

위와 같이 다양한 형태로 상수 값을 인자로 전달할 수 있습니다.

 

상수 값을 타입 파라미터로 사용하는 경우는 보통 크기가 다른 배열에 대해 같은 동작을 하는 코드를 작성해야하는 경우 자주 사용합니다.

fn double<const N: usize>(arr: [i32; N]) {
    for i in arr {
        println!("{}", i * 2);
    }
}

fn main() {
    let arr = [1, 2, 3, 4, 5];
    double::<5>(arr);
}

기존에는 크기가 다른 배열에 대해 각가 다른 타입을 정의해야 했지만, 배열의 크기를 타입으로 받으면서 코드 중복을 줄이고 잘못된 크기의 배열에 접근하는 것을 방지할 수 있습니다.

 

turbofish

타입 추론이 불가능하거나 명시적으로 제네릭 타입을 지정해야할 때 ::<> 형태의 구문을 통해 타입을 명시할 수 있으며, 이를 turbofish 라고 부릅니다.

fn main() {
    let text = "123";
    
    let number = text.parse::<i32>().unwrap();
    println!("parsed number: {}", number);
}

컴파일러는 text를 어떤 타입으로 변환될지 추론할 수 없기 때문에 type annotaions 또는 turbofish를 사용하여 어떤 타입으로 파싱할지 명시해주어야 합니다.


이번 포스팅에서는 제네릭에 대해 알아보았습니다. 다음 포스팅에서는 라이프타임에 대해 알아보도록 하겠습니다.

 

참고

'Rust' 카테고리의 다른 글

[Rust] 라이프타임  (1) 2025.09.25
[Rust] 참조와 대여  (1) 2025.07.03
[Rust] 소유권과 메모리 모델  (3) 2025.06.17
[Rust] 패턴 매칭  (0) 2025.05.31
[Rust] 열거형  (0) 2025.05.18
Comments