제어 흐름

제어 흐름 (Control Flow)

어떤 조건이 true일 때만 코드를 실행하고, 조건이 true인 동안엔 같은 코드를 반복해서 실행하는 능력은 대부분의 프로그래밍 언어에서 가장 기본적인 재료예요. Rust에서 실행 흐름을 제어하는 가장 흔한 도구가 바로 if 표현식과 반복문(loop) 구문입니다. 이번 장에서는 이 두 가지를 하나씩 직접 실행해 보면서 익혀볼게요.

출처: The Rust Book

if 표현식

if 표현식은 조건에 따라 코드를 두 갈래로 나누는 역할을 해요. 조건을 하나 주고, "이 조건이 맞으면 이 코드 블록을 실행하고, 맞지 않으면 실행하지 말자"라고 쓰는 거죠.

직접 확인하기 위해 projects 디렉터리에 branches라는 새 프로젝트를 만들고, src/main.rs 파일에 다음 코드를 입력해 볼게요.

Filename: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

모든 if 표현식은 키워드 if로 시작하고, 그 뒤에 조건이 따라와요. 이 예시에서 조건은 변수 number의 값이 5보다 작은지를 검사하죠. 조건이 true일 때 실행할 코드 블록은 중괄호 안에 조건 바로 뒤에 두면 돼요. 이렇게 if 표현식의 조건과 짝을 이루는 코드 블록을 arm(팔)이라고 부르기도 해요. 2장의 "비밀 숫자와 추측값 비교하기"에서 봤던 match 표현식의 arm과 같은 역할이에요.

선택적으로 else 표현식을 붙일 수도 있어요. 지금 코드처럼 조건이 false일 때 실행할 대안 코드 블록을 미리 준비해 두는 거죠. else를 제공하지 않았는데 조건이 false라면, 프로그램은 if 블록을 그냥 건너뛰고 다음 코드로 넘어가요.

이 코드를 실행하면 다음과 같은 출력을 볼 수 있어요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

이번엔 number 값을 조건이 false가 되도록 바꿔서 어떤 일이 벌어지는지 확인해 볼게요.

Filename: src/main.rs

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

프로그램을 다시 실행하면 이제 condition was false가 출력돼요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

한 가지 짚고 넘어갈 점은, 이 코드에서 조건은 반드시 bool이어야 한다는 거예요. 조건이 bool이 아니라면 에러가 발생해요. 예를 들어 다음 코드를 실행해 보세요.

Filename: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

이번엔 if의 조건이 값 3으로 평가되는데, Rust는 에러를 던져요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

에러 메시지는 Rust가 bool을 기대했는데 정수(integer)를 받았다고 알려줘요. Ruby나 JavaScript 같은 언어와 달리, Rust는 bool이 아닌 타입을 자동으로 bool로 변환하지 않아요. 항상 명시적으로 ifbool 조건을 주어야 해요. 예를 들어 숫자가 0이 아닐 때만 if 블록을 실행하고 싶다면, if 표현식을 다음과 같이 바꾸면 돼요.

Filename: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

이 코드는 number was something other than zero를 출력해요.

else if로 여러 조건 다루기

ifelse를 조합한 else if 표현식으로 조건을 여러 개 사용할 수 있어요. 예를 들면 이렇게요.

Filename: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

이 프로그램은 네 가지 경로 중 하나를 택할 수 있어요. 실행하면 다음과 같은 출력이 나와요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

프로그램이 실행될 때 각 if 표현식을 차례로 검사해서, 조건이 true로 평가되는 첫 번째 코드 블록만 실행해요. 6은 2로도 나누어떨어지는데 number is divisible by 2가 출력되지 않고, else 블록의 메시지도 출력되지 않는 점에 주목해 보세요. Rust는 처음으로 true인 조건을 만나면 그 블록만 실행하고, 나머지 조건은 아예 검사하지 않기 때문이에요.

else if를 너무 많이 쓰면 코드가 지저분해질 수 있어요. 조건이 여러 개라면 리팩터링을 고려해 보는 게 좋고, 그런 경우에 쓰라고 Rust에는 더 강력한 분기 구문인 match가 준비되어 있어요. match는 6장에서 자세히 다룰게요.

let 문에서 if 사용하기

if는 표현식이기 때문에 let 문의 오른쪽에 사용해 그 결과를 변수에 할당할 수 있어요. Listing 3-2를 보세요.

Filename: src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

Listing 3-2: if 표현식의 결과를 변수에 할당하기

number 변수는 if 표현식의 결과에 따라 값이 정해져요. 이 코드를 실행해 보세요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

코드 블록은 그 안의 마지막 표현식으로 평가되고, 숫자 자체도 표현식이라는 걸 기억하세요. 이 경우 if 표현식의 값은 어떤 코드 블록이 실행되느냐에 따라 달라져요. 여기서 중요한 건 if의 각 arm이 결과로 낼 수 있는 값들이 모두 같은 타입이어야 한다는 점이에요. Listing 3-2에서는 if arm과 else arm의 결과가 모두 i32 정수였죠. 타입이 서로 어긋나면, 다음 예시처럼 에러가 발생해요.

