Spring Boot 실무 가이드 · Part 10

DB 연동 운영

개발에서는 안 보이다가 운영에서 터지는 데이터 계층 문제 다루기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

커넥션 풀동시 수정과 락스키마 마이그레이션페이징과 대량 처리
01

커넥션 풀이 고갈되면 전체 서비스가 멈춥니다

Spring Boot는 HikariCP를 기본 커넥션 풀로 씁니다. 애플리케이션은 미리 만들어 둔 커넥션을 빌려 쓰고 반납합니다.

풀에 남은 커넥션이 없으면 요청은 대기합니다. 대기 시간이 설정값을 넘으면 예외가 나고, 이 시점에는 DB와 무관한 API까지 함께 실패합니다.

maximum-pool-size
핵심 설정

무작정 크게 잡으면 DB 쪽 연결 수가 한계를 넘습니다. 서버 인스턴스 수 × 풀 크기가 DB 최대 연결 수 안에 들어와야 합니다.

connection-timeout
대기 한도

커넥션을 기다리는 최대 시간입니다. 너무 길면 장애 상황에서 요청이 계속 쌓이고, 너무 짧으면 정상 부하에서도 실패합니다.

leak-detection-threshold
누수 탐지

설정한 시간 이상 반납되지 않은 커넥션을 로그로 알려 줍니다. 원인 코드를 찾는 데 유용합니다.

고갈의 진짜 원인
대부분

풀 크기가 작아서가 아니라 커넥션을 오래 붙잡고 있어서입니다. 트랜잭션 범위를 먼저 점검합니다. Part 7 내용과 이어집니다.

커넥션 풀 설정yaml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 10
      connection-timeout: 3000
      max-lifetime: 1800000
      leak-detection-threshold: 20000

max-lifetime은 DB나 중간 장비의 연결 종료 시간보다 짧게 잡아야 합니다. 그렇지 않으면 이미 끊긴 커넥션을 빌려 쓰다 실패합니다.

02

동시에 같은 행을 수정하면 값이 사라집니다

두 사용자가 같은 데이터를 조회한 뒤 각각 수정하면, 나중에 저장한 값이 앞선 변경을 덮어씁니다. 어느 쪽도 오류를 보지 못하고 데이터만 조용히 사라집니다.

이 문제를 막는 방법은 크게 두 가지입니다. 충돌이 드물다고 보고 감지만 하는 방식과, 아예 잠가서 막는 방식입니다.

낙관적 락
@Version

버전 컬럼을 두고, 수정 시 버전이 바뀌었으면 예외를 발생시킵니다. 잠금이 없어 성능 부담이 적고 대부분의 웹 서비스에 적합합니다.

비관적 락
SELECT FOR UPDATE

조회 시점에 행을 잠급니다. 확실하지만 대기가 발생하고, 잠금 순서가 엇갈리면 교착 상태가 생길 수 있습니다.

격리 수준
기본값 확인

대부분의 DB가 READ COMMITTED를 기본으로 씁니다. 격리 수준을 올리면 정합성은 높아지지만 잠금과 대기가 늘어납니다. 함부로 바꾸지 않습니다.

재시도 설계
낙관적 락과 함께

충돌 예외가 났을 때 사용자에게 그대로 오류를 보일지, 다시 시도할지 정해야 합니다. 정하지 않으면 사용자가 원인을 알 수 없는 실패를 겪습니다.

낙관적 락 적용java
@Entity
public class Product {

    @Id
    private Long id;

    private int stock;

    @Version
    private Long version;   // 수정될 때마다 자동 증가합니다
}

// 다른 트랜잭션이 먼저 수정했다면
// ObjectOptimisticLockingFailureException 이 발생합니다
@Transactional
public void decreaseStock(Long productId, int quantity) {
    Product product = productRepository.findById(productId)
            .orElseThrow(() -> new BusinessException("상품을 찾을 수 없습니다"));
    product.decrease(quantity);
}

재고 차감처럼 충돌이 잦은 로직은 낙관적 락만으로 부족할 수 있습니다. 이때는 비관적 락이나 DB의 원자적 갱신 쿼리를 검토합니다.

03

스키마 변경은 버전 관리된 SQL로 합니다

ddl-auto에 스키마를 맡기면 어떤 변경이 언제 적용됐는지 추적할 수 없고, 운영에서는 데이터가 사라질 위험도 있습니다.

