ALLOWED_UPDATE_FIELDS set으로 PUT body 허용 키 명시. DDG/isNameSimilar/DTO는 후속. 설계서: docs/design/332-restaurant-update-whitelist/README.md (Approved) Refs: #332 (Architect)
95 lines
4.4 KiB
Markdown
95 lines
4.4 KiB
Markdown
# 설계서: 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 도입 표준화 — 별도 후속.
|