본문으로 바로 가기

Go 백엔드 설계

Go 백엔드는 Handler, Service, Repository 및 인프라 계층으로 요청 처리를 구성하고 uber/dig로 의존성을 조립합니다. cmd/server는 시작과 종료를 담당하며, internal/은 라우팅 인증·인가, 비즈니스 서비스 및 스토리지 접근을 구현합니다.

계층형 아키텍처

백엔드는 전형적인 Handler → Service → Repository → 데이터베이스 4계층 구조를 따릅니다. 계층 간 의존성은 모두 인터페이스(internal/types/interfaces/)를 통해 분리되며, 시작 시 DI 컨테이너가 조립합니다.

계층위치책임
Router / Middlewareinternal/router/, internal/middleware/라우트 등록, 인증, RBAC, 속도 제한, 로깅, 오류 봉투
Handlerinternal/handler/(세션 관련 항목은 internal/handler/session/)요청 매개변수 파싱(DTO는 internal/handler/dto/), Service 호출, 응답 작성. 비즈니스 로직은 포함하지 않음
Serviceinternal/application/service/(약 160개 이상의 파일)비즈니스 오케스트레이션: 지식 베이스/지식/청크, 세션과 chat_pipeline/ 파이프라인, Agent, 테넌트와 구성원, 모델, 데이터 소스 동기화, Wiki, 감사 등
Repositoryinternal/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.goBuildContainer입니다.

go
// 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.NewAsyncqClientinterfaces.TaskEnqueuer로 바인딩
dig.Name 명명된 의존성동일 인터페이스의 여러 인스턴스: 추출 서비스 4개(chunkExtractor/dataTableSummary/imageMultimodal/knowledgePostProcess), Asynq server 6개(coreAsynqServer/postProcessAsynqServer/enrichmentAsynqServer/maintenanceAsynqServer/sharedAsynqServer/wikiAsynqServer), wikiIngest
dig.In 매개변수 구조체router.RouterParamsdig.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의 유무가 실행 형태를 결정합니다.

