Skip to content

인증 로직 및 질의 입력/스트리밍 구현(+ 헥사고날 아키텍처 전환)#24

Closed
VitoJeong wants to merge 45 commits intoon-seoul-apifrom
API-6-endpoint
Closed

인증 로직 및 질의 입력/스트리밍 구현(+ 헥사고날 아키텍처 전환)#24
VitoJeong wants to merge 45 commits intoon-seoul-apifrom
API-6-endpoint

Conversation

@VitoJeong
Copy link
Copy Markdown
Collaborator

개요

Epic 3(Phase 10) ~ Epic 6(Phase 14)의 벡터 스키마 구성, Spring Boot 앱 기반 구축, OAuth2 + JWT 인증, /query SSE 릴레이를 구현했습니다.
또한 collector 단일 모듈에서 헥사고날 아키텍처(common / domain / application / adapter / app 5개 모듈)로 전환했습니다.

작업 내용 문서는 on-seoul-api/docs/api-service-implementation.md를 참고해주세요


구현 항목

헥사고날 아키텍처 전환

  • common / domain / application / adapter / bootstrap / collector 모듈 구성으로 전환
  • 의존 방향 단방향 강제: adapter → application → domain
  • ArchUnit 테스트로 모듈별 잘못된 의존성 포함되는지 검증

Epic 3 — 벡터 스키마 구성 (Skip - AI 서비스에서 구현)

Epic 4 — 프로젝트 기반 구축 (Phase 11)

  • Spring Boot 멀티모듈 기동, /actuator/health 헬스체크

Epic 5 — OAuth2 + JWT 인증 (Phase 12–13)

  • Google / Kakao 소셜 로그인
  • JWT Access(15분) + Refresh Token(7일, Redis 저장)
  • Token Rotation — Refresh 시 기존 토큰 즉시 삭제

Epic 6 — /query SSE 릴레이 (Phase 14)

  • POST /query → AI 서비스 /chat/stream WebClient 호출 → SseEmitter 릴레이
  • 스트림 완료 후 ChatRoom / ChatMessage(USER·ASSISTANT) 이력 저장

changha and others added 30 commits April 20, 2026 23:34
- RouterAgent: 사용자 의도를 SQL_SEARCH/VECTOR_SEARCH/MAP/FALLBACK으로 분류
- SqlAgent: _SqlParams 추출 → 동적 WHERE 절 SQL 생성 (카테고리/지역/키워드/상태 필터)
- VectorAgent: 질의 정제 → 임베딩 → pgvector cosine 유사도 검색 (threshold 0.4)
- AnswerAgent: 검색 결과 통합 → 자연어 답변 생성 + title_needed 시 대화 제목 생성
- 공통: AgentState spread 패턴({**state, key: value}) 으로 불변 상태 업데이트
- RouterAgent: 4종 intent 분류, state 보존, message 전달 검증 (6건)
- SqlAgent: sql_results 적재, 동적 필터 빌드, state 보존 (7건)
- VectorAgent: vector_results 적재, 정제 질의, 임베딩 호출, query_vector 전달 (6건)
- AnswerAgent: 답변/제목 생성, fallback URL, 결과 병합, state 보존 (8건)
- 전체 테스트 58개 통과
- AgentWorkflow: Router → Branch → Answer 순서로 실행
- 의도별 분기: SQL/Vector/MAP(stub)/FALLBACK
- chat_agent_traces 적재: intent, node_path, elapsed_ms, error (best-effort)
- core/database.py: ai_session_ctx / data_session_ctx context manager 추가
- 워크플로우 통합 테스트 13건 추가 (전체 67개 통과)
[Critical]
- workflow: VectorAgent를 data_session 대신 ai_session으로 라우팅
  (service_embeddings는 on_ai DB에 존재)
- workflow._dispatch 시그니처에 ai_session 파라미터 추가

[Important]
- workflow: 오류 발생 시 fallback 답변 채움 (answer=None 반환 방지)
- vector_agent: _SCORE_THRESHOLD(거리) → _MIN_SIMILARITY(유사도=0.6)로 통일,
  내부 변환(1-x) 제거로 가독성 개선
- sql_agent: LIMIT {_TOP_K} f-string → :top_k bind 파라미터로 분리

[Docs/Nit]
- workflow docstring: 세션 용도 오기 수정
- workflow logger.exception: 중복 exc 인자 제거
- test_workflow: VectorAgent 세션 라우팅 검증 테스트 추가

