실무에서 발생하는 장애 중 상당수가 타임아웃 설정 누락에서 비롯된다. 연결은 됐는데 응답이 없어 스레드가 무한 대기하고, 결국 서버 전체가 응답 불가 상태에 빠지는 패턴이다. 이번 글에서는 두 종류의 타임아웃을 명확히 이해하고, 어떻게 적용해야 하는지 정리한다
연결 타임아웃 (Connect Timeout)
OS 기본 타임아웃
public class ConnectTimeoutMain1 {
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
try {
Socket socket = new Socket("192.168.1.250", 45678);
} catch (ConnectException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + (end - start));
}
}
**실행 결과**
java.net.ConnectException: Operation timed out
end = 75008
- 사설 IP 대역 (
192.168.x.x)에서 사용 중이지 않은 IP로 연결을 시도하면, 해당 IP를 사용하는 서버가 없기 때문에 TCP 응답이 오지 않는다. 서버가 너무 바빠 SYN 패킷에 대한 SYN-ACK를 보내지 못하는 경우도 마찬가지다. - 이 때 OS는 무한정 기다리지 않고 기본 타임아웃을 적용한다.
- 75초(Windows의 경우 21초)를 기다려야 사용자에게 오류를 알려줄 수 있다는 뜻이다. 현실적으로 허용할 수 없는 시간이다
직접 타임아웃 설정
public class ConnectTimeoutMain2 {
public static void main(String[] args) throws IOException {
try {
Socket socket = new Socket(); // 인자 없이 생성 → 연결 시도 안 함
socket.connect(new InetSocketAddress("192.168.1.250", 45678), 3000); // 3초 타임아웃
} catch (SocketTimeoutException e) {
e.printStackTrace();
}
}
}
**실행 결과**
java.net.SocketTimeoutException: Connect timed out
new Socket(IP, PORT)형태로 생성하면 생성자에서 즉시 TCP 연결을 시도한다. 반면new Socket()으로 인자 없이 생성하면 객체만 만들어지고 연결은 하지 않는다. 이후socket.connect()를 호출할 때 연결과 함께 타임아웃을 지정할 수 있다.InetSocketAddress는SocketAddress의 구현체로, IP와 포트를 하나의 객체로 묶어 전달한다. 두 번째 인자가 밀리초 단위로 타임아웃이다.- 설정한 시간(3초)이 지나면
SocketTimeoutException이 발생한다. OS의 기본 타임아웃을 기다리는 대신 애플리케이션 수준에서 빠르게 실패를 감지하고 사용자에게 알릴 수 있다
읽기 타임아웃 (Read Timeout / SO_TIMEOUT)
연결 타임아웃은 TCP 연결 단계의 문제다. 하지만 연결이 성공한 이후에도 문제가 생길 수 있다. 클라이언트가 요청을 보냈는데 서버가 응답을 주지 않는 상황이다. 서버에 트래픽이 폭주해 CPI 100%를 찍고 있거나, 서버 내부에서 다른 외부 시스템을 호출하다가 멈춘 경우가 대표적이다
응답하지 않는 서버 시뮬레이션
// 연결은 받지만 아무 응답도 하지 않는 서버
public class SoTimeoutServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(12345);
Socket socket = serverSocket.accept();
Thread.sleep(1_000_000); // 사실상 무한 대기
}
}
// 읽기 타임아웃을 설정하지 않은 클라이언트
public class SoTimeoutClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 12345);
InputStream input = socket.getInputStream();
try {
int read = input.read(); // 무한 대기
System.out.println("read = " + read);
} catch (Exception e) {
e.printStackTrace();
}
socket.close();
}
}
setSoTimeout을 설정하지 않으면 read()는 응답이 올 때까지 무한 대기한다. OS 환경에 따라 다르지만 1시간 이상, 경우에 따라서는 하루 이상 블로킹 상태를 유지할 수 있다
읽기 타임아웃 적용
Socket socket = new Socket("localhost", 12345);
InputStream input = socket.getInputStream();
try {
socket.setSoTimeout(3000); // 3초 타임아웃 설정
int read = input.read();
System.out.println("read = " + read);
} catch (SocketTimeoutException e) {
e.printStackTrace();
}
socket.close();
**실행 결과**
java.net.SocketTimeoutException: Read timed out
setSoTimeout()은 밀리초 단위로 읽기 타임아웃을 설정한다. 이 소켓에서read()계열의 블로킹 메서드를 호출할 때 지정한 시간 안에 데이터가 오지 않으면 예외를 던진다
두 타임아웃 비교
| 구분 | 연결 타임아웃 | 읽기 타임아웃 |
| 발생 시점 | TCP 3-way handshake 단계 | 연결 후 데이터 수신 대기 중 |
| 설정 방법 | socket.connect(address, timeout) | socket.setSoTimeout(timeout) |
| 발생 예외 | SocketTimeoutException: Connect timed out | SocketTimeoutException: Read timed out |
| 미설정시 | OS 기본값(21초~180초) 후 타임아웃 | 사실상 무한 대기 |
실무 관점
- 서버는 클라이언트 요청만 처리하지 않는다. 결제 서버를 호출하거나 외부 API를 호출하는 등, 서버 자신이 다른 서버의 클라이언트가 되는 경우가 매우 많다
- 이때 외부 서버가 응답을 주지 않는다면 어떻게 될까? 타임아웃이 설정되어 있지 않으면, 요청을 처리하는 스레드가 무한 대기 상태에 빠진다. 요청이 계속 들어오면 스레드가 하나씩 쌓이고, 결국 스레드 풀이 고갈되어 서버 전체가 응답 불가 상태가 된다
- 원인은 외부 서버 하나의 장애였지만, 결과는 내 서버의 전체 장애다. 이런 장애는 원인을 찾기도 어렵다. 연결 타임아웃과 읽기 타임아웃, 이 두 가지는 외부 시스템을 호출하는 모든 코드에 반드시 설정해야 한다