Filename: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

이 코드를 컴파일하면 에러가 발생해요. if arm과 else arm의 값 타입이 서로 호환되지 않기 때문이죠. Rust는 문제가 있는 위치를 정확히 알려줘요.

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error

if 블록의 표현식은 정수로, else 블록의 표현식은 문자열로 평가되어요. 이렇게는 동작하지 않아요. 변수는 단 하나의 타입을 가져야 하고, Rust는 컴파일 시점에 number 변수의 타입이 무엇인지 확정적으로 알아야 하거든요. number의 타입을 알면 컴파일러는 number를 사용하는 모든 곳에서 타입이 유효한지 검증할 수 있어요. 만약 number의 타입이 런타임에야 정해진다면 Rust는 그런 검증을 할 수 없고, 컴파일러가 어떤 변수에 대해 여러 개의 추정 타입을 다 관리해야 하니 코드에 대한 보장도 줄어들고 훨씬 복잡해질 거예요.

반복: 반복문(Loops)

코드 블록을 두 번 이상 실행해야 할 때가 자주 있어요. 이런 작업을 위해 Rust는 여러 반복문을 제공해요. 반복문은 루프 본문 안의 코드를 끝까지 실행하고 나서 바로 다시 처음으로 돌아가 같은 과정을 반복해요. 실습을 위해 loops라는 새 프로젝트를 만들어 볼게요.

Rust에는 loop, while, for 세 종류의 반복문이 있어요. 하나씩 시도해 보죠.

loop로 코드 반복하기

loop 키워드는 Rust에게 코드 블록을 영원히, 또는 명시적으로 멈추라고 하기 전까지 계속 반복해서 실행하라고 지시해요.

예시로 loops 디렉터리의 src/main.rs 파일을 이렇게 바꿔 보세요.

Filename: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

이 프로그램을 실행하면 again!이 프로그램을 수동으로 멈출 때까지 끝없이 출력돼요. 대부분의 터미널은 무한 루프에 빠진 프로그램을 중단하는 단축키 ctrl-C를 지원해요. 한번 시도해 보세요.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

^C 기호는 ctrl-C를 누른 위치를 나타내요. 인터럽트 신호를 받았을 때 코드가 루프의 어느 지점에 있었는지에 따라 ^C 뒤에 again!이 보일 수도 있고 안 보일 수도 있어요.

다행히 Rust는 코드로 루프에서 빠져나오는 방법도 제공해요. 루프 안에 break 키워드를 두면 프로그램이 루프 실행을 멈춰야 할 때를 알려줄 수 있어요. 2장 추측 게임의 "정답을 맞힌 뒤 종료하기"에서 사용자가 정답을 맞혀 게임에서 이겼을 때 프로그램을 종료하기 위해 이 방법을 썼던 걸 기억하세요.

추측 게임에서 continue도 사용했는데, continue는 루프에서 이번 반복의 남은 코드를 건너뛰고 다음 반복으로 넘어가라고 프로그램에 지시해요.

반복문에서 값 반환하기

loop의 쓰임새 중 하나는 실패할 수도 있는 작업을 다시 시도하는 거예요. 예를 들어 스레드가 작업을 끝냈는지 확인하는 경우죠. 그런데 그 작업의 결과를 루프 밖으로 꺼내서 나머지 코드로 전달해야 할 때도 있어요. 그럴 때는 루프를 멈추는 데 사용한 break 표현식 뒤에 반환하고 싶은 값을 붙이면 돼요. 그러면 그 값이 루프 밖으로 반환되어 사용할 수 있게 되죠. 다음 코드를 보세요.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

루프 전에 counter라는 변수를 선언하고 0으로 초기화해요. 그다음 루프에서 반환되는 값을 담을 result 변수를 선언하죠. 루프의 매 반복마다 counter1을 더하고, counter10과 같은지 확인해요. 같아지면 break 키워드에 값 counter * 2를 붙여서 루프를 종료해요. 루프 뒤에서는 세미콜론으로 result에 값을 할당하는 문장을 끝내요. 마지막으로 result의 값을 출력하는데, 이 경우엔 20이 출력돼요.

루프 안에서 return을 쓸 수도 있어요. break는 현재 루프만 빠져나가지만, return은 항상 현재 함수 자체를 빠져나가요.

루프 라벨로 구분하기

루프 안에 루프가 있는 경우, breakcontinue는 그 시점에서 가장 안쪽 루프를 대상으로 적용돼요. 필요하다면 루프에 loop label(루프 라벨)을 지정할 수 있는데, 그러면 breakcontinue와 함께 사용해 그 키워드들이 가장 안쪽 루프 대신 해당 라벨이 붙은 루프를 대상으로 하도록 지정할 수 있어요. 루프 라벨은 작은따옴표로 시작해야 해요. 중첩된 두 루프의 예시를 볼게요.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

바깥 루프에는 'counting_up 라벨이 붙어 있고, 0부터 2까지 세어 올라가요. 라벨이 없는 안쪽 루프는 10에서 9까지 내려가죠. 라벨을 지정하지 않은 첫 번째 break는 안쪽 루프만 빠져나가요. break 'counting_up; 문장은 바깥 루프를 빠져나가죠. 이 코드의 출력은 다음과 같아요.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while로 조건 반복 간결하게 하기

