실무 API를 단단하게 만드는 입력 검증 설계법

실무 API를 단단하게 만드는 입력 검증 설계법 관련 이미지
Photo by StockSnap via Pixabay

웹 서비스의 장애는 거대한 알고리즘보다 예상하지 못한 입력 하나에서 시작되는 경우가 많습니다. 숫자가 와야 할 자리에 문자열이 들어오거나, 필수 값이 비어 있거나, 날짜의 시작과 끝이 뒤바뀌면 애플리케이션은 모호한 오류를 내거나 잘못된 데이터를 저장합니다. 그래서 API 입력 검증은 단순히 오류를 막는 방어 코드가 아니라 서비스가 받아들일 수 있는 데이터의 계약을 명확히 표현하는 설계 작업입니다. 좋은 검증 규칙은 클라이언트 개발자의 시행착오를 줄이고, 운영자가 장애 원인을 빠르게 찾게 하며, 데이터베이스의 품질까지 지켜 줍니다.

문법 검증과 업무 규칙을 분리하기

첫 단계는 값의 형태를 확인하는 것입니다. 이메일 형식인지, 정수 범위 안에 있는지, 문자열 길이가 제한을 넘지 않는지 같은 규칙이 여기에 해당합니다. 그다음에는 업무 의미를 확인해야 합니다. 쿠폰의 만료일이 지났는지, 주문 수량이 현재 재고보다 많지 않은지, 사용자가 해당 프로젝트를 수정할 권한이 있는지를 판단합니다. 두 종류의 검증을 한 함수에 뒤섞으면 재사용이 어려워지고 오류 메시지도 불명확해집니다. 요청 스키마에서는 형식과 범위를 확인하고, 서비스 계층에서는 데이터 조회가 필요한 업무 규칙을 처리하는 편이 이해하기 쉽습니다.

요청 스키마에 담을 기본 조건

  • 필수 필드와 선택 필드를 명확히 구분합니다.
  • 문자열의 최소·최대 길이와 허용 문자를 정합니다.
  • 숫자의 범위와 소수점 정밀도를 지정합니다.
  • 날짜와 시간은 표준 형식과 시간대 기준을 통일합니다.
  • 배열은 원소 형식뿐 아니라 최대 개수도 제한합니다.

클라이언트가 보내지 않은 값과 명시적으로 보낸 빈 값도 구분해야 합니다. 예를 들어 프로필 수정 요청에서 필드가 없다는 것은 기존 값을 유지한다는 뜻일 수 있지만, 빈 문자열은 값을 지우겠다는 뜻일 수 있습니다. 이 차이를 무시하면 부분 수정 API에서 예기치 않은 데이터 손실이 생깁니다.

오류 응답은 사람이 고칠 수 있게 만들기

검증 실패 때 단순히 400 오류만 반환하면 사용자는 무엇을 고쳐야 하는지 알기 어렵습니다. 응답에는 안정적인 오류 코드, 문제가 있는 필드, 이해하기 쉬운 설명을 포함하는 것이 좋습니다. 메시지는 내부 구현이나 데이터베이스 구조를 노출하지 않으면서도 해결 방법을 알려야 합니다. 여러 필드가 동시에 잘못되었다면 가능한 범위에서 한 번에 알려 주면 왕복 요청을 줄일 수 있습니다. 다만 인증과 권한 관련 오류는 공격자가 계정이나 자원의 존재 여부를 추측하지 못하도록 표현을 통일해야 합니다.

실무 원칙: 오류 메시지는 개발자의 디버깅 로그가 아니라 API 사용자가 다음 행동을 결정하는 제품 인터페이스입니다.

정규화는 검증 전에 신중하게 적용하기

앞뒤 공백 제거, 이메일 주소의 일부 소문자화, 전화번호 구분 기호 제거처럼 같은 의미의 값을 일정한 형태로 바꾸는 정규화도 필요합니다. 그러나 모든 문자열을 무조건 변경하면 비밀번호나 국제화된 이름처럼 원문 보존이 중요한 값이 손상될 수 있습니다. 필드별 정책을 정하고, 원본과 정규화된 값 중 무엇을 저장할지 문서화해야 합니다. 서버가 암묵적으로 값을 고치는 것보다 명백히 잘못된 입력은 거절하는 편이 안전한 경우도 많습니다.

