화요일

GitHub Actions secrets 환경변수 비어있음 해결 — secret not available 원인 분석

🔍 검색 키워드: GitHub Actions secrets undefined, GitHub Actions 환경변수 비어있음, secrets not available 해결, GitHub Actions secret 적용 안 됨, CI 시크릿 설정, GitHub Actions secret empty

증상: 분명히 설정했는데 값이 없다고 한다

GitHub Actions 워크플로우는 돌리면 이런 상황이 생긴다.

Error: API_KEY is undefined
Error: Cannot read properties of undefined (reading 'length')

아니면 시크릿 값이 빈 문자열로 들어오거나, 더 황당하게는 배포가 그냥 조용히 실패한다. Secrets 탭에서 분명히 등록했는데 워크플로우가 못 읽는다.

CI 처음 세팅할 때, 또는 레포를 포크하거나 환경(Environment)을 새로 만들었을 때 이 문제를 자주 만난다.

원인 분류

1. 시크릿 이름 대소문자 불일치

가장 흔한 실수. Secrets UI에서 API_KEY로 등록했는데 워크플로우에서 ${{ secrets.api_key }}로 참조하면 빈 값이 온다. 시크릿은 대소문자 구분한다.

# 잘못된 예
env:
  API_KEY: ${{ secrets.api_key }}  # 실제 이름이 API_KEY면 못 읽음

# 올바른 예
env:
  API_KEY: ${{ secrets.API_KEY }}

2. Environment 시크릿인데 job에 environment 지정 안 함

GitHub에서 환경(Environment)을 별도로 만들어서 거기에 시크릿을 등록했다면, job에서 그 environment를 명시해야 한다. 안 하면 해당 시크릿은 아예 조회 자체가 안 된다.

# 잘못된 예 — environment 지정 없이 env 시크릿 접근 시도
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo ${{ secrets.PROD_DB_URL }}  # production environment의 시크릿이면 빈 값

# 올바른 예
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # ← 이게 있어야 environment 시크릿 접근 가능
    steps:
      - run: echo ${{ secrets.PROD_DB_URL }}

3. Fork PR에서는 시크릿 접근이 차단된다

외부 기여자가 fork해서 올린 PR의 워크플로우는 보안 정책상 시크릿에 접근할 수 없다. pull_request 이벤트 트리거를 쓰면 이 제한이 적용된다.

Warning: Context access might be invalid: secrets

의도적인 제한이다. 악의적인 코드가 PR로 들어와서 시크릿을 탈취하는 걸 막기 위한 것.

4. 시크릿 참조 문법 오류

# 틀린 문법들
${{ secret.API_KEY }}       # secrets가 아니라 secret (오타)
${{ secrets[API_KEY] }}     # 대괄호 안에 따옴표 필요
${{ env.secrets.API_KEY }}  # env와 secrets 혼용

# 올바른 문법
${{ secrets.API_KEY }}
${{ secrets[env.SECRET_NAME] }}  # 동적 키 참조는 이렇게

해결 방법

Step 1. 시크릿 이름 확인

# GitHub CLI로 현재 등록된 시크릿 목록 확인
gh secret list

# environment 시크릿 확인
gh secret list --env production

워크플로우 YAML의 이름과 정확히 일치하� 체크.

Step 2. 시크릿이 실제로 전달되는지 테스트

값을 직접 출력하면 안 된다 (마스킹됨). 대신�길이나 존재 여부를 확인:

steps:
  - name: Check secrets
    run: |
      if [ -z "${{ secrets.API_KEY }}" ]; then
        echo "API_KEY is EMPTY"
      else
        echo "API_KEY is set (length: ${#API_KEY})"
      fi
    env:
      API_KEY: ${{ secrets.API_KEY }}

Step 3. Environment 시크릿 설정

