Go 백엔드 설계
Go 백엔드는 Handler, Service, Repository 및 인프라 계층으로 요청 처리를 구성하고 uber/dig로 의존성을 조립합니다. cmd/server는 시작과 종료를 담당하며, internal/은 라우팅 인증·인가, 비즈니스 서비스 및 스토리지 접근을 구현합니다.
계층형 아키텍처
백엔드는 전형적인 Handler → Service → Repository → 데이터베이스 4계층 구조를 따릅니다. 계층 간 의존성은 모두 인터페이스(internal/types/interfaces/)를 통해 분리되며, 시작 시 DI 컨테이너가 조립합니다.
| 계층 | 위치 | 책임 |
|---|---|---|
| Router / Middleware | internal/router/, internal/middleware/ | 라우트 등록, 인증, RBAC, 속도 제한, 로깅, 오류 봉투 |
| Handler | internal/handler/(세션 관련 항목은 internal/handler/session/) | 요청 매개변수 파싱(DTO는 internal/handler/dto/), Service 호출, 응답 작성. 비즈니스 로직은 포함하지 않음 |
| Service | internal/application/service/(약 160개 이상의 파일) | 비즈니스 오케스트레이션: 지식 베이스/지식/청크, 세션과 chat_pipeline/ 파이프라인, Agent, 테넌트와 구성원, 모델, 데이터 소스 동기화, Wiki, 감사 등 |
| Repository | internal/application/repository/(약 60개 파일) | 데이터 접근에 GORM을 일관되게 사용(type knowledgeRepository struct { db *gorm.DB }, 작업은 r.db.WithContext(ctx)를 통해 수행). 검색 엔진의 저장소 구현은 엔진별로 repository/retriever/{postgres,elasticsearch,qdrant,milvus,weaviate,doris,opensearch,tencentvectordb,sqlite,neo4j}에 패키지로 분리 |
| 도메인 모델 | internal/types/ | GORM 엔티티, 열거형, context key, 인터페이스 정의(types/interfaces) |
| 인프라 | internal/infrastructure/(docparser gRPC 클라이언트, web_search), internal/models/(chat/embedding/rerank 모델 어댑터), internal/stream/, internal/sandbox/, internal/mcp/, internal/im/ | 외부 시스템 어댑테이션 |
핵심 규칙:
- Handler는 Service 인터페이스(예:
interfaces.KnowledgeService)에만 의존하고, Service는 Repository 인터페이스와 다른 Service 인터페이스에만 의존합니다. - 모든 인터페이스는
internal/types/interfaces/에 모아 선언하며, 구현체는 dig를 통해 바인딩합니다. - Asynq worker와 HTTP server는 동일한 프로세스에서 실행되며, 작업 처리 함수는 같은 Service 집합을 재사용합니다.
의존성 주입: internal/container(uber/dig)
WeKnora는 go.uber.org/dig v1.19.0(생성자 주입 컨테이너이며 코드 생성 방식의 wire가 아님)을 사용합니다. 진입점은 internal/container/container.go의 BuildContainer입니다.
// cmd/server/main.go
c := container.BuildContainer(runtime.GetContainer())
// internal/container/container.go
func BuildContainer(container *dig.Container) *dig.Container {
must(container.Provide(NewResourceCleaner, dig.As(new(interfaces.ResourceCleaner))))
must(container.Provide(config.LoadConfig))
must(container.Provide(initDatabase)) // *gorm.DB
must(container.Provide(initRedisClient)) // *redis.Client(nil일 수 있음: Lite 모드)
...
must(container.Provide(repository.NewTenantRepository))
must(container.Provide(service.NewTenantService))
...
must(container.Provide(router.NewRouter)) // 최종적으로 *gin.Engine 생성
return container
}runtime.GetContainer()(internal/runtime/container.go)는 전역 싱글턴 dig.Container를 보유합니다. must(err)는 등록 실패 시 즉시 panic을 발생시킵니다. DI 조립 오류는 시작 단계의 치명적 오류입니다.
사용하는 dig 기능
| 기능 | 사용 예시 |
|---|---|
dig.As | 구체 타입을 인터페이스로 바인딩: container.Provide(NewResourceCleaner, dig.As(new(interfaces.ResourceCleaner))). router.NewAsyncqClient를 interfaces.TaskEnqueuer로 바인딩 |
dig.Name 명명된 의존성 | 동일 인터페이스의 여러 인스턴스: 추출 서비스 4개(chunkExtractor/dataTableSummary/imageMultimodal/knowledgePostProcess), Asynq server 6개(coreAsynqServer/postProcessAsynqServer/enrichmentAsynqServer/maintenanceAsynqServer/sharedAsynqServer/wikiAsynqServer), wikiIngest |
dig.In 매개변수 구조체 | router.RouterParams가 dig.In을 임베드하여 약 60개의 Handler/Service 의존성을 한 번에 주입하고, 지나치게 긴 생성자 시그니처를 방지 |
container.Invoke 부수 효과 실행 | 등록 즉시 시작되는 백그라운드 컴포넌트: registerPoolCleanup, registerWebSearchProviders, startDataSourceScheduler, startHousekeepingService, startAuditLogRetention, startTemporaryDocumentCleanup, 15개의 chatpipeline.NewPluginXxx(Search/Rerank/WebFetch/Merge/DataAnalysis/QueryUnderstand/LoadHistory/ChatCompletionStream 등의 플러그인이 EventManager에 자체 등록), router.RunAsynqServer, recoverPendingWikiTasks 등 |
| 어댑터 Provide | 클로저로 인터페이스 변환: func(s *service.StorageBackendService) interfaces.StorageBackendService { return s }. 동일한 RetrieveEngineRegistry 인스턴스를 StoreRegistry로도 노출 |
등록 순서와 조건부 조립
BuildContainer의 등록은 9단계로 나뉩니다(소스 코드에 해당 로그가 있음). ① 핵심 인프라(config/langfuse/db/file/redis/ants 풀) → ② 검색 엔진 레지스트리 → ③ 외부 클라이언트(docreader gRPC, Ollama, Neo4j, StreamManager, DuckDB) → ④ Repository 계층(30개 이상) → ⑤ Service 계층(50개 이상, MCP Manager, 이벤트 버스, Agent 승인 게이트 approval.Gate 포함) → ⑥ 작업 실행기 조건부 조립 → ⑦ chat_pipeline 플러그인 → ⑧ Handler 계층(40개 이상)과 IM 어댑터 → ⑨ Router 및 Asynq server 시작.
⑥단계는 전체 저장소에서 가장 중요한 조건 분기입니다. Redis의 유무가 실행 형태를 결정합니다.
redisAvailable := os.Getenv("REDIS_ADDR") != ""
if redisAvailable {
must(container.Provide(router.NewAsyncqClient, dig.As(new(interfaces.TaskEnqueuer))))
must(container.Provide(router.NewCoreAsynqServer, dig.Name("coreAsynqServer")))
... // 총 6개 worker 풀 + AsynqInspector
must(container.Invoke(registerModelConcurrencyLimiter)) // Redis 분산형 모델별 동시성 게이트
} else {
syncExec := router.NewSyncTaskExecutor() // Lite 모드: 프로세스 내 동기 실행기
must(container.Provide(func() interfaces.TaskEnqueuer { return syncExec }))
must(container.Provide(router.NewNoopTaskInspector))
must(container.Invoke(registerLiteModelConcurrencyLimiter)) // 프로세스 내 세마포어
}6개 Asynq worker 풀의 동시성은 system settings / 환경 변수로 조정할 수 있습니다(기본값 Core=8, PostProcess=2, Enrichment=12, Maintenance=4, Shared=6, Wiki=8, WEKNORA_ASYNQ_*_CONCURRENCY). 큐 토폴로지는 internal/types/task.go에 정의되어 있습니다(default, chat_attachment, postprocess, summary, multimodal, graph, question, memory, sync, low/maintenance, wiki 등이며 자동 태깅과 메모리 추출 포함).
리소스 정리와 팩토리
ResourceCleaner(internal/container/cleanup.go): 각 컴포넌트는RegisterWithName(name, cleanupFunc)으로 소멸 작업(ants 풀, Langfuse flush, 데이터 소스 스케줄러, Housekeeping 등)을 등록하며, 종료 시Cleanup(ctx)로 일괄 정리합니다.EngineFactory(internal/container/engine_factory.go): 시작 단계에서 단일 엔진을 정적으로 바인딩하는 대신vector_stores테이블 행에 따라 런타임에 검색 엔진 인스턴스(createQdrantEngine/createMilvusEngine/createDorisEngine/createOpenSearchEngine...)를 생성합니다.initDatabase는 연결 설정 외에도 다음을 담당합니다. golang-migrate 자동 마이그레이션(AUTO_MIGRATE, 실패해도 경고만 하고 중단하지 않음),__pending_env__에 저장된 provider 보완, 레거시 StorageBackend 마이그레이션, 시퀀스 동기화, Lite 모드 pending 작업 재설정,config/builtin_models.yaml에 선언된 내장 모델의 선언적 UPSERT. SQLite에서는 쓰기를 직렬화하도록SetMaxOpenConns(1)을 강제합니다.
cmd/server 시작 흐름
cmd/server에는 논리 파일이 3개뿐입니다. main.go(진입점과 HTTP 생명주기), bootstrap.go(일회성 부트스트랩 훅), listen.go(포트 재시도)이며, 그 외에 signals_unix.go/signals_windows.go가 플랫폼별 shutdownSignals를 제공합니다.
핵심 사항:
- 부트스트랩 실패는 시작을 중단하지 않음:
bootstrap.go는 실패를logger.Warnf로 기록합니다. 시스템 관리자 부트스트랩은 배포에 시스템 관리자가 아직 없을 때만 적용되며, 재시작해도 이미 철회된 권한을 복구하지 않습니다. - 2단계 정상 종료: 첫 번째 SIGTERM/SIGINT에서는 먼저 listener를 닫아 새 프로세스가 즉시 포트를 바인딩할 수 있게 한 다음
Shutdown으로 기존 연결을 드레이닝합니다. 두 번째 신호는Close를 강제합니다. - 포트 점유 재시도:
listenWithRetry는 300ms부터 시작하는 지수 백오프로 10회 재시도하여, 롤링 재시작 시 이전 프로세스가 아직 포트를 해제하지 않았더라도 즉시 실패하지 않게 합니다.
라우트 구성과 RBAC 조립(internal/router)
NewRouter의 조립 순서
internal/router/router.go의 NewRouter(params RouterParams)(RouterParams는 dig.In 구조체)는 다음 순서로 조립되며, 순서 자체가 보안 의미를 가집니다.
gin.New()+SetTrustedProxies(WEKNORA_TRUSTED_PROXIES, 기본적으로 루프백과 사설 네트워크 대역만 신뢰하여 위조된X-Forwarded-For가 IP 기반 속도 제한을 우회하지 못하게 함)- 전역 미들웨어:
cors→RequestID→Language→Logger→Recovery→ErrorHandler - 인증 불필요 엔드포인트:
GET /health. release가 아닌 모드에서는/swagger/*any마운트 - Embed 페이지의
frame-ancestorsCSP 미들웨어. Lite 버전의 임베디드 프런트엔드 정적 리소스(handler.Edition == "lite") - 인증 전에 등록되는 공개 라우트: IM 플랫폼 콜백(
/api/v1/im, 각 플랫폼 자체 서명 검증), Web Embed 공개 라우트(/api/v1/embed/:channel_id,middleware.EmbedAuthpublish-token 인증 + Redis 속도 제한), 단기 유효 기능 URL(resource grants) middleware.Auth(...)전역 인증. 그 뒤 인증이 필요한 파일 프록시 라우트, 인증은 필요 없지만 서명을 검증하는 presigned 파일 라우트, Langfuse trace 미들웨어,AuditServiceProviderv1 := r.Group("/api/v1"): 먼저v1.Use(rbacGuards.apiKeyAuthorizer.Middleware())(API Key 게이트웨이, JWT 세션은 바로 통과)를 적용하고, 이어서 30개의RegisterXxxRoutes(v1, handler, rbacGuards)를 차례로 호출- 마무리 자체 검사:
rbacGuards.assertAPIKeyPoliciesMatchRoutes(r)— 선언된 API Key 정책이 존재하지 않는 라우트 템플릿을 가리키면(경로 변경/오타) 시작 즉시 panic하여, 배포 후 항상 403을 반환하는 무효 정책이 생기는 것을 방지
라우트 그룹 개요
| 그룹 접두사 | Register 함수 | API Key 정책 예시 |
|---|---|---|
/auth, /me | RegisterAuthRoutes / RegisterMyInvitationRoutes | 대부분 Key 불필요 |
/tenants, /tenants/:id/*(구성원/초대/감사) | RegisterTenantRoutes | manage_members / manage_spaces. /:id 그룹에 PathTenantMatch() 적용 |
/knowledge-bases, `/knowledge-bases/:id/knowledge | faq | tags |
/knowledge, /chunks | RegisterKnowledgeRoutes / RegisterChunkRoutes | ingest |
/sessions, /knowledge-chat, /agent-chat, /knowledge-search, /messages | RegisterSessionRoutes / RegisterChatRoutes 등 | chat / retrieve |
/models, /evaluation | RegisterModelRoutes / RegisterEvaluationRoutes | manage_models / run_evaluations |
/system, /system/admin | RegisterSystemRoutes / RegisterSystemAdminRoutes | admin 그룹은 g.SystemAdmin() 강제 |
/mcp-services, /agent, /web-search, /web-search-providers | 해당 Register 함수 | manage_mcp_services / manage_web_search |
/vector-stores, /storage-backends | RegisterVectorStoreRoutes / RegisterStorageBackendRoutes | manage_vector_stores / manage_storage_backends |
/agents, `/agents/:id/shares | embed-channels | im-channels` |
/organizations, /user/favorites, /skills | 해당 Register 함수 | manage_spaces 등 |
/im-channels, /embed-channels, /wechat | RegisterIMChannelRoutes / RegisterEmbedChannelRoutes | manage_channels |
/datasource, /knowledgebase/:kb_id/wiki, /chunker/preview | RegisterDataSourceRoutes / RegisterWikiPageRoutes / RegisterChunkerDebugRoutes | manage_datasources / ingest |
rbacGuards: 중앙 집중식 권한 매트릭스
internal/router/rbac.go는 rbacGuards를 정의합니다. NewRouter가 한 번 생성한 뒤 각 Register 함수에 전달합니다. 가드는 세 종류로 나뉘며 라우트 행에서 인라인으로 사용되어 필요한 권한을 한눈에 알 수 있습니다.
kb.PUT("/:id", g.OwnedKBOrAdmin(), handler.UpdateKnowledgeBase)- 역할 가드("호출자가 테넌트에서 어떤 역할인가"를 확인):
Viewer()/Contributor()/Admin()/Owner()/AdminOrSystemAdmin()/SystemAdmin(). 내부적으로middleware.RequireRole호출 - 소유권 가드("해당 리소스의 생성자 또는 Admin+인가"를 확인):
OwnedKBOrAdmin(),OwnedAgentOrAdmin(),OwnedKnowledgeKBOrAdmin(),OwnedChunkKBOrAdmin(),OwnedWikiKBOrAdmin()등. 하위 리소스(chunk/wiki/FAQ/tag)는KBCreatorLookupFromKnowledgeID등의 클로저로 URL 매개변수를 따라 소속 KB의creator_id를 역추적하여 상위 리소스와 같은 규칙을 공유 - 지식 베이스 접근 가드(3단계 해석: 자체 KB / 조직 간 공유 KB / 공유 Agent에 표시되는 KB):
KBAccessRead|Write(param)및...FromKnowledgeIDParam/...FromChunkIDParam변형. 내부적으로middleware.RequireKBAccess사용 - 테넌트 경계 가드:
CrossTenant()(플랫폼 수준 작업에는EnableCrossTenantAccess+CanAccessAllTenants필요),PathTenantMatch()(/tenants/:id는 context의 테넌트와 일치해야 함)
소스 코드 주석은 가드 선택 결정 트리를 제시합니다(creator가 있는 리소스에는 OwnedXxxOrAdmin, 테넌트 수준 인프라에는 Admin, 생성 진입점에는 Contributor 사용). 또한 모든 가드가 cfg.Tenant.EnableRBAC 스위치를 따른다고 명시합니다. 비활성화하면 "원래 거부했어야 함" 로그만 기록하고 통과시킵니다(점진적 마이그레이션 기간의 동작).
API Key 정책은 역할 가드와 직교합니다. apiKeyGroup(grp, policy)은 gin RouterGroup을 감싸며, 라우트를 등록하는 동시에 (method, fullPath) → APIKeyRoutePolicy를 APIKeyRouteAuthorizer 정책 테이블에 기록합니다. 정책 생성자에는 apiKeyFullAccess(), apiKeyPlatform(...) 및 17종의 기능 래퍼(apiKeyRetrieve / apiKeyChat / apiKeyIngest / apiKeyManageModels ...)가 있습니다. 정책이 등록되지 않은 라우트는 API Key 주체에 대해 기본적으로 fail-closed로 거부합니다.
미들웨어 목록(internal/middleware)
요청이 거치는 순서:
| 미들웨어 | 파일 | 책임과 핵심 로직 |
|---|---|---|
cors.New(gin-contrib) | router.go | Authorization, X-API-Key, X-Tenant-ID, X-Embed-Session 등의 헤더 허용. MaxAge 12h |
RequestID() | logger.go | 요청 헤더의 X-Request-ID를 재사용하거나 UUID를 생성하여 gin context와 Request.Context()에 기록하고 로그/추적 전체에 전달 |
Language() | language.go | 문서 처리 언어 결정: WEKNORA_LANGUAGE 환경 변수 > Accept-Language의 첫 번째 태그 > 기본값 zh-CN |
Logger() | logger.go | 요청/응답 전체 로깅. 정규식으로 비밀번호/토큰 필드 마스킹, base64 이미지 data URL 축약, SSE 응답은 건너뛰도록 표시, 항목당 최대 10KB |
Recovery() | recovery.go | panic 포착 + 스택 기록 + 500 응답 |
ErrorHandler() | error_handler.go | c.Errors의 마지막 오류를 읽음. *errors.AppError는 해당 HTTPCode에 따라 {success:false, error:{code,message,details}} 공통 봉투로 반환하며 나머지는 500 |
EmbedAuth(...) | embed_auth.go | /api/v1/embed/:channel_id 공개 그룹에만 적용. publish token을 검증하고 Embed 채널 context를 주입. Redis 3단계 속도 제한(IP별/분, 채널 전체/분, 채널/일) |
PublicAuthRateLimit() | auth_public_ratelimit.go | 인증 불필요 초대 조회/초대 가입 라우트: 프로세스 메모리 슬라이딩 윈도, IP당 60s/30회. 백그라운드에서 2분마다 만료 버킷 정리, 제한 초과 시 429 반환 |
Auth(...) | auth.go | 핵심 인증의 세 가지 상태: ① JWT(Authorization: Bearer, userService.ValidateToken), ② API Key(X-API-Key, AuthenticateAPIKey), ③ noAuthAPI 허용 목록. X-Tenant-ID를 통한 테넌트 전환 지원(IsTenantAccessible 3단계 검증: 자체 테넌트/교차 테넌트 최고 관리자/active membership), resolveTenantRole로 테넌트 내 역할 해석. context에 TenantIDContextKey, TenantInfoContextKey, UserContextKey, UserIDContextKey, TenantRoleContextKey, SystemAdminContextKey, PrincipalContextKey 등을 기록 |
langfuse.GinMiddleware() | tracing/langfuse | LLM 관측성 trace. LANGFUSE_*가 구성되지 않으면 no-op |
AuditServiceProvider() | audit_provider.go | RBAC 거부 경로에서 감사를 기록할 수 있도록 AuditLogService를 gin context에 주입. 서비스가 nil이면 정상적으로 기능 축소 |
APIKeyRouteAuthorizer.Middleware() | api_key_gate.go | API Key 주체의 라우트 수준 게이트웨이. (method, fullPath) 정책 테이블을 조회하고 PlatformOnly / RequireFullAccess / Capabilities 검증. 선언되지 않은 라우트는 기본적으로 거부. JWT 사용자는 바로 통과 |
RequireRole(min) 등 | rbac.go | 테넌트 내 최소 역할 검증(owner=40 > admin=30 > contributor=20 > viewer=10). RequireOwnershipOrRole(min, creatorLookup)은 리소스 생성자가 최소 역할 제한을 넘을 수 있도록 허용. API Key 주체는 단축 처리(권한 부여는 APIKeyGate 담당). 교차 테넌트 최고 관리자는 일시적으로 Admin 권한 획득. 거부 시 AuditService.LogDenied 호출 |
RequireCrossTenantAccess() / RequirePathTenantMatch() | access.go | 플랫폼 수준 작업 게이트웨이와 URL 테넌트 일치 여부 검증 |
RequireKBAccess(resolver, perm, ...) | kb_access.go | KB 3단계 접근 해석(자체 → 조직 공유 → 공유 Agent 읽기 전용) 후 Request.Context()의 TenantIDContextKey를 KB 원본 테넌트로 재작성하여 다운스트림 검색이 자동으로 올바른 테넌트 데이터를 사용하도록 함 |
asynqdl.Middleware() | asynqdl/ | 비 HTTP. Asynq 작업의 재시도 예산 소진 시 task_dead_letters 테이블에 기록. OnDeadLetter 콜백을 연결하여 비즈니스 상태와 연동 가능(예: 지식 파싱 실패 표시) |
도메인 모델 개요(internal/types)
internal/types/에는 약 26개의 GORM 영속 엔티티가 있습니다. 핵심 관계는 다음과 같습니다.
설계 핵심 사항:
- 멀티테넌트 격리: 거의 모든 엔티티에
TenantID가 있습니다.tenant_id=0은 시스템 수준(예: 시스템 감사)을 의미합니다. - 민감 필드 저장 시 암호화:
Model.Parameters,VectorStore.ConnectionConfig,StorageBackend.Config,DataSource.Config,TenantAPIKey.APIKey등은 GORMValue()시SYSTEM_AES_KEY(32바이트)로 AES-256-GCM 암호화하고,Scan()시 관대하게 복호화합니다(복호화 실패는 오류가 아니라 미구성으로 간주). - 생성 시 바인딩 후 불변: KB의
VectorStoreID(gorm tag<-:create)와StorageBackendID는 생성 후 수정할 수 없어 인덱스/파일 일관성을 보장합니다. - 비동기 상태 머신:
Knowledge.ParseStatus의 7개 상태 +PendingSubtasksCount로 finalizing 단계의 병렬 보강 하위 작업(summary/question/graph)을 추적합니다. - 감사 append-only:
AuditLog에는 업데이트/소프트 삭제 필드가 없으며 50종 이상의AuditAction을 포괄합니다. - 엔티티가 아닌 중요 타입:
SearchResult검색 결과,Pagination,Task/큐 토폴로지(task.go), 각종 JSONB 구성 구조체(ChunkingConfig,IndexingStrategy,CustomAgentConfig등), context key와 값 조회 도우미(context_helpers.go).
오류 처리 규약(internal/errors)
공통 오류 전달 객체는 AppError입니다.
// internal/errors/errors.go
type AppError struct {
Code ErrorCode // 비즈니스 오류 코드
Message string
Details any
HTTPCode int // HTTP 상태 매핑
}- 오류 코드 구간: 1000–1999 공통 HTTP 의미(
ErrBadRequest=1000,ErrUnauthorized=1001,ErrForbidden=1002,ErrNotFound=1003,ErrTooManyRequests=1006,ErrServiceUnavailable=1008), 2000–2099 테넌트, 2100–2199 Agent, 2200–2299 벡터 저장소 - 생성자 함수:
NewBadRequestError/NewUnauthorizedError/NewForbiddenError/NewNotFoundError/NewValidationError/NewConflictError/NewTooManyRequestsError/NewServiceUnavailableError등 - 연동 방식: Handler/미들웨어는
c.Error(appErr)로 오류를 연결하고, 마지막에ErrorHandler미들웨어가{success:false, error:{code,message,details}}봉투로 일괄 렌더링합니다. 프런트엔드는error.code를 기준으로 i18n을 수행합니다.AppError가 아닌 오류는 모두 500입니다. session.go는 세션 도메인 센티널 오류(ErrSessionNotFound등)를 제공합니다.parse_error_codes.go는 문서 파싱 단계의 문자열 오류 코드(DOCREADER_TIMEOUT,EMBEDDING_RATE_LIMIT,VECTORSTORE_WRITE_FAILED,TASK_TIMEOUT등)를 정의하며, 프런트엔드가 번역해 표시하도록Knowledge.ErrorMessage에 저장합니다.
로깅 시스템(internal/logger)
- logrus 기반의 비공개
appLogger싱글턴 + 사용자 정의 Formatter(컬러 터미널 출력.LOG_FORMAT은%d,%level,%traceId,%msg등의 자리표시자로 템플릿 사용자 정의 가능.LOG_PATH를 설정하면 lumberjack으로 순환 파일에 기록하면서 ANSI 색상 코드를 제거) - request_id 전 구간 전달:
middleware.RequestID가 context에 기록 →logger.GetLogger(ctx)가 자동으로 추출하여request_id필드 주입. 일반적으로logger.Infof/Warnf/Errorf(ctx, format, ...)와ErrorWithFields를 사용 - LLM 디버그 로그(
llm_logger.go):LLM_DEBUG_LOG=true일 때 활성화되며, 각 LLM 호출을 request_id별 파일에 기록(LLMCallRecord: CallType Chat/Embedding/Rerank/VLM, 모델, 소요 시간, 전체 메시지와 도구 호출, 오류). 7일 후 자동 정리되며 프롬프트와 context 문제를 조사하는 데 사용
핵심 유틸리티 라이브러리(internal/common, internal/utils)
| 위치 | 도구 | 용도 |
|---|---|---|
common/tools.go | Deduplicate / DeduplicateWithScore, ParseLLMJsonResponse, CleanInvalidUTF8, PipelineLog 계열 | 제네릭 중복 제거(검색 병합 시 최고 점수 유지), LLM이 반환한 ```json 코드 블록 파싱, 잘못된 UTF-8 정리, RAG 파이프라인 단계 로깅 |
common/db_retry.go | WithDeadlockRetry(ctx, fn) | 데이터베이스 교착 상태 감지 후 재시도(최대 3회, 50→100→200ms 백오프) |
common/redis_tls.go | RedisTLSConfig() | REDIS_USE_TLS 등의 환경 변수에 따라 Redis TLS 구성 생성 |
utils/crypto.go | EncryptAESGCM / DecryptAESGCM(enc:v1: 접두사, 멱등) | 위에서 설명한 모든 민감 필드 저장 시 암호화의 기반 구현 |
utils/security.go | SanitizeHTML, ValidateFilePath, SanitizeForLog | XSS 정리, 디렉터리 순회 방어, 로그 마스킹 |
utils/inject.go | ValidateSQL(pganalyze/pg_query_go 기반) | Agent 데이터 분석으로 생성된 SQL의 허용 목록 테이블 검증 및 인젝션 패턴 탐지 |
utils/presign.go | GeneratePresignURL / ValidatePresignURL | HMAC-SHA256 사전 서명 파일 URL(기본 2h, IM 인라인 이미지에 사용) |
utils/oidc_state.go | GenerateState / ValidateState | OIDC 인증 state의 HMAC 서명과 10분 TTL(CSRF 방지) |
utils/log_sanitize.go | CompactImageDataURLForLog | 지나치게 긴 이미지 data URL을 축약하여 로그 폭증 방지 |
utils/storage_error.go | SanitizeStorageConnectivityError | 스토리지 연결 오류를 사용자 친화적 안내로 변환하고 내부 호스트 이름 숨김 |
| 기타 | taskid.go / fileutil.go / filesize.go / httputil.go / json.go | 작업 ID, 파일 및 크기 형식화, HTTP 다운로드, JSON Schema 생성 등 |
이로써 백엔드는 프로세스 시작과 의존성 조립부터 요청 수신 및 데이터 저장까지 전체 흐름이 완결됩니다. 다음 장에서는 RAG 검색 파이프라인(chat_pipeline), Agent 엔진(internal/agent), 문서 파싱 서비스(docreader)의 내부 구현을 각각 자세히 설명합니다.