월요일

Python SSL 인증서 에러 완전 정복: CERTIFICATE_VERIFY_FAILED 원인과 해결법

🔍 검색 키워드: CERTIFICATE_VERIFY_FAILED · Python SSL 에러 · ssl.SSLCertVerificationError · Python requests SSL 오류 · macOS Python SSL · Python urllib SSL 인증서 오류

Python으로 외부 API 호출하거나 크롤링하다 보면 이 에러 한 번쯤은 만난다.

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

이거 처음 보면 당황스럽다. 에러 메시지만 봐서는 뭐가 문제인지 감도 안 온다. 원인부터 단계별 해결까지 정리한다.


왜 이 에러가 나는가

Python이 HTTPS 요청을 보낼 때 서버의 SSL 인증서를 검증한다. 이 과정에서 신뢰할 수 있는 루트 인증서(CA) 목록을 참조하는데, 여기에 서버 인증서 체인이 없으면 에러가 난다.

주요 원인은 크게 세 가지다.

  • Python 자체 CA 번들이 오래됨 — macOS에서 Python 3.6+ 공식 배포판은 시스템 인증서를 쓰지 않고 번들로 따로 관리하는데, 이게 업데이트 안 되면 발생한다.
  • 회사 네트워크의 프록시/방화벽 — 기업 환경에서 HTTPS 트래픽을 자체 인증서로 중간에서 가로채는(MITM) 경우, Python이 그 인증서를 모른다.
  • 자체 서명(self-signed) 인증서를 가진 서버 — 내부 개발 서버나 테스트 환경에서 자주 발생.

상황별 체크리스트

상황원인권장 해결책
macOS + Python 공식 설치번들 CA 미업데이트Install Certificates.command 실행
회사 내부 네트워크기업 프록시 인증서기업 CA 인증서 추가
내부 개발 서버self-signed 인증서verify=인증서경로 지정
Docker/CI 환경CA 번들 미포함certifi 패키지 + 환경변수 설정
갑자기 발생 (예전엔 됐는데)Python 또는 OS 업그레이드certifi 재설치

레벨별 해결 방법

초보자 — 일단 돌아가게 만들기 (비추천)

개발 중에 빠르게 확인만 할 때 쓰는 방법이다. 절대 프로덕션에 쓰면 안 된다.

import requests

# SSL 검증 비활성화 — 개발용으로만!
response = requests.get("https://example.com", verify=False)

중급자 — certifi로 CA 번들 업데이트

가장 권장하는 방법이다. certifi는 Mozilla가 관리하는 신뢰할 수 있는 CA 목록을 Python에서 쓸 수 있게 패키징한 라이브러리다.

pip install --upgrade certifi
import requests
import certifi

response = requests.get("https://example.com", verify=certifi.where())
print(response.status_code)

환경변수로 전역 적용 (추천)

# Linux/macOS
export SSL_CERT_FILE=$(python -c "import certifi; print(certifi.where())")
export REQUESTS_CA_BUNDLE=$(python -c "import certifi; print(certifi.where())")

macOS 전용 — Install Certificates.command 실행

# 해당 버전의 Install Certificates 스크립트 실행 (버전에 맞게 경로 수정)
open /Applications/Python\ 3.12/Install\ Certificates.command

고급자 — 기업 CA 인증서 추가

import requests

# 단일 파일
response = requests.get("https://internal.company.com", verify="/path/to/company-ca.crt")
import certifi

with open("/path/to/company-ca.crt", "r") as f:
    company_cert = f.read()

with open(certifi.where(), "a") as bundle:
    bundle.write("\n" + company_cert)

Docker/CI 환경

FROM python:3.12-slim

RUN pip install certifi