레포 Settings → Environments → 환경 선택 → Environment secrets에서 등록했다면:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # 반드시 명시
    steps:
      - name: Deploy
        env:
          DB_URL: ${{ secrets.DB_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: ./deploy.sh

environment 이름은 Environments 탭에 있는 이름과 정확히 일치해야 한다.

Step 4. Fork PR 시크릿 접근 처리

내부 PR이라면 pull_request_target 이벤트 사용을 고려할 수 있다. 단, pull_request_target은 base 브랜치의 코드를 실행하므로 PR 코드를 checkout해서 실행하면 보안 취약점이 된다.

# 안전한 패턴 — PR 코드는 테스트만 하고, 배포는 main 병합 후에
on:
  pull_request:
    # 시크릿 없이 테스트만
  push:
    branches: [main]
    # 여기서 시크릿 써서 배포

Step 5. Organization 시크릿 접근 권한

조직(organization) 레벨 시크릿은 레포별로 접근 권한이 따로 있다. 조직 Settings → Secrets → 해당 시크릿 → Repository access에서 해당 레포가 포함되어 있는지 확인.

상황별 체크리스트

증상원인조치
시크릿 값이 빈 문자열이름 대소문자 불일치gh secret list로 정확한 이름 확인
environment 시크릿 못 읽음job에 environment 미지정environment: 필드 추가
Fork PR에서만 실패보안 정책으로 차단시크릿 없이 동작하도록 CI 설계 변경
조직 시크릿 못 읽음레포 접근 권한 없음Organization 설정에서 레포 추가
로컬에서는 되는데 CI에서만환경변수 주입 누락env: 블록에서 명시적 주입 확인

자주 하는 실수 — 디버깅 시 값 출력하려다 마스킹에 막히는 경우

# 이렇게 하면 *** 로 마스킹되어 아무 의미 없음
- run: echo ${{ secrets.API_KEY }}

# 디버깅 목적이면 이렇게
- run: |
    echo "Length: ${#MY_SECRET}"
    echo "First char: ${MY_SECRET:0:1}"
  env:
    MY_SECRET: ${{ secrets.API_KEY }}

시크릿 값 자체를 로그에 출력하는 건 GitHub이 자동 마스킹한다. 길이나 첫 글자 정도로 존재 여부 확인하는 게 현실적인 디버깅 방법이다.

실무 팁: 시크릿 관리 패턴

# 공통 시크릿은 레포 레벨에
# 환경별 시크릿(prod DB URL 등)은 environment 레벨에 분리

jobs:
  test:
    runs-on: ubuntu-latest
    # environment 없음 — 레포 레벨 시크릿만 접근
    steps:
      - run: npm test
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}  # 레포 레벨 시크릿

  deploy-prod:
    runs-on: ubuntu-latest
    environment: production   # environment 레벨 시크릿 접근
    needs: test
    steps:
      - run: ./deploy.sh
        env:
          DB_URL: ${{ secrets.DB_URL }}        # production environment 시크릿
          API_KEY: ${{ secrets.API_KEY }}      # production environment 시크릿

시크릿을 환경별로 분리하면 실수로 개발용 키를 프로덕션에 쓰는 사고를 막을 수 있다.

정리

GitHub Actions 시크릿이 안 읽히는 케이스는 이름 대소문자 불일치, environment 미지정, fork PR 보안 정책 세 가지가 대부분을 차지한다. gh secret list로 정확한 이름 확인하고, environment 시크릿이면 job에 environment: 명시하는 것만 체크해도 80%는 해결된다.

관련 글: GitHub Actions Node.js 빌드 실패 해결 — CI 에러 원인과 대처법

HikariCP Connection is not available 해결 — Spring Boot 커넥션 풀 고갈 완벽 분석

🔍 검색 키워드: HikariCP connection is not available, HikariPool request timed out, Spring Boot 커넥션 풀 부족, HikariCP 해결, Spring Boot 데이터베이스 연결 실패, HikariCP maximum-pool-size 설정

증상: 이런 에러 보셨으면 이 글이 맞다

운영 서버에서 갑자기 API가 죽기 시작한다. 로그를 열면 이게 보인다.

Unable to acquire JDBC Connection
HikariPool-1 - Connection is not available, request timed out after 30000ms
(total=10, active=10, idle=0, waiting=23)

total=10, active=10, idle=0, waiting=23 — 커넥션 10개 전부 사용 중이고 23개 요청이 줄 서서 기다리고 있다는 뜻이다. 30초 기다리다가 포기한 것.

트래픽이 갑자기 몰릴 때, 아니면 슬로우 쿼리 하나가 터졌을 때 이 에러가 나온다. Spring Boot 기본 설정을 그대로 쓰다가 프로덕션에서 처음 맞닥뜨리는 경우가 많다.

원인 분류: 고갈의 원인은 크게 세 가지다

1. 풀 사이즈가 애초에 작다

Spring Boot HikariCP 기본값이 maximum-pool-size=10이다. 트래픽이 조금만 몰려도 금방 바닥난다.

2. 슬로우 쿼리로 커넥션이 묶인다

한 번에 처리돼야 할 쿼리가 인덱스 빠진 채로 5초씩 걸리면? 10개 커넥션이 전부 그 느린 쿼리 잡고 늘어진다. 새 요청은 줄 서서 기다리다 timeout.

3. 커넥션 누수 (Connection Leak)

코드에서 커넥션을 받아 쓰고 닫지 않은 경우. 특히 try-catch 안에서 예외 발생 시 close()를 빠뜨렸거나, @Transactional 없이 직접 커넥션 관리할 때 흔히 나온다. 커넥션이 풀로 돌아오지 않으니까 시간이 지날수록 고갈된다.

해결 방법

Step 1. 현재 풀 상태 먼저 파악

로그에서 풀 상태를 보려면 설정 추가:

# application.yml
logging:
  level:
    com.zaxxer.hikari: DEBUG
    com.zaxxer.hikari.HikariConfig: DEBUG

출력되는 로그:

HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=23)