[QA 추가 테스트]
- 미사용 import ruff auto-fix (8건)
- SqlAgent: keyword=None ILIKE 미추가, SQL Injection 방어 검증
- VectorAgent: 빈 결과 시 [] 반환, threshold/top_k 파라미터 검증
- AnswerAgent: 결과 모두 None 시 빈 목록 처리, metadata 추출 경로
- Workflow: trace commit 호출 검증

전체 78 passed
- pre-filter 전략 채택: max_class_name/area_name/service_status를 WHERE 절 적용
  (metadata JSONB 경로, 1000건 미만에서 카테고리 내 유사도 비교로 정확도 향상)
- 하이브리드 검색(tsvector) 미채택: 소규모 데이터 충분, 5000건 이상 시 도입
- 재순위화/MMR 미채택: min_similarity=0.6 기준 중복 발생 빈도 낮음
- VectorAgent._similarity_search → tools.vector_search.vector_search 위임
- CAST(:query_vector AS vector) asyncpg 호환 유지
- 테스트 11건 추가 (pre-filter bind 검증, 파라미터 오버라이드 등)
[embed_metadata.py]
- --incremental 플래그: service_embeddings에 없는 service_id만 임베딩 (신규 데이터 전용)
  two-phase: on_ai에서 기존 ID set 조회 → Python 필터 (크로스 DB JOIN 불가)
- 로그: 기존 N건 제외, M건 신규 임베딩

[ddl_chat_entities.sql]
- HNSW 인덱스 활성화: m=16, ef_construction=64 (1000건 기본값)
- ef_search=40 주석 안내
- 10000건 이상 m=32, ef_construction=128 재검토 권고 주석 추가
- 내장 샘플 쿼리셋 20건 (카테고리·지역·키워드·의미 다양하게 구성)
- --query: 단일 질의 실행, --top-k: 결과 수 조정 (기본 5)
- tools.vector_search.vector_search 사용, tabulate 의존성 없음
- 사용: uv run python scripts/eval_search.py [--query '...'] [--top-k N]
[MUST-FIX]
- embed_metadata: LIMIT f-string 삽입 → :limit bind 파라미터로 교체
- eval_search: _run_query 타입 힌트 추가 (embeddings: Embeddings, session: AsyncSession)

[SHOULD-FIX]
- vector_search: _ALLOWED_PREFILTER_CLAUSES 화이트리스트 상수 선언
  pre-filter 조건 문자열을 상수에서만 조립, 값은 bind 파라미터 전달 명시
- eval_search: 환경변수 미설정 시 친절한 오류 메시지 출력 (try/except + sys.exit)
- test_vector_search: 중복 pytestmark = pytest.mark.asyncio 제거

[NIT]
- vector_agent: pre-filter 미전달에 대한 TODO(Phase 15) 주석 추가
- ddl_chat_entities: ef_search 세션/영속 적용 방법 주석 보완
asyncio.gather로 배치 요청을 동시 발사하면 aiolimiter 토큰 버킷이
순식간에 소진되어 Gemini Embedding API 429가 발생한다.
aembed_query를 순차 호출로 변경해 rate limiter가 정확히 동작하도록 수정.
문제 1 (버스트): AsyncLimiter(max_rate=70) 는 버킷이 70토큰으로 가득 찬 채 시작해
순차 처리 이후에도 API 응답이 빠르면 수십 개 요청이 수초 내 발사됨 → RPM/TPM 초과.
max_rate=1, time_period=60/rpm 으로 버킷 크기를 1로 고정해 요청 간격을 강제.

문제 2 (재시도 없음): RPM 외 TPM 초과도 429를 유발하므로 limiter가 정상이어도
일시 스파이크에서 429가 발생할 수 있음.
aembed_query에 지수 백오프 재시도(최대 5회, 10s·20s·40s·80s·160s) 추가.

