결론: 비밀정보는 코드가 아니라 .env가 들고, .env는 Git이 들지 않는다
API 키·DB 비밀번호를 소스 코드에 문자열로 적으면, 그 파일이 GitHub 같은 원격 저장소에 올라가는 순간 누구나 볼 수 있는 값이 된다. 한 번이라도 커밋 이력에 남으면 나중에 파일을 지워도 과거 커밋에는 여전히 남아 있다. 해결 방법은 비밀정보를 코드에서 분리해 .env 파일에 두고, 코드는 os.getenv로 그 값을 읽어 오게 하는 것이다. .env 자체는 .gitignore에 등록해 저장소에 올라가지 않게 한다.
왜 .env가 필요한가
이유 | 설명 |
|---|---|
보안 | 비밀번호·API 키를 코드에 하드코딩하지 않아 저장소 노출 위험을 없앤다 |
이식성 | 개발·운영·로컬 환경마다 다른 설정을 코드 수정 없이 .env 교체만으로 전환한다 |
재현성 | 값이 빠진 .env.example만 공유하면 팀원이 같은 구조로 자기 환경을 구성할 수 있다 |
환경변수란 무엇인가
환경변수는 운영체제나 프로그램을 실행하는 셸이 프로세스에 넘겨주는 키-값 쌍이다. Python 프로세스는 이 값을 os.environ으로 읽을 수 있다.
export DB_USER=admin
export DB_PASS=secret123
python3 app.py
$env:DB_USER = "admin"
$env:DB_PASS = "secret123"
python app.py
import os
print(os.getenv("DB_USER")) # admin
이렇게 셸에서 직접 값을 내보내는(export) 방식은 터미널을 새로 열 때마다 반복해야 하고, 값이 많아지면 관리하기 번거롭다. .env 파일은 이 값을 프로젝트 폴더의 파일 하나로 모아 관리하기 위한 관례다.
.env 파일 형식
.env는 한 줄에 KEY=VALUE 형태로 값을 적는 평범한 텍스트 파일이다. #로 시작하는 줄은 주석이고, 값에 공백이나 특수문자가 있을 때만 따옴표를 쓴다.
# 데이터베이스 설정
DB_HOST=localhost
DB_PORT=5432
DB_USER=analyst
DB_PASSWORD=s3cr3t!
DEBUG=True
.env는 프로젝트 루트에 두고, 절대 값 예시가 아닌 실제 비밀번호·키를 채운 상태로 커밋하지 않는다. 이 파일을 커밋하는 순간부터는 이미 새 비밀번호로 바꿔야 하는 상태다.
python-dotenv로 .env 읽기
표준 라이브러리에는 .env를 직접 읽는 기능이 없어 널리 쓰이는 python-dotenv 패키지를 추가로 설치한다. load_dotenv()는 현재 작업 디렉터리부터 상위로 올라가며 .env 파일을 찾아 그 내용을 os.environ에 채워 넣는다.
pip install python-dotenv
import os
from dotenv import load_dotenv
load_dotenv() # 프로젝트 루트의 .env를 찾아 os.environ에 반영
print(os.getenv("DB_HOST"))
print(os.getenv("DB_PORT"), type(os.getenv("DB_PORT")))
print(os.getenv("DB_TIMEOUT")) # 정의하지 않은 키
print(os.getenv("DB_TIMEOUT", "30")) # 기본값 지정
localhost
5432 <class 'str'>
None
30
os.getenv가 돌려주는 값은 항상 문자열이거나 None이다. DB_PORT가 "5432"라는 문자열로 나온다는 점, 정의하지 않은 키는 조용히 None이 된다는 점을 코드가 예상하고 처리해야 한다.
타입 변환과 흔한 함정: 불리언 문자열
환경변수 값은 항상 문자열이므로 정수·불리언으로 쓰려면 직접 변환해야 한다. 특히 bool("False")가 True라는 사실을 놓치기 쉽다. 빈 문자열이 아닌 이상 모든 문자열은 참으로 평가되기 때문이다.
import os
# DEBUG=True 로 설정된 상황을 가정
debug_wrong = bool(os.getenv("DEBUG"))
print("wrong bool:", debug_wrong) # 값이 "False"였어도 True가 나온다
debug_correct = os.getenv("DEBUG", "False").strip().lower() in ("1", "true", "yes")
print("correct bool:", debug_correct)
port = int(os.getenv("DB_PORT", "5432"))
print(port, type(port))
wrong bool: True
correct bool: True
5432 <class 'int'>
불리언 값은 bool(값)이 아니라 알려진 참 문자열 목록과 직접 비교하고, 숫자는 int()·float()로 명시적으로 변환한다. 반복해서 쓰는 프로젝트라면 이런 파싱 규칙을 Pydantic BaseSettings처럼 검증 라이브러리로 한 곳에 모으는 방법도 있으며, 다음 시리즈에서 다룬다.
필수 환경변수 검증하기
필수 값이 빠진 채로 프로그램이 조용히 실행되면 나중에 엉뚱한 곳에서 오류가 난다. 꼭 있어야 하는 값은 시작 시점에 os.environ[키]로 접근해 없으면 곧바로 실패하게 만든다.
import os
from dotenv import load_dotenv
load_dotenv()
try:
api_key = os.environ["API_KEY"]
except KeyError:
raise RuntimeError("환경변수 API_KEY가 설정되지 않았습니다.") from None
Traceback (most recent call last):
...
RuntimeError: 환경변수 API_KEY가 설정되지 않았습니다.
os.environ[키]는 없으면 KeyError를 던지는 반면 os.getenv(키)는 조용히 None을 준다. 없으면 안 되는 값은 environ[]로, 없어도 되는 값은 기본값을 지정한 getenv로 구분해서 쓴다.
.env와 Git을 함께 쓸 때 주의점
.env는 반드시 .gitignore에 등록해 커밋 대상에서 제외한다. 대신 값이 비어 있거나 예시로 채운 .env.example을 함께 커밋해, 팀원이 그 파일을 복사해 자신의 실제 값을 채우게 한다.
.env
.env.local
.env.production
DB_USER=
DB_PASSWORD=
API_KEY=
DEBUG=False
이미 비밀번호가 담긴 .env를 실수로 커밋했다면 .gitignore에 추가하는 것만으로는 부족하다. 그 값은 이미 커밋 이력에 남아 있으므로 해당 비밀번호·키를 즉시 재발급하고, 이력에서 제거하는 별도 절차(예: 저장소 이력 재작성)를 진행한다.
보안을 한 단계 더 강화하는 방법
여러 환경을 나눠 관리해야 하면
.env.dev·.env.prod처럼 파일을 분리하고, 실행 시 어떤 파일을 로드할지 명시적으로 지정한다.CI/CD 파이프라인에서는 파일 대신 GitHub Actions의 Secrets, 클라우드 제공자의 비밀 관리 서비스로 대체하는 경우가 많다. 파일로 다루는 비밀은 로컬 개발 편의를 위한 것이고, 운영 배포는 플랫폼의 비밀 관리 기능을 우선한다.
여러 사람이 .env 자체를 공유해야 하면 dotenv-vault 같은 암호화 도구로 저장소에 안전하게 커밋 가능한 형태로 바꾸는 방법도 있다.
자주 발생하는 문제
.env가 프로젝트 루트가 아닌 다른 폴더에 있으면load_dotenv()가 찾지 못해 값이 모두None이 된다. 경로가 다르면load_dotenv(dotenv_path=경로)로 직접 지정한다.bool(os.getenv("KEY"))은 값이"False"라는 문자열이어도True가 된다. 항상 알려진 참 문자열과 직접 비교한다..env를.gitignore에 등록하지 않고 커밋하면 비밀정보가 저장소 이력에 영구히 남는다. 커밋 전에git status로 스테이징된 파일을 항상 확인한다.필수 환경변수를
os.getenv로만 읽으면 값이 없어도 프로그램이 일단 실행되다가 한참 뒤 엉뚱한 곳에서 실패한다. 필수 값은 시작 시점에 검증한다.환경변수 이름이 겹치면 어느 쪽 값이 실제로 쓰이는지 헷갈린다. 셸에 이미 설정된 환경변수가 있으면 기본적으로 python-dotenv가 그 값을 덮어쓰지 않으므로, 예상과 다른 값이 읽힐 때는 셸의 기존 환경변수부터 확인한다.
다음 글: Python 타입 힌트 입문
설정값을 문자열로 읽어 직접 변환하는 이번 글의 패턴은 프로젝트가 커질수록 검증 코드가 흩어지기 쉽다. 다음 글부터는 시리즈 4 데이터 검증과 코드 품질로 넘어가, 컬렉션 타입과 Optional·Union·Literal 같은 타입 힌트로 값의 형태를 코드에 명시하는 방법을 다룬다.
참고 자료
Python 공식 문서: os.environ / os.getenv — 프로세스 환경변수 접근 API (2026-08-10 확인)
python-dotenv 공식 저장소 — load_dotenv 동작과 .env 탐색 규칙 (2026-08-10 확인)
GitHub Docs: Using secrets in GitHub Actions — CI/CD 환경의 비밀정보 관리 (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…