클라우드 Mac에서 검증하는 iOS 정수 오버플로와 용량 계산

클라우드 Mac에서 검증하는 iOS 정수 오버플로와 용량 계산

클라우드 Mac에서 검증하는 iOS 정수 오버플로와 용량 계산

다운로드 모듈은 ‘청크 수 × 청크 크기’로 용량을 예약하고, 이미지 처리는 ‘행 수 × 행당 바이트 수’로 버퍼를 할당합니다. 평소에는 문제가 없는 계산이지만, 손상된 파일이나 비정상적인 API 데이터가 들어오면 정수 오버플로가 먼저 발생하고 앱이 비정상 종료될 수 있습니다. 클라우드 Mac에서는 순수 산술 로직을 Swift 검증용 실행 파일로 분리해, 시뮬레이터를 실행하지 않고도 iOS 프로젝트에 안정적인 경계값 회귀 테스트를 마련할 수 있습니다.

먼저 용량 계산의 계약 정의하기

이 글에서는 음이 아닌 정수 용량만 다루며, 부동소수점 근삿값은 다루지 않습니다. 규칙은 세 가지입니다. 음수는 잘못된 입력으로 처리하고, Int 범위를 벗어나면 오버플로 오류를 반환하며, 올림 나눗셈의 제수는 반드시 양수여야 합니다. 용량이 0인 것은 유효하지만, 제수가 0인 것은 허용하지 않습니다.

검증하기 전에 부호 없는 정수형으로 변환해서는 안 됩니다. 음수를 일반적인 방식으로 부호 없는 정수형으로 변환하면 즉시 트랩이 발생해 종료될 수 있고, 잘라내기 변환을 사용하면 매우 큰 양수로 바뀔 수 있습니다. 입력이 문자열이라면 먼저 Int 파싱에 성공했는지 확인해야 하며, 파싱 실패를 기본값 0으로 대체해서는 안 됩니다.

산술 결과를 정수형으로 표현할 수 있다고 해서 그만큼의 용량을 할당할 수 있는 것은 아닙니다. 정수 범위 검사와 서비스의 리소스 한도 검사는 서로 독립적인 검증 단계이며, 두 검사를 모두 통과해야 리소스를 할당할 수 있습니다.

‘개수’, ‘항목당 바이트 수’, ‘총 바이트 수’는 이름을 구분해 서로 다른 단위가 섞이지 않도록 합니다. 오류에는 어느 연산 단계에서 발생했는지도 담되, 민감한 원본 입력 전체를 로그에 기록해서는 안 됩니다.

래핑 대신 오버플로를 보고하는 연산 사용하기

일반적인 +, *는 오버플로가 발생하면 런타임 오류를 일으킵니다. 반면 &+, &*는 명시적으로 래핑 연산을 수행하므로, 매우 큰 용량이 작은 값으로 바뀔 수 있습니다. 용량 검증에는 계산 결과와 오버플로 플래그를 함께 반환하는 연산이 필요합니다. 먼저 플래그를 확인한 뒤 결과를 사용해야 합니다.

다음 코드를 arithmetic.swift로 저장합니다. 앱 프로젝트에 의존하지 않으며, 실제로 대용량 메모리를 할당하지도 않습니다.

enum CapacityError: Error, Equatable {
    case invalidInput
    case overflow
}

func checkedAdd(_ a: Int, _ b: Int) throws -> Int {
    guard a >= 0, b >= 0 else {
        throw CapacityError.invalidInput
    }
    let result = a.addingReportingOverflow(b)
    guard !result.overflow else {
        throw CapacityError.overflow
    }
    return result.partialValue
}

func checkedMultiply(_ a: Int, _ b: Int) throws -> Int {
    guard a >= 0, b >= 0 else {
        throw CapacityError.invalidInput
    }
    let result = a.multipliedReportingOverflow(by: b)
    guard !result.overflow else {
        throw CapacityError.overflow
    }
    return result.partialValue
}

func ceilingDivide(_ n: Int, by d: Int) throws -> Int {
    guard n >= 0, d > 0 else {
        throw CapacityError.invalidInput
    }
    let q = n / d
    return n % d == 0 ? q : try checkedAdd(q, 1)
}

func require(_ condition: Bool) {
    guard condition else {
        fatalError("Boundary check failed")
    }
}

func expectError(
    _ expected: CapacityError,
    _ operation: () throws -> Int
) {
    do {
        _ = try operation()
        fatalError("Expected capacity error")
    } catch let error as CapacityError {
        require(error == expected)
    } catch {
        fatalError("Unexpected error type")
    }
}

require(try checkedAdd(Int.max, 0) == Int.max)
require(try checkedMultiply(Int.max, 1) == Int.max)
require(try checkedMultiply(0, Int.max) == 0)
require(try ceilingDivide(0, by: 4) == 0)
require(try ceilingDivide(8, by: 4) == 2)
require(try ceilingDivide(9, by: 4) == 3)
require(try ceilingDivide(Int.max, by: 1) == Int.max)
require(try ceilingDivide(Int.max, by: 2) == Int.max / 2 + 1)

expectError(.overflow) {
    try checkedAdd(Int.max, 1)
}
expectError(.overflow) {
    try checkedMultiply(Int.max, 2)
}
expectError(.invalidInput) {
    try checkedMultiply(-1, 0)
}
expectError(.invalidInput) {
    try ceilingDivide(1, by: 0)
}
expectError(.invalidInput) {
    try ceilingDivide(Int.min, by: -1)
}