go
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.goNewRouter(params RouterParams)(RouterParamsdig.In 구조체)는 다음 순서로 조립되며, 순서 자체가 보안 의미를 가집니다.

  1. gin.New() + SetTrustedProxies(WEKNORA_TRUSTED_PROXIES, 기본적으로 루프백과 사설 네트워크 대역만 신뢰하여 위조된 X-Forwarded-For가 IP 기반 속도 제한을 우회하지 못하게 함)
  2. 전역 미들웨어: corsRequestIDLanguageLoggerRecoveryErrorHandler
  3. 인증 불필요 엔드포인트: GET /health. release가 아닌 모드에서는 /swagger/*any 마운트
  4. Embed 페이지의 frame-ancestors CSP 미들웨어. Lite 버전의 임베디드 프런트엔드 정적 리소스(handler.Edition == "lite")
  5. 인증 전에 등록되는 공개 라우트: IM 플랫폼 콜백(/api/v1/im, 각 플랫폼 자체 서명 검증), Web Embed 공개 라우트(/api/v1/embed/:channel_id, middleware.EmbedAuth publish-token 인증 + Redis 속도 제한), 단기 유효 기능 URL(resource grants)
  6. middleware.Auth(...) 전역 인증. 그 뒤 인증이 필요한 파일 프록시 라우트, 인증은 필요 없지만 서명을 검증하는 presigned 파일 라우트, Langfuse trace 미들웨어, AuditServiceProvider
  7. v1 := r.Group("/api/v1"): 먼저 v1.Use(rbacGuards.apiKeyAuthorizer.Middleware())(API Key 게이트웨이, JWT 세션은 바로 통과)를 적용하고, 이어서 30개의 RegisterXxxRoutes(v1, handler, rbacGuards)를 차례로 호출
  8. 마무리 자체 검사: rbacGuards.assertAPIKeyPoliciesMatchRoutes(r) — 선언된 API Key 정책이 존재하지 않는 라우트 템플릿을 가리키면(경로 변경/오타) 시작 즉시 panic하여, 배포 후 항상 403을 반환하는 무효 정책이 생기는 것을 방지

라우트 그룹 개요

그룹 접두사Register 함수API Key 정책 예시
/auth, /meRegisterAuthRoutes / RegisterMyInvitationRoutes대부분 Key 불필요
/tenants, /tenants/:id/*(구성원/초대/감사)RegisterTenantRoutesmanage_members / manage_spaces. /:id 그룹에 PathTenantMatch() 적용
/knowledge-bases, `/knowledge-bases/:id/knowledgefaqtags
/knowledge, /chunksRegisterKnowledgeRoutes / RegisterChunkRoutesingest
/sessions, /knowledge-chat, /agent-chat, /knowledge-search, /messagesRegisterSessionRoutes / RegisterChatRoutes 등chat / retrieve
/models, /evaluationRegisterModelRoutes / RegisterEvaluationRoutesmanage_models / run_evaluations
/system, /system/adminRegisterSystemRoutes / RegisterSystemAdminRoutesadmin 그룹은 g.SystemAdmin() 강제
/mcp-services, /agent, /web-search, /web-search-providers해당 Register 함수manage_mcp_services / manage_web_search
/vector-stores, /storage-backendsRegisterVectorStoreRoutes / RegisterStorageBackendRoutesmanage_vector_stores / manage_storage_backends
/agents, `/agents/:id/sharesembed-channelsim-channels`
/organizations, /user/favorites, /skills해당 Register 함수manage_spaces
/im-channels, /embed-channels, /wechatRegisterIMChannelRoutes / RegisterEmbedChannelRoutesmanage_channels
/datasource, /knowledgebase/:kb_id/wiki, /chunker/previewRegisterDataSourceRoutes / RegisterWikiPageRoutes / RegisterChunkerDebugRoutesmanage_datasources / ingest

rbacGuards: 중앙 집중식 권한 매트릭스

internal/router/rbac.gorbacGuards를 정의합니다. NewRouter가 한 번 생성한 뒤 각 Register 함수에 전달합니다. 가드는 세 종류로 나뉘며 라우트 행에서 인라인으로 사용되어 필요한 권한을 한눈에 알 수 있습니다.

go
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) → APIKeyRoutePolicyAPIKeyRouteAuthorizer 정책 테이블에 기록합니다. 정책 생성자에는 apiKeyFullAccess(), apiKeyPlatform(...) 및 17종의 기능 래퍼(apiKeyRetrieve / apiKeyChat / apiKeyIngest / apiKeyManageModels ...)가 있습니다. 정책이 등록되지 않은 라우트는 API Key 주체에 대해 기본적으로 fail-closed로 거부합니다.

미들웨어 목록(internal/middleware)

요청이 거치는 순서:

미들웨어파일책임과 핵심 로직
cors.New(gin-contrib)router.goAuthorization, 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.gopanic 포착 + 스택 기록 + 500 응답
ErrorHandler()error_handler.goc.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/langfuseLLM 관측성 trace. LANGFUSE_*가 구성되지 않으면 no-op
AuditServiceProvider()audit_provider.goRBAC 거부 경로에서 감사를 기록할 수 있도록 AuditLogService를 gin context에 주입. 서비스가 nil이면 정상적으로 기능 축소
APIKeyRouteAuthorizer.Middleware()api_key_gate.goAPI 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.goKB 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 등은 GORM Value()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입니다.

go
// 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.goDeduplicate / DeduplicateWithScore, ParseLLMJsonResponse, CleanInvalidUTF8, PipelineLog 계열제네릭 중복 제거(검색 병합 시 최고 점수 유지), LLM이 반환한 ```json 코드 블록 파싱, 잘못된 UTF-8 정리, RAG 파이프라인 단계 로깅
common/db_retry.goWithDeadlockRetry(ctx, fn)데이터베이스 교착 상태 감지 후 재시도(최대 3회, 50→100→200ms 백오프)
common/redis_tls.goRedisTLSConfig()REDIS_USE_TLS 등의 환경 변수에 따라 Redis TLS 구성 생성
utils/crypto.goEncryptAESGCM / DecryptAESGCM(enc:v1: 접두사, 멱등)위에서 설명한 모든 민감 필드 저장 시 암호화의 기반 구현
utils/security.goSanitizeHTML, ValidateFilePath, SanitizeForLogXSS 정리, 디렉터리 순회 방어, 로그 마스킹
utils/inject.goValidateSQL(pganalyze/pg_query_go 기반)Agent 데이터 분석으로 생성된 SQL의 허용 목록 테이블 검증 및 인젝션 패턴 탐지
utils/presign.goGeneratePresignURL / ValidatePresignURLHMAC-SHA256 사전 서명 파일 URL(기본 2h, IM 인라인 이미지에 사용)
utils/oidc_state.goGenerateState / ValidateStateOIDC 인증 state의 HMAC 서명과 10분 TTL(CSRF 방지)
utils/log_sanitize.goCompactImageDataURLForLog지나치게 긴 이미지 data URL을 축약하여 로그 폭증 방지
utils/storage_error.goSanitizeStorageConnectivityError스토리지 연결 오류를 사용자 친화적 안내로 변환하고 내부 호스트 이름 숨김
기타taskid.go / fileutil.go / filesize.go / httputil.go / json.go작업 ID, 파일 및 크기 형식화, HTTP 다운로드, JSON Schema 생성 등

이로써 백엔드는 프로세스 시작과 의존성 조립부터 요청 수신 및 데이터 저장까지 전체 흐름이 완결됩니다. 다음 장에서는 RAG 검색 파이프라인(chat_pipeline), Agent 엔진(internal/agent), 문서 파싱 서비스(docreader)의 내부 구현을 각각 자세히 설명합니다.

WeKnora v0.8.0 소스 코드 기반 한국어 번역 · MIT License