Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

오버플로우(Overflow)

팩토리얼 값은 숫자가 커질수록 아주 빠르게 증가합니다. 예를 들어, 20의 팩토리얼은 무려 2,432,902,008,176,640,000이나 되죠. 이 숫자는 이미 32비트 정수(i32)가 담을 수 있는 최대값인 2,147,483,647을 훌쩍 넘어버립니다.

산술 연산 결과가 해당 정수 타입이 담을 수 있는 최대값보다 커지는 상황을 **정수 오버플로우(Integer overflow)**라고 부릅니다.

정수 오버플로우는 프로그래밍 규칙을 위반하는 문제이기 때문에 주의해야 합니다. 컴퓨터 입장에서 어떤 정수 타입끼리 연산한 결과는 반드시 그 타입에 맞는 값이어야 하는데, 수학적으로는 맞는 답이 정해진 크기를 초과해서 담을 수 없게 되기 때문입니다.

반대로 결과가 해당 정수 타입의 최소값보다 작아지는 상황은 **정수 언더플로우(Integer underflow)**라고 합니다. 이 섹션에서는 편의상 오버플로우를 위주로 설명하겠지만, 모든 내용은 언더플로우에도 똑같이 적용된다는 사실을 기억해 주세요.

이전에 “변수” 섹션에서 작성했던 speed 함수도 사실 일부 입력값에서 언더플로우가 발생할 가능성이 있었습니다. 만약 end 값이 start보다 작다면 end - start는 음수가 되어야 하는데, 우리가 썼던 u32 타입은 음수를 담을 수 없으므로 언더플로우가 발생하게 되죠.

자동 타입 승격은 없습니다

오버플로우 문제에 대응하는 한 가지 방법은 결과를 더 큰 타입으로 자동으로 바꿔주는 것입니다. 예를 들어 u8 타입 두 개를 더했는데 결과가 256(u8::MAX + 1)이 나왔다면, Rust가 알아서 더 큰 타입인 u16으로 결과를 처리해 줄 수도 있겠죠.

하지만 이미 살펴보았듯이, Rust는 타입 변환에 매우 엄격합니다. 따라서 이런 자동 승격은 Rust가 오버플로우 문제를 해결하는 방식이 아닙니다.

어떻게 처리할까요?

자동 승격을 하지 않는다면, 오버플로우가 났을 때 어떤 선택을 할 수 있을까요? 크게 두 가지 방향이 있습니다.

  • 연산을 거부하기 (프로그램 중단)
  • 어떻게든 정해진 타입 범위 안에서 “합리적인” 값을 내놓기

연산 거부 (패닉)

가장 보수적이고 안전한 방식입니다. 오버플로우가 발생하면 즉시 프로그램을 멈추는 것이죠. 앞서 “패닉” 섹션에서 보았던 패닉(Panic) 메커니즘이 바로 이럴 때 쓰입니다.

“합리적인” 값 내놓기 (래핑)

산술 연산 결과가 최대값을 넘었을 때, 마치 원형 궤도처럼 다시 최소값부터 시작하게 만드는 방식입니다. 이를 **래핑(Wrapping around)**이라고 합니다.

예를 들어, u8 타입에서 1과 255(u8::MAX)를 래핑 덧셈하면 결과는 0(u8::MIN)이 됩니다. 부호 있는 정수도 마찬가지입니다. i8 타입에서 127(i8::MAX)에 1을 더하면 -128(i8::MIN)이 됩니다.

overflow-checks 설정

Rust는 오버플로우 상황에서 어떤 방식으로 동작할지 개발자가 선택할 수 있게 해줍니다. 이 동작은 overflow-checks라는 프로필(Profile) 설정에 의해 결정됩니다.

  • overflow-checkstrue이면, 오버플로우 발생 시 런타임에 패닉을 일으킵니다.
  • overflow-checksfalse이면, 오버플로우 발생 시 값을 래핑합니다.

그런데 ’프로필’이 무엇인지 궁금하시죠? 한번 자세히 알아볼까요?

프로필(Profiles)

프로필은 Rust 코드가 컴파일되는 방식을 결정하는 여러 옵션들의 모음입니다.

Cargo에는 크게 4가지 내장 프로필이 있습니다.

  • dev 프로필: cargo build, cargo run, cargo test를 실행할 때 기본적으로 사용됩니다. 로컬 개발을 위한 용도이며, 런타임 성능보다는 컴파일 속도와 풍부한 디버깅 정보를 우선시합니다.
  • release 프로필: 실제 배포용(Production) 빌드를 위해 런타임 성능을 극한으로 최적화합니다. 대신 컴파일 시간은 훨씬 오래 걸립니다. --release 플래그를 붙여야만 활성화됩니다. (예: cargo build --release)
  • test 프로필: cargo test에서 사용되며 dev 프로필의 설정을 그대로 물려받습니다.
  • bench 프로필: 성능 측정용인 cargo bench에서 사용되며 release 프로필 설정을 물려받습니다.

Rust 커뮤니티에는 “릴리스 모드로 빌드하셨나요?“라는 유명한 밈(Meme)이 있습니다. Rust에 입문한 지 얼마 안 된 개발자가 소셜 미디어 등에 “Rust 성능이 왜 이렇게 안 나오죠?“라며 투덜대는데, 알고 보니 --release 플래그 없이 빌드했던 상황을 비꼬는 표현이죠.

물론 기본 프로필 외에 나만의 설정을 추가하거나 기존 프로필 내용을 직접 수정할 수도 있습니다.

프로필별 오버플로우 체크 설정

기본적으로 overflow-checks는 다음과 같이 설정되어 있습니다.

  • dev 프로필: true
  • release 프로필: false

각 프로필의 목적에 맞게 합리적으로 정해진 것이죠. dev는 개발 중에 문제를 빨리 찾을 수 있게 패닉을 일으키고, release는 오버플로우 체크로 인해 속도가 느려지는 것을 방지하기 위해 래핑을 허용합니다.

하지만 두 프로필의 동작이 서로 다르면, 개발할 때는 몰랐던 버그가 실제 서비스 중에 튀어나올 수도 있습니다. 그래서 저희는 가급적 두 프로필 모두에서 overflow-checks를 활성화(True)하는 것을 추천합니다. 잘못된 계산 결과를 조용히 남기는 것보다는, 차라리 프로그램이 죽는 것이 문제 해결에 훨씬 도움이 되기 때문입니다. 오버플로우 체크로 인한 성능 저하는 대부분의 경우 무시할 수 있는 수준이며, 정말 성능이 중요한 부분이라면 직접 벤치마크를 해보고 결정하면 됩니다.

더 읽어보기

Exercise

The exercise for this section is located in 02_basic_calculator/08_overflow