세금과 송장을 위한 다중 통화 가격 데이터 모델
환율, 반올림, 세금, 현지화된 송장 표시를 놀라움 없이 처리하는 다중 통화 가격 데이터 모델을 배우세요.

다중 통화 송장에서 주로 무엇이 잘못되는가
다중 통화 송장은 지루하지만 비용이 큰 방식으로 실패합니다. UI 상 숫자는 괜찮아 보였다가 누군가 PDF로 내보내고 회계 시스템이 가져오면 항목 합계가 맞지 않습니다.
근본 원인은 간단합니다. 금액 계산은 단순히 환율을 곱하는 것이 아닙니다. 세금, 반올림, 환율을 캡처한 정확한 시점 등이 결과에 영향을 줍니다. 가격 데이터 모델이 이런 선택들을 명시하지 않으면 시스템의 서로 다른 부분이 "도움이 된다"며 재계산을 하고 다른 결과를 냅니다.
세 가지 관점이 합의해야 합니다. 비록 표시되는 통화가 달라도:
- 고객 관점: 고객 통화로 명확한 가격과 합계가 맞아야 합니다.
- 회계 관점: 보고와 조정을 위한 일관된 기준 금액이 필요합니다.
- 감사 관점: 어떤 환율과 반올림 규칙으로 송장이 만들어졌는지 보여주는 기록이 필요합니다.
불일치는 보통 서로 다른 곳에서 내려진 작은 결정들에서 옵니다. 어떤 팀은 각 라인 항목마다 반올림을 하고, 다른 쪽은 합계만 반올림합니다. 어떤 화면은 현재 환율을 쓰고, 다른 쪽은 송장일자 환율을 씁니다. 할인 전 과세인지 후 과세인지, 세금이 가격에 포함되어 있는지 추가되는지 등도 다릅니다.
구체적 예: EUR로 19.99인 상품을 판매하고 GBP로 송장 발행하며 USD로 보고하는 경우, 라인별로 변환하고 소수점 두 자리로 반올림하면 세금 합계가 다르게 나올 수 있습니다. 라인을 먼저 합산한 뒤 한 번만 변환하면 또 다른 결과가 나옵니다. 두 방법 모두 합리적일 수 있지만 규칙은 하나만 선택해야 합니다.
목표는 예측 가능한 계산과 명확한 저장값입니다. 모든 송장은 추측 없이 답할 수 있어야 합니다: 어떤 금액을 입력했는가, 어떤 통화로 입력했는가, 어떤 환율을 사용했는가(그리고 언제였는가), 무엇을 반올림했는가(어떻게 했는가), 어떤 세금 규칙을 적용했는가. 이러한 명확성은 UI, PDF, 내보내기, 감사 전반에서 합계를 안정적으로 유지합니다.
스키마 설계 전에 합의할 주요 용어
테이블을 설계하기 전에 모두가 같은 용어를 쓰는지 확인하세요. 대부분의 다중 통화 버그는 기술적 문제가 아니라 "우리가 다르게 의미했다"는 문제입니다. 명확한 스키마는 제품, 재무, 엔지니어링이 모두 받아들일 정의에서 시작합니다.
데이터베이스에 영향을 주는 통화 용어들
각 금전 흐름에 대해 세 가지 통화를 합의하세요:
- 거래 통화(Transactional currency): 고객이 보고 동의하는 통화(가격표, 장바구니, 송장 표시).
- 결제 통화(Settlement currency): 실제로 결제받는 통화(결제 제공자나 은행이 정산하는 통화).
- 보고 통화(Reporting currency): 대시보드와 회계 요약에 사용하는 통화.
또한 **소단위(minor units)**를 정의하세요. 예: USD는 2(센트), JPY는 0, KWD는 3입니다. 이는 중요한데, "12.34"를 부동소수점으로 저장하면 오차가 생기지만 소단위 정수(예: 1234 센트)로 저장하면 정확성이 유지되고 반올림이 예측 가능해집니다.
합계를 바꾸는 세금 용어들
세금도 동일한 수준의 합의가 필요합니다. 가격이 **세금 포함(tax-inclusive)**인지 **세금 별도(tax-exclusive)**인지 결정하세요. 또한 세금을 **라인별 계산(per line)**할지 **송장별 계산(on subtotal)**할지도 선택하세요. 이 선택들은 반올림에 영향을 주며 최종 납부 금액을 소액 단위로 바꿀 수 있습니다.
마지막으로 무엇을 저장할지와 무엇을 유도(derive)할지를 결정하세요:
- 법적·재무적으로 중요한 것들은 저장하세요: 합의된 가격, 적용된 세율, 최종 반올림된 합계, 사용된 통화.
- 안전하게 재계산할 수 있는 것은 유도하세요: 포맷된 문자열, 표시용 변환, 대부분의 중간 계산값.
핵심 금전 필드: 무엇을 저장하고 어떻게 저장할지
어떤 숫자가 사실(fact)으로서 저장되어야 하고 어떤 숫자가 결과(result)로서 재계산 가능한지 먼저 결정하세요. 둘을 섞으면 화면과 내보내기에서 서로 다른 합계를 보여주는 송장이 됩니다.
금액은 소단위 정수(센트 등)로 저장하고 항상 통화 코드도 함께 저장하세요. 통화 없는 금액은 불완전한 데이터입니다. 정수는 많은 라인을 더할 때 나타나는 작은 부동소수점 오류를 피합니다.
실무적 패턴으로는 원시 입력(raw inputs)과 계산 결과(calculated outputs)를 모두 보관하는 것이 좋습니다. 입력은 사용자가 무엇을 입력했는지 설명하고, 결과는 청구한 내용을 설명합니다. 누군가가 몇 달 뒤 송장에 이의를 제기하면 둘 다 필요합니다.
송장 라인에는 다음과 같은 견고한 필드 세트를 유지하세요:
unit_price_minor+unit_currencyquantity(필요하면uom)line_subtotal_minor(세금/할인 전)line_discount_minorline_tax_minor(또는 세금 유형별 분해)line_total_minor(라인의 최종 금액)
반올림은 단순한 UI 디테일이 아닙니다. 특히 JPY와 USD처럼 소단위가 다른 통화를 지원하거나 현금 반올림 규칙이 있을 때는 사용된 반올림 방법과 정밀도를 영구적으로 보관하세요. 작은 "계산 컨텍스트" 레코드에 calc_precision, rounding_mode, 반올림이 라인별인지 송장 합계만인지 같은 정보를 담을 수 있습니다.
표시 형식과 저장값을 분리하세요. 저장값은 단순 숫자와 코드여야 하고, 포맷(통화 기호, 구분자, 현지화된 숫자 형식)은 표현층에서 처리하세요. 예: 12345 + EUR를 저장하고 UI가 "€123.45"나 "123,45 €" 중 무엇을 보여줄지 결정하도록 하세요.
환율: 테이블, 타임스탬프, 감사 기록
환율은 시계열 데이터로 취급하고 출처를 명확히 하세요. "오늘 환율"은 나중에 안전하게 재계산할 수 있는 것이 아닙니다.
실무적인 환율 테이블에는 보통 다음이 포함됩니다:
base_currency(변환의 출발 통화, 예: USD)quote_currency(변환 대상 통화, 예: EUR)rate(1 base 당 quote 비율, 고정소수점 고정밀로 저장)effective_at(환율이 유효한 타임스탬프)source(제공자) 및source_ref(제공자 ID나 페이로드 해시)
감사 시에는 이 출처 정보가 중요합니다. 고객이 금액에 이의를 제기하면 숫자가 어디에서 왔는지 정확히 가리킬 수 있어야 합니다.
다음으로, 송장이 어떤 환율을 사용하는지 한 가지 규칙을 정하고 지키세요. 일반적인 옵션은 주문 시점, 출하 시점, 송장 시점 등입니다. 최선의 선택은 비즈니스에 따라 다르지만 일관성 및 문서화가 핵심입니다.
어떤 규칙을 선택하든 송장에 사용된 정확한 환율을 저장하세요(종종 각 송장 라인에도). 나중에 다시 조회하지 말고 fx_rate, fx_rate_effective_at, fx_rate_source 같은 필드를 추가해 송장을 정확히 재현할 수 있게 하세요.
주말·공휴일·제공자 장애로 환율이 없을 때의 대체 동작도 명확히 하세요. 일반적 접근은: 이전의 가장 최근 환율을 사용, 환율이 있을 때까지 송장 발행 차단, 또는 수동 환율을 허용하되 승인 플래그를 두는 것입니다.
예: 주문이 토요일에 발생하고 월요일에 출하되어 월요일에 송장 발행 규칙을 따르는데 제공자가 주말 환율을 발행하지 않으면 금요일의 마지막 환율을 사용하고 effective_at = Friday 23:59, 그리고 추적을 위한 source_ref를 기록할 수 있습니다.
일관성 있는 통화 변환 및 반올림 규칙
반올림 문제는 명백한 버그처럼 보이지 않습니다. 라인 합계와 송장 합계 사이 1센트 차이로 나타나거나 표시와 결제 제공자가 기대하는 세금 간 작은 차이로 드러납니다. 좋은 모델은 반올림을 설명할 수 있는 규칙으로 만들고, 나중에 덧붙이는 임시 조치가 아니게 합니다.
반올림이 어디에서 발생하는지 정확히 결정하세요
반올림을 허용할 지점을 정하고 나머지는 높은 정밀도로 유지하세요. 일반적인 반올림 지점:
- 라인 확장(수량 x 단가, 할인 후)
- 각 세금 금액(라인별 또는 송장별로 관할 구역에 따라 다름)
- 최종 송장 합계
이 지점을 정의하지 않으면 시스템의 서로 다른 부분이 편한 대로 반올림하고 합계가 어긋납니다.
하나의 반올림 모드를 사용하고 세금 규칙 예외를 명확히 하세요
반올림 모드(half-up 또는 bankers)를 선택하고 일관되게 적용하세요. 고객에게 설명하기는 half-up이 쉽습니다. 대량 거래에서 편향을 줄이려면 bankers rounding이 유리할 수 있습니다. 어느 쪽이든 API, UI, 내보내기, 회계 보고서가 같은 모드를 사용해야 합니다.
변환과 중간 단계에서는 추가 정밀도를 유지하세요(예: FX 환율은 많은 소수 자리로 저장). 그런 다음 선택한 반올림 지점에서만 반올림하세요.
할인도 단일 규칙이 필요합니다: 세금 전 할인(쿠폰에 일반적)인지 세금 후 할인(특정 수수료 규정상 요구되는 경우)인지 정하고 문서화 후 한 번만 구현하세요.
일부 관할 구역은 라인별, 세금별, 또는 송장 총액 기준으로 반올림을 요구합니다. 코드베이스 곳곳에 하드코딩하지 말고 국가/주/세법 체계별로 rounding policy 설정을 저장하고 계산은 그 정책을 따르게 하세요.
간단한 검사: 동일한 저장된 환율과 정책으로 내일 같은 송장을 재구성하면 매번 같은 센트 값이 나와야 합니다.
세금 필드: 부가가치세, 판매세, 다중 세금에 대한 패턴
세금은 구매자 위치, 판매 품목, 가격 표시 방식에 따라 복잡해집니다. 깔끔한 모델은 세금을 암시하지 않고 명시적으로 유지합니다.
과세 기준(tax basis)을 모호하지 않게 하세요. 과세하는 가격이 순액(net)인지 총액(gross)인지 저장하세요. 그리고 적용한 세율과 계산된 세금 금액을 스냅샷으로 저장해 추후 규칙 변경으로 과거 기록이 바뀌지 않게 하세요.
각 송장 라인에 최소한 다음을 남기세요:
tax_basis(NET 또는 GROSS)tax_rate(예: 0.20)taxable_amount_minor(실제로 과세한 기준 금액)tax_amount_minortax_method(PER_LINE 또는 ON_SUBTOTAL)
두 개 이상의 세금이 적용될 수 있다면(예: VAT + 시 부과금) InvoiceLineTax 같은 분해 테이블을 추가해 각 적용 세금에 대해 한 행을 두세요. 각 행에는 tax_code, tax_rate, taxable_amount_minor, tax_amount_minor, 통화, 계산 시 사용된 관할 구역 힌트(국가, 지역, 우편번호 등)를 포함하세요.
적용된 규칙의 스냅샷(예: rule_version 또는 의사결정 입력값의 JSON 블랍)을 송장 또는 송장 라인에 저장하세요. VAT 규정이 내년에 바뀌어도 과거 송장은 실제로 청구한 내용과 일치해야 합니다.
예: 독일 고객에게 SaaS 구독을 판매하면 NET 라인 가격에 19% VAT를 적용하고 1%의 지역세를 추가할 수 있습니다. 라인 합계는 청구된 대로 저장하고 각 세금에 대해 분해 행을 보관하세요.
단계별 테이블 설계 방법
요령은 영리한 수학이 아니라 적절한 사실을 적절한 시점에 고정(freeze)하는 것입니다. 목표는 몇 달 뒤 송장을 다시 열어도 동일한 숫자를 보여주는 것입니다.
먼저 제품 가격의 진실이 어디에 있는지 결정하세요. 많은 팀이 제품당 기준 통화 가격을 유지하고 시장별 오버라이드를 선택적으로 둡니다(예: USD와 EUR의 별도 가격 행). 무엇을 선택하든 카탈로그 가격과 변환 가격을 섞지 않도록 스키마에서 명시하세요.
이해하기 쉬운 단순한 순서:
- 제품 및 가격:
product_id,price_amount_minor,price_currency,effective_from(가격 변경 시) - 주문 및 송장 헤더:
document_currency,customer_locale,billing_country, 그리고 타임스탬프(issued_at,tax_point_at) - 라인 아이템:
unit_price_amount_minor,quantity,discount_amount_minor,tax_amount_minor,line_total_amount_minor및 각 저장된 금액에 대한 통화 - 환율 스냅샷: 사용된 정확한 환율(
rate_value,rate_provider,rate_timestamp)을 주문 또는 송장에서 참조 - 세금 분해 레코드: 세금당 한 행(
tax_type,rate_percent,taxable_base_minor,tax_amount_minor) 및calculation_method플래그
나중에 재계산에 의존하지 마세요. 송장을 생성할 때 최종 단가, 할인, 합계를 송장 라인 항목에 복사하세요(주문에서 왔더라도).
추적성을 위해 송장에 calculation_version(또는 calc_hash)과 누가 왜 재계산을 촉발했는지 기록하는 작은 calculation_log 테이블을 추가하세요(예: "발행 전 환율 업데이트").
숫자를 깨뜨리지 않는 현지화된 송장 표시
현지화는 송장의 모양을 바꾸지 결과를 바꾸지 않아야 합니다. 계산은 저장된 숫자(소단위 정수 또는 고정 소수)로 수행하고 마지막에 현지화 형식을 적용하세요.
송장 자체에 표현 설정을 보관하세요. 고객은 시간이 지나며 국가나 결제 연락처를 바꿀 수 있습니다. 송장은 법적 스냅샷입니다. invoice_language, invoice_locale, 소수점 표시 여부 같은 포맷 플래그를 문서에 저장해 재인쇄 시 원본과 일치하게 하세요.
통화 기호는 표시 문제입니다. 일부 로케일은 기호를 금액 앞에, 다른 곳은 뒤에 둡니다. 기호 배치, 공백, 소수 구분자, 천 단위 구분은 렌더링 시점에 처리하세요. 기호를 저장된 금액에 넣지 말고 포맷된 문자열을 다시 숫자로 파싱하지 마세요.
보고를 위해 보조 통화(종종 집계 통화)를 보여줘야 한다면 문서 통화를 대체하지 말고 보조 합계로 명확히 표시하세요. 문서 통화가 법적 기준입니다.
송장 출력의 실무적 설정:
- 문서 통화로 라인 항목과 합계를 invoice-locale 포맷으로 표시
- 선택적으로 환율 출처와 타임스탬프를 라벨로 한 보조 보고 합계 표시
- 세금 분해를 별도 라인(과세 기준, 각 세금, 총세금)으로 표시
- 동일한 저장된 합계로 PDF와 이메일을 렌더링해 숫자가 어긋나지 않게 함
예: 프랑스 고객이 CHF로 청구되면 송장 로케일은 소수점에 쉼표를 쓰고 통화 기호를 금액 뒤에 배치하지만 계산은 여전히 저장된 CHF 금액과 세금 합계를 사용합니다. 표시만 달라질 뿐 숫자는 동일합니다.
흔한 실수와 함정
다중 통화 송장을 망치는 가장 빠른 방법은 금액을 일반 숫자처럼 취급하는 것입니다. 가격, 세금, 합계에 부동소수점 타입을 쓰면 나중에 "0.01달러 차이" 문제가 생깁니다. 금액을 소단위 정수(센트)로 저장하거나 확실한 스케일이 있는 고정 소수 타입을 사용하고 일관되게 적용하세요.
또 다른 고전적 함정은 실수로 기록을 바꾸는 것입니다. 오래된 송장을 오늘의 환율이나 업데이트된 세법으로 재계산하면 고객이 본 문서와 일치하지 않게 됩니다. 송장은 불변(immutable)이어야 합니다: 발행 후에는 정확한 환율, 반올림 규칙, 세금 방식을 저장하고 저장된 합계를 재계산하지 마세요.
한 라인 항목 안에 여러 통화를 섞는 것도 조용한 스키마 버그입니다. 단가가 EUR, 할인이 USD, 세금이 GBP라면 수학을 나중에 설명할 수 없습니다. 표시/정산 문서 통화 하나와 내부 보고용 기준 통화 하나를 선택하세요(필요한 경우). 모든 저장 금액은 명시적 통화를 가져야 합니다.
반올림 실수는 너무 자주 반올림해서 생기는 경우가 많습니다. 단가에서 반올림하고, 라인 합계에서 다시 반올림하고, 라인별 세금에서 반올림하고, 소계에서 또 반올림하면 합계가 라인 합과 맞지 않게 됩니다.
주의할 함정들:
- 금액이나 환율에 부동소수점을 사용하고 고정 정밀도를 적용하지 않음
- 오래된 송장을 지금의 환율로 재계산함
- 하나의 라인에 여러 통화를 허용함
- 선명히 정의된 지점이 아니라 여러 단계에서 반올림함
- 문서별로 환율 타임스탬프, 반올림 모드, 세금 방식을 저장하지 않음
예: CAD로 송장을 만들고 EUR로 가격된 서비스를 변환한 뒤 환율 테이블을 나중에 변경하면 EUR 금액만 저장하고 표시 시 변환하면 CAD 합계가 다음 주에 바뀝니다. EUR 금액, 적용된 FX 레코드(시간 포함), 그리고 송장에 사용된 최종 CAD 금액을 저장하세요.
출시 전 빠른 체크리스트
다중 통화 송장을 "완료"로 선언하기 전에 일관성에 초점을 맞춘 최종 점검을 하세요. 여기서 대부분 버그는 복잡하지 않습니다. 무엇을 저장하고 무엇을 표시하며 무엇을 합산하는지의 불일치에서 옵니다.
릴리스 게이트로 사용하세요:
- 각 송장 헤더에 정확히 하나의 문서 통화가 있고, 송장에 저장된 모든 합계가 그 통화로 되어 있는가.
- 저장하는 모든 금전 값이 소단위 정수인지(라인 합계, 세금, 할인, 운송비 포함).
- 송장이 사용한 정확한 환율(정밀 소수), 타임스탬프, 환율 출처를 저장하는가.
- 반올림 규칙은 문서화되어 있고 한 곳에 구현되어 있는가.
- 여러 세금이 적용될 수 있다면 헤더의 단일 세금 합계만이 아니라 라인별(또는 관할별) 세금 분해를 저장하는가.
스키마 점검 후에는 감사자가 할 법한 방식으로 수학을 검증하세요. 송장 합계는 저장된 라인 합계와 저장된 세금 합계의 합과 일치해야 합니다. 표시된 값이나 포맷된 문자열에서 합계를 재계산하지 마세요.
실무 테스트: 적어도 세 개 라인이 있는 송장을 골라 할인과 하나의 라인에 두 가지 세금을 적용해 보고 다른 로케일로 출력했을 때(구분자와 통화 기호가 다른 경우) 저장된 숫자가 변하지 않는지 확인하세요.
예시 시나리오: 한 주문, 세 통화, 그리고 세금
미국 고객에게 USD로 청구하고, EU 공급자는 EUR로 비용을 청구하며, 재무팀은 GBP로 보고하는 상황입니다. 이 지점에서 모델은 안정적으로 유지되거나 1센트 미스매치의 산더미가 됩니다.
주문: 제품 3개
- 고객 가격: $19.99/단위 (USD)
- 할인: 라인에 10%
- 미국 판매세: 8.25% (할인 후 과세)
- 공급자 원가: €12.40/단위 (EUR)
- 보고 통화: GBP
무슨 일이 언제 일어나고 언제 변환하는가
한 번의 변환 시점을 선택하고 지키세요. 많은 시스템에서 안전한 선택은 송장 발행 시점에 변환하고 사용된 정확한 환율을 저장하는 것입니다.
송장 생성 시:
- USD 라인 소계 계산: 3 x 19.99 = 59.97 USD.
- 할인 적용: 59.97 x 10% = 5.997 → 6.00 USD로 반올림.
- 라인 순액: 59.97 - 6.00 = 53.97 USD.
- 세금: 53.97 x 8.25% = 4.452525 → 4.45 USD로 반올림.
- 합계: 53.97 + 4.45 = 58.42 USD.
반올림은 정의된 지점(할인, 각 세금 금액, 라인 합계)에서만 일어납니다. 그 반올림된 결과들을 저장하고 항상 저장된 값들을 더하세요. 이렇게 하면 PDF에는 58.42가 보이지만 내보내기가 58.43으로 재계산되는 고전적 문제를 막을 수 있습니다.
나중에 송장을 재현할 수 있게 저장하는 항목들
송장(및 송장 라인)에 통화 코드(USD), 소단위 금액(센트), 세금 유형별 분해, USD→GBP로 변환할 때 사용된 환율 레코드 ID들을 저장하세요. 공급자 원가에 대해서도 EUR 비용과 GBP로 변환했다면 그 환율 레코드를 저장하세요.
고객은 깔끔한 USD 송장을 보고(가격, 할인, 세금, 합계) 재무는 고정된 GBP 환산값과 정확한 환율 타임스탬프를 포함한 수출물을 받습니다. 환율이 내일 바뀌어도 월말 수치가 일치합니다.
다음 단계: 구현, 테스트, 유지보수 가능하게 하기
최소 스키마를 짧은 계약서처럼 문서화하세요: 어떤 금액을 저장하는지(원본, 변환, 세금), 각 금액의 통화, 어떤 반올림 규칙을 적용하는지, 송장에 환율을 고정하는 타임스탬프는 무엇인지. 지루하고 구체적으로 만드세요.
UI 화면을 만들기 전에 테스트부터 만드세요. 정상 송장만 테스트하지 말고 반올림 노이즈를 드러내는 작은 엣지 케이스와 집계 문제를 드러내는 큰 케이스를 추가하세요.
초기 테스트 케이스:
- 매우 작은 단가(예: 0.01)에 높은 수량을 곱한 경우
- 변환 후 반복 소수가 생기는 할인
- 주문일과 송장일 사이 환율 변경
- 동일 송장에서 세금 포함 vs 세금 별도 혼용 규칙
- 원 송장과 동일하게 맞아야 하는 환불 및 크레딧 노트
지원 티켓을 줄이려면 송장의 모든 숫자를 설명하는 감사 뷰를 추가하세요: 저장된 금액들, 통화 코드, 환율 ID와 타임스탬프, 사용된 반올림 방식. 누군가 "왜 합계가 다른가?"라고 물으면 저장된 사실로 바로 답할 수 있어야 합니다.
내부 청구 도구를 만든다면 AppMaster (appmaster.io) 같은 노코드 플랫폼이 스키마를 한곳에 두고 계산 로직을 재사용 가능한 워크플로로 만들어 웹과 모바일 화면이 각자 다른 방식으로 수학을 하지 않게 도와줄 수 있습니다.
마지막으로 소유권을 정하세요. 누가 환율을 업데이트하고, 누가 세금 규칙을 업데이트하며, 발행된 송장에 영향을 주는 변경을 누가 승인할지 결정하세요. 안정성은 스키마뿐 아니라 프로세스입니다.