보안 한계와 자원 사용량까지 검증하기

입력 검증은 SQL 인젝션 같은 공격을 완전히 해결하는 수단이 아닙니다. 데이터베이스 질의에는 매개변수 바인딩을 사용하고, 출력 위치에 맞는 이스케이프와 권한 검사를 별도로 적용해야 합니다. 동시에 요청 본문 크기, 중첩 객체 깊이, 배열 원소 수, 파일 용량을 제한해야 합니다. 형식상 올바른 요청이라도 지나치게 크거나 복잡하면 메모리와 CPU를 소모해 서비스 거부 문제를 만들 수 있기 때문입니다. 파일 업로드는 확장자만 믿지 말고 실제 콘텐츠 유형과 저장 위치, 악성 코드 검사 정책을 함께 고려합니다.

데이터베이스 제약을 마지막 안전망으로 두기

애플리케이션 검증을 통과했다고 해서 데이터가 항상 안전한 것은 아닙니다. 동시에 들어온 두 요청이 같은 사용자 이름을 생성하거나 재고를 함께 차감할 수 있습니다. 고유 키, 외래 키, 널 허용 여부, 체크 제약 같은 데이터베이스 규칙을 마지막 안전망으로 유지해야 합니다. 데이터베이스 오류는 그대로 노출하지 말고 서비스가 정의한 오류 코드로 변환합니다. 애플리케이션과 데이터베이스의 규칙이 서로 다르면 운영 중 혼란이 생기므로 마이그레이션과 스키마 정의를 한 흐름에서 관리하는 것이 좋습니다.

테스트는 정상 사례보다 경계값에 집중하기

  1. 최솟값과 최댓값 바로 안쪽·바깥쪽을 시험합니다.
  2. 필드 누락, 빈 문자열, null을 각각 확인합니다.
  3. 잘못된 자료형과 지나치게 큰 요청을 보냅니다.
  4. 서로 의존하는 두 필드의 조합을 검사합니다.
  5. 동시 요청에서 고유성과 수량 제약이 유지되는지 확인합니다.
  6. 오류 코드와 필드 경로가 계약대로 반환되는지 검증합니다.

속성 기반 테스트를 사용하면 사람이 떠올리지 못한 문자 조합이나 숫자 범위를 자동으로 탐색할 수 있습니다. 운영 환경에서 발생한 검증 오류도 개인정보를 제거한 뒤 유형별로 집계하면 문서가 부족한지, 클라이언트 버그가 있는지, 공격성 요청이 늘었는지 파악할 수 있습니다. 오류율이 갑자기 증가하면 배포 변경과 함께 살펴보되 원문 입력 전체를 로그에 남기는 일은 피해야 합니다.

버전 변경에도 계약을 지키는 방법

기존에 허용하던 값을 갑자기 거절하면 서버 입장에서는 개선이지만 클라이언트에는 호환성 깨짐입니다. 규칙을 강화하기 전 실제 사용 데이터를 확인하고, 경고 기간이나 새 API 버전을 제공해야 합니다. 반대로 필드를 추가할 때 클라이언트가 모르는 필드를 무시할 수 있는지도 점검합니다. 스키마 문서와 실행 코드가 따로 움직이지 않도록 하나의 정의에서 문서, 검증기, 테스트 자료를 생성하면 차이를 줄일 수 있습니다.

입력 검증의 목표는 모든 오류를 한곳에서 막는 것이 아니라 책임을 여러 층에 정확히 배치하는 것입니다. 요청 스키마는 형태를 설명하고, 서비스 계층은 업무 의미를 판단하며, 권한 계층은 접근 가능성을 확인하고, 데이터베이스는 동시성 속에서도 불변 조건을 지킵니다. 이 구조 위에 일관된 오류 응답과 경계값 테스트를 더하면 API는 기능이 늘어나도 예측 가능한 계약을 유지할 수 있습니다.