Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- 네트워킹데이
- zsh theme
- 오블완
- 터미널
- mycli
- MySQL
- 동적 프로그래밍 방법
- 분할정복 방법
- table status
- 맥
- mysql 표 출력
- spring boot
- VI
- mysql 표
- 인프런워밍업클럽
- 인프런
- cs
- CS스터디
- Pager
- 데이크스트라
- oh-my-zsh
- 스터디2기
- 알고리즘
- 오일러 경로
- 이진탐색
- 티스토리챌린지
- 욕심쟁이 방법
- 순차탐색
- Less
- zsh
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 16. JWT 적용 + 스테이징 배포 트러블슈팅 기록 본문
안녕하세요 ~ .ᐟ
어제 게시판 프로젝트에 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 호스트로 붙어보기” 두 가지만 해도 절반은 해결된다.
- 이번 경험 덕분에 “환경변수/인프라 꼬임”에 대한 감도가 조금은 올라간 것 같고, 다음에 비슷한 구성을 짤 때 훨씬 덜 헤매지 않을 수 있을 것 같습니다.

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 18. 이메일 인증 구현 (0) | 2025.12.19 |
|---|---|
| [Spring Boot] 게시판 - 17. JWT 인증에 권한 관리 더하기 - @PreAuthorize로 메서드 레벨 보안 구현 (0) | 2025.12.18 |
| [Spring Boot] 게시판 - 15. JWT 인증 구현하기 (Token 기반으로 전환) (0) | 2025.12.16 |
| [Spring Boot] 게시판 - 14. Spring Security 로그인 구현 (0) | 2025.12.14 |
| [Spring Boot] 게시판 - 13. 코드 리팩토링과 배포 파이프라인 개선 (0) | 2025.12.11 |