여기서는 검증 순서를 확인하기 위해 의도적으로 음수에 0을 곱하는 테스트를 넣었습니다. 구현에서 ‘곱하는 수가 0이면 바로 0을 반환’하는 단축 처리를 먼저 수행하면, 잘못된 입력이 계약 검증을 우회하게 됩니다. 테스트는 단순히 ‘어떤 오류가 발생했다’는 사실만 확인하지 말고, 오류의 종류까지 검사해야 합니다.

올림 나눗셈에서 제수를 먼저 더하지 않기

청크 수는 흔히 (n + d - 1) / d로 계산합니다. 수학적으로 맞는 식이라고 해서 고정 폭 정수에서도 안전한 것은 아닙니다. 최종 몫이 작더라도 앞선 덧셈에서 범위를 벗어날 수 있습니다.

예제에서는 먼저 몫과 나머지를 구한 뒤, 나머지가 있을 때만 1을 더합니다. 입력을 음이 아닌 수로, 제수를 양수로 제한했으므로 부호 있는 정수의 최솟값을 -1로 나누는 특수한 오버플로 경계에도 도달하지 않습니다.

테스트에는 최소한 0, 나누어떨어지는 경우, 나머지가 1인 경우, 정수 최댓값, 제수가 0인 경우를 포함해야 합니다. Int.max를 특정 십진수 상수로 하드코딩하지 마세요. 타입이 제공하는 경계값을 직접 사용해야 개발자가 기억하는 플랫폼 가정이 아니라, 실제 컴파일 대상의 정수 폭을 검증할 수 있습니다.

두 가지 컴파일 모드에서 실행하기

클라우드 Mac에는 사용할 개발 도구 체인이 선택되어 있어야 합니다. 먼저 컴파일러를 사용할 수 있는지 확인한 다음, 각 모드로 컴파일하고 실행합니다.

set -eu
xcrun swiftc --version
mkdir -p .arithmetic-check
xcrun swiftc -Onone arithmetic.swift \
  -o .arithmetic-check/debug
.arithmetic-check/debug
xcrun swiftc -O arithmetic.swift \
  -o .arithmetic-check/optimized
.arithmetic-check/optimized

성공하면 프로그램은 아무 출력 없이 종료됩니다. 검증이 하나라도 실패하거나 처리되지 않은 오류가 발생하면 해당 단계가 실패합니다. 파이프를 사용해 프로그램의 종료 코드가 로그 도구의 성공 상태로 바뀌지 않도록 하세요.

예제에서 assert를 사용하지 않은 이유는 최적화 빌드에서 제거될 수 있기 때문입니다. 직접 정의한 require는 명시적인 조건 검사와 fatalError를 사용하므로, 여기서 사용하는 두 모드 모두에서 검사가 유지됩니다. 런타임 안전성 검사를 약화하는 비표준 최적화 설정도 추가로 활성화하지 마세요.

앱에 통합할 때 리소스 상한도 확인하기

이 독립 실행 프로그램은 산술 계약을 검증하기 위한 최소 구성입니다. 실제 앱에 통합할 때는 비슷한 함수를 두 벌로 계속 관리하지 말고, 앱과 테스트가 동일한 프로덕션 구현을 참조하도록 해야 합니다.

호출 흐름을 다음 순서로 점검합니다.

  • 원본 입력 파싱에 실패하면 즉시 오류를 반환하고, 기본 용량으로 실행을 이어 가지 않습니다.
  • 각 값의 단위를 통일한 뒤 오버플로 검사가 포함된 곱셈과 덧셈을 수행해, 중간 식에서 먼저 오버플로가 발생하지 않도록 합니다.
  • 결과를 제품에서 정한 1회당 한도와 비교한 다음 할당 여부를 결정합니다.
  • 폭이 더 좁은 정수형으로 변환할 때는 실패 가능한 정확한 변환을 사용하고, 잘라내기를 허용하지 않습니다.
  • 실패 경로에서 파일 생성, 버퍼 할당, 후속 작업 제출이 이루어지지 않았는지 확인합니다.

마지막으로 ‘산술적으로는 유효하지만 서비스 한도를 초과하는’ 사례를 추가해, 오버플로 오류와 별도로 처리되는지 확인합니다. 이렇게 하면 실패 원인을 추적할 때 잘못된 입력, 타입의 표현 범위 초과, 리소스 정책에 따른 거부를 명확히 구분할 수 있으며, 모든 경계값 문제를 메모리 부족으로 뭉뚱그리지 않을 수 있습니다.

자주 묻는 질문

용량 계산이 넘치면 Int.max를 반환해도 되나요?

기본적으로 명시적인 오류를 반환해야 합니다. 최댓값으로 대체하면 유효한 큰 값과 실패를 구분하기 어렵습니다. 포화 연산이 제품 명세에 포함된 경우에만 사용하세요.

iOS 시뮬레이터가 필요한가요?

예제는 Swift 표준 라이브러리만 사용하므로 필요하지 않습니다. 다만 화면 동작, 플랫폼별 타입, 실제 메모리 할당은 별도의 플랫폼 테스트로 확인해야 합니다.

빌드 작업은 전용 클라우드 Mac에 맡기세요

Apple Silicon 물리 노드를 선택해 한 대를 단독으로 사용하세요. macOS 그래픽 인터페이스와 터미널을 그대로 이용할 수 있습니다. 모델, 이용 기간, 노드는 주문 확인 내용을 기준으로 합니다.