배포하자마자 터진 500 에러
새 기능을 열심히 만들어서 배포했다. 로컬에서도 됐고, 개발 서버에서도 됐다. 운영 서버에 올리자마자 500 에러가 터졌다.
로그를 보니 원인은 허무했다.
org.postgresql.util.PSQLException: Connection refused: connect to localhost:5432
운영 서버에서 ‘localhost:5432’로 DB 연결을 시도하고 있었다. 운영 DB는 별도 서버에 있는데, application.yml에 로컬 DB 주소가 박혀 있었던 것이다. 환경 분리라는 개념 자체를 모르고 ‘일단 되게 만들자’로만 개발하던 시절의 실수다.
지금 보면 너무 초보적인 실수지만, Spring Boot의 Profile 기능을 제대로 모르면 누구나 한 번쯤 겪는 일이기도 하다. 환경별로 달라야 하는 값들이 설정 파일 하나에 뒤섞여 있으면, 어느 순간 반드시 사고가 난다.
‘DB 주소, API 키, 로그 레벨, 외부 서비스 URL…’ 환경마다 달라야 하는 것들은 의외로 많다. 이 글에서는 Spring Boot의 Profile 기능으로 환경을 깔끔하게 분리하는 방법과, 실무에서 사고가 나기 쉬운 ‘설정 우선순위’와 ‘프로필 활성화’를 정리한다.

