
코드 한 줄 짜기 전에, 인프라부터 코드로 박았다.
1편에서 기획과 설계를 끝냈다. 이제 코드를 짜야 한다.
"일단 IntelliJ 켜고 Spring Boot 깔고, MySQL은 내 컴퓨터에 직접 설치하고, Redis도 직접 설치하고"
이렇게 가면 두 가지 문제가 생긴다.
실무에서는 인프라를 코드로 관리한다고 한다. 이걸 IaC(Infrastructure as Code)라고 한다. 그래서 나도 그렇게 가기로 했다./
처음엔 Spring Initializr에서 최신 버전인 Spring Boot 4를 받았다. 근데 실행해보니 첫 줄부터 에러가 났다.
Could not find org.springframework.boot:spring-boot-starter-webmvc:.
Could not find org.springframework.boot:spring-boot-starter-session-data-redis:.
원인은 단순했다. Spring Boot 3과 4 사이에 의존성 이름이 바뀐 것이다.
Spring Boot 3:
spring-boot-starter-web
spring-boot-starter-data-redis
Spring Boot 4:
spring-boot-starter-webmvc ← 새 이름
spring-boot-starter-session-data-redis
여기서 결정해야 했다. 최신 버전을 쓸 것인가, 안정 버전을 쓸 것인가.
결론은 Spring Boot 3.4.5 다운그레이드.
새 기술(캐시, 서킷브레이커 등)을 처음 적용하는데 새 버전까지 같이 배우면 디버깅이 두 배가 된다. 자료가 풍부한 안정 버전을 쓰는 게 학습 효율 면에서 압도적으로 나은 것 같아 선택했다.
핵심만 골라서 추가했다.
| 의존성 | 용도 |
|---|---|
| Spring Web | REST API 만들기 |
| Spring Data JPA | DB 접근 |
| Spring Data Redis | Redis 연동 |
| MySQL Driver | MySQL 접속 |
| Spring Security | 관리자 인증 |
| Validation | 요청 값 검증 |
| Lombok | 보일러플레이트 제거 |
여기서 빠진 게 있다. Resilience4j와 Caffeine. 이건 나중에 캐시랑 서킷브레이커 적용할 때 추가할 예정이다.
비교하면 답이 나온다.
방법 A — 로컬에 직접 설치
MySQL 설치 → 환경변수 설정 → 포트 충돌 디버깅
Redis 설치 → 또 설정...
→ 컴퓨터 더러워짐, 다른 프로젝트랑 충돌
방법 B — Docker Compose
docker-compose.yml 한 파일로
MySQL + Redis 한 번에 실행
→ 깔끔, 재현 가능, 다른 개발자가 클론해도 동일
services:
redis:
image: redis:7.2-alpine
container_name: emergencylink-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
restart: unless-stopped
mysql-master:
image: mysql:8.0
container_name: emergencylink-mysql-master
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root1234
MYSQL_DATABASE: emergencylink
MYSQL_USER: emergency
MYSQL_PASSWORD: emergency1234
TZ: Asia/Seoul
volumes:
- mysql-master-data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --default-time-zone=+09:00
restart: unless-stopped
volumes:
redis-data:
mysql-master-data:
redis:7.2-alpine — 왜 alpine인가alpine은 경량 리눅스 배포판이다. 일반 redis 이미지가 100MB 넘는데 alpine 버전은 30MB 정도다. 컨테이너 띄우는 속도, 빌드 속도, 배포 속도가 다 빨라진다. 운영에서도 자주 쓰는 선택이다.
--appendonly yes — Redis가 캐시인데 왜 디스크에 쓰는가Redis는 인메모리 DB다. 재시작하면 데이터가 다 날아간다. AOF(Append Only File) 모드를 켜면 모든 쓰기 명령을 디스크에 기록해서 재시작 시 복구할 수 있다.
캐시인데 영속화가 왜 필요하냐고? 재난 상황에서 Redis가 한 번 재시작되면 모든 트래픽이 DB로 직행한다. 그 순간 DB가 죽는다. AOF가 있으면 재시작 직후에도 캐시가 살아있어서 DB로 트래픽이 쏠리지 않는다.
volumes: mysql-master-data:/var/lib/mysql — 데이터 영속화컨테이너는 일회성이다. 내려가면 안의 데이터가 다 사라진다. volumes 설정으로 호스트 파일시스템에 데이터를 영속화해야 컨테이너를 다시 띄워도 DB 데이터가 유지된다.
utf8mb4 — utf8이 아닌 이유MySQL의 기본 utf8은 사실 3바이트 UTF-8이다. 이모지 같은 4바이트 문자가 깨진다. utf8mb4가 진짜 4바이트 UTF-8이라 한글, 이모지, 특수문자 모두 안전하다.
restart: unless-stopped — 자동 재시작 정책no 재시작 안 함
always 무조건 재시작 (내가 stop해도 다시 켜짐)
on-failure 비정상 종료 시에만
unless-stopped 비정상 종료 시 재시작, 내가 stop하면 그대로 멈춤
운영 표준은 unless-stopped다. 장애는 자동 복구하되, 의도된 중지는 존중한다.
docker-compose up -d 했더니 첫 번째 에러가 떴다.
ports are not available: exposing port TCP 0.0.0.0:3306
listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address
이게 무슨 뜻인지 한 번에 보였다.
"3306 포트를 누가 이미 쓰고 있다."
원인은 명확했다. 옛날에 로컬에 MySQL을 설치해뒀는데, Windows 서비스로 자동 실행되면서 3306을 점유하고 있었다.
해결은 두 가지였다.
A. Docker MySQL의 포트를 3307로 바꾸기
B. 로컬 MySQL 서비스 중지하기
B를 선택했다. Docker MySQL이 표준 포트인 3306을 쓰는 게 자연스럽다. 두 개를 같이 띄우면 application.yml이나 코드에서 포트를 헷갈릴 위험이 있다.
net stop MySQL80
서비스 멈추고 다시 docker-compose up -d. 이번엔 깔끔하게 떴다.
이 파일이 진짜 중요하다. 거의 모든 설정이 여기 있다.
spring:
application:
name: emergencylink
datasource:
url: jdbc:mysql://localhost:3306/emergencylink?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: emergency
password: emergency1234
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
show_sql: true
open-in-view: false
data:
redis:
host: localhost
port: 6379
server:
port: 8080
username: emergency — root를 쓰지 않는 이유root 계정은 모든 권한을 가진다. 만약 SQL 인젝션이 뚫리면 DB 전체가 날아간다. 애플리케이션은 항상 권한이 제한된 일반 유저로 접속해야 한다. 운영 환경에서는 더 엄격하게 해당 DB에 대한 권한만 부여한다.
ddl-auto: validate — 5가지 옵션 중 왜 이걸 골랐나none 아무것도 안 함
validate 엔티티-테이블 일치 여부만 검증 (실무 권장)
update 차이점 자동 추가
create 시작 시마다 테이블 새로 만듦
create-drop create + 종료 시 drop
update나 create는 운영에서 절대 쓰면 안 된다. 데이터 손실 위험이 있다. 우리는 schema.sql로 DDL을 명시적으로 관리하고, JPA는 매핑이 잘 됐는지만 검증하는 방식으로 가기로 했다.
open-in-view: falseOSIV(Open Session In View)
→ 트랜잭션이 끝난 뒤에도 영속성 컨텍스트를 유지하는 설정
기본값은 true.
근데 실무에선 거의 무조건 false로 쓴다고한다.
왜? OSIV가 켜져 있으면 컨트롤러까지 DB 커넥션을 들고 가서, 요청 처리 내내 커넥션이 점유된다. 트래픽이 폭증하면 커넥션 풀이 마른다. 우리 프로젝트는 10만 동시 사용자를 가정하니 무조건 false다.
여기까지 하고 IntelliJ에서 ▶ 눌렀다. 처음엔 콘솔에 빨간 ERROR가 떴다.
Schema-validation: missing table [country]
이건 의도된 에러다.
validate라서 "엔티티랑 테이블이 일치하는지 검증"한다.이 에러가 떴다는 건 오히려 "환경 설정은 잘 됐다"는 뜻이다.
Hibernate가 검증 단계까지 갔다는 게 보이면 인프라는 통과한 거다.
여기까지가 1단계의 끝
✅ Spring Boot 3.4.5 프로젝트 생성
✅ Docker Compose로 MySQL Master + Redis 띄움
✅ MySQL 컨테이너 정상 접속 확인
✅ Redis 컨테이너 SET/GET 테스트 통과
✅ application.yml 핵심 옵션 검토
✅ 의도된 에러까지 도달
잘 보고 있습니다~ㅇㅅㅇ