백엔드 개발 일지 (5·완) — application.yml의 숫자엔 전부 이유가 있다

Junyoung·2026년 9월 15일

아카이빙

목록 보기
15/15

백엔드 일지 마지막 편이다. 화려한 기능 대신 설정 파일을 꺼냈다.
application.yml에 박힌 숫자들은 대부분 "기본값을 왜 바꿨는가"의 기록이기 때문이다.

기본값은 우리 사정을 모른다

프레임워크의 기본값은 "대부분의 상황에서 무난한" 값이지,
"우리 상황에서 최적인" 값이 아니다. 운영하면서 기본값을 하나씩
의심했고, 바꾼 값에는 주석으로 이유를 박아뒀다. 그 이유들을 풀어본다.

3초의 철학: 빨리 실패하기

이 설정 파일에서 가장 자주 나오는 숫자는 3초다. 두 군데에 있다.

datasource:
  hikari:
    connection-timeout: 3000   # 기본 30s → 3s
data:
  redis:
    timeout: 3s                # Lettuce 기본 60s → 3s

HikariCP 30초의 문제. 커넥션 풀이 고갈됐다고 하자. 기본값이면
요청 스레드가 커넥션을 기다리며 30초씩 매달린다. 그동안 톰캣 스레드는
점유된 채고, 새 요청은 계속 쌓인다. DB가 아픈 게 앱 전체의 스레드 고갈로
번져나간다. 3초면 빨리 실패하고 스레드를 돌려준다. 사용자는 에러를
보지만, 서비스 전체가 마비되는 것보단 낫다.

Redis 60초는 더 심각했다. 우리 인증 필터는 요청마다 Redis에서
토큰 블랙리스트를 조회한다. Redis가 순단되면 기본값 기준 모든 인증 요청이
60초씩 행
에 걸린다. 1편에서 "Redis 죽어도 서비스는 산다"를 설계해놨는데,
타임아웃이 60초면 그 설계가 무의미하다 — 죽진 않지만 60초 걸리는 서비스는
죽은 거나 다름없으니까.

두 값의 공통 철학: 의존성 장애는 "빨리 실패"로 격리한다.
느린 성공보다 빠른 실패가 시스템 전체엔 이롭다.

20초의 산수: graceful shutdown 예산

server:
  shutdown: graceful
lifecycle:
  timeout-per-shutdown-phase: 20s

배포하면 구버전 태스크는 SIGTERM을 받는다. graceful은 진행 중인 요청을
마저 처리하고 죽겠다는 선언인데, 문제는 얼마나 기다릴 거냐다.

이 20초는 임의의 숫자가 아니라 산수의 결과다.

ECS stop_timeout           = 30초   ← 이 안에 안 죽으면 SIGKILL (강제종료)
─────────────────────────────────
진행 중 HTTP 요청 마무리    ≈ 수 초
viewCountExecutor 큐 소진   ≤ 10초   (4편의 그 10초)
컨텍스트 정리 여유          = 나머지
─────────────────────────────────
합계                        = 20초  < 30초  ✓

20초를 30초보다 크게 잡으면? 앱이 "아직 정리 중"인데 ECS가 SIGKILL을
날린다. 진행 중이던 요청은 5xx, 큐에 있던 조회수는 유실.
graceful shutdown은 앱 설정과 인프라 설정(stop_timeout)의 합의다.
한쪽만 보고 정하면 안 된다.

10의 근거: 커넥션 풀은 곱셈이다

hikari:
  maximum-pool-size: 10   # 기본값과 같지만 명시 고정

기본값이랑 같은데 왜 적어놨냐면, 이 10이 다른 계산의 입력이기 때문이다.
인프라 일지 5편의 그 계산이다.

태스크당 풀 10 × 최대 태스크 4 = 40 커넥션 ≪ RDS 한계 ≈ 85

누군가 "풀이 작네?" 하고 30으로 올리면, 오토스케일 max 4대 기준
120 커넥션 — RDS가 죽는다. 이 값은 앱 혼자 정하는 게 아니라
RDS 한계 ÷ 최대 태스크 수로 역산되는 값이라는 걸 주석으로 박아뒀다.
바꾸면 안 되는 값이 아니라, 바꿀 때 계산이 필요한 값이다.

나머지 한 줄짜리 결정들

open-in-view: false — 기본값 true는 HTTP 응답이 끝날 때까지
DB 커넥션을 물고 있는다. 뷰 렌더링에서 LAZY 로딩을 허용하기 위한
낡은 편의인데, API 서버엔 이득이 없고 커넥션 점유 시간만 는다.
10개뿐인 귀한 풀이라 더더욱. 대신 LAZY 접근은 서비스 레이어에서
fetch join과 batch size(3편)로 끝내야 한다 — false는 그 규율의 강제장치다.

default_batch_fetch_size: 100 — 3편에서 다뤘다. N+1의 안전망.

ddl-auto: validate — 스키마 변경은 Flyway만 한다(사건편 참고).
Hibernate에겐 "만지지 말고 검증만 해라". 엔티티와 스키마가 어긋나면
런타임 어딘가에서 터지는 대신 부팅 시점에 죽는다. 빨리 실패의 또 다른 얼굴.

management.health.redis.enabled: false — 1편에서 다룬 헬스체크와
장애 설계의 정합. Redis는 "없어도 되는 의존성"으로 설계했으니
헬스체크도 그렇게 답해야 한다.

compression: min-response-size: 1024 — 1KB 미만 응답은 압축 안 한다.
압축에도 CPU가 들고, 작은 응답은 압축해봐야 몇 바이트라 배보다 배꼽이 크다.

배운 것

1. 설정값은 코드보다 감사(audit)받지 않는다.
코드 리뷰는 다들 열심히 하는데 yml의 숫자는 그냥 지나친다.
기본값 하나(Redis 60s)가 장애 시나리오 전체를 바꾸는데도.

2. 숫자엔 주석이 필요하다.
"왜 3초인가"를 주석으로 남기지 않으면, 미래의 누군가(대부분 나)가
근거 없이 되돌린다. 계산으로 나온 값은 계산식을 함께 적는다.

3. 앱 설정은 인프라 설정과 짝이 있다.
graceful 20s ↔ ECS stop_timeout 30s, 풀 10 ↔ RDS 85 ÷ 태스크 4,
헬스체크 ↔ ALB 판정. 한쪽만 고치면 짝이 깨진다. 설정 파일은
독립 문서가 아니라 인프라와의 계약서였다.


백엔드 일지는 여기까지. 인프라 8편 + 백엔드 5편, 돌아보면 결론은 하나로
수렴한다. 기본값과 초록불을 믿지 말고, 이유와 실측만 믿기.
다음 글은 또 사건이 터지면 돌아오겠다. (안 터지는 게 최선이지만.)

profile
라곰

0개의 댓글