MBean으로 실시간 모니터링도 가능:

spring:
  datasource:
    hikari:
      register-mbeans: true

Step 2. 커넥션 풀 사이즈 조정

무조건 늘리는 게 답이 아니다. HikariCP 공식 권장 공식:

connections = (core_count × 2) + effective_spindle_count

SSD 서버 4코어라면: (4 × 2) + 1 = 9. 생각보다 작다. DB 서버가 받을 수 있는 최대 연결 수도 같이 고려해야 한다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 20       # 트래픽에 맞게
      minimum-idle: 5             # 유휴 최소 유지 수
      connection-timeout: 10000   # 30초 → 10초로 줄여서 빠르게 실패
      idle-timeout: 300000        # 유휴 커넥션 유지 시간 5분
      max-lifetime: 1800000       # 커넥션 최대 수명 30분
      validation-timeout: 5000    # 커넥션 유효성 검사 타임아웃
      leak-detection-threshold: 60000  # 60초 이상 반납 안 되면 누수 경고

connection-timeout을 30초에서 10초로 줄이는 게 핵심이다. 30초씩 기다리다 실패하면 그동안 요청이 쌓이고 상황이 더 나빠진다. 빨리 실패해서 로드밸런서가 다른 인스턴스로 트래픽 보내게 해야 한다.

Step 3. 슬로우 쿼리 잡기

커넥션 풀 늘려도 근본 원인이 슬로우 쿼리면 도로 막힌다.

spring:
  jpa:
    properties:
      hibernate:
        generate_statistics: true
logging:
  level:
    org.hibernate.stat: DEBUG
    org.hibernate.SQL: DEBUG
-- MySQL 슬로우 쿼리 로그 활성화
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;  -- 2초 이상 쿼리 기록

-- 실행 중인 쿼리 확인
SHOW PROCESSLIST;

Step 4. 커넥션 누수 탐지

leak-detection-threshold 설정하면 누수가 있을 때 이런 로그가 찍힌다:

HikariPool-1 - Connection leak detection triggered for connection com.zaxxer.hikari.pool.ProxyConnection

스택 트레이스도 같이 출력되니까 어느 코드에서 커넥션을 안 닫고 있는지 바로 파악 가능.

// 잘못된 코드 — 예외 발생 시 close() 호출 안 됨
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT ...");
conn.close(); // 예외 터지면 여기 안 옴

// 올바른 코드 — try-with-resources 사용
try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT ...")) {
    // 처리
} // 자동으로 close() 호출

상황별 체크리스트

