CI에서 깨졌는데 로컬에서는 왜 된다고 나와요?
- 04 Dec, 2025
CI에서 깨졌는데 로컬에서는 왜 된다고 나와요?
새벽 2시의 슬랙 알람
슬랙이 울렸다. 새벽 2시.
“Jenkins Build #847 Failed”
침대에서 폰 켰다. 빨간 불. 또다.
로컬에서는 분명 다 통과했다. 커밋 푸시 전에 세 번 돌렸다. 전부 초록불.
근데 CI에서는 깨졌다.
이게 벌써 이번 주만 다섯 번째다.
“환경 차이요? 그게 뭔데요?”
신입이 물었다. 어제.
설명했다. 30분.
근데 오늘 또 똑같은 문제로 슬랙이 왔다.
그래서 쓴다. 이 글.
로컬은 네 컴퓨터, CI는 남의 집
로컬에서 테스트 돌린다. MacBook Pro. 16GB 램. Python 3.9.7. Chrome 119.
Jenkins에서 테스트 돌린다. Ubuntu 20.04. 8GB 램. Python 3.9.2. Chrome 118.
다르다.
당연히 다르다.
근데 개발자들은 모른다. “똑같은 코드잖아요.”
코드는 같다. 환경이 다르다.

Python 버전부터 다르다
로컬: Python 3.9.7 Jenkins: Python 3.9.2
“마이너 버전 차이인데요?”
맞다. 근데 dict 순서 보장이 3.9.2에서 미묘하게 달랐다.
테스트가 dict key 순서에 의존하고 있었다.
로컬에서는 통과. CI에서는 실패.
원인 찾는 데 4시간 걸렸다.
크롬 버전도 다르다
Selenium으로 E2E 테스트 돌린다.
로컬에서는 크롬 자동 업데이트 됐다. 119.
Jenkins에서는 Docker 이미지에 박혀 있다. 118.
버튼 클릭 타이밍이 달라졌다.
WebDriverWait 타임아웃이 로컬에서는 충분했다. 3초.
Jenkins에서는 부족했다. 5초 필요.
타임존이 다르다
로컬: Asia/Seoul (UTC+9) Jenkins: UTC
날짜 테스트가 깨졌다.
datetime.now() 썼다.
로컬에서는 오늘. Jenkins에서는 어제.
assert 실패.
가장 흔한 5가지 원인
이제 패턴이 보인다. 7년 하니까.
1. 환경 변수가 없다
로컬에는 .env 파일 있다.
API_KEY=abc123
DB_HOST=localhost
TIMEOUT=30
Jenkins에는 없다.
당연히 없다. 깃에 안 올렸으니까.
테스트가 환경 변수 읽는다. None 나온다. 터진다.
해결책: Jenkins 환경 변수 설정.
Credentials Plugin 쓴다. 시크릿 관리.
근데 매번 까먹는다. 새 변수 추가하면.
체크리스트 만들었다. 커밋 전에 확인.