Profile 기본: application-{profile}.yml
Spring Boot에는 ‘환경별 설정 파일’을 위한 기능이 내장되어 있다. 파일 이름 규칙만 지키면 된다.
src/main/resources/ ├── application.yml # 모든 환경 공통 설정 ├── application-local.yml # 로컬 개발 전용 ├── application-dev.yml # 개발 서버 전용 └── application-prod.yml # 운영 서버 전용
애플리케이션이 시작할 때, 먼저 application.yml을 읽고, 그 다음 활성 프로필에 해당하는 파일을 읽어서 ‘덮어쓴다’. 공통 설정은 application.yml에, 환경별로 달라지는 설정만 application-{profile}.yml에 넣는 것이 기본 구조다.
설정 우선순위: 누가 누구를 덮어쓰는가
여기서 많은 개발자가 놓치는 포인트가 있다. 설정값은 한 곳에서만 오는 게 아니라, ‘여러 소스에서 동시에 오고, 우선순위에 따라 덮어써진다’는 것이다.
실무에서 자주 사용하는 소스의 우선순위를 단순화하면 이렇다 (위쪽이 더 강함).
| 우선순위 | 설정 소스 | 예시 |
|---|---|---|
| 1 (가장 강함) | 명령행 인자 | –spring.datasource.url=… |
| 2 | OS 환경변수 | SPRING_DATASOURCE_URL |
| 3 | application-{profile}.yml | application-prod.yml |
| 4 (가장 약함) | application.yml | 기본값 |
즉, application.yml에 DB 주소를 써놓고, application-prod.yml에 다른 주소를 쓰면 운영 환경에서는 application-prod.yml 값이 이긴다. 거기에 환경변수로 SPRING_DATASOURCE_URL을 주입하면, 환경변수가 또 이긴다.
이 규칙을 알고 있으면 운영에서 이런 플로우가 자연스럽다.
- application.yml: 개발자가 읽을 때 전체 흐름이 보이도록 기본값 작성
- application-prod.yml: 운영에 필요한 설정 중 ‘값이 공개되어도 되는 것’만 작성
- 환경변수: DB 비밀번호, API 키 등 민감 정보를 주입
반대로 우선순위를 모르면 이런 사고가 난다. “분명히 application-prod.yml을 고쳤는데 반영이 안 돼요” → 알고 보니 배포 스크립트의 환경변수가 더 높은 우선순위로 덮어쓰고 있었던 것이다.
프로필 활성화하는 방법
환경 분리에서 실제로 사고가 나는 지점이 여기다. ‘프로필이 활성화되지 않으면, application.yml만 적용되고 application-prod.yml은 무시된다.’ 운영 서버에서 프로필을 잘못 지정하면, 개발용 기본값으로 서비스가 돌아가는 무서운 일이 생긴다.
활성화 방법은 여러 가지다. 실무에서 모두 쓰이므로 정확히 알아두자.
1. 환경변수 (운영에서 가장 권장)
export SPRING_PROFILES_ACTIVE=prod java -jar app.jar
환경변수 방식이 운영에서 가장 선호되는 이유는, 빌드된 JAR 파일은 그대로 두고 배포 환경에서만 프로필을 제어할 수 있기 때문이다.
2. 실행 옵션
# 방법 1: Spring Boot 실행 옵션 (--) java -jar app.jar --spring.profiles.active=prod # 방법 2: JVM 시스템 프로퍼티 (-D) java -jar app.jar -Dspring.profiles.active=prod
두 방법 모두 동작한다. 간단한 로컬 실행에 적합하다.
3. IDE 실행 설정
IntelliJ나 Eclipse에서 Run Configuration에 ‘Active profiles’ 또는 VM options에 -Dspring.profiles.active=local을 추가한다. 로컬 개발 시 매번 명령행을 치지 않아도 되므로 편하다.
4. Docker/컨테이너 환경
Docker에서는 환경변수로 주입하는 것이 정석이다.
docker run -e SPRING_PROFILES_ACTIVE=prod \
-e DB_PASSWORD=xxx \
myapp:latest
docker-compose.yml에서는 이렇게 쓴다.
services:
app:
image: myapp:latest
environment:
SPRING_PROFILES_ACTIVE: prod
DB_PASSWORD: ${DB_PASSWORD}
5. Kubernetes
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
K8s에서는 민감 정보를 Secret 리소스로 관리하고, 일반 설정은 ConfigMap이나 환경변수로 주입한다.
기본 프로필 설정: application.yml에
spring.profiles.active: local을 써두면, 아무 설정도 안 해도 로컬로 뜬다. 운영 배포 시에는 환경변수로 덮어쓰는 것이 안전하다.
환경별 설정 분리 실전
설정 파일을 실제로 어떻게 나누는지 살펴보자.
공통 설정 (application.yml)
# application.yml - 모든 환경에서 공통
spring:
application:
name: project-management
profiles:
active: local # 기본 프로필 (운영에서는 환경변수로 덮어쓰기)
jpa:
hibernate:
ddl-auto: validate # Flyway가 스키마 관리, Hibernate는 검증만
server:
port: 8080
로컬 (application-local.yml)
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: postgres
data:
redis:
host: localhost
port: 6379
jpa:
show-sql: true # SQL 로그 출력 (디버깅)
logging:
level:
org.hibernate.SQL: DEBUG
com.example: DEBUG
개발 서버 (application-dev.yml)
spring:
datasource:
url: jdbc:postgresql://dev-db.internal:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
data:
redis:
host: dev-redis.internal
port: 6379
logging:
level:
com.example: DEBUG
운영 서버 (application-prod.yml)
spring:
datasource:
url: jdbc:postgresql://prod-db.internal:5432/mydb
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20 # 운영은 커넥션 풀 크게
data:
redis:
host: prod-redis.internal
port: 6379
logging:
level:
root: WARN
com.example: INFO
springdoc:
api-docs:
enabled: false # Swagger UI 비활성화 (보안)
swagger-ui:
enabled: false
세 파일을 비교하면 ‘환경마다 달라져야 하는 것’이 선명해진다. DB 주소, Redis 주소, 로그 레벨, 커넥션 풀 크기, Swagger 노출 여부 등.
민감 정보 관리 원칙
위 dev/prod 설정에서 비밀번호가 ${DB_PASSWORD}로 되어 있다. 이건 환경변수를 읽어오는 문법이다.
password: ${DB_PASSWORD} # 환경변수 DB_PASSWORD 값을 읽어옴
password: ${DB_PASSWORD:defaultPw} # 없으면 defaultPw 사용
왜 이렇게 할까? ‘비밀번호, API 키, 토큰 같은 민감 정보는 Git에 올라가면 안 되기 때문’이다.
| 실수 | 결과 |
|---|---|
| application-prod.yml에 운영 DB 비밀번호 하드코딩 | Git에 푸시되는 순간 유출 |
| GitHub public repo에 올림 | 수초 내에 봇이 스캔해서 악용 |
| 나중에 커밋에서 지워도 | Git 히스토리에 영구 보존 |
실무 원칙 세 가지
- '민감 정보는 환경변수로 주입한다.' 설정 파일에는
${VAR_NAME}참조만 남긴다. - '민감 정보는 배포 시스템이 주입한다.' Docker는
-e, K8s는 Secret, CI/CD 파이프라인은 비밀 변수로. - '민감 정보가 들어 있는 파일은 Git에서 제외한다.' .gitignore에 반드시 추가.
# .gitignore .env *.env application-local.yml # 로컬 전용 설정에 비밀번호가 있다면 제외 application-secret.yml # 별도 비밀 파일을 두는 경우 제외
.env 파일은 로컬 보조 도구일 뿐
가끔 .env 파일을 Spring Boot의 기본 메커니즘처럼 설명하는 글이 있는데, 엄밀히는 다르다. Spring Boot는 .env 파일을 기본으로 읽지 않는다. .env는 dotenv 같은 별도 도구나 IDE 플러그인을 통해 환경변수로 ‘변환’해서 주입하는 로컬 개발 보조 도구다.
로컬에서 매번 export 치기 번거로울 때 쓰는 도구 정도로 이해하고, 운영에서는 쓰지 않는다.
규모가 커지면
팀과 서비스 규모가 커지면 민감 정보 관리도 고도화된다. AWS Secrets Manager, HashiCorp Vault, Azure Key Vault 같은 시크릿 매니저가 그 자리를 채운다. 애플리케이션이 시작 시 이 도구들에서 비밀번호를 가져오는 구조다. 이번 글의 주제는 Profile이므로, 이런 도구가 ‘운영 성숙도가 올라가면 쓰는 확장’이라는 것만 알아두고 넘어가자.
@Profile로 Bean 자체를 분리하기
설정값뿐 아니라, ‘Bean 자체를 환경별로 다르게 등록’하고 싶을 때도 있다. 예를 들어, 로컬/개발에서는 실제 이메일을 보내지 않고 콘솔에 출력하는 가짜 메일 서비스를 쓰고, 운영에서만 실제 메일을 보내는 식이다.
public interface MailService {
void send(String to, String subject, String body);
}
// 로컬/개발 환경에서만 사용
@Service
@Profile({"local", "dev"})
public class FakeMailService implements MailService {
@Override
public void send(String to, String subject, String body) {
System.out.println("[Fake Mail] to=" + to + ", subject=" + subject);
}
}
// 운영 환경에서만 사용
@Service
@Profile("prod")
public class RealMailService implements MailService {
private final JavaMailSender mailSender;
@Override
public void send(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
}
}
같은 인터페이스를 구현하지만, 활성 프로필에 따라 다른 Bean이 주입된다. 테스트 환경에서 진짜 메일이 나가는 사고를 방지할 수 있다.