설계: limiter를 _GeminiEmbeddings 생성자로 주입하도록 변경해
프로덕션은 모듈 수준 limiter, 테스트는 _FAST_LIMITER 사용.
gemini_embed_rpm 기본값을 70 → 60(무료 티어 안전값)으로 조정.
- User: OAuth2 소셜 계정 정보 (provider, provider_id, email, nickname, status)
- ChatRoom: 사용자 대화 세션 단위
- ChatMessage: 메시지 단위 저장 (USER/ASSISTANT role)
- UserStatus / ChatMessageRole enum으로 타입 안전성 확보
- ErrorCode에 UNAUTHORIZED / FORBIDDEN / INVALID_TOKEN / EXPIRED_TOKEN / INVALID_REFRESH_TOKEN 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JwtProvider: HS256 Access Token(15분) / Refresh Token(7일) 생성·검증
- JwtAuthenticationFilter: Bearer 토큰 파싱 + SecurityContext 설정
- OAuth2LoginSuccessHandler: 소셜 로그인 후 users upsert + JWT 발급 + Redis(RT:{userId}) 저장
- AuthService: Token Rotation — 갱신 시 구 토큰 즉시 폐기 후 신 토큰 발급
- POST /auth/token/refresh, POST /auth/logout 엔드포인트 추가
- SecurityConfig: /admin/** 인증 필수, STATELESS 세션, 401/403 JSON 응답
- GlobalExceptionHandler: OnSeoulApiException → JSON 응답
- SUSPENDED/DELETED 계정 로그인·갱신 차단

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SecurityConfig: SessionCreationPolicy STATELESS → IF_REQUIRED
  OAuth2 Code Flow의 state 검증을 위해 세션 필요
- SecurityConfig: /oauth2/authorization/**, /login/oauth2/code/** permitAll 추가
- OAuth2LoginSuccessHandler: providerId 추출 시 char[] ClassCastException 수정
  (String.valueOf → Object.toString() 방어 처리)
- OAuth2LoginSuccessHandler: Kakao 중첩 속성(kakao_account/properties) 파싱 추가
- application.yml: Google user-name-attribute id → sub (OIDC openid 스코프 호환)
- application.yml: Google/Kakao registration 및 provider 블록 추가
- application-test.yml: Kakao provider stub 추가
- OAuth2LoginSuccessHandlerIntegrationTest: Spring 컨텍스트 없이 핸들러 직접 검증
  (Google 신규/재로그인, SUSPENDED 차단, Kakao 속성 파싱, email null 허용)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AI 서비스 기초 코드 & 에이전트 워크플로 구축
## 모듈 구조 변경
- collector 모듈 제거 (settings.gradle에서 제외)
- application 모듈 신설 — 유스케이스 구현체 (spring-tx만 의존)
- adapter 모듈 신설 — 어댑터 단일 모듈 (Spring/JPA/Redis/WebClient)
- domain 모듈 정리 — 순수 POJO + port 인터페이스 (프레임워크 무의존)
- app 모듈 — OnSeoulApiApplication.java + yml만 유지

## domain (프레임워크 무의존)
- model/: User, ChatRoom, ChatMessage, PublicServiceReservation 등 순수 POJO
- port/in/: SocialLoginUseCase, RefreshTokenUseCase, LogoutUseCase, CollectDatasetUseCase
- port/out/: LoadUserPort, SaveUserPort, TokenIssuerPort, RefreshTokenStorePort,
             SeoulDatasetFetchPort, GeocodingPort 등

## application (spring-tx만 의존)
- SocialLoginService, RefreshTokenService, LogoutService
- CollectDatasetService, GeocodingService, UpsertService

## adapter/in/
- security/: JjwtTokenIssuer(TokenIssuerPort 구현), JwtAuthenticationFilter, SecurityConfig,
             OAuth2LoginSuccessHandler (UseCase 호출로 슬림화)
- web/: AuthController, CollectionController, GlobalExceptionHandler
- scheduler/: CollectionScheduler

## adapter/out/
- persistence/: UserJpaEntity+Adapter+Mapper, 동일 패턴으로 ChatRoom/ChatMessage/
                PublicServiceReservation/ApiSourceCatalog/CollectionHistory/ServiceChangeLog
- redis/: RefreshTokenRedisAdapter
- seoulapi/: SeoulOpenApiAdapter
- kakao/: KakaoGeocodingAdapter

## 품질 게이트
- HexagonalArchTest: domain→Spring/JPA 금지, application→adapter 금지 (ArchUnit)
- ./gradlew build, test 전체 통과

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
changha and others added 15 commits April 25, 2026 17:14
- on-seoul-api/README.md: 모듈 구조를 5모듈 헥사고날(common/domain/application/adapter/app)로 갱신,
  collector 제거, 의존 방향 다이어그램 추가
- on-seoul-api/app/README.md: app 모듈이 부트스트랩만 담당하도록 재작성.
  주요 컴포넌트 위치(adapter/in/security, adapter/in/web, application/service),
  토큰 정책, SecurityConfig 규칙, 예외 코드표, 인증 흐름 시퀀스 다이어그램 반영
- docs/architecture.md: API Service 섹션을 헥사고날 구조로 교체.
  collector 모듈 제거, 포트/어댑터 설명 추가, 의존 방향 다이어그램 추가
- on-seoul-api/docs/hexagonal-architecture.md: 전환 완료로 삭제

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
바운디드 컨텍스트 4개(인증/수집/채팅/AI에이전트)와
각 도메인의 애그리거트 경계·관계를 Mermaid 다이어그램으로 문서화

- 바운디드 컨텍스트 개요 (graph LR)
- 인증 도메인: User 애그리거트 (classDiagram)
- 수집 도메인: ApiSourceCatalog / CollectionHistory / PublicServiceReservation
  3개 애그리거트 + ServiceChangeLog 엔티티 (classDiagram)
- 채팅 도메인: ChatRoom 루트 + ChatMessage 엔티티 (classDiagram)
- AI 에이전트 도메인: 외부 BC, 포트 인터페이스 계약 (graph LR)
- 크로스 도메인 ID 참조 요약 테이블

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatAgentTrace (on_ai DB): LangGraph 실행 메타 저장 애그리거트
  - messageId: on_data.chat_messages 논리 참조 (DB 다름, 물리 FK 없음)
  - trace: JSONB (node 경로, tool call 결과, 소요 시간)
  - ASSISTANT 메시지에만 생성, 생성 후 불변
- 바운디드 컨텍스트 개요 다이어그램에 ChatAgentTrace 노드 추가
- 크로스 도메인 참조 테이블에 messageId 논리 참조 항목 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 알림 bounded context 신규 추가 (NotificationSubscription, DeviceToken, NotificationTemplate, NotificationDispatch)
- 크로스 도메인 참조 테이블에 알림 도메인 4개 참조 항목 추가
- 섹션 2~6 classDiagram에서 핵심 필드 외 불필요한 타임스탬프·중복 필드 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SendQueryUseCase, SendQueryCommand, SaveChatRoomPort, LoadChatRoomPort, SaveChatMessagePort 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- prepare(): 새 방 생성 또는 기존 방 재사용 후 USER 메시지 저장
- saveAnswer(): 스트림 완료 후 ASSISTANT 메시지 저장 (nextSeq 채번)
- ErrorCode: AI_SERVICE_ERROR(502), CHAT_ROOM_NOT_FOUND(404) 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatPersistenceAdapter: SaveChatRoomPort, LoadChatRoomPort, SaveChatMessagePort 구현
- ChatMessageJpaRepository: native query로 chat_message_seq 채번
- AiServiceAdapter: ServerSentEvent<String> 타입 파라미터로 한글 토큰 올바르게 디코딩
- AiServicePort: Flux 타입이 domain에 반응형 의존성을 끌지 않도록 adapter 내부 인터페이스로 선언

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatController: SseEmitter로 AI 서비스 스트림 릴레이, onError 시 SSE error 이벤트 전송
- QueryRequest: roomId(nullable) + question(@notblank)
- application.yml: ai.service.url, stream-timeout-seconds 설정 추가
- AiServiceAdapterTest: 정상/오류/keep-alive 필터링 4개 케이스
- ChatControllerTest: 성공·에러·미인증·빈질의 5개 케이스

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
모든 코드가 domain/application/adapter 모듈로 이식 완료된 상태였음.
중복 코드 18개 소스 파일 + 9개 테스트 파일 삭제.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CollectorApplication: @SpringBootApplication + @EnableScheduling (배치 전용 부트)
- CollectionScheduler: adapter/in/scheduler → collector 패키지로 이동
- collector/application.yml: DB/Seoul API/Kakao 설정 (Web/Security 의존 없음)
- collector/build.gradle: adapter·application 의존 + bootJar → collector.jar
- OnSeoulApiApplication: @EnableScheduling 제거 (스케줄링은 collector만 담당)

app.jar(Web API) / collector.jar(배치) 두 산출물로 분리됨

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
application(유스케이스 레이어)과의 명칭 혼동 해소.
bootstrap은 Web API 전용 부트스트랩 역할을 명확히 표현.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- architecture.md: adapter/in/scheduler 제거, bootstrap+collector 이중 부트 구조 반영
- on-seoul-api/README.md: 모듈 구조·의존 관계·실행 명령어 업데이트
- bootstrap/README.md: app→bootstrap 리네임, ChatController·SendQueryService 항목 추가, ai.service 설정 추가
- collector/README.md: 옛 pre-헥사고날 내용 전면 재작성 — 배치 부트 역할·구조·설정으로 교체
- docs/collector-module-cleanup-plan.md: 작업 완료로 삭제

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@VitoJeong VitoJeong requested a review from f-lab-ted April 26, 2026 13:13
@VitoJeong VitoJeong closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant