Loading the catalog…
Loading the catalog…
프로그래밍을 처음 배울 때 환경변수는 보통 코드 밖에 비밀번호나 API 키를 저장하는 변수라고 배운다. const databaseUrl = process.env.DATABASE_URL; 애플리케이션은 DATABASE_URL 이라는 이름으로 외부에서 전달된 데이터베이스 주소를 읽는다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 개발하면 값을 코드 밖으로 옮기는 것보다 더 많은 판단이 필요하다. 개발·테스트·스테이징·운영 환경은 어떤 설정을 다르게 가져야 하는가? 숫자나 불리언 설정을 문자열에서 어떻게 변환할 것인가? 필수 설정이 없을 때 기본값을 사용할 것인가, 실행을 중단할 것인가? 환경변수에 저장한 값은 자동으로 비밀이 되는가? 프론트엔드에 공개해도 되는 설정과 서버에만 있어야 하는 설정을 어떻게 구분하는가? 운영 중 자주 바뀌는 정책도 환경변수로 관리해야 하는가? 비밀값을 교체하면 실행 중인 애플리케이션은 언제 새 값을 읽는가? 환경변수는 비밀값을 저장하는 변수가 아니다. 환경변수는 같은 코드를 서로 다른 실행 환경에서 사용할 수 있도록 코드와 배포별 설정을 분리하는 경계다. 같은 기능이라도 실행 환경에 따라 연결 대상은 달라진다 크리스가 온라인 강의 서비스를 개발한다고 생각해 보자. 서비스는 강의 정보 데이터베이스, 동영상 저장소, 이메일 발송 시스템에 연결된다. 처음에는 개발 환경의 주소를 코드에 직접 작성할 수 있다. const databaseUrl = "postgres://localhost:5432/course_dev"; const courseAssetBaseUrl = "http://localhost:4000/videos"; 이 코드는 크리스의 컴퓨터에서는 동작한다. 그러나 테스트 환경과 운영 환경에서는 다른 데이터베이스와 저장소를 사용해야 한다. 환경마다 조건문을 추가하는 방식도 생각할 수 있다. const databaseUrl = process.env.NODE_ENV === "production" ? "postgres://production-db/course" : "postgres://localhost:5432/course_dev"; 이제 운영 인프라 주소가 코드 안에 들어왔다. 새로운 스테이징 환경이 생기면 조건을 더 추가해야 하고, 연결 주소를 변경하려면 코드를 다시 수정하고 배포해야 한다. 실행 환경에 따라 달라지는 값은 코드 바깥에서 전달하는 편이 낫다. const databaseUrl = process.env.DATABASE_URL; const courseAssetBaseUrl = process.env.COURSE_ASSET_BASE_URL; 같은 애플리케이션 코드가 개발 환경에서는 개발용 데이터베이스를, 운영 환경에서는 운영용 데이터베이스를 사용한다. 달라지는 것은 코드가 아니라 배포 시 전달되는 설정이다. 같은 애플리케이션 코드 + 개발 환경의 설정 → 개발 서비스 + 운영 환경의 설정 → 운영 서비스 코드와 환경 설정을 분리하면 환경을 추가하거나 연결 대상을 변경할 때 비즈니스 로직까지 수정하지 않아도 된다. 코드 밖에서 들어온 설정도 검증 전까지 신뢰할 수 없다 환경변수는 애플리케이션 안에서 직접 선언한 값이 아니다. 배포 시스템, 운영체제, 컨테이너 설정, CI/CD 파이프라인 등 프로세스 바깥에서 들어온다. 따라서 환경변수도 외부 입력이며 검증 전까지 신뢰할 수 없다. 다음 코드는 타입이 맞는 것처럼 보이지만 실제로는 그렇지 않다. const port = process.env.PORT; const trialEnabled = Boolean(process.env.ENABLE_TRIAL_COURSE); Node.js에서 환경변수 값은 문자열 또는 undefined 로 다뤄진다. PORT=4000 은 숫자 4000 이 아니라 문자열 "4000" 이다. Boolean("false") 도 true 가 된다. 비어 있지 않은 문자열이기 때문이다. 애플리케이션이 시작될 때 형식과 필수 여부를 검증할 수 있다. import { z } from "zod"; const EnvironmentSchema = z.object({ NODE_ENV: z.enum([ "development", "test", "staging", "production", ]), DATABASE_URL: z.string().min(1), COURSE_ASSET_BASE_URL: z.string().url(), EMAIL_API_KEY: z.string().min(1), PORT: z.coerce .number() .int() .min(1) .max(65535) .default(3000), ENABLE_TRIAL_COURSE: z .enum(["true", "false"]) .default("false") .transform((value) => value === "true"), }); const result = EnvironmentSchema.safeParse(process.env); if (!result.success) { throw new Error( `Invalid environment configuration: ${ result.error.message }` ); } export const environment = result.data; 이 코드는 문자열을 애플리케이션이 사용할 타입으로 변환하고, 필수 설정이 누락되거나 허용하지 않은 값이 들어오면 시작을 중단한다. 설정 오류를 첫 번째 사용자 요청이 들어온 뒤 발견하는 것보다 애플리케이션 시작 단계에서 발견하는 편이 낫다. 기본값은 편리하지만 잘못된 환경을 숨길 수도 있다 모든 설정에 기본값을 제공하면 애플리케이션은 쉽게 실행된다. const databaseUrl = process.env.DATABASE_URL ?? "postgres://localhost:5432/course_dev"; 개발 환경에서는 편리할 수 있다. 하지만 운영 배포에서 DATABASE_URL 전달을 빠뜨려도 애플리케이션이 실행될 수 있다. 운영 서버가 로컬 데이터베이스에 연결을 시도하거나, 더 위험하게는 의도하지 않은 공용 데이터베이스를 사용할 수 있다. 기본값의 적절성은 설정의 책임에 따라 달라진다. 설정 기본값 판단 로컬 개발 서버 포트 안전한 기본값을 둘 수 있다 로그 상세 수준 환경에 맞는 보수적 기본값을 검토할 수 있다 운영 데이터베이스 주소 누락 시 실행을 중단하는 편이 낫다 이메일 서비스 인증 키 누락 시 관련 기능 또는 실행을 명확히 실패시켜야 한다 결제·서명·암호화 키 임의의 기본값을 사용해서는 안 된다 선택적 실험 기능 비활성화 상태를 기본값으로 둘 수 있다 필수 인프라와 보안 설정에 조용한 기본값을 두면 배포 오류가 정상 실행처럼 보인다. function requireEnvironmentValue( name: string ): string { const value = process.env[name]; if (!value) { throw new Error( `Missing required environment variable: ${name}` ); } return value; } const databaseUrl = requireEnvironmentValue("DATABASE_URL"); 이 코드는 중요한 설정이 없을 때 추측하지 않는다. 애플리케이션을 명확히 실패시켜 잘못된 배포를 조기에 발견하게 한다. 모든 변경 가능한 값을 환경변수에 넣어야 하는 것은 아니다 환경변수는 코드 밖의 값이지만, 코드 밖에 있는 모든 값을 환경변수로 관리해야 한다는 뜻은 아니다. 온라인 강의 서비스에는 여러 종류의 설정이 존재한다. 값의 성격 적절한 관리 위치 배포마다 다른 데이터베이스 주소 환경변수 또는 배포 설정 서버가 사용하는 외부 서비스 인증 정보 비밀값 관리 시스템 강의 동영상 저장소 이름 환경변수 또는 배포 설정 무료 체험 기간 정책 데이터베이스 또는 운영 설정 시스템 사용자별 자막 언어 사용자 데이터베이스 일부 사용자에게만 공개할 기능 기능 플래그 시스템 강의 수료 조건 도메인 로직 또는 운영 정책 저장소 애플리케이션 모듈 연결 방식 코드 브라우저에 공개할 서비스 이름 공개 프론트엔드 설정 무료 체험 기간이 14일 에서 7일 로 자주 바뀌고 운영자가 직접 변경해야 한다면 환경변수는 불편한 저장 위치다. const trialPeriodDays = Number( process.env.TRIAL_PERIOD_DAYS ); 이 방식에서는 정책을 변경할 때 애플리케이션을 다시 시작하거나 배포해야 할 수 있다. 누가 언제 값을 바꿨는지 기록하기도 어렵다. 운영 정책으로 다루면 책임이 더 명확해진다. const trialPolicy = await coursePolicyRepository.getTrialPolicy(); const trialEndsAt = addDays( enrollment.startedAt, trialPolicy.periodDays ); 이 코드는 체험 기간을 배포 설정이 아니라 서비스가 관리하는 정책에서 읽는다. 변경 이력, 관리자 권한, 적용 시점도 함께 설계할 수 있다. 환경변수에 적합한 기준은 “변경 가능한가”가 아니다. 코드 배포와 독립적이면서도 실행 환경별로 달라지는 값인가를 먼저 물어야 한다. 환경변수라는 이름이 비밀을 보장하지는 않는다 API 키를 소스 코드에 직접 작성하는 것보다 환경변수로 분리하는 편이 낫다. const emailApiKey = process.env.EMAIL_API_KEY; 그러나 환경변수에 저장했다는 사실만으로 값이 안전해지는 것은 아니다. 환경변수는 다음 위치에서 노출될 수 있다. 배포 플랫폼의 설정 화면 잘못 구성된 CI/CD 로그 오류 보고서와 진단 정보 프로세스 정보나 디버깅 도구 전체 환경 객체를 출력한 애플리케이션 로그 접근 권한이 지나치게 넓은 운영 계정 프론트엔드 빌드 결과물 다음 로그는 설정 확인에 도움이 되는 것처럼 보이지만 비밀값 전체를 남길 수 있다. console.log("Environment:", process.env); 로그에는 데이터베이스 비밀번호, API 키, 서명 키가 포함될 수 있다. 환경 전체를 기록해서는 안 된다. 필요한 비밀값은 전용 시스템에서 관리하고 최소 권한으로 애플리케이션에 전달할 수 있다. const emailApiKey = await secretStore.get( "course-service/email-api-key" ); 비밀값 관리 시스템은 저장 위치만 제공하는 것이 아니다. 시스템에 따라 접근 제어, 감사 기록, 버전 관리, 교체, 만료와 폐기 과정을 지원할 수 있다. 환경변수는 비밀값의 전달 수단이 될 수 있지만 완전한 비밀값 관리 체계는 아니다. 브라우저에 전달된 환경변수는 더 이상 서버 비밀값이 아니다 프론트엔드 코드에서도 빌드 도구를 통해 환경 설정을 참조할 수 있다. const publicApiBaseUrl = import.meta.env.VITE_API_BASE_URL; 이 값이 브라우저용 번들에 포함되면 사용자가 내려받은 JavaScript에서 확인할 수 있다. 변수 이름에 API_KEY 나 SECRET 을 붙여도 비밀이 되지 않는다. 프론트엔드에 공개할 수 있는 설정과 서버에만 존재해야 하는 설정을 구분해야 한다. type PublicCourseConfig = { apiBaseUrl: string; supportEmail: string; }; type ServerCourseConfig = { databaseUrl: string; emailApiKey: string; sessionSigningKey: string; }; PublicCourseConfig 는 사용자가 확인해도 되는 값이다. ServerCourseConfig 는 브라우저 번들, HTML, 공개 API 응답에 포함되면 안 된다. 다음과 같은 구조는 피해야 한다. export const config = { publicApiBaseUrl: process.env.PUBLIC_API_BASE_URL, emailApiKey: process.env.PUBLIC_EMAIL_API_KEY, }; PUBLIC_ 같은 접두사는 값을 보호하지 않는다. 일부 빌드 시스템에서는 오히려 해당 값을 클라이언트 번들에 포함하라는 의미로 사용된다. 비밀값이 필요한 작업은 서버에서 수행하고 프론트엔드는 필요한 결과만 받아야 한다. 빌드 시점의 설정과 실행 시점의 설정은 생명주기가 다르다 서버 애플리케이션은 보통 프로세스가 시작될 때 환경변수를 읽는다. 프론트엔드 빌드는 다른 생명주기를 가질 수 있다. 빌드 도구가 환경변수 값을 JavaScript 파일에 삽입하면 그 값은 빌드 결과물에 고정된다. const apiBaseUrl = import.meta.env.VITE_API_BASE_URL; 이 코드가 빌드 시 "https://staging-api.example.com" 으로 치환되었다면, 같은 파일을 운영 환경으로 옮겨도 운영 API 주소로 자동 변경되지 않는다. 따라서 배포 방식을 먼저 정해야 한다. 환경마다 프론트엔드를 별도로 빌드할 것인가? 하나의 빌드 결과물을 여러 환경으로 승격할 것인가? 실행 시 공개 설정을 별도 엔드포인트에서 불러올 것인가? 설정 변경에 재빌드, 재배포, 프로세스 재시작 중 무엇이 필요한가? 하나의 결과물을 여러 환경에서 사용해야 한다면 공개 런타임 설정을 따로 제공할 수 있다. type RuntimePublicConfig = { apiBaseUrl: string; }; const runtimeConfig: RuntimePublicConfig = await fetch("/runtime-config.json") .then((response) => response.json()); 브라우저는 실행 시점에 현재 환경의 공개 설정을 불러온다. 이 파일에는 누구나 볼 수 있는 값만 포함해야 한다. 설정의 저장 위치뿐 아니라 값을 읽는 시점도 설계해야 한다. .env 파일은 로컬 개발 도구이지 비밀 저장소가 아니다 크리스는 로컬 개발을 위해 .env 파일을 사용할 수 있다. DATABASE_URL=postgres://localhost:5432/course_dev COURSE_ASSET_BASE_URL=http://localhost:4000/videos ENABLE_TRIAL_COURSE=true 이 파일은 반복해서 환경변수를 입력하지 않아도 되게 해준다. 그러나 .env 라는 이름이 파일을 암호화하거나 접근을 제한하지는 않는다. 실제 비밀값이 들어 있는 파일은 버전 관리 대상에서 제외해야 한다. .env .env.local .env.*.local 다만 .gitignore 는 앞으로의 추가를 막을 뿐 이미 커밋된 비밀값을 없애지 않는다. 비밀값이 저장소에 들어갔다면 기록에서 파일을 지우는 것만으로 끝내지 말고 해당 값을 폐기하고 새 값으로 교체해야 한다. 필요한 설정 이름은 예제 파일로 공유할 수 있다. # .env.example DATABASE_URL= COURSE_ASSET_BASE_URL= EMAIL_API_KEY= ENABLE_TRIAL_COURSE=false 예제 파일은 필요한 키와 안전한 예시만 설명한다. 실제 인증 정보는 포함하지 않는다. .env 파일은 로컬 환경변수를 편리하게 전달하는 형식이다. 운영 비밀값의 Source of Truth로 취급해서는 안 된다. 설정은 한 번 검증한 뒤 타입이 있는 객체로 전달하는 편이 낫다 애플리케이션 곳곳에서 process.env 를 직접 읽으면 어떤 설정이 필요한지 파악하기 어렵다. // course-service.ts const trialEnabled = process.env.ENABLE_TRIAL_COURSE === "true"; // email-service.ts const emailApiKey = process.env.EMAIL_API_KEY; // upload-service.ts const assetUrl = process.env.COURSE_ASSET_BASE_URL; 각 모듈이 문자열 변환과 누락 처리를 제각각 수행하게 된다. 테스트도 실행 중인 컴퓨터의 환경에 의존할 수 있다. 시작 단계에서 한 번 읽고 검증한 설정을 명시적으로 전달할 수 있다. export type CourseServiceConfig = { trialEnabled: boolean; courseAssetBaseUrl: string; emailApiKey: string; }; export function createCourseService( config: CourseServiceConfig ) { return new CourseService({ trialEnabled: config.trialEnabled, courseAssetBaseUrl: config.courseAssetBaseUrl, emailApiKey: config.emailApiKey, }); } 이제 CourseService 는 환경변수의 존재를 알 필요가 없다. 이미 검증된 애플리케이션 설정만 받는다. 테스트에서도 필요한 설정을 직접 전달할 수 있다. const courseService = createCourseService({ trialEnabled: true, courseAssetBaseUrl: "https://assets.test.example", emailApiKey: "test-key", }); 테스트 결과가 개발자의 컴퓨터에 우연히 설정된 환경변수에 좌우되지 않는다. 환경변수는 프로세스 경계에서 읽고, 내부에서는 의미가 분명한 타입으로 사용하는 편이 낫다. 설정의 Source of Truth와 전달된 복사본을 구분해야 한다 실행 중인 process.env 는 설정의 영구적인 원본이 아닐 수 있다. 배포 설정 또는 비밀값 관리 시스템 ↓ 프로세스에 환경변수로 전달 ↓ 시작 시 검증 ↓ 타입이 있는 애플리케이션 설정 ↓ 서비스 코드가 사용 이를 흐름으로 표현하면 다음과 같다. flowchart TD A[배포 설정] --> C[프로세스 환경] B[비밀값 관리 시스템] --> C C --> D[시작 시 검증] D --> E[타입이 있는 설정] E --> F[서비스 코드] 각 단계의 책임은 다르다. 배포 설정은 환경별 연결 주소와 실행 옵션의 Source of Truth가 될 수 있다. 비밀값 관리 시스템은 인증 정보와 키의 Source of Truth가 될 수 있다. 프로세스 환경은 현재 실행에 전달된 값의 복사본이다. 검증된 설정 객체는 애플리케이션 내부에서 사용하는 표현이다. 서비스 코드는 설정을 소비하지만 원본을 소유하지 않는다. 배포 플랫폼에서 값을 변경해도 이미 실행 중인 프로세스가 자동으로 새 값을 읽는다고 가정해서는 안 된다. 많은 환경에서는 재시작이나 재배포가 필요하다. 반대로 실행 중 즉시 바뀌어야 하는 운영 정책이라면 환경변수보다 데이터베이스나 동적 설정 시스템이 더 적합할 수 있다. 환경을 분리한다는 것은 이름만 다르게 붙이는 일이 아니다 개발, 테스트, 스테이징, 운영 환경에 서로 다른 이름을 붙였더라도 같은 인증 정보와 저장소를 공유하면 실제 경계는 약하다. 온라인 강의 서비스는 환경별로 다음 자원을 분리할 수 있다. 데이터베이스와 사용자 데이터 강의 동영상 저장소 이메일 발송 계정과 수신 대상 외부 결제·분석 시스템의 프로젝트 세션 서명 키 로그와 모니터링 대상 접근 가능한 운영 계정 예를 들어 스테이징 환경에서 실제 수강생에게 이메일이 발송되지 않도록 수신자를 제한할 수 있다. const recipient = environment.NODE_ENV === "production" ? student.email : environment.TEST_EMAIL_RECIPIENT; 이 코드는 비운영 환경의 이메일을 지정된 테스트 주소로 보낸다. 실제 구현에서는 발송 서비스 자체도 테스트 계정이나 샌드박스로 분리하는 편이 더 안전하다. 환경 분리는 설정값의 차이뿐 아니라 장애와 실수의 영향 범위를 제한하는 설계다. 비밀값은 생성부터 폐기까지 생명주기를 가진다 이메일 API 키를 환경변수로 전달하는 것만으로 비밀값 관리가 끝나지는 않는다. 비밀값에는 다음 과정이 존재한다. 안전한 방식으로 생성한다. 필요한 서비스만 읽을 수 있도록 권한을 제한한다. 전달 과정과 사용 기록을 관리한다. 정기적으로 또는 사고 발생 시 교체한다. 이전 값을 일정한 전환 기간 뒤 폐기한다. 노출 여부를 탐지하고 대응한다. 키를 교체할 때 두 값을 잠시 함께 허용해야 하는 시
What RADAR observed and classified to build this opportunity. It is what the source published, not a verification that the offer is still active.
환경변수는 비밀값을 저장하는 변수가 아니다. 프로그래밍을 처음 배울 때 환경변수는 보통 코드 밖에 비밀번호나 API 키를 저장하는 변수라고 배운다. const databaseUrl = process.env.DATABASE_URL; 애플리케이션은 DATABASE_URL 이라는 이름으로 외부에서 전달된 데이터베이스 주소를 읽는다. 기본 개념을 이해하기에는 충분한 설명이다. 하지만 실제 서비스를 개발하면 값을 코드 밖으로 옮기는 것보다 더 많은 판단이 필요하다. 개발·테스트·스테이징·운영 환경은 어떤 설정을 다르게 가져야 하는가? 숫자나 불리언 설정을 문자열에서 어떻게 변환할 것인가? 필수 설정이 없을 때 기본값을 사용할 것인가, 실행을 중단할 것인가? 환경변수에 저장한 값은 자동으로 비밀이 되는가?…
Open source