시작 절차와 규칙은 CONTRIBUTING.md를 따릅니다. 이 문서는 application.yml 구성, 환경 프로파일, 비밀값 주입의 기준입니다. 운영 배포·주입 계약은 infra/docs/backend-conventions.md를 따릅니다.
- 비밀값을 저장소에 넣지 않습니다. 저장소는 public입니다. 비밀번호·토큰·키는 환경변수로 주입받습니다.
- 환경에 따라 달라지는 값(접속 주소·자격증명)은 프로파일 또는 환경변수로 분리하고, 코드에 하드코딩하지 않습니다.
- 기본 동작은 환경 독립적으로 두고, 환경별 차이만 프로파일이 덮습니다.
| 파일 | 역할 |
|---|---|
application.yml |
환경 독립 기본 설정. 어느 환경에서나 동일한 값 |
application-local.yml |
로컬 개발 override (로컬 Compose DB/Redis) |
application-prod.yml |
운영 override (클러스터 주소, 주입 자격증명) |
- 활성 프로파일은
SPRING_PROFILES_ACTIVE환경변수로 선택합니다. - 프로파일 파일에는 그 환경에서만 다른 값만 둡니다. 공통값을 프로파일마다 복제하지 않습니다.
어느 환경에서나 같은 값은 여기에 둡니다.
- context path는
/api/core로 고정합니다. 컨트롤러 매핑에 다시 쓰지 않습니다(API 규약, infra/backend-conventions). - Hibernate는
ddl-auto=validate입니다. 스키마는 Flyway가 관리합니다(데이터베이스 규약). open-in-view=false입니다. 기본값true는 서비스 계층 밖에서도 영속성 컨텍스트를 열어 두어, DTO로 변환하기 전에 지연 로딩이 일어나 N+1이 조용히 발생합니다. 트랜잭션 경계를service가 갖는다는 계층 규칙과도 맞지 않습니다. 엔티티 밖에서 연관을 읽어야 하면fetch join이나 전용 조회 메서드로 명시합니다.- Actuator는 필수이며
health와prometheus만 노출합니다. 둘 다 필수입니다 —DeploymentContractTests가 두 경로를 모두 검증하고, 어긋나면 배포와 모니터링이 함께 깨집니다. - readiness 그룹은
readinessState,db입니다. Kubernetes probe는 집계/health가 아니라/health/liveness·/health/readiness를 씁니다.probes.enabled만 켜면 두 그룹의 구성원이 애플리케이션 내부 상태뿐이어서 DB가 죽어도 Ready로 남습니다.include는 기본 구성원을 대체하므로readinessState를 함께 적습니다 — 빼면 기동 완료 전에도 UP이 됩니다.redis는 넣지 않고 liveness 그룹은 기본값을 유지합니다(외부 의존성을 넣으면 DB 순단이 Pod 재시작으로 번집니다). 인프라와 합의한 운영 정책이며 근거와 감수하는 점은 BD-28에 있습니다.
server:
port: 8080
servlet:
context-path: /api/core
spring:
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
management:
endpoints:
web:
exposure:
include: health,prometheus
endpoint:
health:
probes:
enabled: true
group:
readiness:
include: readinessState,db비밀값만 Kubernetes Secret으로 봉인하고, 주소·DB 이름·사용자명은 비밀이 아니므로 GitOps values에 평문으로 둡니다 — 값이 무엇인지 매니페스트를 읽어서 알 수 있어야 하고, Secret으로 옮기면 그 값이 어디서 오는지 추적할 수 없게 됩니다.
Team-PinLog/infra가 실제로 주입하는 이름은 Spring Boot 표준 relaxed-binding 이름 그대로입니다(infra/apps/prod/back/values.yaml):
env:
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://postgres:5432/pinlog
- name: SPRING_DATASOURCE_USERNAME
value: pinlog
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef: {name: postgres-credentials, key: password}
- name: SPRING_DATA_REDIS_HOST
value: redis
- name: SPRING_DATA_REDIS_PORT
value: "6379"application.yml에는 spring.datasource.*·spring.data.redis.*를 아예 적지 않습니다. 위 다섯 개가 실제 k8s 환경변수로 주입되고, Spring Boot의 표준 이름 자동 바인딩이 그대로 spring.datasource.url 등으로 매핑하기 때문입니다 — ${...} placeholder도, 리터럴 기본값도 필요 없습니다. SPRING_DATASOURCE_PASSWORD만 postgres-credentials Secret에서 오고 나머지 네 개는 평문 GitOps 값입니다. 새 비밀번호·API 키가 필요하면 저장소에 넣지 말고 인프라 담당자에게 요청합니다. (이전엔 이 값들을 application-prod.yml에 리터럴로 다시 적어 두었으나, OS 환경변수가 profile yaml보다 우선순위가 높아 애초에 무시되고 있었다 — BD-50.)
정정 (2026-08-03): 이 절은 한동안 "환경변수로 주입되는 건
DB_PASSWORD하나뿐, url·username·host는 yaml 리터럴"이라고 적혀 있었으나, 그 이름·계약 모두 실제 배포(infra/apps/prod/back/values.yaml)와 달랐다. 실제 계약은docs(팀 공용) 레포의docs/static/12_배포_변수_및_Secret_표준.md와 일치하며, 위 내용이 그것으로 교체한 결과다.infra/docs/backend-conventions.md§5도 같은 이유로 낡아서 인프라 담당자에게 별도로 전달했다.
DB_PASSWORD 외에 아래가 필요합니다. 위 §5의 datasource 계약과 별개로, 이들은 back-owner-secrets에 봉인돼 envFrom으로 주입됩니다(Jira 작업). 허용 키 집합은 infra/policy/sealedsecrets/back-prod.yaml이 규정하므로, 새 키가 필요하면 저장소에 넣지 말고 인프라 담당자에게 요청해 그 집합에 더합니다.
| 변수 | 필수 여부 | 없으면 |
|---|---|---|
JWT_PRIVATE_KEY |
운영 필수 | 운영 프로파일은 기동 실패. 로컬·테스트는 임시 키쌍 생성 |
PINLOG_AI_INTERNAL_SECRET |
운영 필수 | 운영 프로파일은 기동 실패. 그 외는 경고 후 기동하고 AI 호출이 전부 401로 거절됨 |
GOOGLE_CLIENT_ID |
로그인에 필요 | unset으로 기동은 되고 인가 요청 URL 생성까지만 동작 (Jira 작업) |
GOOGLE_CLIENT_SECRET |
로그인에 필요 | 위와 같음 (Jira 작업) |
KAKAO_CLIENT_ID · KAKAO_CLIENT_SECRET |
Kakao 로그인에 필요 | 위와 같음 (Jira 작업) |
NAVER_CLIENT_ID · NAVER_CLIENT_SECRET |
Naver 로그인에 필요 | 위와 같음 (Jira 작업) |
쓰지 않는 키는 빈 값으로 두지 말고 정의 자체를 하지 않습니다. application.yml이 spring.config.import로 .env를 프로퍼티로 올리므로, KAKAO_CLIENT_ID=처럼 정의만 하면 프로퍼티가 "없음"이 아니라 빈 문자열이 되어 ${KAKAO_CLIENT_ID:unset}의 기본값이 적용되지 않습니다. 그러면 Client id of registration 'kakao' must not be empty로 기동이 실패합니다. 로컬 .env도, 운영 Secret도 같습니다 — 자격증명을 아직 받지 못한 공급자는 주입하지 않는 것이 정상 상태입니다(BT-05).
JWT_PRIVATE_KEY는 RSA 2048 이상 PKCS#8 PEM입니다. 기동을 막는 것은 이것과 PINLOG_AI_INTERNAL_SECRET 둘뿐이고 소셜 로그인 자격증명은 막지 않습니다. 둘 다 같은 이유입니다 — 없어도 뜨게 두면 조용히 망가집니다. 임시 서명 키를 만들면 파드마다 키가 달라져 스케일아웃·재시작 때 전면 로그아웃이 되고(BD-31), AI 시크릿이 비면 FastAPI가 401을 주는데 클라이언트가 실패를 삼켜 임베딩이 하나도 생성되지 않는 것을 아무도 알 수 없습니다(AiProcessClient.requireSecret).
PINLOG_AI_BASE_URL·PINLOG_AI_EMBEDDING_PROFILE은 비밀값이 아니고 요청 대상도 아닙니다. 전자는 주소라 인프라가 평문env로 넣고, 후자는application.yml에 리터럴 기본값이 있어 환경변수는 덮어쓰기 수단일 뿐입니다(BD-39). 다만PINLOG_AI_BASE_URL이 현재 운영에 주입돼 있지 않아 AI 호출이 전부 자기 자신의 8000 포트로 나갑니다 — back#122.
pinlog:
auth:
jwt:
private-key: ${JWT_PRIVATE_KEY:} # 비어 있으면 프로파일에 따라 갈린다기본값을 비워 둔 이유는 placeholder 해석 실패로 죽이면 "왜 죽었는지"가 스택트레이스에만 남기 때문입니다. 코드에서 판정하면 무엇을 주입해야 하는지 메시지로 알려 줄 수 있습니다.
로컬은 저장소 루트의 compose.yaml이 띄운 PostgreSQL·Redis에 접속합니다(CONTRIBUTING.md의 로컬 시작). 로컬 전용 개발값은 커밋해도 되지만, 실제 비밀번호는 넣지 않습니다.
# application-local.yml
spring:
datasource:
url: jdbc:postgresql://localhost:15432/pinlog
username: pinlog
password: pinlog-local # 로컬 개발 전용 값
data:
redis:
host: localhost
port: 16379로컬 실행 주소는 context path가 붙어 http://localhost:8080/api/core/... 입니다.
운영은 클러스터 내부 주소를 쓰고, 접속 정보 다섯 개(url·username·password·Redis host·port) 전부를 infra가 환경변수로 주입합니다 — 위 "비밀값과 자격증명 주입" 절 참고.
application-prod.yml에는 spring.datasource.*·spring.data.redis.*를 적지 않습니다(BD-50). 예전엔 주소·사용자명을 리터럴로 파일에 적어 "코드로 접속 정보를 통제한다"는 의도였지만, OS 환경변수가 profile yaml보다 우선순위가 높아 그 리터럴은 애초에 무시되고 있었다 — infra가 이미 표준 이름 환경변수로 다섯 개를 전부 주입하고 있었기 때문이다. ConfigurationContractTests는 이제 반대로 이 키들이 재선언되지 않았는지를 감시합니다.
Redis는 캐시·세션 전용이라 재시작하면 비워집니다. 유실되면 안 되는 데이터를 넣어야 하면 사전에 인프라와 협의합니다.
- 설정 변경은 애플리케이션 기동과 관련 통합 테스트로 검증합니다. DB가 필요한 테스트는 PostgreSQL Testcontainers를 사용합니다(테스트 규약).
- 헬스체크·actuator 노출을 바꾸면
DeploymentContractTests가 여전히 통과하는지 확인합니다. probe 그룹 구성은ReadinessProbeDatabaseOutageTests(DB가 안 닿을 때 readiness가 UP이 아니고 liveness는 UP)와ReadinessProbeRedisOutageTests(Redis가 안 닿아도 readiness는 UP)가 함께 감시합니다.
- 비밀번호·토큰·키를 저장소에 넣지 않았다 (환경변수 주입)
- context path는
/api/core, 컨트롤러에 중복하지 않았다 -
ddl-auto=validate이고 스키마는 Flyway가 관리한다 -
open-in-view=false이고, 연관 조회는fetch join이나 전용 메서드로 명시한다 - actuator는
health·prometheus만 노출한다 (둘 다 필수) - readiness 그룹은
readinessState,db이고,redis와 liveness 그룹은 건드리지 않았다 - 비밀값은 전부 환경변수 주입이고(datasource는
DB_PASSWORD, 나머지는back-owner-secrets), 주소·사용자명은 프로파일 파일에 있다 - 환경별 차이만 프로파일에 두고 공통값을 복제하지 않았다