주문 항목을 하나씩 더한 금액이 서버의 총액과 1센트 차이 나고, 환불 금액도 음수의 반올림 중간값에서 어긋난다면 스크린샷만으로는 검증할 수 없습니다. 금액 파싱과 반올림 규칙을 UI에서 분리해 순수 함수로 만들고, 클라우드 Mac에서 고정된 테스트 케이스를 실행하세요. 클라이언트와 서버가 같은 계산 계약을 따르는지 먼저 확인한 뒤 주문 화면에 연결해야 합니다. 이 글은 수치 계산 규칙만 다루며, 결제 게이트웨이에 연결하거나 실제 거래 데이터를 사용하지 않습니다.
금액 계산 계약부터 명확히 정의하기
테스트에 앞서 네 가지를 정해야 합니다. 입력은 소수점 이하 몇 자리까지 허용할지, 정산 결과는 몇 자리까지 유지할지, 어느 단계에서 반올림할지, 값이 정확히 중간에 놓일 때 어느 방향으로 반올림할지입니다. 아래 예제에서는 입력을 소수점 이하 최대 여섯 자리로 제한하고, 정산 결과는 두 자리로 맞추며, 중간값은 0에서 멀어지는 방향으로 반올림합니다. 이는 예시 계약일 뿐, 모든 업무에 적용할 수 있는 규칙은 아닙니다.
할인율, 단가, 정산 금액은 서로 다른 종류의 값입니다. 단가에 수량을 곱한 뒤에는 중간 계산의 정밀도를 유지할 수 있지만, 최종 금액은 반드시 합의된 정산 함수를 거쳐야 합니다. 각 호출부가 임의로 값을 포맷한 뒤 다시 숫자로 변환하도록 두지 마세요.
Decimal은 십진수 표현 문제를 해결할 뿐, 업무 규칙까지 정해 주지는 않습니다. 항목별 반올림과 합계를 구한 뒤의 반올림은 모두 타당한 방식일 수 있지만, 한 주문 안에서 두 방식을 섞어 쓰면 안 됩니다.
먼저 서버가 십진수 문자열을 반환하는지, 통화의 최소 단위를 나타내는 정수를 반환하는지 확인하세요. 실제로 부동소수점 값을 반환한다면 API 계약부터 수정해야 합니다. 이미 정밀도가 손실된 값에서 클라이언트가 원래 값을 복원할 수는 없습니다.
독립적으로 실행할 수 있는 규칙 패키지 만들기
빈 디렉터리에 Swift 라이브러리 패키지를 만들고, 생성된 소스 파일에 규칙 코드를 작성한 뒤 해당 테스트 디렉터리에 테스트를 추가합니다. 명령은 클라우드 Mac의 프로젝트 디렉터리에서 실행하며, 시뮬레이터를 시작할 필요는 없습니다.
mkdir MoneyRules
cd MoneyRules
swift package init --type library --name MoneyRules
swift --version
Sources/MoneyRules/MoneyRules.swift를 아래 구현으로 교체하세요. 파싱에 사용할 로케일을 고정하고 입력 길이를 제한하며, 천 단위 구분 기호, 지수 표기, 공백 문자, 합의된 범위를 넘는 소수 자릿수를 거부합니다. 이 코드는 API 데이터를 처리하기 위한 것으로, 사용자 입력의 지역화는 담당하지 않습니다.
import Foundation
public enum MoneyRules {
public static func parse(_ text: String) -> Decimal? {
guard text.utf8.count <= 20,
text.range(
of: #"^-?[0-9]+(?:\.[0-9]{1,6})?\z"#,
options: .regularExpression
) != nil else {
return nil
}
return Decimal(
string: text,
locale: Locale(identifier: "en_US_POSIX")
)
}
public static func settle(_ value: Decimal) -> Decimal {
var input = value
var output = Decimal()
NSDecimalRound(&output, &input, 2, .plain)
return output
}
}
Decimal(Double(text)!)는 사용하지 마세요. 최종 결과가 Decimal이더라도 중간에 이진 부동소수점으로 변환하면서 이미 원래 수치가 달라졌을 수 있습니다. 입력 길이의 상한도 실제 업무에서 허용하는 금액 범위에 맞아야 합니다. 테스트를 통과시키려고 금액을 임의로 잘라서는 안 됩니다.
중간값과 계산 순서로 차이 찾아내기
Tests/MoneyRulesTests/MoneyRulesTests.swift에 아래 내용을 작성해 기본 테스트를 교체하세요. Decimal 값은 정확히 비교하고, ‘허용 오차’ 범위를 두지 않습니다. 그렇지 않으면 테스트 자체가 1센트 차이를 숨길 수 있습니다.
import XCTest
@testable import MoneyRules
final class MoneyRulesTests: XCTestCase {
private func number(_ text: String) throws -> Decimal {
try XCTUnwrap(MoneyRules.parse(text))
}
func testMidpoints() throws {
XCTAssertEqual(
MoneyRules.settle(try number("1.005")),
try number("1.01")
)
XCTAssertEqual(
MoneyRules.settle(try number("-1.005")),
try number("-1.01")
)
XCTAssertEqual(
MoneyRules.settle(try number("1.004")),
try number("1.00")
)
}
func testRoundingStage() throws {
let item = try number("0.335")
let perLine = MoneyRules.settle(item) * Decimal(3)
let total = MoneyRules.settle(item * Decimal(3))
XCTAssertEqual(perLine, try number("1.02"))
XCTAssertEqual(total, try number("1.01"))
}
func testRejectedInputs() {
for text in ["", "1,25", "1e2", "NaN", " 1.00",
"1.0000001", "1.00\n"] {
XCTAssertNil(MoneyRules.parse(text))
}
}
}
swift test를 실행해 세 테스트가 모두 통과하는지 확인하세요. 계산 순서 테스트는 특정 정산 방식을 권장하는 것이 아니라, 두 방식의 결과가 실제로 다르다는 점을 보여 줍니다. 업무에서 한 방식을 선택했다면 주문 단위 테스트도 추가해 호출 흐름 중에 다른 방식으로 바뀌지 않도록 해야 합니다.
이어서 0, 음의 0, 환불, 최대 허용 금액에 대한 테스트 케이스와 중간값보다 작은 값, 정확히 같은 값, 큰 값으로 구성된 세 그룹을 추가하세요. 중간값을 가장 가까운 짝수 쪽으로 반올림하는 규칙으로 바꾼다면 구현만 수정하지 말고, 그 규칙에 맞는 기댓값도 새로 정해야 합니다.
전송과 표시의 경계를 따로 검증하기
API 전송은 UI 포맷터에 의존해서는 안 됩니다. 화면에는 자릿수 구분 기호를 넣거나 지역에 맞는 소수점 구분 기호를 사용할 수 있지만, 전송에는 계속 합의된 십진수 문자열을 사용해야 합니다. 아래 코드는 정산 결과를 문자열로 바꾼 뒤 정확히 같은 값으로 다시 읽을 수 있는지 확인합니다. 문자열이 항상 소수점 이하 두 자리로 표현된다는 보장은 하지 않습니다.
import Foundation
var settled = MoneyRules.settle(MoneyRules.parse("12.345")!)
let wire = NSDecimalString(
&settled,
Locale(identifier: "en_US_POSIX")
)
precondition(MoneyRules.parse(wire) == settled)
잘못된 데이터를 만났을 때 운영 프로그램이 즉시 종료되도록 두는 대신, 이 검사를 테스트에 넣으세요. 프로토콜이 "12.30"처럼 고정된 형식을 요구한다면 별도의 인코더를 구현하고 테스트해야 합니다. 수치가 같다는 이유만으로 문자열 검증을 통과시켜서는 안 됩니다.
서버와 클라이언트가 함께 사용할 테스트 케이스에는 입력 문자열, 수량, 반올림 단계, 기대 문자열을 저장하고 실제 주문 번호는 저장하지 마세요. 서버와 클라이언트가 동일한 테스트 케이스를 각각 읽어야 양쪽이 같은 규칙을 적용하는지 확인할 수 있습니다.
파이프라인에 연결하고 재검토할 수 있는 근거 남기기
독립 패키지 테스트를 먼저 실행한 다음 앱 테스트를 실행하세요. 독립 패키지 테스트가 실패하면 이후 패키징 작업을 중단해 수치 규칙 오류가 UI 검증 단계까지 넘어가지 않도록 합니다. 커밋 식별자, Swift 버전, 실패한 테스트 케이스를 기록하세요. 실행 시간 수치로 정확성에 대한 결론을 대신해서는 안 됩니다.
점검 항목은 짧고 명확하게 유지하세요.
- 입력은 십진수 문자열에서 가져오며, Double을 거치지 않는다.
- 반올림은 합의된 단계에서만 수행하고, 중간 결과의 소수 자릿수를 반복해서 줄이지 않는다.
- 음수 중간값과 환불 규칙은 별도의 단언문으로 검증한다.
- 전송한 값은 정확히 같은 값으로 다시 읽을 수 있으며, 표시 문자열은 계산에 사용하지 않는다.
- 양쪽이 같은 테스트 케이스를 사용하고, 규칙을 변경할 때 기댓값과 업무 설명을 함께 갱신한다.
Foundation만 사용하는 테스트의 통과는 계산 계층이 현재 계약을 충족한다는 사실만 입증합니다. 마지막으로 iOS 테스트 타깃에서 화면 입력과 표시를 검증하고, 발견한 1센트 단위의 차이를 고정된 테스트 케이스로 추가해야 합니다. 수정할 때마다 다시 실행할 수 있는 테스트를 남기는 편이 한 번 수동으로 대조한 결과를 기억하는 것보다 더 신뢰할 수 있습니다.
자주 묻는 질문
Decimal을 사용하면 금액 오류가 모두 사라지나요?
아닙니다. Double을 거쳐 변환하거나 너무 일찍 반올림하면 차이가 남습니다. 검증한 십진 문자열로 값을 만들고 반올림할 단계를 명시해야 합니다.
이 테스트에도 iOS 시뮬레이터가 필요한가요?
Foundation 기반 순수 계산은 Swift 패키지에서 swift test로 실행할 수 있습니다. 화면 입력, 지역별 표시 형식, iOS 전용 코드는 별도 앱 테스트가 필요합니다.
빌드 대기열을 전용 클라우드 Mac에 맡기세요
물리 노드, 모델, 요금제를 선택하면 Xcode 빌드와 자동화 작업을 전용 macOS 환경에서 실행할 수 있습니다. 실제 사용 가능 여부는 콘솔에 표시되는 실시간 상태를 확인하세요.