프로그램은 루프 안에서 조건을 평가해야 하는 경우가 많아요. 조건이 true인 동안 루프가 실행되고, 조건이 더 이상 true가 아니면 프로그램이 break를 호출해서 루프를 멈춰요. 이런 동작을 loop, if, else, break를 조합해서 구현하는 것도 가능해요. 원한다면 직접 프로그램에서 시도해 봐도 좋아요. 하지만 이 패턴이 너무 흔해서 Rust는 이를 위한 내장 언어 구문인 while 루프를 제공해요. Listing 3-3에서는 while을 사용해 프로그램을 세 번 반복하면서 매번 숫자를 줄여 내려가고, 루프가 끝나면 메시지를 출력하고 종료해요.

Filename: src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Listing 3-3: 조건이 true인 동안 코드를 실행하는 while 루프

이 구문은 loop, if, else, break를 다 썼다면 필요했을 많은 중첩을 없애주고, 훨씬 명확해요. 조건이 true로 평가되는 동안 코드가 실행되고, 그렇지 않으면 루프를 빠져나가요.

for로 컬렉션 반복하기

배열 같은 컬렉션의 요소를 순회할 때 while 구문을 선택할 수도 있어요. 예를 들어 Listing 3-4의 루프는 배열 a의 각 요소를 출력해요.

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Listing 3-4: while 루프로 컬렉션의 각 요소 순회하기

여기서 코드는 배열의 요소를 하나씩 세어 올라가요. 인덱스 0에서 시작해 배열의 마지막 인덱스에 도달할 때까지(즉 index < 5가 더 이상 true가 아닐 때까지) 루프를 반복하죠. 이 코드를 실행하면 배열의 모든 요소가 출력돼요.

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

예상대로 배열 값 다섯 개가 모두 터미널에 출력돼요. index는 언젠가 값 5에 도달하지만, 배열에서 여섯 번째 값을 가져오려고 시도하기 전에 루프가 실행을 멈춰요.

하지만 이 방식은 오류가 나기 쉬워요. 인덱스 값이나 조건이 틀리면 프로그램이 panic을 일으킬 수 있어요. 예를 들어 a 배열의 정의를 네 개 요소로 바꿨는데 조건을 while index < 4로 고치는 걸 잊으면 코드가 panic을 일으켜요. 게다가 느리기도 해요. 인덱스가 배열 범위 안에 있는지 조건을 검사하는 런타임 코드를 루프를 돌 때마다 컴파일러가 추가해야 하거든요.

더 간결한 대안으로 for 루프를 사용해 컬렉션의 각 항목에 대해 어떤 코드를 실행할 수 있어요. for 루프는 Listing 3-5의 코드처럼 생겼어요.

Filename: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Listing 3-5: for 루프로 컬렉션의 각 요소 순회하기

이 코드를 실행하면 Listing 3-4와 같은 출력을 볼 수 있어요. 더 중요한 건, 코드의 안전성이 높아지고 배열의 끝을 넘어가거나 그보다 멀리 가지 않아 항목을 놓치는 버그의 가능성이 사라졌다는 점이에요. for 루프에서 생성된 기계어 코드도 더 효율적일 수 있어요. 인덱스를 매 반복마다 배열의 길이와 비교할 필요가 없거든요.

for 루프를 쓰면 Listing 3-4의 방식처럼 배열의 값 개수를 바꿔도 다른 코드를 고쳐야 할 일이 없어요.

for 루프의 안전성과 간결함 덕분에 이 구문은 Rust에서 가장 자주 쓰이는 루프 구문이에요. Listing 3-3의 카운트다운처럼 코드를 특정 횟수만큼 실행하고 싶은 상황에서도, 대부분의 러스트 개발자(Rustacean)는 for 루프를 사용해요. 그 방법은 표준 라이브러리가 제공하는 Range를 쓰는 건데, Range는 한 숫자에서 시작해 다른 숫자 직전까지의 모든 숫자를 순서대로 생성해 줘요.

for 루프와 아직 다루지 않은 또 다른 메서드인 rev(범위를 뒤집는 메서드)를 사용하면 카운트다운은 이렇게 됩니다.

Filename: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

이 코드는 훨씬 깔끔하죠?

요약

여기까지 도달했네요! 이번 장은 꽤 큰 장이었어요. 변수, 스칼라와 복합 데이터 타입, 함수, 주석, if 표현식, 반복문까지 모두 배웠어요. 이 장에서 다룬 개념을 연습하려면 다음 프로그램을 직접 만들어 보세요.

  • 화씨와 섭씨 온도 변환 프로그램
  • n번째 피보나치 수 생성 프로그램
  • 크리스마스 캐럴 "The Twelve Days of Christmas"의 가사를 노래의 반복을 활용해 출력하는 프로그램

다음 내용으로 넘어갈 준비가 되면, 다른 프로그래밍 언어에는 흔히 없는 Rust의 개념인 소유권(ownership)에 대해 이야기해 볼게요.

더 알아보기 (Learn more)