[EmergencyLink #2] Spring Boot + Docker Compose로 인프라부터 세팅하기

배고픈개발로그·2026년 4월 28일

EmergencyLink

목록 보기
2/3
post-thumbnail

코드 한 줄 짜기 전에, 인프라부터 코드로 박았다.

들어가며

1편에서 기획과 설계를 끝냈다. 이제 코드를 짜야 한다.

"일단 IntelliJ 켜고 Spring Boot 깔고, MySQL은 내 컴퓨터에 직접 설치하고, Redis도 직접 설치하고"

이렇게 가면 두 가지 문제가 생긴다.

  • 컴퓨터가 더러워진다. 다른 프로젝트랑 포트 충돌, 버전 충돌.

실무에서는 인프라를 코드로 관리한다고 한다. 이걸 IaC(Infrastructure as Code)라고 한다. 그래서 나도 그렇게 가기로 했다./


Spring Boot 프로젝트 생성

버전 선택에서 한 번 꼬였다

처음엔 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 다운그레이드.

새 기술(캐시, 서킷브레이커 등)을 처음 적용하는데 새 버전까지 같이 배우면 디버깅이 두 배가 된다. 자료가 풍부한 안정 버전을 쓰는 게 학습 효율 면에서 압도적으로 나은 것 같아 선택했다.

의존성 7개

핵심만 골라서 추가했다.

의존성용도
Spring WebREST API 만들기
Spring Data JPADB 접근
Spring Data RedisRedis 연동
MySQL DriverMySQL 접속
Spring Security관리자 인증
Validation요청 값 검증
Lombok보일러플레이트 제거

여기서 빠진 게 있다. Resilience4j와 Caffeine. 이건 나중에 캐시랑 서킷브레이커 적용할 때 추가할 예정이다.


Docker Compose로 인프라 세팅

왜 Docker로 띄우는가

비교하면 답이 나온다.

방법 A — 로컬에 직접 설치
  MySQL 설치 → 환경변수 설정 → 포트 충돌 디버깅
  Redis 설치 → 또 설정...
  → 컴퓨터 더러워짐, 다른 프로젝트랑 충돌

방법 B — Docker Compose
  docker-compose.yml 한 파일로
  MySQL + Redis 한 번에 실행
  → 깔끔, 재현 가능, 다른 개발자가 클론해도 동일

docker-compose.yml 작성

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다. 장애는 자동 복구하되, 의도된 중지는 존중한다.


첫 번째 함정 — 포트 3306 충돌

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. 이번엔 깔끔하게 떴다.


application.yml — 가장 중요한 한 파일

이 파일이 진짜 중요하다. 거의 모든 설정이 여기 있다.

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

updatecreate는 운영에서 절대 쓰면 안 된다. 데이터 손실 위험이 있다. 우리는 schema.sql로 DDL을 명시적으로 관리하고, JPA는 매핑이 잘 됐는지만 검증하는 방식으로 가기로 했다.

open-in-view: false

OSIV(Open Session In View)
→ 트랜잭션이 끝난 뒤에도 영속성 컨텍스트를 유지하는 설정

기본값은 true.
근데 실무에선 거의 무조건 false로 쓴다고한다.

왜? OSIV가 켜져 있으면 컨트롤러까지 DB 커넥션을 들고 가서, 요청 처리 내내 커넥션이 점유된다. 트래픽이 폭증하면 커넥션 풀이 마른다. 우리 프로젝트는 10만 동시 사용자를 가정하니 무조건 false다.


첫 실행 — 빨간 ERROR 만나기

여기까지 하고 IntelliJ에서 ▶ 눌렀다. 처음엔 콘솔에 빨간 ERROR가 떴다.

Schema-validation: missing table [country]

이건 의도된 에러다.

  • ddl-auto가 validate라서 "엔티티랑 테이블이 일치하는지 검증"한다.
  • 우리는 아직 엔티티도 안 만들었고 테이블도 없다.
  • 그러니 "테이블 없다"고 외치는 게 정상이다.

이 에러가 떴다는 건 오히려 "환경 설정은 잘 됐다"는 뜻이다.

  • DB 접속 성공
  • HikariCP 커넥션 풀 정상 생성
  • JPA 초기화 시도 중

Hibernate가 검증 단계까지 갔다는 게 보이면 인프라는 통과한 거다.


환경 세팅, 완료

여기까지가 1단계의 끝

✅ Spring Boot 3.4.5 프로젝트 생성
✅ Docker Compose로 MySQL Master + Redis 띄움
✅ MySQL 컨테이너 정상 접속 확인
✅ Redis 컨테이너 SET/GET 테스트 통과
✅ application.yml 핵심 옵션 검토
✅ 의도된 에러까지 도달

관련 링크

3개의 댓글

comment-user-thumbnail
2026년 4월 28일

잘 보고 있습니다~ㅇㅅㅇ

1개의 답글