본문으로 건너뛰기

Core Config 모듈 분리 및 앱별 구성 전략

배경

  • 현재 모든 API 서버는 @core/config를 글로벌 모듈로 불러오고 동일한 스키마/네임스페이스 세트를 강제합니다.
  • 기능(특히 국가별 KR/DE 전용 기능)이 증가하면서 CoreConfig 스키마가 무한정 커지고, 필요 없는 환경 변수까지 각 서비스에 요구되는 문제가 발생했습니다.
  • 신규 기능을 추가할 때마다 공통 모듈을 수정해야 하므로 변경 범위가 넓고, 서비스 간 배포 독립성이 저하됩니다.

목표

  1. 앱별 최소 설정만 로드하여 환경 변수 의존성을 줄인다.
  2. 도메인/기능 팀이 자체 config 계약을 소유하도록 하여 책임을 분리한다.
  3. 타입 안전성, 테스트 용이성을 유지하면서도 설정 조합을 선언적으로 관리한다.

아키텍처 개요

┌──────────────────────┐
│ Apps (API / Web) │ ── manifest에서 필요한 feature config만 선택
└──────────┬───────────┘

┌──────────▼───────────┐
│ Core Config Layer │ ── 최소 공통 (app/cloud/db/log/auth)
├───────────────────────┤
│ Feature Config Layer │ ── KR/DE 등 도메인별 네임스페이스, 스키마, 로더
└───────────────────────┘

핵심 원칙

  1. Core 축소: 공통적으로 반드시 필요한 네임스페이스만 유지한다.
  2. Feature 모듈화: 각 기능/국가 라이브러리가 자신의 설정 스키마와 로더를 제공한다.
  3. Manifest 조합: 앱은 선언적인 manifest에서 필요한 feature 묶음만 선택한다.
  4. 검증 범위 축소: Joi 검증은 Core + manifest에 포함된 feature 스키마만 대상으로 한다.

계층별 역할

1. Core Layer (libs/core/config)

  • CoreConfigModule는 더 이상 모든 스키마를 알지 않고, ConfigFeatureDefinition 목록만 받아 조합합니다.
  • 제공 기능:
    • 공통 네임스페이스 로딩 (app, cloud, database, logging, auth 등)
    • defineConfigNamespace, composeConfig 헬퍼
    • Config 검증 파이프라인 + Environment initializer
export interface ConfigFeatureDefinition {
namespace: string;
load: ConfigFactory;
schema: Joi.SchemaMap;
providers?: Provider[]; // Nest 토큰/타입 주입용 (선택)
}

export interface ComposedConfigOptions {
serviceName: string;
features: ConfigFeatureDefinition[];
cache?: boolean;
}

export function composeConfig(options: ComposedConfigOptions): ConfigOptions {
return {
serviceName: options.serviceName,
cache: options.cache ?? true,
featureConfigs: options.features,
};
}

2. Feature Layer (libs/feature/<domain>/config)

  • 각 기능 라이브러리는 defineConfigNamespace 헬퍼를 사용해 자신의 네임스페이스를 선언합니다.
  • 예시: libs/feature/kr-care/config/src/lib/krCareConfig.ts
export const krCareConfig = defineConfigNamespace({
namespace: 'krCare',
schema: {
KR_CARE_GATEWAY_URL: Joi.string().uri().required(),
KR_CARE_FEATURE_FLAG: Joi.boolean().default(false),
},
load: () => ({
krCare: {
gatewayUrl: process.env.KR_CARE_GATEWAY_URL,
enabled: process.env.KR_CARE_FEATURE_FLAG === 'true',
},
}),
providers: [
{
provide: KR_CARE_CONFIG,
useFactory: (cfg: ConfigService) => cfg.getNamespace<KrCareConfig>('krCare'),
inject: [ConfigService],
},
],
});