Flyway 같은 마이그레이션 도구를 쓰면 스키마 변경이 번호가 붙은 SQL 파일로 저장소에 남습니다. 코드와 함께 리뷰되고, 어떤 환경이 어느 버전까지 적용됐는지 관리됩니다.

컬럼 삭제는 두 단계로
무중단 배포

코드에서 먼저 사용을 제거해 배포하고, 다음 배포에서 컬럼을 삭제합니다. 한 번에 하면 구버전 인스턴스가 오류를 냅니다.

인덱스 추가 시점
운영 영향

대용량 테이블에 인덱스를 걸면 잠금과 부하가 발생합니다. 적용 시간대와 소요 시간을 미리 확인합니다.

롤백 계획
필수

되돌릴 수 없는 변경(컬럼 삭제, 타입 변경)은 배포 전에 백업과 복구 절차를 확인합니다.

마이그레이션 파일 규칙text
src/main/resources/db/migration/
├─ V1__create_orders.sql
├─ V2__add_order_memo.sql
└─ V3__create_order_index.sql

# 규칙
# - V + 버전 + 언더스코어 두 개 + 설명
# - 이미 적용된 파일은 절대 수정하지 않습니다
# - 수정이 필요하면 새 버전 파일을 추가합니다

적용된 파일을 고치면 체크섬이 달라져 기동이 실패합니다. 이 제약이 불편해 보이지만, 환경마다 스키마가 달라지는 사고를 막아 줍니다.

04

페이징과 대량 처리는 건수가 늘면 달라집니다

개발 데이터 100건에서는 모든 방식이 빠릅니다. 운영에서 수십만 건이 되면 같은 코드가 전혀 다르게 동작합니다.

count 쿼리 비용
페이징

전체 건수를 세는 쿼리가 목록 조회보다 느린 경우가 많습니다. 총 건수가 꼭 필요한 화면인지 먼저 확인합니다.

깊은 페이지
OFFSET 문제

뒤쪽 페이지로 갈수록 건너뛰는 행이 많아져 느려집니다. 무한 스크롤처럼 순차 탐색이면 마지막 조회 값을 기준으로 다음 구간을 가져오는 방식이 유리합니다.

전체 조회 금지
메모리

findAll로 전체를 가져오면 건수가 늘었을 때 메모리가 터집니다. 목록 조회에는 항상 상한을 둡니다.

대량 저장
청크 처리

수만 건을 한 트랜잭션에 넣으면 커넥션 점유와 메모리 문제가 함께 생깁니다. 일정 건수 단위로 나눠 처리합니다.

확인 습관

쿼리 성능은 데이터가 쌓인 뒤에 드러납니다. 개발 단계에서 실행 계획을 한 번이라도 확인하고, 운영에서는 느린 쿼리 로그를 켜 두는 것이 최소 기준입니다.

05

운영 전 점검 항목

  • 커넥션 풀 크기가 DB 최대 연결 수 안에 들어오는지 계산했다.
  • 트랜잭션 안에서 외부 호출을 하지 않는지 확인했다.
  • 동시 수정이 가능한 데이터에 락 전략을 정했다.
  • 운영 ddl-auto가 validate 또는 none이다.
  • 스키마 변경이 마이그레이션 파일로 관리된다.
  • 목록 조회에 상한과 페이징이 적용돼 있다.
  • 느린 쿼리 로그와 커넥션 풀 지표를 확인할 수 있다.
버전

3.x와 4.x 차이

본문은 현장에서 가장 많이 쓰는 3.x 기준입니다. 4.x에서 달라진 부분만 아래에 정리합니다.

  • HikariCP는 2.x부터 4.x까지 계속 기본 커넥션 풀입니다. 설정 속성 이름도 동일합니다.
  • Java 21 이상에서 가상 스레드를 켜면 동시 실행 요청 수가 크게 늘 수 있습니다. 커넥션 풀이 먼저 병목이 되므로 적용 전에 부하 테스트가 필요합니다.
  • 4.0의 모듈 분리로 JDBC·JPA 관련 자동 설정이 별도 모듈로 나뉘었습니다. 업그레이드 시 필요한 모듈이 빠지지 않았는지 확인합니다.
체크

이 파트 완료 기준