본문 바로가기
Java

RDS 페일오버 후 자바가 옛 IP로 붙을 때

aappsroot·2026년 9월 24일·조회 2

DB 페일오버 자체는 몇십 초면 끝난다. 그런데 애플리케이션만 한참을 옛 IP에 매달려 있다가 결국 재기동으로 푸는 경우를 현장에서 자주 본다. 스탠바이로 승격은 됐고 DNS도 새 IP를 가리키는데, 유독 자바 프로세스만 옛 IP로 Connection refused를 반복한다. 이 글은 그 이유와 재기동 없이 전환을 반영하는 방법을 정리한다.

결론부터 말하자면, 원인은 대체로 두 겹이다. 하나는 JVM이 DNS 조회 결과를 오래(설정에 따라 영구히) 캐시해 옛 IP를 계속 재사용하는 것이고, 다른 하나는 커넥션 풀에 옛 IP로 맺힌 소켓이 그대로 남아 있는 것이다. JVM의 networkaddress.cache.ttl을 짧게 낮추고, HikariCP의 keepaliveTimemaxLifetime으로 죽은 커넥션을 걷어내면 재기동 없이 새 엔드포인트로 붙는다.

1. 개요와 대상 버전

대상은 JDK 8 이상, 커넥션 풀은 HikariCP다. 다만 뒤에서 쓰는 keepaliveTime은 HikariCP 4.0에서 추가된 속성이다. Spring Boot로 보면 2.5부터 HikariCP 4.0.x를 번들한다. Spring Boot 2.3과 2.4는 HikariCP 3.4.5를 쓰므로 keepalive-time 속성 자체가 없다. 이 두 버전 사용자는 뒤의 maxLifetime 위주 설정만 적용된다는 점을 먼저 밝혀 둔다. 기본값과 설명은 HikariCP 공식 README, DNS 캐시 동작은 Oracle InetAddress 문서를 기준으로 했다. 버전 번호는 저장소 상태에 따라 다를 수 있다.

2. 왜 자바만 옛 IP로 붙나

2-1. JVM DNS 캐시

JVM은 호스트명을 IP로 한 번 풀면 그 결과를 프로세스 안에 캐시한다. 이 정책을 정하는 값이 java.securitynetworkaddress.cache.ttl이다. TTL은 성공한 조회를 몇 초 동안 캐시할지를 뜻하고, 단위는 초다.

문제는 기본값이다. Oracle 문서는 이렇게 설명한다. 보안 관리자(SecurityManager)가 설치돼 있으면 DNS 스푸핑을 막기 위해 성공한 조회 결과를 영구히 캐시한다. 보안 관리자가 없으면 구현에 따른 유한한 시간 동안 캐시한다(핫스팟 JVM에서는 이 값이 30초로 알려져 있다). -1은 영구 캐시를 의미한다.

즉 보안 관리자를 쓰거나 TTL을 -1로 둔 애플리케이션은 페일오버가 끝나 DNS가 새 IP를 내려줘도 옛 IP를 계속 재사용한다. 승격된 인스턴스는 옛 IP에서 리스닝하지 않으니 Connection refused가 이어진다.

참고로 실패한 조회의 캐시를 정하는 networkaddress.cache.negative.ttl은 기본 10초다. 페일오버 도중 잠깐 이름 해석이 실패하면 이 값만큼 재시도가 지연될 수 있다.

2-2. 풀에 남은 옛 소켓

DNS TTL을 고쳐도 이미 풀에 맺혀 있는 커넥션은 옛 IP로 향한 TCP 소켓 그대로다. 커넥션 풀은 재사용을 전제로 소켓을 붙잡아 두기 때문에, 이 소켓들을 명시적으로 걷어내지 않으면 다음 대여 때도 죽은 커넥션이 그대로 나간다. 그래서 DNS 캐시와 풀 재연결을 함께 손봐야 한다.

3. 진단

3-1. DNS는 이미 새 IP를 가리키는지 확인

RDS 엔드포인트는 CNAME이고, 페일오버가 끝나면 그 뒤의 A 레코드가 승격된 인스턴스 IP로 바뀐다. 먼저 호스트에서 직접 확인한다.

$ dig +short mydb.abcd1234.ap-northeast-2.rds.amazonaws.com
ec2-10-0-2-51.ap-northeast-2.compute.amazonaws.com.
10.0.2.51

여기서 나온 10.0.2.51이 새 IP다. 앱이 이 IP로 붙어 있으면 정상, 옛 IP로 붙어 있으면 캐시 문제다.

3-2. 앱이 실제로 붙은 IP 확인

자바 프로세스가 어느 IP와 세션을 유지하는지는 ss로 본다.

