Develop

[Spring Boot] 게시판 - 16. JWT 적용 + 스테이징 배포 트러블슈팅 기록 본문

백엔드/게시판 만들기

[Spring Boot] 게시판 - 16. JWT 적용 + 스테이징 배포 트러블슈팅 기록

230801 2025. 12. 17. 06:06

안녕하세요 ~ .ᐟ
어제 게시판 프로젝트에 JWT 인증을 붙였는데 각종 테스트가 다 터지고, 스테이징 서버에 오류까지 나서 하루동안 잡았습니다 ㅎㅎ;;

결론부터 말하면, JWT 설정 + Docker + MySQL + GitHub Actions가 전부 한 번씩 폭발하면서 몇 시간 동안 디버깅을 했고, 그 과정을 기록으로 남겨두려고 합니다.

 

 

 

이번 글에서 다루는 내용

  • JWT 만료 시간 환경변수 바인딩 실패 (Failed to bind properties)
  • Docker / docker-compose 환경변수 전달 이슈
  • EC2 스테이징과 로컬에서 DB 호스트/스키마가 달라서 생긴 문제
  • MySQL 8 인증 방식(Public Key Retrieval is not allowed) 이슈
  • GitHub Actions, EC2, docker-compose, .env 를 어떻게 맞췄는지 에 대해서 다뤄보겠습니다.

 



1. JWT 만료 시간 바인딩 실패 – ${JWT_ACCESS_EXPIRATION} 그대로 남는 문제

  • JWT 관련 설정을 환경변수로 빼서 관리하려고 아래처럼 설정했습니다.
# application.properties / application-dev.properties 
jwt.secret=${JWT_SECRET}
jwt.access-token-validity=${JWT_ACCESS_EXPIRATION}
jwt.refresh-token-validity=${JWT_REFRESH_EXPIRATION}

 

 

  • 스테이징에 배포하자마자, 애플리케이션이 바로 죽으면서 아래 에러가 터졌습니다.
Failed to bind properties under 'jwt.access-token-validity' to java.lang.Long:

Property: jwt.access-token-validity
Value: "${JWT_ACCESS_EXPIRATION}"
Reason: failed to convert java.lang.String to java.lang.Long
  • Spring 이 JWT_ACCESS_EXPIRATION 환경변수를 못 찾으면, ${JWT_ACCESS_EXPIRATION} 을 치환하지 못하고 “그대로 문자열”로 둡니다.
  • jwt.access-token-validity 타입이 Long 이라서, "${JWT_ACCESS_EXPIRATION}" 을 Long 으로 파싱하다가 NumberFormatException 이 발생했습니다.
  • 처음에는 “GitHub Secrets 값이 숫자가 아니어서 그런가?” 를 의심했는데, 실제로는 숫자(예: 3600000)가 잘 들어가 있었습니다.
    진짜 문제는 환경변수가 애플리케이션까지 안 들어오고 있었다는 것이었습니다...

 



2. Dockerfile vs docker-compose: 프로필이 꼬여서 prod 설정이 같이 떠버린 문제

  • JWT 값은 분명 .env 에 있었는데 계속 ${JWT_ACCESS_EXPIRATION} 으로 남아 있어서 로그를 자세히 보니, 에러 Origin 이 이렇게 찍혀 있었습니다.
Origin: class path resource [application-prod.properties]

 

 

 

  • 하지만 docker-compose 에서는 이렇게 dev로 띄우고 있었습니다.
app:
  environment:
    SPRING_PROFILES_ACTIVE: dev

 

 

  • 문제는 Dockerfile 에 있었습니다.
# Dockerfile
ENV SPRING_PROFILES_ACTIVE=prod

ENTRYPOINT ["java",
  "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}",
  "-jar",
  "app.jar"]

 

  • 이미지 레벨에서 SPRING_PROFILES_ACTIVE=prod 를 강제하고 있었기 때문에,
  • compose 에서 dev를 줘도 prod 설정이 같이 섞여서 올라왔고,
  • prod 쪽에서도 ${JWT_ACCESS_EXPIRATION} 이 치환되지 않아 계속 바인딩 에러가 났었습니다.