실수 예방 체크리스트
운영 배포 전 확인
| 항목 | 확인 |
|---|---|
| SPRING_PROFILES_ACTIVE=prod가 주입되는가 | 배포 스크립트/Docker/K8s 설정 |
| 애플리케이션 시작 로그에 ‘The following profiles are active: prod’ 확인 | 프로필이 실제로 적용되었는지 |
| DB 비밀번호가 환경변수로 주입됨 | 설정 파일에 하드코딩 X |
| application-prod.yml이 Git에 포함되어 있음 | 비밀번호 없는 상태로 |
| Swagger UI 비활성화 확인 | springdoc.enabled=false |
| 로그 레벨이 INFO/WARN인가 | DEBUG는 로컬/개발만 |
| show-sql=false 확인 | 운영에서 SQL 로그는 디스크 낭비 |
| ddl-auto=validate 또는 none | create/update는 절대 금지 |
프로필 조합
여러 프로필을 동시에 활성화할 수도 있다.
java -jar app.jar --spring.profiles.active=prod,monitoring
이렇게 하면 application-prod.yml + application-monitoring.yml이 함께 적용된다. 운영 환경에서 모니터링 설정만 별도 파일로 관리하고 싶을 때 유용하다.
마치며: 환경은 절대 같지 않다
로컬과 운영은 절대 같지 않다. DB가 다르고, 네트워크가 다르고, 부하가 다르다. 이 차이를 Profile로 명시적으로 분리하고, 설정 우선순위를 이해하고, 프로필 활성화를 정확히 관리하면, “로컬에선 되는데 운영에선 안 돼요”라는 상황을 미리 예방할 수 있다.
정리하면 이렇다.
- 환경별로 달라지는 설정은 application-{profile}.yml로 분리한다.
- 설정 우선순위는 ‘명령행 > 환경변수 > application-{profile}.yml > application.yml’이다.
- 프로필 활성화는 배포 환경에 맞춰(환경변수, 실행 옵션, K8s 등) 정확히 주입한다.
- 민감 정보는 설정 파일에 하드코딩하지 말고 환경변수로 주입한다.
- @Profile로 환경별 Bean을 분리해서 실수로 운영 외부 호출이 나가는 사고를 방지한다.
여기까지 Spring Garden 시리즈에서 인증, 보안 구조, 쿼리, 성능, 테스트, 트랜잭션, 문서화, DB 마이그레이션, 환경 분리까지 다뤘다. 한 서비스를 제대로 만들고 운영하는 데 필요한 Spring Boot의 핵심 주제들을 거의 한 바퀴 돈 셈이다.
다음 글에서는 Service Layer 설계와 예외 처리 같은 ‘실무 패턴’을 다루며 시리즈를 마무리하려 한다. 기능 단위가 아니라, 코드를 어떻게 정리하고 구조화할 것인가에 대한 이야기다.