3. App Layer (apps/*/src/app/app-config.manifest.ts)

  • 각 앱은 자신에게 필요한 feature config를 manifest로 선언하고 CoreConfigModule.forRoot에 전달합니다.
import { composeConfig } from '@core/config';
import { krCareConfig } from '@feature/kr-care-config';
import { baseWirConfig } from '@feature/wir-config';

export const dtaWirApiConfig = composeConfig({
serviceName: 'dta-wir-api',
features: [
krCareConfig,
baseWirConfig,
],
});

@Module({
imports: [CoreConfigModule.forRoot(dtaWirApiConfig)],
})
export class AppModule {}

세부 설계 요소

Config 검증

  • Core는 getDefaultSchema()로 공통 스키마만 포함.
  • featureConfigs 배열을 순회하며 schemaJoi.object(feature.schema)로 래핑 후 concat.
  • allowUnknown: true, abortEarly: false 설정을 유지해 편의 제공.

Provider 주입/타입 안전성

  • defineConfigNamespaceproviders를 자동 구성하도록 하고, 필요 시 configInjectionToken(namespace) 헬퍼로 토큰을 생성합니다.
  • 서비스 코드 예시:
@Injectable()
export class KrBenefitService {
constructor(
@Inject(KR_CARE_CONFIG) private readonly krCare: KrCareConfig,
) {}
}

테스트 전략

  • Feature config는 자체 spec에서 ENV 목을 주입하여 로더/스키마를 검증.
  • App 단위 테스트에서는 manifest에 mockConfigNamespace를 추가하여 특정 네임스페이스만 오버라이드 가능.
  • CI에 yarn verify:config <service> 스크립트를 추가하고, manifest 기반으로 필수 ENV 정의서를 생성하여 배포 파이프라인에서 검증한다.

환경 변수 소스 구조

  • 각 애플리케이션은 cloudrun-deploy/{appName}/secret/{deployEnv}/{appName}.json 파일에 실제 런타임에서 사용하는 모든 환경 변수를 선언한다. 예: cloudrun-deploy/dha-sleep-api/secret/dev/dha-sleep-api.json.
  • Manifest에 포함된 feature config는 해당 JSON 파일의 키 집합과 1:1 대응되어야 하며, manifest를 기준으로 누락된 키를 탐지하는 verify:config 스크립트가 필요하다.
  • 국가별/배포환경별 차이는 deployEnv 디렉터리 단계에서 분기하며, 동일 앱이라도 dev/stage/prod 파일을 각각 관리한다.
  • 신규 feature를 추가할 때는 (1) feature config 스키마 정의, (2) manifest에 feature 등록, (3) cloudrun-deploy 경로 내 각 환경 JSON에 필요한 키를 반영하는 순서를 따른다.

현행 네임스페이스 인벤토리

Namespace주요 목적스키마 파일현재 의존 앱제안 레이어향후 소유 모듈/이관 메모
app서비스 메타 정보, 기본 포트/호스트, 토큰/보안 관련 공통 설정app.schema.ts모든 API/WebCore 유지CoreConfig 유지. manifest 기본 포함.
cloudGCP 프로젝트/리전, PubSub, Firestore, Storage 등 클라우드 인프라cloud.schema.ts모든 API/WebCore 유지공통 인프라 의존이므로 Core에 잔류.
databasePostgres/Redis 연결 정보, 스키마/풀 설정database.schema.ts모든 API/WebCore 유지커넥터들이 공통 사용.
logging로그 레벨, BigQuery sink, 알림 설정logging.schema.ts대부분 서비스Core 유지관통 concern.
authOAuth(Kakao) 등 인증 클라이언트 설정auth.schema.ts인증 사용 서비스Core 유지 (당분간)추후 국가별 OAuth가 diverge하면 feature 모듈화 고려.
externalApiOpenAI/Gemini/Sendgrid/Atlassian/결제 등 외부 통합external-api.schema.ts선택적Feature 이관세분화 필요: LLM/Communication/Payment 등 도메인별 config 모듈 작성.
privacyGDPR 보존 정책, 익명화 규칙스키마 없음 (추가 필요)특정 데이터 처리 서비스Feature 이관libs/feature/privacy/config (또는 규제 도메인)으로 이동, 스키마 추가.
vectorstoreVertex AI 벡터스토어/임베딩 설정(스키마 미존재)LLM 기반 기능Feature 이관libs/feature/ai/config 계열로 이동, 스키마 정의 후 manifest opt-in.
notificationNHN 알림톡, 유저 참여 Webhook 등 채널(스키마 미존재)동일 채널 사용하는 앱Feature 이관libs/feature/notification/config 신설.
solPredictionSOL 예측/LLM 튜닝/Rate limit(스키마 미존재)SOL 관련 서비스Feature 이관libs/feature/sol-prediction/config로 이동, LLM config와 dependency 정리.

스키마 파일이 없는 네임스페이스(privacy, vectorstore, notification, solPrediction)는 Feature 레이어로 이동하면서 defineConfigNamespace 기반 스키마를 반드시 추가한다.

인벤토리에서 드러난 액션 아이템

  1. Feature 이관 대상 네임스페이스 4종 + externalApi 세분화를 우선 진행한다.
  2. Core에 남을 5종(app/cloud/database/logging/auth)은 ConfigFeatureDefinition 인터페이스 도입 후에도 기본으로 로드되도록 getDefaultSchema() 구성.
  3. 스키마 미존재 항목은 이관을 위해 먼저 스키마를 작성하거나, 새 Feature 모듈에서 스키마와 로더를 동시에 재정의한다.
  4. cloudrun-deploy/{app}/secret/{env}/{app}.json 파일 중 불필요한 키는 manifest 업데이트와 함께 정리할 계획을 세운다.

Feature 모듈 이관 영향 분석

대상 Namespace주요 소비자 (Nx lib/app)제안 Feature Config 모듈Cloud Run Secret 영향
externalApilibs/feature/auth(이메일 발송), libs/feature/agent-treatment-flow(Agent API), libs/feature/support, libs/feature/sol-prediction-wir(Gemini), apps/dta-wide-mcp, apps/dta-wide-agent-qalibs/feature/integration-llm/config, libs/feature/integration-comm/config, libs/feature/integration-payment/config 등 기능별 분리. Manifest에서 필요한 조합 선택cloudrun-deploy/dta-wide-api/secret/*/*.json, cloudrun-deploy/dta-wide-mcp/secret/*/*.json, cloudrun-deploy/dta-wide-agent-qa/secret/*/*.json, cloudrun-deploy/dha-sleep-api/secret/*/*.json
privacylibs/feature/user-wir, libs/feature/data-export-wirlibs/feature/privacy-wir/config (또는 libs/feature/regulation/config)cloudrun-deploy/dta-wir-api-universal/secret/*/*.json, cloudrun-deploy/dta-wir-agent-studio/secret/*/*.json
vectorstorelibs/core/vectorstore, apps/dta-wide-agent-flow (document store 기능)libs/feature/vectorstore/config (LLM/검색 기능용)cloudrun-deploy/dta-wide-agent-flow/secret/*/*.json
notificationlibs/core/notification, apps/dta-wir-api-universal, libs/feature/agent-board, libs/feature/questionnaire(-wir), libs/feature/sleep(-wir)libs/feature/notification/config (채널별 세분화 가능)cloudrun-deploy/dta-wir-api-universal/secret/*/*.json, cloudrun-deploy/dta-wide-api/secret/*/*.json
solPredictionlibs/feature/sol-prediction-wir, libs/feature/auth(PromptAuth guard)libs/feature/sol-prediction/configcloudrun-deploy/dta-wir-backtest-web/secret/*/*.json, cloudrun-deploy/dta-wide-api/secret/*/*.json
  • 표에 적힌 Cloud Run secret 경로는 실제 존재 여부와 무관하게 반드시 cloudrun-deploy/{app}/secret/{env}/{app}.json 패턴을 따라가야 한다. 예: cloudrun-deploy/dha-sleep-api/secret/dev/dha-sleep-api.json.
  • 각 앱 manifest가 어떤 feature config를 선택했는지 기반으로 verify:config 스크립트가 해당 JSON 파일 키를 검증하게 된다.

구현 현황

  • @feature/config 라이브러리가 추가되어 externalApi, notification, privacy, solPrediction, vectorstore 네임스페이스가 Core에서 분리되었습니다.
  • composeConfig/defineConfigNamespace 헬퍼를 이용해 각 앱(dta-wide-api, dta-wir-api-universal, dta-wide-mcp, dta-wide-agent-qa, dha-sleep-api)이 app-config.manifest.ts를 선언하도록 반영했습니다.
  • 앞으로 신규 기능에서 필요한 설정은 feature-config 또는 각 도메인별 config 라이브러리에 정의하고, 해당 앱 manifest에 명시적으로 추가해야 합니다.

국가별/도메인별 Config 모듈화 가이드

  • 도메인 단위 세분화: 하나의 도메인 안에서도 기능/국가별 요구가 다르면 네임스페이스를 더 잘게 나누십시오. 예를 들어 auth의 경우 auth-base, auth-kakao, auth-google, auth-apple 등으로 분리합니다.
  • 도메인 전용 config 라이브러리: libs/feature/auth/config처럼 도메인별 config 라이브러리를 만들고, 내부에서 각 provider별 defineConfigNamespace를 export 합니다.
  • Manifest 조합: 앱 manifest에서는 자신이 필요로 하는 조각만 골라 넣습니다. 한국 서비스는 [authBaseConfig, authKakaoConfig], 독일 서비스는 [authBaseConfig, authAppleConfig, authGoogleConfig]처럼 구성하면 superset 문제가 사라집니다.
  • Feature Pack 패턴: 반복되는 조합은 authKoreaPack, authGermanyPack과 같이 배열 헬퍼를 만들어 제공할 수 있습니다. Pack은 결국 ConfigFeatureDefinition[]이므로 features: [...authKoreaPack] 형태로 전달하면 됩니다.
  • 검증 및 문서화: 각 Pack과 단일 네임스페이스는 어떤 ENV 키를 요구하는지 README/문서에 기재하고, verify:config 스크립트가 manifest ↔ Cloud Run secret JSON을 검증하도록 합니다.

마이그레이션 플랜

  1. Core 슬림화: 기존 Core에 포함된 도메인 네임스페이스 중 공통이 아닌 항목(solPrediction, notification, privacy 등)을 해당 라이브러리로 이동.
  2. 헬퍼 도입: defineConfigNamespace, composeConfig, configInjectionToken 유틸 구현.
  3. 앱 Manifest 작성: 각 앱에 app-config.manifest.ts 추가 후 CoreConfigModule.forRoot(manifest)로 교체.
  4. ENV/시크릿 재정비: manifest에 포함되지 않은 ENV 키를 서비스 시크릿에서 제거하고, 기능별 시크릿 파일을 정리.
  5. 검증 파이프라인: verify:config 스크립트 추가 및 CI 단계에 포함.
  6. 가이드 업데이트: 신규 기능은 반드시 Feature config 모듈을 통해 ENV 요구사항을 정의하도록 개발 가이드 개정.

기대 효과 및 리스크

  • 효과
    • 서비스별 최소 ENV만 필요 → 배포 환경 설정 부담 감소.
    • 도메인 팀이 독립적으로 설정 계약을 관리하므로 변경 영향 범위 축소.
    • 테스트/로컬 개발에서 필요한 네임스페이스만 손쉽게 목 가능.
  • 리스크 및 대응
    • 초기 분리 작업 시 레거시 키 누락 가능 → manifest 기반 체크리스트로 검증.
    • Feature config가 난립할 수 있음 → libs/feature/<domain>/config 네이밍 규칙과 코드 리뷰 체크리스트에 포함.
    • 공통 스키마 변경이 필요한 경우? Core layer에서만 처리하되, 문서화와 버전 태깅으로 호환성 관리.

다음 단계

  1. CoreConfig 모듈에 feature 정의 인터페이스와 compose 헬퍼 추가.
  2. KR/DE 주요 기능부터 파일을 분리해 시범 적용.
  3. README/개발 가이드에 manifest 패턴과 ENV 검증 플로우를 추가.

이 문서는 환경 구성 책임을 기능 단위로 분리하여 모노레포의 복잡도를 완화하고, 국가별/서비스별 요구사항을 유연하게 수용하기 위한 기준으로 삼습니다.