$ ss -tnp | grep 5432
ESTAB 0 0 10.0.1.30:51872 10.0.2.40:5432 users:(("java",pid=2143,fd=88))
ESTAB 0 0 10.0.1.30:51874 10.0.2.40:5432 users:(("java",pid=2143,fd=91))

dig10.0.2.51을 주는데 프로세스는 옛 IP 10.0.2.40에 붙어 있다. 여기서 진단이 끝난다. DNS는 전환됐지만 JVM이 옛 IP를 재사용 중이다.

3-3. 로그에서 증상 확인

애플리케이션 로그에는 대여 시점마다 아래와 같은 예외가 남는다.

org.postgresql.util.PSQLException: Connection to 10.0.2.40:5432 refused.
  Check that the hostname and port are correct and that the postmaster
  is accepting TCP/IP connections.
Caused by: java.net.ConnectException: Connection refused

예외 메시지에 옛 IP가 그대로 박혀 나오면 JVM DNS 캐시를 우선 의심한다.

4. JVM DNS TTL 낮추기

AWS는 RDS에 붙는 자바 앱의 TTL을 낮게 둘 것을 권한다. 페일오버 시 DNS 갱신 전파는 대략 수십 초에서 1~2분 수준이므로, TTL을 이보다 짧게 잡아야 앱이 곧바로 새 IP를 다시 조회한다. 5초에서 30초 사이를 쓴다. 여기서는 5초를 권장한다.

4-1. java.security 파일 수정

가장 확실한 방법이다. JDK 9 이상은 $JAVA_HOME/conf/security/java.security, JDK 8은 $JAVA_HOME/jre/lib/security/java.security에 있다. 해당 줄은 기본적으로 주석 처리돼 있으니 값을 넣어 활성화한다.

# 변경 전 (주석)
#networkaddress.cache.ttl=-1

# 변경 후
networkaddress.cache.ttl=5
networkaddress.cache.negative.ttl=0

실패 조회 캐시(negative.ttl)를 0으로 두면 페일오버 도중 잠깐 실패해도 다음 시도에서 바로 재조회한다.

4-2. 코드에서 설정

JDK 배포본을 건드릴 수 없는 환경이면 애플리케이션 기동 초기에 프로그램으로 지정한다. 반드시 InetAddress를 처음 쓰기 전에 실행해야 한다.

import java.security.Security;

public class Bootstrap {
    public static void main(String[] args) {
        Security.setProperty("networkaddress.cache.ttl", "5");
        Security.setProperty("networkaddress.cache.negative.ttl", "0");
        // 이후 스프링 컨텍스트 기동 등
    }
}

여기서 한 번 걸리는 부분이 있다. System.setProperty("networkaddress.cache.ttl", ...)로 잡으면 동작하지 않는다. 이 값은 시스템 프로퍼티가 아니라 보안 프로퍼티라서 java.security.Security.setProperty(...)로 넣어야 한다.

4-3. JVM 옵션으로 임시 조정

기동 옵션으로 넣는 방법도 있다. 다만 sun.net.inetaddr.ttl은 내부 프로퍼티라 보안 프로퍼티가 우선하며 향후 동작이 바뀔 수 있다. 임시 확인용으로만 쓰고, 운영 반영은 java.security 쪽을 택한다.

-Dsun.net.inetaddr.ttl=5

5. HikariCP 재연결 설정

DNS TTL을 낮췄으면 새로 맺는 커넥션은 새 IP로 붙는다. 남은 일은 풀에 남아 있는 옛 소켓을 걷어내는 것이다. HikariCP는 두 속성으로 이를 처리한다.

5-1. keepaliveTime

유휴 커넥션을 주기적으로 핑해 살아 있는지 확인하고, 죽었으면 풀에서 제거한 뒤 새로 채운다. 옛 IP로 향한 소켓이 이 핑에서 Connection refused로 걸리면 곧바로 퇴출되고, 새 커넥션은 갱신된 DNS로 새 IP에 붙는다. 페일오버 전환을 앞당기는 핵심 속성이다.

공식 README 기준 현행 최신 HikariCP는 keepaliveTime 기본값이 120000(2분)으로 켜져 있다. 다만 4.x에서 5.x 계열은 기본값이 0(꺼짐)이므로 이 대역을 쓴다면 명시적으로 켜야 한다. 최소 허용값은 30초(30000)다.

5-2. maxLifetime

커넥션의 최대 수명이다. 이 시간을 넘긴 커넥션은 유휴 상태가 되는 대로 퇴역시키고 새로 만든다. 기본값은 1800000(30분)이다. DB나 네트워크 장비가 강제로 끊는 시간보다 몇 초 짧게 잡는 것이 원칙이다. keepaliveTime이 없는 HikariCP 3.4.5(Spring Boot 2.3, 2.4)에서는 이 maxLifetime이 옛 커넥션을 걷어내는 주된 수단이 된다.