2. 파일 경로가 절대경로다
테스트 코드에 이렇게 썼다.
test_data = '/Users/jiyeon/project/test_data.json'
로컬에서는 된다. 내 맥북 경로니까.
Jenkins에서는 안 된다. Jenkins 서버에 그 경로 없으니까.
FileNotFoundError.
상대경로 써야 한다.
test_data = os.path.join(os.path.dirname(__file__), 'test_data.json')
이것도 자주 실수한다. 후배들이.
코드 리뷰 때마다 지적한다.
3. 네트워크가 다르다
로컬에서는 회사 내부망 접속 된다. VPN 연결돼 있어서.
Jenkins는 AWS에 있다. 내부망 접속 안 된다.
스테이징 서버 API 호출이 안 된다.
타임아웃 난다. 60초 기다리다가 실패.
해결책: Jenkins에서도 VPN 연결 설정.
또는 테스트용 API 엔드포인트 따로 만들기.
근데 이거 DevOps팀이랑 협의 필요하다. 귀찮다.
4. 캐시가 남아있다
로컬에서는 pytest 캐시 쌓인다.
.pytest_cache/ 폴더.
이전 테스트 결과 기억한다.
Jenkins에서는 매번 clean build.
캐시 없다.
테스트가 캐시에 의존하고 있었다. 몰랐다.
Jenkins에서만 실패.
pytest --cache-clear 로컬에서 돌려봤다.
재현됐다.
5. Docker 컨테이너 리소스 부족
Jenkins에서 Docker 컨테이너로 테스트 돌린다.
메모리 제한 걸려있다. 2GB.
로컬에서는 제한 없다. 16GB 다 쓴다.
Selenium 테스트 10개 동시 실행.
로컬: 문제없음. Jenkins: OOMKilled.
메모리 터졌다.
해결책: 병렬 실행 수 줄이기. 또는 컨테이너 메모리 늘리기.
디버깅 방법론
패턴 알았으니 대응한다.
1단계: 로컬에서 CI 환경 재현
Jenkins에서 쓰는 Docker 이미지 똑같이 받는다.
docker pull jenkins/jenkins:lts
로컬에서 같은 이미지로 컨테이너 띄운다.
docker run -it jenkins/jenkins:lts /bin/bash
그 안에서 테스트 돌린다.
재현되면 환경 문제 확정.

