Files
joungmin 80b553ec19 docs: 현행화 17개 설계서 Approved + 후속 이슈 백로그 등록
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 (백로그)
2026-06-15 11:08:18 +09:00

10 KiB

설계서: 백엔드 - 채널 관리 (#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 우선 매칭, 실패 시 DB id로 재시도 (양쪽 모두 실패 → 404)
  • 채널 관련 쓰기 작업 후 cache.flush() 호출되어 다음 GET에서 최신 데이터 반환

4. 컨텍스트 & 제약

  • DB: Oracle 23ai. 테이블 channels. 유니크 제약 UQ_CHANNELS_CID on channel_id.
  • 외부 의존:
    • YouTubeService.scanChannel(channelId, full) (#270 추출 파이프라인)
    • CacheService (Redis 캐시, makeKey/getRaw/set/flush)
  • 권한: 조회(GET)는 공개, 그 외 모두 AuthUtil.requireAdmin()로 관리자만.
  • 캐시: 목록 응답은 Redis에 JSON 직렬화 저장. 쓰기 시 flush.
  • Soft delete: 비활성화는 active = 0 UPDATE (물리 삭제 아님).
  • 가정: channel_id는 YouTube의 외부 ID (UCxxxx...). DB id는 32자 UUID.

5. 아키텍처 개요

  • 모듈/파일 구조:
    • controller/ChannelController.java (5개 엔드포인트)
    • service/ChannelService.java (CRUD 비즈니스)
    • service/YouTubeService.java (스캔 위임, 외부)
    • service/CacheService.java (Redis 캐시, 외부)
    • mapper/ChannelMapper.java + XML
    • domain/Channel.java
    • security/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: String
    • titleFilter: String (정규식/포함 문자열, 영상 제목 필터)
    • description: String
    • tags: String (콤마 구분)
    • sortOrder: Integer
    • videoCount: 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 형식(UC prefix) 검증 미적용. 길이 제한은 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. 흐름 / 알고리즘

  1. 목록 조회 (캐시):
    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
    
  2. 채널 등록: 관리자 검증 → IdGenerator.newId() → INSERT → cache.flush(){id, channel_id} 응답. SQL 예외의 message에 UQ_CHANNELS_CID 포함되면 409로 매핑.
  3. 비활성화 폴백:
    • mapper.deactivateByChannelId(channelId) 시도 →
    • 0행이면 mapper.deactivateById(channelId) 시도 →
    • 둘 다 0행 → false → 404.
    • 이유: 운영자가 YouTube ID 또는 DB UUID 중 어느 것으로도 비활성화 가능.
  4. 스캔 트리거: 관리자 검증 → YouTubeService.scanChannel(channelId, full) 호출 → null 응답 시 404 → 성공 시 cache.flush() (영상 추가로 채널 메타 변동 가능성) → 결과 반환.
  5. 메타 갱신: 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 / 둘 다 실패 → false
    • ChannelController.list: 캐시 hit 시 ObjectMapper 호출, miss 시 mapper 호출
    • ChannelController.create: 유니크 메시지 포함 예외 → 409
    • ChannelController.scan: YouTubeService null → 404
  • 통합
    • 채널 등록 후 GET 목록에 반영 (캐시 flush 검증)
    • 중복 channel_id 등록 시 409
    • 비관리자 인증으로 POST 시 403
    • update 후 sort_order 반영 + 캐시 무효화
  • 모킹: YouTubeService, CacheService 모킹. Redis는 embedded-redis 또는 testcontainers.
  • 현재 테스트 디렉토리 없음 → TBD.

11. 리스크 & 대안 검토

  • 유니크 충돌 감지를 메시지 문자열로 판정: 깨지기 쉬움.
    • 대안 A: Spring의 DuplicateKeyException catch → 깔끔.
    • 대안 B: 사전 SELECT 후 INSERT → 경합 시 여전히 위험.
    • 트레이드오프: 현 방식은 빠르지만 fragile. ADR 후보.
  • 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 정책 의존).