증상원인조치
트래픽 급증 시 발생풀 사이즈 부족maximum-pool-size 증가
특정 기능 호출 후 발생슬로우 쿼리쿼리 최적화 / 인덱스 추가
서버 오랜 시간 운영 후 발생커넥션 누수leak-detection-threshold 설정, 코드 점검
에러 응답이 30초 후에 옴connection-timeout 기본값10초 이하로 줄이기
DB 서버 부하 높음풀 사이즈 과다공식 기반으로 적정값 계산

실무에서 자주 놓치는 것

connection-timeout 줄이기를 무서워한다. "30초면 여유 있지 않냐"고 생각하는데 틀렸다. 요청이 30초씩 대기하면 그 사이에 더 많은 요청이 쌓인다. 10초 안에 실패 처리하고 클라이언트에 503 돌려주는 게 훨씬 낫다.

풀 사이즈를 무조건 크게 잡는다. DB 서버의 max_connections도 같이 봐야 한다. 애플리케이션 인스턴스가 3개인데 인스턴스당 커넥션 50개 잡으면 DB로는 150개 연결이 맺어진다. DB가 먼저 터진다.

개발환경과 운영환경 설정을 같이 쓴다. 개발 때는 minimum-idle=1, maximum-pool-size=5 정도로 작게 쓰고, 운영은 별도 프로파일로 관리하는 게 기본이다.

정리

HikariCP timeout은 대부분 풀 사이즈 부족, 슬로우 쿼리, 커넥션 누수 셋 중 하나다. 로그에 찍히는 (total=N, active=N, idle=0, waiting=M) 패턴 보고 원인부터 분류하고, connection-timeout은 줄여서 빠른 실패 전략 쓰고, 근본 원인을 잡는 순서로 접근하면 된다.

관련 글: Spring Boot UnexpectedRollbackException 해결 — 트랜잭션 롤백 에러 원인 분석

일요일

Python ModuleNotFoundError 해결 — pip install 했는데 왜 못 찾냐

🔍 검색 키워드: ModuleNotFoundError No module named, pip install 했는데 에러, Python 모듈 못 찾는 문제, ImportError No module named 해결, 파이썬 가상환경 모듈 에러, pip install 후 import 에러

pip install pandas 했다. 분명히 잘 됐다. 근데 코드 실행하면 이 에러가 난다.

ModuleNotFoundError: No module named 'pandas'

이걸 처음 보는 사람은 당황하고, 두 번 이상 본 사람은 원인을 알면서도 순간 헷갈린다. 이 에러는 "모듈이 없다"는 게 아니라 "네가 실행하는 Python이 설치된 곳을 못 찾는다"는 뜻이다. 패턴별로 원인과 해결을 정리한다.


핵심 원인: pip와 python이 같은 환경을 보고 있지 않다

이 에러의 90%는 여기서 나온다. 시스템에 Python이 여러 개 있거나, 가상환경이 활성화되지 않은 상태에서 설치하면 발생한다.

먼저 어떤 Python, 어떤 pip를 쓰는지 확인한다:

# 현재 사용 중인 Python 경로 확인
which python
which python3
python --version

# 현재 사용 중인 pip 경로 확인
which pip
which pip3

# pip가 어디에 설치하는지 확인
pip show pandas

pip show pandas를 했는데 나오면 설치는 된 것이다. 근데 python이 다른 경로면 서로 다른 환경이다.


해결 1: python -m pip 를 쓴다 (가장 확실한 방법)

# ❌ 이렇게 쓰면 pip가 어떤 Python에 연결됐는지 불확실
pip install pandas

# ✅ 이렇게 쓰면 실행하는 Python과 100% 같은 환경에 설치
python -m pip install pandas
python3 -m pip install pandas

python -m pip는 현재 실행 중인 Python 인터프리터에 pip를 직접 연결한다. 이 방법이 가장 확실하다.


해결 2: 가상환경 활성화 확인

가상환경(venv)을 쓰면 activate가 되어 있어야 한다. 안 된 상태에서 설치하면 시스템 Python에 설치되고, 가상환경 Python은 못 찾는다.

# 가상환경 생성
python -m venv venv

# 활성화 — macOS/Linux
source venv/bin/activate

# 활성화 — Windows
venv\Scripts\activate

# 활성화 확인 (앞에 (venv) 표시 나옴)
# (venv) user@machine:~$

