클라우드 Mac에서 최대잔여법으로 iOS 백분율 배분 검증하기
iOS 통계 화면에서 가중치가 같은 세 항목을 각각 33%로 표시하면 합계는 99%에 그칩니다. 그렇다고 부족한 값을 마지막 항목에 몰아주면 가중치가 0인 항목에 백분율이 배정될 수도 있습니다. 클라우드 Mac의 빌드 파이프라인에서는 이런 문제를 확인하기 위해 시뮬레이터를 실행할 필요가 없습니다. 배분 알고리즘을 순수 Swift 함수로 분리하면 합계, 순서, 잘못된 입력을 검사하고, UI에서도 동일한 결과를 사용할 수 있습니다.
표시 단위와 입력 계약부터 정의하기
여기서 다루는 입력은 세 분류의 완료 작업 수처럼 음이 아닌 정수 개수입니다. 금액, 부동소수점 측정값, 이미 반올림한 백분율은 대상이 아닙니다. 표시할 총 단위를 100으로 정하면 한 단위는 1퍼센트포인트를 뜻합니다. 소수점 첫째 자리까지 표시해야 한다면 총 단위를 1000으로 바꾸고, 한 단위를 0.1퍼센트포인트로 사용합니다.
계약은 네 가지입니다. 출력 길이는 입력과 같아야 하고, 출력값은 모두 음이 아니며 합계가 총 단위와 같아야 합니다. 가중치가 0인 항목에는 0단위를 배정해야 하며, 같은 입력에는 항상 같은 결과를 반환해야 합니다. 빈 배열, 음수 가중치, 가중치 합계가 0인 입력은 즉시 실패로 처리합니다. 임의로 균등 배분한 결과를 만들지 않습니다.
“데이터가 없음”과 “모든 항목의 비중이 같음”은 서로 다른 비즈니스 상태입니다. 입력이 모두 0이면 화면에서 빈 상태를 표시해야 하며, 배분 알고리즘으로 유효한 통계처럼 보이게 해서는 안 됩니다.
나머지가 같으면 입력 인덱스 순서로 우선순위를 정합니다. 따라서 호출하는 쪽에서 먼저 안정적인 분류 식별자를 기준으로 데이터를 정렬해야 합니다. 딕셔너리 순회 순서를 제품 규칙으로 삼으면 안 됩니다. 화면을 새로고침할 때마다 보정 단위를 받는 항목이 달라질 수 있기 때문입니다.
정수 최대잔여법으로 부족한 단위 채우기
먼저 각 항목의 가중치에 총 단위를 곱한 뒤 가중치 합계로 나눕니다. 몫은 기본 배분량이 되고, 나머지는 보정 순서를 정하는 데 사용합니다. 기본 배분량의 합계에서 부족한 만큼, 나머지가 큰 항목부터 차례로 한 단위씩 더합니다.
각 항목을 내림 처리하므로 추가로 배정해야 할 단위 수는 항상 항목 수보다 작습니다. 값이 수렴할 때까지 반복할 필요도, 부동소수점 허용 오차를 둘 필요도 없습니다. 다음 코드를 percentage-check.swift로 저장하세요. 구현과 최소한의 검사가 함께 들어 있습니다.
enum AllocationError: Error {
case invalidInput
case overflow
}
func allocate(_ weights: [Int], units: Int = 100) throws -> [Int] {
guard !weights.isEmpty,
(1...10_000).contains(units),
weights.allSatisfy({ $0 >= 0 }) else {
throw AllocationError.invalidInput
}
var total = 0
for weight in weights {
let sum = total.addingReportingOverflow(weight)
guard !sum.overflow else {
throw AllocationError.overflow
}
total = sum.partialValue
}
guard total > 0 else {
throw AllocationError.invalidInput
}
var result = [Int]()
var remainders = [Int]()
for weight in weights {
let product = weight.multipliedReportingOverflow(by: units)
guard !product.overflow else {
throw AllocationError.overflow
}
result.append(product.partialValue / total)
remainders.append(product.partialValue % total)
}
let remaining = units - result.reduce(0, +)
let order = weights.indices.sorted {
if remainders[$0] == remainders[$1] {
return $0 < $1
}
return remainders[$0] > remainders[$1]
}
for index in order.prefix(remaining) {
result[index] += 1
}
return result
}
func expectFailure(_ weights: [Int], units: Int = 100) {
do {
_ = try allocate(weights, units: units)
} catch {
return
}
preconditionFailure("Expected allocation to fail")
}
let equal = try allocate([1, 1, 1])
precondition(equal == [34, 33, 33])
let withZero = try allocate([0, 1, 1])
precondition(withZero == [0, 50, 50])
let tenths = try allocate([1, 1, 1], units: 1000)
precondition(tenths == [334, 333, 333])
for a in 0...12 {
for b in 0...12 {
for c in 0...12 {
let weights = [a, b, c]
let total = weights.reduce(0, +)
if total == 0 { continue }
let output = try allocate(weights)
precondition(output.count == weights.count)
precondition(output.reduce(0, +) == 100)
precondition(output.allSatisfy { $0 >= 0 })
for index in weights.indices {
let floorQuota = weights[index] * 100 / total
precondition(
output[index] == floorQuota ||
output[index] == floorQuota + 1
)
if weights[index] == 0 {
precondition(output[index] == 0)
}
}
}
}
}
expectFailure([])
expectFailure([0, 0])
expectFailure([-1, 2])
expectFailure([1], units: 0)
expectFailure([Int.max, 1])
expectFailure([Int.max])
이 코드는 의도적으로 합산과 곱셈 두 곳에서 오버플로를 검사합니다. 최종 백분율이 작더라도 중간에 계산하는 가중치 곱은 정수 범위를 넘을 수 있습니다. 여기서는 부동소수점으로 변환해 정밀도 변화를 가리는 대신 입력을 거부합니다. 더 큰 개수를 받아야 한다면 비트 폭이 더 큰 정수 연산을 별도로 설계해야 합니다.
클라우드 Mac에서 독립 검증 실행하기
먼저 xcrun swift --version으로 현재 도구 체인을 확인한 다음 아래 명령을 실행합니다. 검사가 성공하면 아무 출력도 나오지 않습니다. 단언 실패나 처리되지 않은 오류가 발생하면 프로세스가 실패하므로, 파이프라인에서도 이 종료 상태를 그대로 유지해야 합니다.
set -eu
xcrun swift percentage-check.swift
정식 품질 게이트로 사용할 때는 xcrun swiftc -Onone으로 컴파일한 뒤 실행할 수 있습니다. 이 단언들을 비활성화하는 최적화 모드는 사용하면 안 됩니다. 장기적으로 더 안전한 방법은 함수를 비즈니스 로직 모듈에 넣고, 단언을 테스트 타깃으로 옮긴 뒤 기존 테스트 명령으로 실행하는 것입니다. 이렇게 하면 스크립트와 제품에서 같은 알고리즘을 따로 유지하지 않아도 됩니다.
여기서는 세 가중치의 작은 범위 조합을 전수 검사합니다. 성능 벤치마크가 아닙니다. 모두 0에 가까운 입력, 한 항목이 전체를 차지하는 경우, 나머지가 같은 경우, 작은 가중치가 내림 처리되어 0이 되는 경우 등의 구조를 다룹니다. 테스트를 추가할 때는 같은 균등 샘플을 반복하기보다 입력 형태를 넓혀야 합니다.
반올림 오차와 비즈니스 규칙 구분하기
가장 놓치기 쉬운 문제는 정렬 후 항목과 결과의 대응 관계를 잃는 것입니다. 이 구현은 인덱스만 정렬하므로 최종 배열은 여전히 원래 항목 순서에 대응합니다. UI에서 나머지 순위를 그대로 분류 표시 순서로 사용해서는 안 됩니다.
또한 “양의 가중치를 가진 모든 항목에 최소 한 단위를 표시한다”는 조건을 요구하면 안 됩니다. 분류 수가 표시 단위 수보다 많으면 이 조건과 합계 보존을 동시에 만족시킬 수 없습니다. 아주 작은 비중도 보여줘야 한다면 표시 정밀도를 높이거나 “표시 기준 미만”이라는 별도 문구를 사용할 수 있습니다. 다만 배분량을 몰래 바꿔서는 안 됩니다.
마지막으로, 이 방법이 서로 다른 데이터셋 사이의 단조성을 보장한다고 생각해서는 안 됩니다. 다른 분류의 가중치나 표시 정밀도를 바꾸면 나머지 순위도 달라질 수 있습니다. 테스트에서는 현재 데이터의 배분량이 허용 범위에 있는지 검증해야 합니다. 특정 항목의 값이 새로고침 전후로 절대 줄지 않는다고 단언해서는 안 됩니다.
소수점 서식 지정은 표시만 담당하기
총 단위가 1000일 때 334는 33.4%를 뜻합니다. 서식 지정 API가 0에서 1 사이의 비율을 받는다면 334 / 1000에 해당하는 값을 전달해야 합니다. 여기에 다시 100을 곱하면 안 됩니다. 서식 지정 단계에서 원래 가중치로 항목별 비율을 다시 계산하거나, 다른 정밀도로 한 번 더 반올림해서도 안 됩니다.
실패를 검증 가능한 증거로 남기기
병합 전에는 입력 가중치, 총 단위, 기본 배분량, 나머지 순위, 최종 출력을 남겨 두세요. 운영 통계에 민감한 분류가 포함될 수 있다면 고정된 테스트 픽스처로 재현하고, 실제 업무 상세 데이터를 빌드 산출물에 기록하지 않습니다.
리뷰에서는 세 가지 경계를 중점적으로 확인합니다. 잘못된 입력이 명확하게 실패하는지, 나머지가 같을 때 우선순위가 일관되게 결정되는지, UI 서식 지정이 정해진 정밀도를 유지하는지입니다. 검증을 통과한 뒤에는 데이터 계층이 가중치만 제공하고, 배분 계층이 정수 단위만 제공하며, 표시 계층이 현지화된 서식만 담당하도록 합니다. 세 계층의 책임을 분리하면 백분율 합계에 문제가 생겼을 때 정확히 어느 단계에서 발생했는지 찾을 수 있습니다. 마지막 항목에 차액을 몰아주어 겉으로만 맞추는 방식에 의존할 필요가 없습니다.
자주 묻는 질문
각 비율을 따로 반올림하면 안 되나요?
개별 반올림은 합계를 보존하지 않습니다. 같은 가중치 세 개를 각각 삼십삼 퍼센트로 표시하면 합계는 구십구 퍼센트입니다. 최대잔여법은 먼저 정수 몫을 배분한 뒤 나머지가 큰 순서대로 부족한 단위를 채웁니다.
가중치가 커지면 표시 비율도 반드시 커지나요?
서로 다른 입력 사이의 단조성까지 보장하지는 않습니다. 다른 가중치나 표시 단위 수가 바뀌면 나머지 순위도 달라질 수 있습니다. 여기서는 현재 입력의 합계, 할당 범위와 결정성을 보장합니다.
내 파이프라인으로 클라우드 Mac 검증하기
ArmRDP는 가상 머신이 아닌 전용 물리 Mac mini를 제공합니다. 하루 단위로 대여해 툴체인과 빌드 결과를 먼저 확인한 뒤, 팀에 맞는 대여 기간을 선택하세요.
클라우드 Mac 대여하기