ENV SSL_CERT_FILE=/usr/local/lib/python3.12/site-packages/certifi/cacert.pem
ENV REQUESTS_CA_BUNDLE=/usr/local/lib/python3.12/site-packages/certifi/cacert.pem
# GitHub Actions
- name: Fix SSL certificates
  run: |
    pip install --upgrade certifi
    echo "SSL_CERT_FILE=$(python -c 'import certifi; print(certifi.where())')" >> $GITHUB_ENV
    echo "REQUESTS_CA_BUNDLE=$(python -c 'import certifi; print(certifi.where())')" >> $GITHUB_ENV

urllib / aiohttp 사용자 참고

import ssl, urllib.request, certifi

ctx = ssl.create_default_context(cafile=certifi.where())
req = urllib.request.Request("https://example.com")
with urllib.request.urlopen(req, context=ctx) as response:
    data = response.read()
import aiohttp, ssl, certifi

async def fetch(url):
    ssl_ctx = ssl.create_default_context(cafile=certifi.where())
    connector = aiohttp.TCPConnector(ssl=ssl_ctx)
    async with aiohttp.ClientSession(connector=connector) as session:
        async with session.get(url) as response:
            return await response.text()

절대 하면 안 되는 것

⚠️ 프로덕션에서 이런 코드 보이면 반드시 수정해야 한다.
# ❌ 절대 하지 말 것
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

# ❌ 이것도 마찬가지
requests.get(url, verify=False)

# ❌ 환경변수로 전체 비활성화
# PYTHONHTTPSVERIFY=0

SSL 검증을 끄는 순간 중간자 공격(MITM)에 완전히 노출된다. 개발 편의를 위해 껐다가 프로덕션에 그대로 배포되는 사고가 실제로 발생한다.


정리

우선순위해결책상황
1순위Install Certificates.commandmacOS + 공식 Python 설치
2순위pip install --upgrade certifi + 환경변수대부분의 환경
3순위기업 CA 인증서 추가회사 내부 네트워크
4순위verify=False로컬 개발 일시적 확인 (프로덕션 절대 불가)
💡 실무에서 이 에러를 자주 만나는 패턴은 Python 버전 업그레이드 직후, 또는 새 팀원이 회사 환경 세팅할 때다. 팀 위키에 이 내용 정리해두면 반복 질문 많이 줄어든다.

Redis 연결 에러 완전 정복: ECONNREFUSED 127.0.0.1:6379 트러블슈팅

🔍 검색 키워드: redis connection refused, redis ECONNREFUSED 6379, redis 연결 안됨, ioredis 연결 에러, spring boot redis 연결 실패, node redis ECONNREFUSED, docker redis 연결 에러

Redis 붙이다가 처음 보는 에러 아니다. 누구나 한 번쯤은 밟는다.

Error: connect ECONNREFUSED 127.0.0.1:6379

이거 뜨면 일단 당황하지 말고 순서대로 확인하면 금방 해결된다. 원인은 대부분 세 가지 중 하나다.


에러가 뜨는 주요 상황