# 활성화된 상태에서 설치
pip install pandas

# 비활성화
deactivate

IDE(VS Code, PyCharm)를 쓰면 인터프리터 설정도 확인한다.

  • VS Code: Ctrl+Shift+PPython: Select Interpreter → 가상환경 경로 선택
  • PyCharm: SettingsProjectPython Interpreter → 가상환경 선택

해결 3: PyPI 패키지명과 import명이 다른 경우

설치는 됐는데 이름이 달라서 못 찾는 케이스다. 대표적인 예시들:

# 설치명 → import명 (다른 경우)
pip install Pillow          # import PIL
pip install opencv-python   # import cv2
pip install scikit-learn    # import sklearn
pip install python-dotenv   # import dotenv
pip install beautifulsoup4  # import bs4
pip install pyyaml          # import yaml
# ❌ 잘못 쓰는 경우
import Pillow  # ModuleNotFoundError

# ✅ 맞는 import
import PIL
from PIL import Image

해결 4: Python 버전이 여러 개인 환경 (pyenv, conda 등)

# 현재 활성 Python 버전 확인
pyenv version

# 프로젝트 디렉토리에서 로컬 버전 설정
pyenv local 3.11.8

# 해당 버전으로 설치
python -m pip install pandas

conda를 쓰는 경우:

# conda 환경 목록
conda env list

# 환경 활성화
conda activate myenv

# 해당 환경에 설치 (conda 우선, pip는 없을 때만)
conda install pandas
# 또는
pip install pandas

해결 5: 같은 이름의 .py 파일이 있는 경우

프로젝트 폴더에 numpy.py, random.py 같이 표준 라이브러리나 패키지와 같은 이름의 파일을 만들면 충돌한다.

my_project/
├── main.py
├── numpy.py        ← ❌ 이게 문제 (패키지보다 먼저 import됨)
└── utils.py

# main.py에서
import numpy  # 실제 numpy가 아닌 my_project/numpy.py를 로드 → 에러

파일명을 바꿔야 한다. 표준 라이브러리나 외부 패키지와 이름이 겹치지 않도록 한다.


해결 6: __init__.py 누락 (직접 만든 패키지)

my_project/
├── main.py
└── utils/
    ├── helper.py   ← 있음
    └── __init__.py ← ❌ 없으면 에러

# __init__.py 생성 (내용은 비워도 됨)
touch utils/__init__.py

진단 체크리스트

상황 확인 명령어 해결
설치가 됐는지 모르겠다 pip show 패키지명 없으면 python -m pip install
설치됐는데 못 찾는다 which python vs which pip python -m pip install 사용
가상환경 쓰는데 에러 which python이 venv 경로인지 source venv/bin/activate
IDE에서만 에러 IDE 인터프리터 설정 가상환경 Python 경로로 변경
설치명 헷갈린다 PyPI에서 패키지 검색 import명 확인 후 설치
직접 만든 모듈 에러 디렉토리에 __init__.py 있는지 touch 패키지디렉토리/__init__.py

빠른 진단 스크립트

어디에 설치됐는지 한 번에 보고 싶을 때:

import sys

print("Python 실행 경로:", sys.executable)
print("Python 버전:", sys.version)
print("\nPATH 목록:")
for path in sys.path:
    print(" -", path)

이걸 실행해서 나오는 경로들을 확인한다. 내가 설치한 경로가 sys.path에 없으면 Python이 찾을 수가 없다.


정리

ModuleNotFoundError는 모듈 자체가 없는 게 아니라 현재 Python이 보는 경로에 없는 것이다. 순서대로 확인하면 빠르게 해결된다:

  1. python -m pip install 패키지명으로 설치 — pip와 python 경로 일치 보장
  2. 가상환경 활성화 여부 확인 (which python이 venv 경로인지)
  3. 패키지명과 import명이 다른 경우 확인 (Pillow→PIL, opencv-python→cv2 등)
  4. 같은 이름의 .py 파일 충돌 여부 확인
  5. 직접 만든 패키지라면 __init__.py 누락 확인

python -m pip install을 습관화하면 1~2번 문제는 거의 안 만난다.