5-3. Spring Boot 설정 예시

페일오버 감지를 빠르게 하려면 대여 대기와 검증 시간을 짧게 잡는다. 여기서 주의할 규칙이 하나 있다. 공식 README는 validationTimeoutconnectionTimeout보다 작아야 한다고 못 박는다. 두 값을 같게 잡으면 안 된다. 아래는 connection-timeout 3초, validation-timeout 2초로 규칙을 지킨 예다.

spring:
  datasource:
    hikari:
      connection-timeout: 3000   # 대여 대기 최대 3초 (최소 250ms)
      validation-timeout: 2000   # connection-timeout 보다 작게
      keepalive-time: 30000      # 30초마다 유휴 커넥션 핑 (HikariCP 4.0+)
      max-lifetime: 300000       # 5분, DB 타임아웃보다 짧게
      minimum-idle: 5
      maximum-pool-size: 20

HikariCP 4.0 미만(Spring Boot 2.3, 2.4)이라면 keepalive-time 줄을 지우고 max-lifetime을 조금 더 짧게(예: 180000) 잡아 옛 커넥션이 빨리 교체되게 한다.

5-4. 적용 후 확인

설정 반영 뒤 다시 페일오버를 시키거나 강제로 유발한 다음, ss로 앱이 붙은 IP가 새 IP로 바뀌는지 본다.

$ ss -tnp | grep 5432
ESTAB 0 0 10.0.1.30:52310 10.0.2.51:5432 users:(("java",pid=2143,fd=88))

대여 실패 없이 옛 IP 10.0.2.40이 사라지고 새 IP 10.0.2.51로 채워지면 전환이 반영된 것이다. 전환에 걸리는 시간은 환경에 따라 다르지만 대략 DNS TTL과 keepaliveTime 주기를 합한 수준으로 수렴한다.

6. 정리

페일오버 후 옛 IP 고착은 JVM DNS 캐시와 풀에 남은 소켓이라는 두 겹의 문제다. networkaddress.cache.ttl을 5초 수준으로 낮춰 새 IP를 다시 조회하게 하고, HikariCP keepaliveTimemaxLifetime로 죽은 커넥션을 걷어내면 재기동 없이 새 엔드포인트로 붙는다. 버전 조합만 유의한다. keepalive-time은 HikariCP 4.0(Spring Boot 2.5) 이상에서만 동작하고, 그 아래는 maxLifetime으로 대응한다.

자주 묻는 질문

networkaddress.cache.ttl을 System.setProperty로 설정했는데 왜 안 먹나

이 값은 시스템 프로퍼티가 아니라 보안 프로퍼티다. java.security.Security.setProperty("networkaddress.cache.ttl", "5")로 넣거나 java.security 파일을 수정해야 한다. 또한 InetAddress를 처음 사용하기 전, 애플리케이션 기동 초기에 설정해야 반영된다.

보안 관리자를 안 쓰는데도 옛 IP로 계속 붙는다

보안 관리자가 없어도 핫스팟 JVM은 성공한 조회를 유한 시간(대체로 30초) 캐시하며, 여기에 풀에 남은 옛 소켓이 겹치면 전환이 늦어진다. TTL을 5초 수준으로 낮추고 HikariCP keepaliveTime, maxLifetime로 죽은 커넥션을 걷어내면 해소된다.

keepalive-time을 설정했는데 속성이 무시된다

keepaliveTime은 HikariCP 4.0에서 추가됐다. Spring Boot 2.3과 2.4는 HikariCP 3.4.5를 번들해 이 속성이 없다. 이 경우 maxLifetime을 짧게 잡아 옛 커넥션을 교체하거나, HikariCP 4.0 이상을 쓰는 Spring Boot 2.5+로 올려야 한다.

validationTimeout과 connectionTimeout을 같은 값으로 잡아도 되나

안 된다. HikariCP 공식 문서는 validationTimeout이 connectionTimeout보다 작아야 한다고 명시한다. 두 값이 같거나 검증 시간이 더 크면 안 되고, 두 값 모두 최소 250ms 이상이어야 한다.

TTL을 낮추면 성능에 영향이 없나

조회 결과를 짧게만 캐시하므로 DNS 조회가 늘지만, RDS 엔드포인트처럼 소수 호스트에 대한 조회는 부담이 작다. 페일오버 전환 속도를 얻기 위한 합리적 교환이며 5초에서 30초 사이면 실무에서 문제되지 않는다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.