상황에러 메시지
Node.js (ioredis)[ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
Node.js (node-redis)Error: Redis connection to 127.0.0.1:6379 failed - connect ECONNREFUSED
Spring BootUnable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException
Python (redis-py)redis.exceptions.ConnectionError: Error 111 connecting to localhost:6379.
Docker 컨테이너 간connect ECONNREFUSED 127.0.0.1:6379 (컨테이너 내부에서 호스트 Redis 접근 시도)

레벨 1: 입문자 — Redis가 켜져 있나요?

가장 흔한 원인. Redis 서버가 꺼져 있으면 당연히 붙을 수 없다.

Redis 실행 상태 확인

# 프로세스 확인
ps aux | grep redis-server

# 포트 리스닝 확인
ss -tlnp | grep 6379

# systemd 기반 (Ubuntu/CentOS)
sudo systemctl status redis

Redis 직접 연결 테스트

redis-cli ping
# 정상이면: PONG
# 실패하면: Could not connect to Redis at 127.0.0.1:6379: Connection refused

Redis 실행 방법

# systemd로 시작
sudo systemctl start redis
sudo systemctl enable redis   # 부팅 시 자동 시작

# 백그라운드 실행
redis-server --daemonize yes

레벨 2: 실무자 — 포트/바인딩/방화벽 확인

Redis는 켜져 있는데 연결이 안 되면 이쪽을 본다.

bind 설정 문제

/etc/redis/redis.conf 기본 설정:

# 기본값: 127.0.0.1만 허용 (로컬호스트 전용)
bind 127.0.0.1

# 외부 접속 허용하려면 (주의: 보안 설정 필수)
bind 0.0.0.0

# 변경 후 재시작
sudo systemctl restart redis

방화벽 확인

# UFW (Ubuntu)
sudo ufw status
sudo ufw allow 6379

# 외부에서 포트 테스트
nc -zv <서버IP> 6379

requirepass 설정 시 인증 필요

redis-cli -a yourpassword ping

# 또는 연결 후 AUTH
redis-cli
> AUTH yourpassword
> PING

레벨 3: 고급 — Docker, Kubernetes 환경

이게 제일 헷갈린다. 컨테이너 안에서 127.0.0.1:6379컨테이너 자신을 가리킨다.

Docker Compose로 Redis 연결

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    environment:
      - REDIS_HOST=redis      # 127.0.0.1이 아니라 서비스명!
      - REDIS_PORT=6379
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
// Node.js - 잘못된 예
const redis = new Redis({ host: '127.0.0.1', port: 6379 });

// 올바른 예
const redis = new Redis({
  host: process.env.REDIS_HOST || 'redis',
  port: parseInt(process.env.REDIS_PORT || '6379'),
});

호스트 머신의 Redis에 접근하는 경우

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - REDIS_HOST=host.docker.internal

언어별 연결 코드 및 에러 핸들링

Node.js — ioredis

const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || undefined,
  retryStrategy(times) {
    return Math.min(times * 50, 2000);
  },
  maxRetriesPerRequest: 3,
});

redis.on('error', (err) => console.error('Redis 연결 에러:', err.message));
redis.on('connect', () => console.log('Redis 연결 성공'));

Python — redis-py

import redis, os

r = redis.Redis(
    host=os.getenv('REDIS_HOST', '127.0.0.1'),
    port=int(os.getenv('REDIS_PORT', 6379)),
    password=os.getenv('REDIS_PASSWORD'),
    decode_responses=True,
    socket_connect_timeout=5,
    retry_on_timeout=True,
)

try:
    r.ping()
    print("Redis 연결 성공")
except redis.exceptions.ConnectionError as e:
    print(f"Redis 연결 실패: {e}")

Spring Boot — application.yml

spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      timeout: 5000ms
      lettuce:
        pool:
          max-active: 10
          max-idle: 10
          min-idle: 2

원인별 체크리스트

체크 항목확인 방법조치
Redis 서버 실행 중?ps aux | grep redissystemctl start redis
포트 리스닝 중?ss -tlnp | grep 6379포트 충돌 확인
bind 설정 맞음?redis.conf 확인bind 0.0.0.0
방화벽 열려 있음?ufw status포트 허용
Docker 환경?컨테이너 여부 확인서비스명으로 host 변경
인증 필요?requirepass 설정 확인password 파라미터 추가
TLS 사용 중?Redis 6.0+ 설정 확인tls:// 스킴 및 인증서 설정
원격 서버?네트워크 경로 확인VPN, 보안그룹 확인

자주 하는 실수 TOP 3

1. Docker에서 localhost 씀
컨테이너 안에서 localhost는 컨테이너 자신이다. 다른 컨테이너의 Redis에 붙으려면 서비스명을 써야 한다.

2. 환경변수 안 쓰고 하드코딩
로컬에서 127.0.0.1로 하드코딩해두고 스테이징/프로덕션에 그대로 올리면 터진다. 처음부터 환경변수로 빼두자.

