상품 네 건의 이미지를 API로 고치는 일이었고 세 필드만 보내는 요청을 이미 신버전으로 짜 둔 상태였다. 그런데 대상 계정이 신버전을 못 쓴다는 것을 알게 됐고 스펙을 보니 경로만 다르길래 구버전으로 같은 요청을 보내면 되겠다고 생각했다.
Table of contents
Open Table of contents
v1.2가 막힌 이유
403이 나와서 처음에는 인증 문제로 봤는데 접근 제어 코드를 보니 판정 기준이 상품이 아니라 입점사였다.
if (!$manager->is_v2_by_role && !in_array($provider->id, self::WHITELIST)) {
abort(403);
}
이 계정은 구버전 입점사이고 화이트리스트에도 없으므로 상품이 무엇이든 신버전은 403이다. 판정 단위가 계정이면 대상을 바꿔도 결과가 안 바뀐다. 구버전이 유일한 경로였다.
두 매핑 함수의 차이
구버전으로 그냥 보내기 전에 요청 매핑 함수를 열어 봤다. 이름이 비슷한 두 함수가 있었고 신버전 쪽에만 이런 블록이 있었다.
// 미전송 필드는 기존 DB 값 사용
$defaults = [
'name' => $product->name,
'price' => $product->price,
'discount_price' => $product->discount_price,
// ...
];
$data = array_merge($defaults, $request->only(array_keys($defaults)));
구버전에는 이 블록이 없어서 모든 필드를 요청에서 무조건 채운다. 그러면 세 필드만 보냈을 때 이름이 비고 가격이 0이 되고 배송정책이 풀린다. 판매 중인 상품의 이름과 가격이 사라지는 것이다.
검증이 안 막는 이유
필수 필드가 비면 검증에서 걸리지 않는지 한 번 더 확인했다.
'name' => 'sometimes|required|string',
'price' => 'sometimes|required|numeric',
조건부 규칙이 붙어 있어서 키가 없으면 검증 자체를 건너뛴다. 부분 업데이트를 지원하려고 완화한 규칙인데 부분 업데이트가 구현되지 않은 쪽에서는 경고를 없애는 역할만 한다. 요청은 200으로 성공하고 데이터는 지워진다.
스펙과 실제의 거리
나중에 두 버전 스펙 파일을 통째로 비교했다. 신버전은 구버전을 복제한 뒤 경로와 제목과 설명만 바꾼 파일이었고 실제 동작 차이는 하나도 반영돼 있지 않았다.
코드에서 역추적하니 다섯 가지가 달랐다. 구 입점사가 호출하면 403이라는 것과 응답에 필드 두 개가 추가된다는 것과 마이그레이션된 상품의 번호가 원래 번호로 내려온다는 것과 배송정책 조회가 채널 정책을 변환해 응답한다는 것과 계약이 끝난 정산 API가 신버전에 없다는 것이다.
게다가 두 스펙 모두 응답 스키마가 정의돼 있지 않고 상태 코드만 있었으며, 스펙에서 주석 처리된 엔드포인트 두 개가 실제 라우트에는 등록돼 호출되고 있었다. 스펙이 다른 버전의 복제본이면 그 스펙은 근거가 못 된다.
실제 조치
구버전을 쓰되 DB에서 현재 값을 읽어 전체 요청을 재구성했다. 세 필드만 바꾸고 나머지는 읽은 값을 그대로 실어 보낸다.
번거롭지만 이것이 맞다. 구버전은 전체 교체 시맨틱이고 전체 교체 API에는 전체를 보내야 한다.
전제의 이동
이번 건의 위험은 코드가 아니라 전제에 있었다. 신버전에서 세 필드만 보내도 됐으니 구버전도 되겠다는 생각이다. 버전 번호가 비슷하고 스펙상 경로만 다르면 자연스럽게 그렇게 넘어간다.
부분 업데이트 지원은 프로토콜이 아니라 구현이 정한다. 같은 메서드라도 서버가 병합하느냐 교체하느냐는 코드에 달려 있다. 그래서 엔드포인트를 갈아탈 때 요청 매핑 코드에 기존 값 병합 블록이 있는지와 검증 규칙이 필수를 조건부로 완화하고 있는지와 접근 제어의 판정 단위가 무엇인지와 스펙이 다른 버전의 복제본인지를 본다. 첫 항목이 이번 건의 전부였고 병합 블록 하나가 두 버전의 시맨틱을 가르고 있었다.
정리
- 부분 업데이트 지원은 HTTP 메서드가 아니라 구현이 정한다
- 요청 매핑 코드에 기존 값 병합 블록이 있는지로 판별한다
- 조건부 검증은 부분 업데이트가 없는 쪽에서 경고만 없앤다
- 접근 제어의 판정 단위가 계정이면 대상을 바꿔도 403이다
- 스펙이 다른 버전의 복제본이면 스펙은 근거가 아니다
- 코드나 실제 호출로 확인한다
- 엔드포인트를 갈아탈 때 전제를 그대로 옮기지 않는다