해결 방법

  • Dockerfile 에서 프로필 강제 제거
# 삭제
# ENV SPRING_PROFILES_ACTIVE=prod

ENTRYPOINT ["java",
  "-Djava.security.egd=file:/dev/./urandom",
  "-jar",
  "app.jar"]

 

  • 실제 프로필 결정은 docker-compose, docker run 에서만 하도록 정리했습니다.
    → 스테이징은 SPRING_PROFILES_ACTIVE=dev, 프로덕션은 prod 로 각각 명시
  • 이걸 정리하고 나니, JWT 바인딩 에러는 사라지고 dev 프로필 기준 로그가 정상적으로 찍히기 시작했습니다.

 



3. docker-compose 와 .env – “같은 디렉터리, 같은 파일”을 보고 있는지

  • JWT 에러가 사라진 뒤에도, 컨테이너 안에서 env | grep JWT 를 찍어보면 값이 안 보이는 문제가 있었습니다.
# EC2 호스트
cat .env
# → JWT_ACCESS_EXPIRATION=3600000 (잘 들어있음)

docker exec -it board-app env | grep JWT
# → (아무것도 안 찍힘)

 

 

  • env_file 을 사용해서 docker-compose.yml 과 같은 경로의 `.env` 를 읽도록 설정했습니다.
services:
  app:
    image: ghcr.io/0802222/board_r/board-staging:latest
    container_name: board-app
    env_file:
      - .env
    environment:
      SPRING_PROFILES_ACTIVE: dev

 

 

  • .env 설정 
JDBC_DATABASE_URL=jdbc:mysql://mysql:3306/board_rio?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
JDBC_DATABASE_USERNAME=    # DB 유저
JDBC_DATABASE_PASSWORD=     # DB 비밀번호

JWT_SECRET=...                       # 실제 시크릿 값
JWT_ACCESS_EXPIRATION=3600000        # 1시간 (ms 기준 예시)
JWT_REFRESH_EXPIRATION=604800000     # 7일


이 구조로 맞춘 뒤, docker exec -it board-app env | grep JWT 에서 세 값이 정상적으로 찍히는 것을 확인했습니다.

 

 



4. DB 연결 지옥: host.docker.internal, localhost, mysql, 스키마 이름

  • JWT 와 프로필이 정리되자, 이번에는 DB 쪽에서 에러가 쏟아졌습니다.

 

1. localhost vs mysql

  • 중간에 로컬에서 테스트하면서 localhost 를 쓴 설정이 남아 있었습니다.
JDBC_DATABASE_URL=jdbc:mysql://localhost/board_rio...

 

 

 

  • 해결
    • 컨테이너 입장에서 localhost 는 “자기 자신(board-app 컨테이너)” 이라, MySQL 이 떠 있는 board-mysql 로 연결되지 않습니다. 이때는 Connection refused 가 발생했습니다.
JDBC_DATABASE_URL=jdbc:mysql://mysql:3306/board_rio?...

 

 

 

2. DB 스키마 이름 불일치

  • IntelliJ DB 뷰에서는 스키마 이름이 board_rio 인데,
  • MySQL 컨테이너 env 는 MYSQL_DATABASE=board_db, JDBC URL 도 .../board_db 로 되어 있었습니다.

 

  • 컨테이너 로그
Unknown database 'board_rio'

 

 

  • MySQL 컨테이너 env
environment:
  MYSQL_ROOT_PASSWORD: 비밀번호
  MYSQL_DATABASE: board_rio
  MYSQL_USER: ID
  MYSQL_PASSWORD: 비밀번호

 

 

  • JDBC URL
JDBC_DATABASE_URL=jdbc:mysql://mysql:3306/board_rio?...

 

 

  • 또는, DB를 수동으로 만들어 주고 싶을 때