3. Redis 안 뜨고 앱부터 뜸
depends_on은 컨테이너 시작 순서만 보장하지 Redis 준비를 보장하지 않는다. healthcheck를 써야 한다.

redis:
  image: redis:7-alpine
  healthcheck:
    test: ["CMD", "redis-cli", "ping"]
    interval: 10s
    timeout: 5s
    retries: 5

app:
  depends_on:
    redis:
      condition: service_healthy

마무리

Redis 연결 에러의 90%는 위 체크리스트로 해결된다. 나머지 10%는 TLS, 클러스터 모드, Sentinel 설정 같은 고급 주제인데 그건 따로 다루겠다.

에러 메시지를 봤을 때 "Redis가 켜져 있냐 → 주소/포트가 맞냐 → 네트워크가 열려 있냐" 이 순서로만 확인해도 대부분 잡힌다.

금요일

npm install이 안 된다고? 원인 파악 없이 --force 치지 마라

npm install 에러 npm ERR! code ERESOLVE node_modules 삭제 후 재설치 npm ci 차이 package-lock.json 충돌 peer dependency 에러

프로젝트 클론하고 npm install 한 번에 되면 그날은 운이 좋은 날이다. 실무에서 이게 한 번에 되는 경우가 얼마나 되냐면, 팀이 클수록, 프로젝트 오래될수록 확률이 줄어든다.

문제는 에러 메시지를 제대로 읽지 않고 npm install --force 또는 npm install --legacy-peer-deps를 무지성으로 치는 경우다. 이러면 당장은 되는 것처럼 보이지만 나중에 런타임에서 이상한 에러로 돌아온다. 진짜 실무자는 에러 메시지를 읽는다.

에러 메시지별 원인과 해결

ERESOLVE: peer dependency 충돌

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: myapp@1.0.0
npm ERR! Found: react@18.2.0
npm ERR!   react@"^18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.0" from some-library@2.1.0

이 에러는 some-library가 React 17을 요구하는데 프로젝트에는 React 18이 깔려 있다는 뜻이다. npm 7 버전부터 peer dependency를 엄격하게 검사하기 시작해서 이 에러가 갑자기 늘었다.

선택지는 세 가지다.

1. 라이브러리를 업데이트한다 (가장 좋음)

npm install some-library@latest

해당 라이브러리가 최신 버전에서 React 18을 지원하면 끝난다. 이게 제일 먼저 시도해야 할 옵션이다.

2. --legacy-peer-deps 옵션 (차선책)

npm install --legacy-peer-deps

npm 6 시절 방식으로 peer dependency를 그냥 무시하고 설치한다. 대부분의 경우 문제없이 동작하지만, 진짜 비호환 라이브러리가 섞이면 런타임 에러가 난다. 팀 전체가 이 옵션을 공유한다면 .npmrc에 박아두는 게 낫다.

# .npmrc
legacy-peer-deps=true

3. --force는 마지막 수단

npm install --force

--force는 캐시도 무시하고 버전 충돌도 무시하고 그냥 밀어붙인다. 개발 의존성이나 급할 때 쓰는 거고, CI/CD에서 이걸 쓰고 있다면 뭔가 잘못된 거다.

ENOENT: node_modules 꼬임

npm ERR! code ENOENT
npm ERR! syscall open
npm ERR! path /project/node_modules/.package-lock.json
npm ERR! errno -2
npm ERR! enoent ENOENT: no such file or directory

node_modules가 중간에 망가진 경우다. 삭제하고 다시 설치하면 된다.

# node_modules 통째로 삭제
rm -rf node_modules package-lock.json

# 클린 설치
npm install

Windows라면:

Remove-Item -Recurse -Force node_modules
Remove-Item package-lock.json
npm install