2단계: 환경 변수 출력
테스트 시작할 때 환경 정보 전부 출력한다.
import os
import sys
import platform
print(f"Python: {sys.version}")
print(f"OS: {platform.system()} {platform.release()}")
print(f"ENV: {os.environ}")
로컬 출력이랑 Jenkins 출력 비교한다.
차이 찾는다.
보통 여기서 원인 나온다.
3단계: 단계별 로그 추가
테스트 실패하는 부분 찾는다.
그 앞뒤로 로그 추가한다.
logger.info("Before API call")
response = api.get('/endpoint')
logger.info(f"Response: {response.status_code}")
logger.info(f"Body: {response.text}")
Jenkins 콘솔 로그 본다.
어디서 멈췄는지 안다.
타임아웃인지, 에러인지, 응답이 다른지.
4단계: 환경 통일 자동화
매번 수동으로 맞추기 귀찮다.
자동화한다.
Docker Compose 쓴다.
version: '3'
services:
test:
image: python:3.9.7
environment:
- TZ=Asia/Seoul
- API_KEY=${API_KEY}
volumes:
- .:/app
command: pytest tests/
로컬에서도 이걸로 돌린다.
Jenkins에서도 이걸로 돌린다.
환경 일치.
환경 변수 관리하는 법
제일 골치 아픈 부분이다.
.env 파일 vs Jenkins Credentials
로컬 개발: .env 파일
CI/CD: Jenkins Credentials Plugin
문제: 변수 추가할 때 두 곳 다 업데이트해야 함.
자주 까먹는다.
해결책: 변수 목록 문서화.
README에 필수 환경 변수 리스트 적는다.
## Required Environment Variables
- API_KEY: API 인증 키
- DB_HOST: 데이터베이스 호스트
- TIMEOUT: 테스트 타임아웃 (초)
신입이 보고 설정할 수 있게.
민감 정보 관리
API 키, DB 비밀번호. 깃에 올리면 안 된다.
로컬: .env (gitignore에 추가)
Jenkins: Credentials 저장
테스트 코드에서는 환경 변수로만 접근.
api_key = os.getenv('API_KEY')
if not api_key:
raise ValueError("API_KEY not set")
없으면 즉시 실패. 명확한 에러 메시지.
“근데 Jenkins에서는 어떻게 주입하나요?”
Jenkinsfile에서.
withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) {
sh 'pytest tests/'
}
이렇게.
Docker로 환경 통일하기
가장 확실한 방법.
Dockerfile 작성
테스트 전용 이미지 만든다.
FROM python:3.9.7
# 타임존 설정
ENV TZ=Asia/Seoul
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
# 크롬 설치
RUN apt-get update && apt-get install -y \
wget \
gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \
&& apt-get update \
&& apt-get install -y google-chrome-stable
# 의존성 설치
COPY requirements.txt .
RUN pip install -r requirements.txt
WORKDIR /app
로컬에서도 이걸로 돌린다.
docker build -t test-env .
docker run -v $(pwd):/app test-env pytest
Jenkins에서도 이걸로 돌린다.
docker.build('test-env').inside {
sh 'pytest tests/'
}
환경 일치. 문제 사라짐.
Docker Compose로 전체 스택 띄우기
DB, Redis 필요한 테스트 있다.
로컬에서 직접 설치 귀찮다.
Docker Compose 쓴다.
version: '3'
services:
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: test
redis:
image: redis:6
test:
build: .
depends_on:
- db
- redis
environment:
DB_HOST: db
REDIS_HOST: redis
volumes:
- .:/app
실행.
docker-compose up --abort-on-container-exit test
전체 환경 일관성 있게 띄워짐.
로컬이든 Jenkins든 똑같이.
타임존과 날짜 테스트
자주 무시되는 부분. 근데 자주 터진다.
문제 상황
def test_today():
today = datetime.now().date()
assert get_today_logs() == today
로컬: Asia/Seoul. 2024-01-15 14:00. Jenkins: UTC. 2024-01-15 05:00.
datetime.now() 결과 다르다.
assert 실패.
해결책 1: UTC로 통일
모든 시간을 UTC로.
from datetime import datetime, timezone
def test_today():
today = datetime.now(timezone.utc).date()
assert get_today_logs() == today
서버도 UTC. 테스트도 UTC.
통일.
해결책 2: 환경 변수로 타임존 설정
Docker에서 타임존 주입.
environment:
- TZ=Asia/Seoul
이러면 컨테이너 내부 시간이 서울 시간.
근데 권장은 UTC 통일.
서버 시간은 항상 UTC가 표준이니까.
네트워크 이슈 대응
API 테스트할 때 자주 나온다.
문제: 내부망 접근 불가
로컬: 회사 네트워크. 내부 API 접근 가능. Jenkins: AWS. 내부망 차단.
테스트 실패.
해결책 1: VPN 설정
Jenkins 서버에서 VPN 연결.
근데 복잡하다. DevOps 도움 필요.
해결책 2: 테스트용 공개 엔드포인트
스테이징 서버를 외부 접근 가능하게.
보안팀 승인 받아야 한다.
귀찮다.
해결책 3: Mock 사용
외부 API는 Mock으로.
@patch('requests.get')
def test_api_call(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {'data': 'test'}
result = fetch_data()
assert result == {'data': 'test'}
네트워크 의존성 제거.
가장 안정적.
근데 실제 API 동작은 못 테스트한다.
Trade-off.
실전 체크리스트
커밋 전에 확인한다.
환경 독립성 체크
- 절대경로 사용 안 함
- 환경 변수 의존성 문서화
-
.env.example파일 업데이트 - 로컬 캐시에 의존 안 함
CI 재현 테스트
- Docker로 로컬 재현 성공
- 환경 변수 누락 없음
- 타임존 영향 확인
- 네트워크 의존성 명확화
로그 및 디버깅
- 실패 시 충분한 로그 출력
- 환경 정보 출력 코드 추가
- 타임아웃 값 명시적 설정
이거 지키면 90% 해결된다.
나머지 10%는 경험.
결론
“CI에서 깨졌는데 로컬에서는 돼요.”
이제 안 무섭다.
체계적으로 접근한다.
- 환경 차이 파악
- Docker로 재현
- 로그로 원인 찾기
- 환경 통일 자동화
시간은 걸린다. 근데 한 번 세팅하면 끝.
그 다음부터는 편하다.
신입 후배한테 이 글 링크 보낸다.
다음에 또 물어보면.
새벽 2시 슬랙. 이제 덜 무섭다. 체크리스트 있으니까.
