Reviewer 결과 17 PASS / 1 REJECT (#267 admin 권한 critical). - 17개 설계서를 Draft → Approved로 갱신 - #267(backend-user)은 critical 결함으로 06-Reviewer 유지 - 후속 17개 개선 이슈(#289~#305) 자동 등록 — 결함 124건 백로그 반영 (critical 3 / major 46 / minor 75) - docs/README.md에 18개 설계서 인덱스 추가 - CHANGELOG.md 2026-06-15 섹션 추가 Refs: #266 #268-#283 (현행화 완료) #267 (대기) #289-#305 (백로그)
설계서: 백엔드 - 채널 관리 (#273)
상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #273 · 관련 ADR: 없음 · 구현 파일:
backend-java/src/main/java/com/tasteby/service/ChannelService.java,backend-java/src/main/java/com/tasteby/controller/ChannelController.java· 테스트: TBD (현재 없음)
1. 목적 (Why)
Tasteby가 식당 정보를 수집하는 YouTube 채널을 관리(등록/수정/비활성화/스캔)하여, 추출 파이프라인의 데이터 원천을 통제 가능하게 한다. 채널은 사용자 프론트엔드의 "채널 필터" UI 데이터 소스이기도 하다.
2. 범위 (Scope)
- 포함:
- 활성 채널 목록 조회 (공개 API, 캐시 적용)
- 채널 등록 (관리자 전용)
- 채널 메타데이터 수정 (description/tags/sort_order, 관리자 전용)
- 채널 비활성화 (soft delete, 관리자 전용)
- 채널 영상 스캔 트리거 (관리자 전용,
YouTubeService.scanChannel위임)
- 제외 (out of scope):
- 채널 영상 자체의 추출/요약 로직 (#270 추출 파이프라인)
- 채널 통계/대시보드 (#274 통계)
- YouTube API 인증/쿼터 관리 세부사항 (YouTubeService 책임)
- 채널 카테고리 트리/계층화
3. 인수조건 (Acceptance Criteria)
GET /api/channels호출 시 활성 채널 목록 반환, 캐시 hit/miss 모두 동일 결과- 관리자 외 사용자가
POST /api/channels호출 시 권한 거부 (AuthUtil.requireAdmin()throw) - 동일
channel_id로 중복 등록 시 HTTP 409 + "Channel already exists" (유니크 제약UQ_CHANNELS_CID) DELETE /api/channels/{channelId}시channel_id우선 매칭, 실패 시 DBid로 재시도 (양쪽 모두 실패 → 404)- 채널 관련 쓰기 작업 후
cache.flush()호출되어 다음 GET에서 최신 데이터 반환
4. 컨텍스트 & 제약
- DB: Oracle 23ai. 테이블
channels. 유니크 제약UQ_CHANNELS_CIDonchannel_id. - 외부 의존:
YouTubeService.scanChannel(channelId, full)(#270 추출 파이프라인)CacheService(Redis 캐시,makeKey/getRaw/set/flush)
- 권한: 조회(GET)는 공개, 그 외 모두
AuthUtil.requireAdmin()로 관리자만. - 캐시: 목록 응답은 Redis에 JSON 직렬화 저장. 쓰기 시 flush.
- Soft delete: 비활성화는
active = 0UPDATE (물리 삭제 아님). - 가정:
channel_id는 YouTube의 외부 ID (UCxxxx...). DBid는 32자 UUID.
5. 아키텍처 개요
- 모듈/파일 구조:
controller/ChannelController.java(5개 엔드포인트)service/ChannelService.java(CRUD 비즈니스)service/YouTubeService.java(스캔 위임, 외부)service/CacheService.java(Redis 캐시, 외부)mapper/ChannelMapper.java+ XMLdomain/Channel.javasecurity/AuthUtil.java
- I/O ↔ 순수 로직 경계: Controller는 캐시 hit/miss + 권한, Service는 식별자 매칭 폴백 로직, Mapper는 SQL.
[Client]
│ HTTP
▼
[ChannelController] ← AuthUtil.requireAdmin() (쓰기)
│ cache hit? ─┐
│ ▼
│ [CacheService(Redis)] ← GET/SET/FLUSH
│ miss
▼
[ChannelService] ← deactivate: channel_id → id 폴백
│
▼
[ChannelMapper] (MyBatis XML)
│
▼
[Oracle 23ai: channels]
[ChannelController.scan] → [YouTubeService.scanChannel] → (영상 수집 파이프라인)
6. 데이터 모델
- Channel (
domain/Channel.java):id: String(32자 UUID, PK)channelId: String(YouTube 외부 ID, 유니크)channelName: StringtitleFilter: String(정규식/포함 문자열, 영상 제목 필터)description: Stringtags: String(콤마 구분)sortOrder: IntegervideoCount: int(조인 집계)lastVideoAt: String(조인 집계)
- POST 요청 본문:
{ channel_id, channel_name, title_filter } - PUT 요청 본문:
{ description, tags, sort_order } - POST 응답:
{ id, channel_id } - scan 응답:
YouTubeService.scanChannel반환 Map (영상 수, 신규 추출 수 등) - 경계 검증: 현재 명시적 검증 없음.
channel_id형식(UCprefix) 검증 미적용. 길이 제한은 DB 컬럼 의존.
7. 함수 명세 (Function Specs)
| 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? |
|---|---|---|---|---|---|---|
ChannelService.findAllActive |
활성 채널 목록 조회 | List<Channel>() |
없음 | List | DB 오류 → 전파 | 단순 |
ChannelService.create |
채널 신규 등록 | String(channelId, channelName, titleFilter) |
YouTube ID/이름/필터 | 생성 PK | 유니크 충돌 → SQLException | 단순 |
ChannelService.deactivate |
soft delete (폴백) | boolean(channelId) |
channel_id 또는 DB id | 성공 여부 | 둘 다 0행 → false | 복잡 |
ChannelService.findByChannelId |
단건 조회 | Channel(channelId) |
channel_id | Channel or null | 없음 | 단순 |
ChannelService.update |
메타데이터 부분 갱신 | void(id, description?, tags?, sortOrder?) |
DB id + 필드 | 없음 | DB 오류 → 전파 | 단순 |
ChannelController.list |
캐시 우선 목록 응답 | List<Channel>() |
없음 | List | 캐시 파싱 실패 → 무시, DB 조회 | 단순 |
ChannelController.create |
등록 + 캐시 flush | Map(body) |
body | {id, channel_id} |
유니크 → 409, 그 외 → 전파 | 복잡 |
ChannelController.scan |
채널 스캔 트리거 | Map(channelId, full) |
channelId, full | 스캔 결과 Map | 미존재 → 404 | 복잡 |
ChannelController.update |
메타 갱신 + flush | Map(id, body) |
id, body | {ok:true} |
권한 → 403 | 단순 |
ChannelController.delete |
비활성화 + flush | Map(channelId) |
channelId | {ok:true} |
미존재 → 404 | 단순 |
deactivate(이중 매칭 폴백),create(충돌 → 메시지 파싱),scan(외부 위임)은 복잡. fn 설계서 후보.
8. 흐름 / 알고리즘
- 목록 조회 (캐시):
key = cache.makeKey("channels") if cache.getRaw(key) != null: try return objectMapper.readValue(cached) catch: fall through result = mapper.findAllActive() cache.set(key, result) return result - 채널 등록: 관리자 검증 → IdGenerator.newId() → INSERT →
cache.flush()→{id, channel_id}응답. SQL 예외의 message에UQ_CHANNELS_CID포함되면 409로 매핑. - 비활성화 폴백:
mapper.deactivateByChannelId(channelId)시도 →- 0행이면
mapper.deactivateById(channelId)시도 → - 둘 다 0행 → false → 404.
- 이유: 운영자가 YouTube ID 또는 DB UUID 중 어느 것으로도 비활성화 가능.
- 스캔 트리거: 관리자 검증 →
YouTubeService.scanChannel(channelId, full)호출 → null 응답 시 404 → 성공 시cache.flush()(영상 추가로 채널 메타 변동 가능성) → 결과 반환. - 메타 갱신: PUT body의 sort_order는 Number → int 변환.
tags,description,sort_order부분 갱신.
9. 엣지케이스 & 에러 처리
- 캐시 직렬화 깨짐:
objectMapper.readValue실패 시 catch 무시 → DB 폴백. 안전한 기본값. - 유니크 충돌 감지: 예외 메시지에
UQ_CHANNELS_CID문자열 의존. DB 제약명이 바뀌면 감지 실패 → 500. → 향후 SQLState 또는 DataIntegrityViolationException 기반 매핑 권장. - deactivate 이중 시도: 동일 channel_id가 DB id와 우연히 충돌하면 의도치 않은 행 비활성화 가능 (UUID 충돌 확률 매우 낮음).
- scan 미존재 채널:
YouTubeService가 null 반환 → 404. YouTube API 자체 장애는 상위로 전파 (현재 별도 매핑 없음). - 권한 누락:
AuthUtil.requireAdmin()예외 → 403. - 빈 본문 / 필수값 누락:
body.get("channel_id")가 null → INSERT 시 NOT NULL 제약 위반 → 500. → 명시적 400 매핑 권장. - 캐시 flush 실패: Redis 다운 시 예외 전파. 운영 안전성 위해 try-catch + WARN 로깅 검토.
10. 테스트 계획
- 단위
ChannelService.deactivate: by-channelId 성공 → true / by-channelId 실패 + by-id 성공 → true / 둘 다 실패 → falseChannelController.list: 캐시 hit 시 ObjectMapper 호출, miss 시 mapper 호출ChannelController.create: 유니크 메시지 포함 예외 → 409ChannelController.scan: YouTubeService null → 404
- 통합
- 채널 등록 후 GET 목록에 반영 (캐시 flush 검증)
- 중복 channel_id 등록 시 409
- 비관리자 인증으로 POST 시 403
- update 후 sort_order 반영 + 캐시 무효화
- 모킹:
YouTubeService,CacheService모킹. Redis는 embedded-redis 또는 testcontainers. - 현재 테스트 디렉토리 없음 → TBD.
11. 리스크 & 대안 검토
- 유니크 충돌 감지를 메시지 문자열로 판정: 깨지기 쉬움.
- 대안 A: Spring의
DuplicateKeyExceptioncatch → 깔끔. - 대안 B: 사전 SELECT 후 INSERT → 경합 시 여전히 위험.
- 트레이드오프: 현 방식은 빠르지만 fragile. ADR 후보.
- 대안 A: Spring의
- deactivate 폴백 패턴: 유연성 vs 명확성.
- 대안: 별도 엔드포인트 (
/by-id,/by-yt-id)로 분리. 운영 UI 합의 필요.
- 대안: 별도 엔드포인트 (
- 캐시 정책: 전체 flush vs 키 단위 invalidate.
- 현재 flush는 다른 모듈(예: 식당 목록)까지 영향. 채널 키만 무효화하도록 개선 가능.
- 스캔의 동기 호출: 대량 영상 채널은 응답 지연 가능.
- 대안: 비동기 큐 + 작업 상태 폴링 (#275 데몬과 통합).
12. 미해결 질문 (Open Questions)
- 채널 활성화 복구(reactivate) API가 필요한지? 현재는 DB 직접 수정만 가능.
title_filter는 정규식인지 단순 contains인지 명세 부재 — 코드 확인 필요.- 채널 단위 권한 (소유자 개념)을 도입할지? 현재는 글로벌 관리자만.
- 스캔 작업의 진행률/실패 재시도 정책 — 데몬(#275)과 통합 범위.
- 캐시 TTL 설정값 (현재 코드에 명시 없음, CacheService 정책 의존).