그냥 node_modules만 지우고 재설치하면 되는데, package-lock.json도 같이 지우는 게 나을 때가 있다. package-lock.json이 실제 package.json과 어긋나 있는 경우에 이 에러가 나기도 해서다. 단, package-lock.json을 지우면 의존성 버전이 최신으로 바뀔 수 있으니 팀 공유 프로젝트에선 조심해야 한다.

EACCES: 권한 문제

npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/lib/node_modules
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied

글로벌 설치할 때 자주 나온다. sudo npm install -g로 해결했다면 잠깐은 괜찮지만, 이 방식이 습관되면 나중에 권한 문제가 더 복잡해진다. 올바른 방법은 npm 기본 디렉토리를 사용자 홈으로 옮기는 거다.

# npm 글로벌 디렉토리를 홈 폴더로 변경
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'

# PATH에 추가 (~/.zshrc 또는 ~/.bashrc)
export PATH=~/.npm-global/bin:$PATH

# 적용
source ~/.zshrc

ETIMEDOUT / ECONNRESET: 네트워크 이슈

npm ERR! code ETIMEDOUT
npm ERR! errno ETIMEDOUT
npm ERR! network request to https://registry.npmjs.org/lodash failed

회사 네트워크나 VPN 환경에서 자주 나온다. 몇 가지 확인 포인트:

# npm 레지스트리 확인
npm config get registry

# 회사 사설 레지스트리 쓰고 있다면
npm config set registry https://your-company-registry.com/

# 원복
npm config set registry https://registry.npmjs.org/

# 프록시 환경이면
npm config set proxy http://proxy.company.com:8080
npm config set https-proxy http://proxy.company.com:8080

타임아웃 에러는 단순히 npm 서버 응답이 느린 경우도 있어서 재시도 먼저 해본다. 그래도 계속 나오면 레지스트리 설정 확인.

npm install vs npm ci — 이 차이 모르는 사람 많다

CI/CD 파이프라인에서 npm install을 쓰고 있다면 npm ci로 바꿔라.

구분npm installnpm ci
package-lock.json 없으면새로 생성에러로 중단
버전 범위(^, ~)최신 버전으로 설치 가능lock 파일 버전 그대로
node_modules있으면 그대로 두고 업데이트무조건 지우고 재설치
용도개발 환경CI/CD, 배포

npm installpackage.json의 버전 범위 안에서 최신 버전을 가져올 수 있다. "react": "^18.0.0"이면 18.x 최신을 잡는다는 얘기다. CI에서 이걸 쓰면 빌드할 때마다 버전이 달라질 수 있다. npm cipackage-lock.json에 적힌 정확한 버전만 설치한다. 재현 가능한 빌드를 원한다면 CI에서 npm ci가 맞다.

자주 보는 실수 모음

package-lock.json을 .gitignore에 넣는 경우

가끔 이렇게 된 프로젝트가 있다. package-lock.json은 반드시 커밋해야 한다. 이게 없으면 팀원마다 설치되는 패키지 버전이 달라지고, "내 로컬에선 되는데 왜 CI가 터지냐"는 상황이 만들어진다.

npm 버전이 팀마다 다른 경우

프로젝트 루트에 .nvmrcpackage.jsonengines 필드로 버전 명시해두는 습관을 들이자.

{
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  }
}

yarn/pnpm 섞어쓰기

package-lock.json(npm), yarn.lock(yarn), pnpm-lock.yaml(pnpm)이 동시에 존재하는 프로젝트가 있다. 이 상태로 팀에서 사람마다 다른 패키지 매니저 쓰면 lock 파일 충돌 지옥이 된다. package.jsonpackageManager 필드로 통일해두자.

{
  "packageManager": "npm@10.2.0"
}

정리

npm 에러는 대부분 세 가지 중 하나다: peer dependency 충돌, node_modules 꼬임, 네트워크/권한 문제. 에러 메시지 첫 줄에 나오는 code를 읽으면 원인이 나온다. --force는 진짜 마지막 수단이고, CI에선 npm ci 써라.