문제의 출발점
클라이언트 검증은 잘못된 입력을 빠르게 알려 주는 데 유용하다. 하지만 주문을 실제로 받아들일지 결정하는 최종 경계로는 충분하지 않다.
이번 사례에서는 브라우저 조건에 의존하던 고객 관할과 주문 가능 여부를 서버에서 다시 검증했다. 고객 주문 시스템과 업무 처리 시스템 양쪽에 오류 응답과 주문 차단 흐름을 구현하고, 본사 적용 후 같은 정책을 두 해외법인으로 확산했다.
실제 공격이나 데이터 유출이 발생한 사례는 아니다. 정상 화면을 거치지 않은 요청도 서버에 도달할 수 있다는 신뢰 경계의 문제를 운영 장애가 되기 전에 보완한 작업이다.
UI 검증과 서버 검증의 목적은 다르다
화면 검증은 사용자가 값을 입력하거나 대상을 선택하는 즉시 안내할 수 있다.
- 선택할 수 없는 옵션을 비활성화
- 주문 불가 사유를 화면에서 즉시 표시
- 필수값 누락을 전송 전에 안내
- 불필요한 서버 왕복 감소
이 기능은 사용자 경험에 중요하므로 제거할 이유가 없다. 문제는 화면의 판단을 서버가 그대로 신뢰할 때 생긴다.
브라우저에서 실행되는 조건은 변경되거나 생략될 수 있고, 오래 열린 화면은 현재 서버 상태와 달라질 수 있다. 직접 구성한 요청, 이전 화면에서 재전송된 요청, 다른 버전의 클라이언트도 같은 서버 경로에 들어올 수 있다.
따라서 역할을 다음처럼 분리했다.
클라이언트는 빠르게 안내하고, 서버는 현재 데이터와 업무 규칙을 기준으로 최종 승인한다.
확인해야 했던 업무 규칙
이번 서버 재검증은 단순한 필수값 확인이 아니었다.
- 요청 고객이 해당 업무의 관할 범위에 속하는가
- 현재 시점에 해당 고객의 주문을 받을 수 있는가
- 고객 주문 시스템의 판단과 업무 처리 시스템의 처리 조건이 일치하는가
- 조건이 맞지 않을 때 저장·후속 처리 전에 중단되는가
형식 검증은 “값이 있는가, 타입이 맞는가”를 확인한다. 반면 주문 가능 여부는 서버의 현재 데이터와 업무 정책을 조회해야 하는 규칙이다. 둘을 같은 의미의 Validation으로 취급하면 실패 원인과 책임 경계가 흐려진다.
서버의 어느 지점에서 판단할 것인가
구체적인 클래스 구조는 공개하지 않지만, 책임은 세 단계로 나눠 생각할 수 있다.
1. 요청 형식 확인
- 필수값과 기본 형식
2. 서버 업무 규칙 확인
- 고객 관할
- 현재 주문 가능 여부
3. 상태 변경
- 주문 저장
- 후속 업무 처리아래는 실제 소스가 아닌 개념용 pseudocode다.
Request request = parse(input);
ValidationResult result =
checkCurrentEligibility(request.customer(), request.orderType());
if (!result.isAllowed()) {
return businessError(result.reason());
}
processOrder(request);업무 규칙 검증은 상태 변경보다 앞에 있어야 한다. 화면에서 이미 확인했다는 이유로 생략하지 않고, 서버가 현재 값을 기준으로 같은 질문에 다시 답하도록 했다.
두 시스템에서 같은 결론을 내리게 했다
주문 요청은 고객이 사용하는 시스템에서 시작해 내부 업무 처리 시스템으로 이어졌다. 한쪽만 검증하면 다음과 같은 불일치가 남을 수 있다.
- 앞단은 허용했지만 뒷단이 처리할 수 없는 요청
- 앞단에서 차단했지만 다른 경로로 뒷단에 도달한 요청
- 실패는 했지만 호출자가 이유를 구분할 수 없는 응답
그래서 양쪽 서버 경로에서 고객 관할과 주문 가능 여부를 재검증하고, 조건이 맞지 않으면 오류 응답과 주문 차단으로 연결했다. 이 글은 전체 주문 시스템을 재설계했다는 주장이 아니라, 확인된 주문 가능 여부의 최종 판단 경계를 보완한 범위만 다룬다.
본사 구현을 해외법인에 그대로 복사하지 않은 이유
본사에서 검증 흐름을 적용한 뒤 두 해외법인에도 동일한 정책을 확산했다. 여기서 “동일 정책”과 “동일 코드”는 같은 말이 아니다.
공통으로 유지할 것은 최종 판단 원칙이었다.
- 클라이언트 결과만 신뢰하지 않음
- 서버에서 고객 관할과 주문 가능 여부 재확인
- 조건 불일치 시 상태 변경 전에 차단
- 호출자가 구분할 수 있는 오류 흐름 유지
반면 법인별 데이터, 설정과 기존 주문 경로는 다를 수 있다. 그래서 해외법인 적용만 확인한 것이 아니라 본사 기존 주문 기능의 회귀 범위도 함께 확인했다. 공통 정책을 확산하면서 원래 동작하던 경로를 깨뜨리지 않는지가 별도의 완료 조건이었다.
검증 수를 실행 기록과 혼동하지 않았다
작업은 본사 적용과 해외법인 확산의 두 범위로 나뉘었다.
| 작업 범위 | 고유 시나리오 | 반복 단계 | 총 성공 기록 |
|---|---|---|---|
| 본사 서버 재검증 | 10개 | 내부·사용자 | 20행 Y |
| 두 해외법인 확산·본사 회귀 | 10개 | 내부·사용자 | 20행 Y |
각 작업에는 10개의 고유 시나리오가 있었고, 같은 시나리오를 내부와 사용자 단계에서 반복했다. 따라서 작업마다 “20개의 서로 다른 시나리오”가 있었던 것은 아니다.
두 범위 모두 최종 완료 상태가 확인됐다. 다만 공개 글에서는 내부 테스트 식별자와 상세 업무 데이터를 노출하지 않고, 검증 단위와 반복 구조만 남긴다.
클라이언트 검증을 없애지 않았다
서버 검증을 추가했다고 화면 검증이 불필요해지는 것은 아니다.
- 화면은 사용자가 실수를 즉시 고칠 수 있게 한다.
- 서버는 요청 출처와 관계없이 데이터 무결성을 지킨다.
- 둘이 같은 규칙을 서로 다른 목적으로 실행한다.
중복처럼 보여도 신뢰 수준이 다르다. 클라이언트는 편의 경계이고, 서버는 권한 있는 최종 판단 경계다.
남는 한계
서버 검증도 조회 시점과 저장 시점 사이에 상태가 바뀌는 경쟁 조건까지 자동으로 해결하지는 않는다. 그런 조건이 존재한다면 트랜잭션 경계, 조건부 갱신 또는 별도의 동시성 제어가 필요할 수 있다.
이번 작업에서 전체 주문 시스템의 동시성 제어나 모든 클라이언트 검증을 재설계한 것으로 확인되지는 않는다. 특정 주문 가능 여부와 고객 관할 규칙을 서버에서 최종 재검증하고 세 개 운영 환경에 적용한 범위다.
비슷한 원칙이 바코드 주문의 중복·유형 검증에 어떻게 적용됐는지는 바코드 발행 주체가 바뀌었을 때: SAP 연동 주문등록의 정합성을 지킨 방법에서 별도 사례로 다룬다.
정리
화면에서 버튼을 숨기거나 조건을 검사하는 것은 좋은 사용자 경험이지만, 업무 상태를 변경할 권한까지 화면에 넘기는 것은 아니다.
이번 개선의 핵심은 클라이언트 검증을 제거하는 데 있지 않았다. 클라이언트의 빠른 안내를 유지하면서도 서버가 현재 데이터와 업무 규칙으로 주문 가능 여부를 다시 판단하게 해, 최종 신뢰 경계를 명확히 한 것이었다.