CREATE DATABASE IF NOT EXISTS board_rio
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER IF NOT EXISTS '만들 ID'@'%' IDENTIFIED BY '비밀번호';
GRANT ALL PRIVILEGES ON board_rio.* TO '만들 ID'@'%';
FLUSH PRIVILEGES;

 



5. MySQL 8 + Spring Boot: Public Key Retrieval is not allowed

  • DB 이름/호스트까지 맞추고 나니, 이번에는 이런 에러가 나왔습니다.
  • MySQL 8 기본 인증 방식이 `caching_sha2_password` 라서, 클라이언트가 서버의 공개키를 가져오는 설정이 맞지 않으면 이 에러가 발생합니다.
java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed



해결 방법 1 – JDBC 옵션 추가

  • JDBC URL 뒤에 옵션을 추가했습니다.
  • 이렇게 하고 나서야 HikariCP 가 정상적으로 커넥션을 맺고, JPA 가 스키마를 생성하기 시작했습니다.
JDBC_DATABASE_URL=jdbc:mysql://mysql:3306/board_rio?useSSL=false&
  serverTimezone=Asia/Seoul&
  characterEncoding=UTF-8&
  allowPublicKeyRetrieval=true



해결 방법 2 – 유저를 mysql_native_password 로 변경 (대안)

  • 조금 더 보수적으로 가려면, 아래처럼 유저 플러그인을 바꾸는 방법도 있습니다.
  • 이 경우 allowPublicKeyRetrieval=true 없이도 연결되는 경우가 많습니다.
ALTER USER '만든 ID'@'%' IDENTIFIED WITH mysql_native_password BY '비밀번호';
FLUSH PRIVILEGES;

 




6. GitHub Actions, EC2, compose, 애플리케이션을 “한 세트”로 맞추기

  • 이번 트러블슈팅을 통해 얻은 가장 큰 교훈은 'JWT, DB 설정은 네 군데가 모두 일관되게 맞아야 한다.' 입니다...
더보기
  • GitHub Actions Secrets
    • STAGING_DB_URL, STAGING_DB_USERNAME, STAGING_DB_PASSWORD
    • JWT_SECRET, JWT_ACCESS_EXPIRATION, JWT_REFRESH_EXPIRATION …
  • deploy-staging.yml
    • 이 시크릿들을 받아서 EC2 .env 파일에 JDBC_*, JWT_* 로 써준다.
  • EC2 의 .env + docker-compose.yml
    • .env 의 값이 compose env_file 과 environment 에서 기대하는 키와 정확히 매칭
    • 컨테이너 안에서 env | grep JDBC, env | grep JWT 로 검증
  • 애플리케이션 설정 (application.properties, application-dev.properties)
    • spring.datasource.url=${JDBC_DATABASE_URL}
    • spring.datasource.username=${JDBC_DATABASE_USERNAME}
    • jwt.access-token-validity=${JWT_ACCESS_EXPIRATION} …

 

Secrets → GitHub Actions env → EC2 .env → 컨테이너 env 이 네 축이 제대로 맞춰지자..
로컬 docker compose (포트만 3307로 조정), EC2 스테이징 서버, GitHub Actions CD 파이프라인 세 군데 모두 동일한 방식으로 서버가 기동되고, /actuator/health 도 안정적으로 통과하기 시작했습니다.

 

 


 

느낀 점

  • DB 관련 이슈는 항상 JDBC URL / 유저 / 비밀번호 / 스키마 이름 / 호스트 이름 5종 세트를 같이 본다.
  • Dockerfile 에서 프로필/환경을 강제하지 말고, 실행 환경 쪽(docker-compose, docker run, k8s)에서 결정하게 두는 게 훨씬 낫다.
  • 막혔을 때는 “도커 안에서 env 찍어보기”, “도커 안에서 실제 DB 호스트로 붙어보기” 두 가지만 해도 절반은 해결된다.
  • 이번 경험 덕분에 “환경변수/인프라 꼬임”에 대한 감도가 조금은 올라간 것 같고, 다음에 비슷한 구성을 짤 때 훨씬 덜 헤매지 않을 수 있을 것 같습니다.