Files
tasteby/docs/design/332-restaurant-update-whitelist
joungmin f4cb95e88c docs(design): #332 Restaurant 화이트리스트 설계서 (Architect)
ALLOWED_UPDATE_FIELDS set으로 PUT body 허용 키 명시. DDG/isNameSimilar/DTO는 후속.

설계서: docs/design/332-restaurant-update-whitelist/README.md (Approved)
Refs: #332 (Architect)
2026-06-15 15:30:38 +09:00
..

설계서: Restaurant 임의 필드 업데이트 화이트리스트 (#332)

상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #332 · 부모: #290 (식당 CRUD Reviewer 후속, 09-Done) · 구현 파일: backend-java/src/main/java/com/tasteby/controller/RestaurantController.java, backend-java/src/main/java/com/tasteby/service/RestaurantService.java · 테스트: 수동 (curl로 화이트리스트 외 필드 시도)

1. 목적 (Why)

PUT /api/restaurants/{id} body가 Map<String, Object>로 임의 키를 받는다. SQL 측 updateFields는 컬럼별 <if test="containsKey('...')"> 가드로 화이트리스트가 이미 적용되어 있어 임의 컬럼 갱신은 차단된다. 다만 Controller 레벨에서 화이트리스트가 명시되지 않아 — (a) 의도 모호, (b) 향후 SQL 측 가드가 무력화되거나 다른 매퍼로 확장되면 위험. 명시적 화이트리스트로 의도 강화.

2. 범위 (Scope)

  • 포함
    • RestaurantController.update(id, body)에서 허용된 키만 통과시키는 ALLOWED_UPDATE_FIELDS set.
    • 허용되지 않은 키는 무시(silently drop) + 디버그 로그. 400 응답은 hard policy라 사용자 경험 영향 큼.
  • 제외 (out of scope)
    • DTO 클래스 도입 (RestaurantUpdateDTO + Bean Validation) — 더 강한 표준화지만 코드 영향 큼. 후속.
    • DDG HTML 검색 → 정식 API 전환 (별도 후속, 비용/계약 결정 필요).
    • isNameSimilar 한국어 자모 알고리즘 (별도 후속).
    • UNIQUE(google_place_id) 제약 강화 (DB 마이그레이션 + 데이터 정리 필요, 별도).

3. 인수조건

  • ALLOWED_UPDATE_FIELDS 상수 정의 (SQL updateFields 가드와 일치).
  • PUT 호출 시 body에서 허용 외 키는 자동 제거 + 디버그 로그.
  • 허용 키만 있는 정상 호출 → 200 정상 동작 회귀 없음.
  • 허용 외 키만 있는 호출 → 200 + 변경 없음 (또는 200 + 빈 업데이트).

4. 컨텍스트 & 제약

  • Spring MVC. 변경 최소화. DTO 도입 없이도 가드 가능.
  • 운영 영향 없음 (어드민이 화면에서 호출하는 키는 모두 허용 set 안에).
  • 가정: 화이트리스트 set은 SQL updateFields<if> 키들과 1:1.

5. 아키텍처 개요

PUT /api/restaurants/{id}  body
        │
        ▼
RestaurantController.update
   ├ requireAdmin
   ├ allowed = ALLOWED_UPDATE_FIELDS
   ├ filtered = body where key ∈ allowed
   ├ if (filtered.isEmpty()) → 200 + no-op
   └ restaurantService.update(id, filtered) → mapper.updateFields

6. 데이터 모델

Set<String> ALLOWED_UPDATE_FIELDS = Set.of(name, address, region, cuisine_type, price_range, phone, website, tabling_url, catchtable_url, latitude, longitude, google_place_id, business_status, rating, rating_count) — SQL updateFields의 키들과 일치.

7. 함수 명세

함수 책임 비고
RestaurantController.update(id, body) (수정) 화이트리스트 필터 후 위임 filtered.isEmpty()면 no-op

8. 흐름

  1. body Map 수신.
  2. body.entrySet().stream().filter(e -> ALLOWED_UPDATE_FIELDS.contains(e.getKey())).collect(toMap).
  3. 비어있으면 200 응답하고 끝.
  4. 아니면 restaurantService.update(id, filtered) 호출.

9. 엣지케이스

  • 빈 body: 200 + no-op.
  • 허용 외 키만: 200 + no-op + 디버그 로그.
  • null 값을 포함한 허용 키: SQL updateFields가 그대로 NULL 저장 — 의도된 동작 (좌표/주소 해제 등).

10. 테스트

  • 수동:
    curl -X PUT -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
         -d '{"name":"테스트", "is_admin":true}' /api/restaurants/<id>
    → name만 갱신, is_admin은 무시
    
  • 자동: 별도 후속(통합 테스트 인프라 도입 시).

11. 리스크 & 대안

  • 선택: Controller 화이트리스트 set + silent drop.
  • 대안 A: DTO + Bean Validation — 표준화 깔끔하지만 변경 범위 큼.
  • 대안 B: 허용 외 키 발견 시 400 — 사용자 경험 부담, 클라이언트가 잘못된 키 보내면 즉시 실패. 본 변경은 보수적으로 silent drop.

12. 미해결 질문

  • DDG → 정식 검색 API 전환 — 별도 후속 (비용/계약 결정).
  • isNameSimilar 한국어 알고리즘 — 별도 후속.
  • DTO 도입 표준화 — 별도 후속.