정산 도메인에서 가장 조심해야 하는 문장은 이것입니다.

지금 계산한 금액이 나중에도 같은 근거로 설명될 수 있는가?

단순히 오늘의 비용을 계산하는 것만으로는 부족합니다. 비용이 확정된 뒤 거래처 단가가 바뀌거나 계약 조건이 바뀌어도, 과거 확정 비용은 당시 기준으로 설명 가능해야 합니다.

문제

초기에는 비용을 직접 입력하는 흐름만으로 충분할 수 있습니다. 하지만 운영이 복잡해지면 비용은 여러 source에서 만들어집니다.

특히 거래처 비용이 단가 x 수량 구조로 바뀌면 문제가 생깁니다. 예를 들어 오늘은 단가가 10,000원이었고 수량이 3개라서 30,000원을 확정했습니다. 그런데 다음 달에 단가가 12,000원으로 바뀌면, 과거 확정 비용을 다시 조회할 때 어떤 단가를 보여줘야 할까요?

정답은 “확정 당시 단가”입니다.

단가표와 확정 snapshot 분리

단가표는 현재 계약 조건을 표현합니다. 반면 비용확정 snapshot은 과거 어느 시점의 결정 기록입니다. 두 개는 목적이 다릅니다.

VendorContractUnitPrice
  - 현재 또는 특정 기간의 단가 정의
  - 변경 가능
  - 이력 관리 필요

DailyCostConfirmationUnitPriceItem
  - 비용확정 시점의 단가와 수량
  - 확정 이후 불변에 가까움
  - 감사/설명 가능성에 필요

그래서 비용확정 시점에 단가 버전, 단가 값, 수량, 금액을 snapshot으로 저장했습니다.

confirmedAmount = confirmedUnitPrice * confirmedQuantity

이렇게 하면 나중에 단가표가 변경되어도 과거 비용확정 row는 흔들리지 않습니다.

preview와 confirm 분리

비용확정에는 preview와 confirm이 필요합니다.

preview는 “현재 기준으로 계산하면 얼마인가”를 보여주는 단계입니다. 이 단계에서는 아직 확정된 것이 아니므로 현재 단가표, 근무 정보, 계약 정보를 읽어 계산합니다.

confirm은 “이 금액을 이 날짜의 비용으로 확정한다”는 단계입니다. 이때부터는 계산 근거가 저장되어야 합니다.

preview
  -> current source data read
  -> calculated result
  -> not immutable

confirm
  -> calculated result accepted
  -> source snapshot saved
  -> audit target

이 둘을 섞으면 운영자가 화면에서 본 금액과 나중에 저장된 금액의 근거가 달라질 수 있습니다.

정산 상태와 연결

비용확정은 독립된 기능이 아니라 정산 프로세스의 한 단계입니다.

매출확정
  -> 근무확정
  -> 비용확정
  -> 이체목록확정
  -> 이체

각 단계가 끝나면 upstream 데이터를 마음대로 바꾸면 안 됩니다. 예를 들어 비용확정 이후 근무 데이터를 바꾸면 인건비와 비용 합계가 달라질 수 있습니다. 그래서 정산 상태 테이블로 단계별 변경 가능 구간을 분리했습니다.

상태 모델이 중요한 이유는 “이 API를 호출할 수 있는가”뿐 아니라 “이 데이터는 지금 바뀌어도 되는가”를 결정하기 때문입니다.

조회 성능

비용확정 목록은 계약, 계정과목, 단가표, 확정 item 등 여러 데이터를 함께 보여줘야 합니다. 여기서 조심하지 않으면 N+1 문제가 쉽게 생깁니다.

개선 방향은 단순합니다.

정산 도메인은 데이터 관계가 깊기 때문에, JPA 연관관계만 믿고 자연스럽게 순회하면 조회 성능이 나빠지기 쉽습니다.

운영 효과

이 설계의 효과는 비용의 감사 가능성입니다.

초기 사용량이 낮아도 이런 설계는 중요합니다. 사용량이 늘어난 뒤 비용 산출 근거가 흔들리면, 그때는 단순 버그가 아니라 정산 신뢰의 문제가 됩니다.

더 개선한다면

비용확정 snapshot은 ledger로 발전시킬 수 있습니다.

정산 시스템은 결국 “왜 이 금액인가”에 답해야 합니다. 그 답을 데이터 구조가 보장하게 만드는 것이 좋은 백엔드 설계라고 생각합니다.