환경변수는 설정 파일이 아니라 런타임 입력이다

Node.js 서비스에서 데이터베이스 주소, 외부 API URL, 포트, 기능 플래그를 환경변수로 관리하는 일은 일반적입니다. 하지만 환경변수는 문자열이며, 배포 환경마다 값이 다르고 누락되어도 프로세스가 시작될 수 있습니다. 이 특성 때문에 운영 장애는 애플리케이션이 기동한 뒤 특정 요청에서 발생하는 경우가 많습니다. 예를 들어 PORT=0을 의도하지 않았거나, API_TIMEOUT_MS가 빈 문자열이거나, 운영 환경에서만 필수 키가 빠진 상태를 늦게 발견할 수 있습니다.

해결 원칙은 단순합니다. 환경변수를 사용하는 곳마다 직접 process.env를 읽지 말고, 애플리케이션 시작 시 한 번만 읽어서 검증하고 타입을 변환합니다. 이후 코드에서는 검증이 끝난 설정 객체만 사용합니다. 이 방식은 오류를 배포 직후 로그에서 발견하게 만들고, 설정의 이름·기본값·범위를 한곳에 모아 코드 리뷰도 쉽게 합니다.

먼저 필수값, 선택값, 비밀값을 구분한다

필수값은 없으면 서비스가 시작되면 안 되는 값입니다. 데이터베이스 연결 문자열, 운영용 서명 키, 외부 연동의 기본 URL이 여기에 해당합니다. 선택값은 안전한 기본값이 있고 기능 수준만 바뀌는 값입니다. HTTP 포트, 요청 타임아웃, 로그 레벨이 대표적입니다. 비밀값은 로그나 오류 메시지에 원문을 남기지 않아야 합니다. 검증 오류에는 변수 이름만 기록하고 값은 출력하지 않는 규칙을 정해 두는 것이 좋습니다.

개발 환경의 .env 파일은 편의용일 뿐 운영 환경의 진실한 소스가 아닙니다. 컨테이너, CI, 서버의 환경변수가 최종 값을 제공한다고 보고, .env는 로컬에서만 로드하도록 제한합니다. 또한 같은 의미의 변수 이름을 여러 개 두지 말아야 합니다. DATABASE_URL과 DB_URL을 동시에 허용하면 잘못된 우선순위가 또 하나의 장애 원인이 됩니다.

의존성 없이 시작 시 설정을 검증하는 예제

아래 예제는 Node.js의 기본 기능만으로 설정 객체를 만드는 방식입니다. 숫자 형식과 허용 범위를 확인하고, 배포 단계라는 제한된 문자열만 받습니다. 실제 서비스에서는 이 모듈을 가장 먼저 불러온 뒤 다른 모듈에 config를 주입하거나 import해서 사용합니다.

function required(name) {
  const value = process.env[name];
  if (!value || value.trim() === '') {
    throw new Error(`Required environment variable is missing: ${name}`);
  }
  return value;
}

function integer(name, fallback, min, max) {
  const raw = process.env[name] ?? String(fallback);
  const value = Number(raw);
  if (!Number.isInteger(value) || value < min || value > max) {
    throw new Error(`Invalid ${name}; expected integer ${min}..${max}`);
  }
  return value;
}

const nodeEnv = process.env.NODE_ENV ?? 'development';
if (!['development', 'test', 'production'].includes(nodeEnv)) {
  throw new Error('Invalid NODE_ENV');
}

export const config = Object.freeze({
  nodeEnv,
  port: integer('PORT', 3000, 1, 65535),
  apiBaseUrl: required('API_BASE_URL'),
  requestTimeoutMs: integer('REQUEST_TIMEOUT_MS', 5000, 100, 60000),
  databaseUrl: required('DATABASE_URL')
});

HTML 본문에 표시되는 코드에서는 비교 연산자 기호를 &lt;와 &gt;로 이스케이프해야 합니다. 원본 JavaScript 파일에는 일반적인 <와 > 연산자를 사용하면 됩니다. 설정 검증이 실패하면 프로세스를 즉시 종료하도록 엔트리 포인트에서 예외를 잡아 오류 코드와 변수 이름만 기록합니다. 이때 DATABASE_URL처럼 민감할 수 있는 값 전체를 출력하면 안 됩니다.

문자열·불리언·URL 변환에서 자주 생기는 문제

환경변수의 false는 문자열이므로 Boolean(process.env.FEATURE_X)를 사용하면 false도 true가 됩니다. true와 false만 허용하는 전용 변환 함수를 두고, 예상하지 못한 값은 오류로 처리해야 합니다. URL도 단순히 문자열 존재 여부만 보지 말고 URL 생성자로 형식을 확인합니다. 접미사의 공백, http와 https 혼동, 경로 누락은 외부 호출 실패로 이어질 수 있습니다.

function boolean(name, fallback = false) {
  const raw = process.env[name];
  if (raw === undefined) return fallback;
  if (raw === 'true') return true;
  if (raw === 'false') return false;
  throw new Error(`Invalid ${name}; use true or false`);
}

function url(name) {
  const value = required(name);
  try {
    return new URL(value).toString();
  } catch {
    throw new Error(`Invalid URL in ${name}`);
  }
}

설정은 모듈 로드 시점에 고정되는 경우가 많으므로 테스트에서도 주의가 필요합니다. 테스트마다 process.env를 직접 바꾼다면 모듈 캐시 때문에 이전 값이 남을 수 있습니다. 설정 생성 함수를 export하고 입력 객체를 받게 만들면, 테스트는 가짜 환경 객체로 정상·누락·범위 초과 사례를 독립적으로 검증할 수 있습니다. 운영 코드에서는 그 함수에 process.env를 전달하면 됩니다.

배포 전 점검 체크리스트

  • 필수 환경변수는 시작 단계에서 누락 여부를 검사한다.
  • 숫자, 불리언, URL은 문자열 상태로 사용하지 않고 명시적으로 변환한다.
  • 포트와 타임아웃처럼 위험한 값에는 최소·최대 범위를 둔다.
  • 오류 로그에는 변수 이름만 남기고 비밀값 원문은 기록하지 않는다.
  • 로컬 .env와 운영 배포 설정의 변수 이름 및 기본값을 정기적으로 비교한다.
  • 테스트에 누락값과 잘못된 형식의 설정 사례를 포함한다.

환경변수 검증은 복잡한 기능이 아니라 서비스의 입력 경계를 정리하는 작업입니다. 시작 시점에 실패하도록 만들고, 변환된 설정 객체만 사용하며, 비밀값을 로그에서 분리하면 배포 환경 차이로 생기는 장애를 크게 줄일 수 있습니다.