전자영수증 기반 경비 처리 시스템을 개발하면서, receiptId와 receiptProcessId 간의 혼동으로 인해
사용자가 승인/반려한 항목과 실제로 처리된 항목이 서로 다른 영수증으로 반영되는 치명적인 문제가 발생했다.
단순한 오타나 로직 실수가 아닌, ID 체계 설계와 데이터 구조 간의 혼동으로 비롯된 문제였다.
문제가 처음 발생한 상황

배포 환경에서 특정 영수증(receiptProcessId = 34)을 승인 처리했더니,
처리 응답에는 receiptId = 35의 데이터가 반영되었다.

하지만 같은 로직을 로컬 테스트 환경에서 실행했을 때는,
.receiptProcessId = 3 → receiptId = 3으로 정상적으로 매핑되었다.
왜 이런 일이 벌어졌을까?
초기 시스템 설계에서는 receiptId와 receiptProcessId가 항상 일치한다느 전제 하에 구현되어 있었다.
실제로 로컬 테스트 DB에서는 다음과 같은 구조로 되어 있었다.
| receiptId | receiptProcessId |
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
하지만 운영 환경에서는 auto_increment 정책 차이 등으로 인해 아래처럼 구조가 달라져 있었다.
| receiptId | receiptProcessId |
| 3 | 2 |
| 4 | 3 |
즉, 사용자가 receiptId = 3을 승인하려고 했을 때,
백엔드에서는 receiptProcessId = 3으로 오해하고 receiptId = 4인 항목을 처리해버린 것이다.
핵심 문제는 '식별자 혼동'
문제를 요약하면 아래와 같다.
- 프론트에서는 receiptId만 알고 있음
- 백엔드는 receiptProcessId를 기준으로 처리하고 있음
- 하지만 receiptId와 receiptProcessId는 실제로 일대일 관계이되, 값은 다를 수 있음
- 테스트 환경은 이 둘이 같게 설정되어 있어서 문제를 가려버림
구조적 리팩토링
이 문제를 해결하기 위해, 처리 대상의 기준을 명확히 receiptId로 통일하기로 했다.
프론트 요청 방식 변경
기존에는 다음과 같이 receiptProcessId를 넘겼다.
{
"receiptProcessId": 3,
"progressState": "accepted",
"rejectedReson": null
}
변경 후에는 receiptId 기준으로 요청
{
"receiptId": 3,
"progressState": "accepted",
"rejectedReson": null
}
백엔드 로직 리팩토링
기존에는 receiptProcessId를 그대로 업데이트에 사용했다.
이제는 receiptId로부터 정확한 receiptProcessId를 조회한 후, 해당 항목을 처리하도록 구조를 바꿨다.
@Override
public ReceiptProcessDto receiptProcessing(Long receiptId, String progressState, String rejectedReason) {
Long receiptProcessId = ceoMapper.getReceiptId(receiptId);
if (receiptProcessId == null) {
throw new IllegalArgumentException("영수 처리 내역이 존재하지 않습니다.");
}
ceoMapper.updateProcessState(receiptProcessId, progressState, rejectedReason);
return ReceiptProcessDto.builder()
.message("영수 처리 완료")
.processStatus(progressState)
.receiptId(receiptId)
.build();
}
여기서 핵심은, 프론트는 receiptId만 알고 있어도 충분하고,
백엔드는 receiptId → receiptProcessId 매핑만 잘 처리하면 된다.
결과
- receiptId를 기준으로 처리 대상이 정확히 일치하게 되었다.
- 프론트와 백엔드 모두 receiptId만 사용함으로써 식별자 혼동이 완전히 사라졌다.
- 테스트 환경에서도 일부러 receiptId ≠ receiptProcessId인 데이터를 넣어 QA 케이스를 보강할 수 있다.
정리
- "ID는 명확한 기준의 하나로 통일하라"
- 기능마다 다른 ID를 사용하는 순간, 유지보수성과 안전성이 무너진다.
- 배포 환경과 동일한 구조의 테스트 테이스를 유지
- 테스트에서는 괜찮았던 구조가 운영에서는 터질 수 있다.
- DTO 설계 시, "이 ID는 누구를 가리키는가?"를 명확히 하기
ID 체계 전체를 재정의하고 시스템의 일관성을 되찾는 과정이었다.
구조적 설계와 데이터 흐름에 대한 감각이 한층 더 깊어졌음을 느꼈다.