반응형
HTTP 통신을 하다 보면 Connection Timeout과 Read Timeout이라는 용어를 자주 접하게 됩니다. 두 타임아웃 모두 네트워크 통신에서 발생하지만, 각각 다른 단계에서 적용됩니다. 이 글에서는 두 타임아웃의 차이점과 실무에서 알아야 할 내용들을 정리했습니다.
커넥션 타임아웃 vs 리드 타임아웃
커넥션 타임아웃 (Connection Timeout)
TCP 연결을 맺는 과정에서의 제한 시간입니다.
- 클라이언트가 서버에 연결 요청 시작
- TCP 3-way handshake 완료까지 대기하는 시간
- 서버가 응답하지 않거나 네트워크가 불안정할 때 발생
- 일반적으로 짧게 설정 (3~5초)
// RestTemplate 예시
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5초
리드 타임아웃 (Read Timeout)
이미 연결이 맺어진 상태에서 데이터를 읽을 때의 제한 시간입니다.
- 연결 성공 후, 서버로부터 응답 데이터를 받을 때까지 대기하는 시간
- 서버가 요청 처리에 오래 걸리거나 응답을 보내지 않을 때 발생
- API 특성에 맞게 설정 (10~60초)
factory.setReadTimeout(30000); // 30초
타임라인으로 이해하기
[클라이언트] [서버]
연결 시도 ─────────────────────────────────────→
←─── Connection Timeout 적용 ────→
←───────────────────────────────────── 연결 수락
요청 전송 ─────────────────────────────────────→
요청 처리 중...
←──── Read Timeout 적용 ────→ (30초 소요)
(응답 대기 중...)
타임아웃! (30초 경과)
연결 종료 여전히 처리 중...
처리 완료! (하지만 클라이언트는 이미 떠남)
Spring에서의 타임아웃 설정
RestTemplate
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 연결 타임아웃: 5초
factory.setReadTimeout(30000); // 읽기 타임아웃: 30초
return new RestTemplate(factory);
}
}
WebClient (Spring WebFlux)
@Bean
public WebClient webClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
))
.build();
}
리드 타임아웃 발생 시 동작
리드 타임아웃이 발생하면 클라이언트와 서버가 각각 다르게 동작합니다.
클라이언트 측
- SocketTimeoutException 또는 ReadTimeoutException 발생
- TCP 연결을 강제로 종료
- 더 이상 서버의 응답을 기다리지 않음
try {
String response = restTemplate.getForObject(url, String.class);
} catch (ResourceAccessException e) {
log.error("Read timeout 발생", e);
// 클라이언트는 여기서 종료
}
서버 측
중요: 서버는 타임아웃을 인지하지 못하고 계속 작업을 수행합니다!
@GetMapping("/slow-api")
public String slowApi() {
log.info("요청 받음");
Thread.sleep(30000); // 클라이언트는 20초에 타임아웃
log.info("처리 완료, 응답 반환"); // 이 로그는 찍힘!
return "result"; // 하지만 클라이언트는 이미 연결을 끊음
}
왜 서버는 연결이 끊긴 걸 모를까?
HTTP는 기본적으로 단방향 통신이기 때문입니다:
- 처리 중에는 확인 안 함: 서버는 요청을 받고 처리하는 동안 클라이언트 상태를 확인하지 않습니다
- 응답 시도할 때 알게 됨: 응답을 보내려고 할 때 비로소 연결 상태를 확인합니다
- 로그에는 정상으로 보임: 컨트롤러 레벨에서는 정상 종료로 보이는 경우가 많습니다
서버가 연결 끊김을 감지할 수 있는 경우
1. 응답 데이터가 큰 경우
@GetMapping("/large-response")
public void largeResponse(HttpServletResponse response) throws IOException {
try (PrintWriter writer = response.getWriter()) {
for (int i = 0; i < 1000000; i++) {
writer.write("large data..."); // IOException 발생 가능
}
} catch (IOException e) {
log.error("클라이언트 연결 끊김 감지", e);
}
}
2. WAS 레벨 로그
Tomcat 같은 WAS에서는 감지할 수 있습니다:
java.io.IOException: Broken pipe
실무에서 발생하는 문제점
1. 리소스 낭비
서버는 이미 끊긴 연결을 위해 계속 작업하므로 CPU, 메모리, DB 커넥션 등을 낭비합니다.
2. 데이터 정합성 문제
상황: 결제 API 호출
- 클라이언트: 20초 Read Timeout 설정
- 서버: 결제 처리에 30초 소요
결과:
- 서버: DB에 결제 완료 저장 ✅
- 클라이언트: 타임아웃으로 실패 처리 ❌
→ 데이터 불일치 발생!
서버 측 타임아웃 설정
클라이언트만이 아니라 서버 측에서도 타임아웃을 설정할 수 있습니다.
1. WAS(Tomcat) 레벨
# application.yml
server:
tomcat:
connection-timeout: 20s
keep-alive-timeout: 60s
threads:
max: 200
min-spare: 10
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
@Override
protected void customizeConnector(Connector connector) {
connector.setProperty("connectionTimeout", "20000");
}
};
}
2. Spring MVC 비동기 요청
spring:
mvc:
async:
request-timeout: 30000 # 30초
@GetMapping("/async-api")
public DeferredResult<String> asyncApi() {
DeferredResult<String> result = new DeferredResult<>(30000L);
CompletableFuture.runAsync(() -> {
String data = heavyProcess();
result.setResult(data);
});
result.onTimeout(() -> {
log.warn("서버 측 타임아웃 발생");
result.setErrorResult("타임아웃");
});
return result;
}
3. 트랜잭션 타임아웃
@Transactional(timeout = 30) // 30초
public void longTransaction() {
// DB 작업이 30초 넘으면 롤백
}
4. 개별 작업 타임아웃 (권장)
@Service
public class MyService {
private final ExecutorService executor = Executors.newCachedThreadPool();
public String processWithTimeout() throws Exception {
Future<String> future = executor.submit(() -> {
return heavyProcess();
});
try {
return future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new RuntimeException("서버 처리 시간 초과");
}
}
}
장시간 작업 처리 패턴
FTP 파일 다운로드처럼 오래 걸리는 작업은 어떻게 처리할까요?
비동기 패턴 (@Async 활용)
@RestController
public class ProcessController {
@Autowired
private FtpDownloadService ftpService;
@PostMapping("/api/process")
public ResponseEntity<String> process(@RequestBody Request request) {
// 1. 메인 비즈니스 로직 처리 (동기)
String result = processBusinessLogic(request);
// 2. DB 저장 (동기)
saveToDatabase(result);
// 3. FTP 다운로드만 백그라운드로 실행 (비동기)
ftpService.downloadAsync(request.getFtpUrl());
// 4. 즉시 응답 (FTP 완료 안 기다림)
return ResponseEntity.ok("처리 완료");
}
}
@Service
public class FtpDownloadService {
@Async("ftpExecutor") // 별도 스레드에서 실행
public void downloadAsync(String ftpUrl) {
log.info("백그라운드 FTP 다운로드 시작");
try {
File file = downloadFromFtp(ftpUrl); // 30분 소요
log.info("FTP 다운로드 완료: {}", file.getName());
} catch (Exception e) {
log.error("FTP 다운로드 실패", e);
}
}
}
@Async 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "ftpExecutor")
public Executor ftpExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("ftp-bg-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}
@Async 동작 원리
// @Async 없이
public void download() {
downloadFromFtp(); // 30분 대기
return; // 30분 후 리턴
}
// @Async 적용
@Async
public void download() {
downloadFromFtp(); // 별도 스레드에서 실행
// 호출한 곳은 즉시 다음 코드로 진행
}
실행 흐름:
클라이언트 요청
↓
메인 로직 처리 (2초)
↓
DB 저장 (1초)
↓
FTP 다운로드 시작 (비동기, 즉시 리턴)
↓
클라이언트에게 응답 (총 3초) ✅
↓
연결 종료
↓
[백그라운드에서 FTP 다운로드 계속 진행... 30분]
실무 권장 설정
# application.yml
server:
tomcat:
connection-timeout: 20s
threads:
max: 200
spring:
mvc:
async:
request-timeout: 60000
# RestTemplate 설정
rest:
client:
connect-timeout: 5000 # 연결: 짧게
read-timeout: 30000 # 읽기: API 특성에 맞게
타임아웃 설정 가이드
타임아웃 종류 권장 시간 이유
| Connection Timeout | 3~5초 | 연결이 빠르게 실패해야 재시도 가능 |
| Read Timeout | 10~60초 | API 응답 시간에 따라 조정 |
| Transaction Timeout | 30초 | DB 락 방지 |
| Async Request | 60초 | 비동기 작업 여유 시간 |
해결 방법 정리
1. 비동기 처리 + @Async
- 장시간 작업은 백그라운드로 처리
- 즉시 응답 반환으로 타임아웃 회피
2. 폴링/웹훅 패턴
// 즉시 작업 ID 반환
@PostMapping("/tasks")
public String createTask() {
String taskId = UUID.randomUUID().toString();
asyncService.processTask(taskId);
return taskId;
}
// 상태 조회
@GetMapping("/tasks/{taskId}")
public TaskStatus getStatus(@PathVariable String taskId) {
return taskRepository.findById(taskId);
}
3. 서버 측 타임아웃 설정
- 무한 실행 방지
- 리소스 낭비 감소
4. 멱등성 보장
- 재시도 가능하도록 설계
- 중복 실행 방지 로직 추가
마치며
커넥션 타임아웃과 리드 타임아웃은 네트워크 통신의 서로 다른 단계에서 적용되는 중요한 개념입니다. 특히 리드 타임아웃 발생 시 클라이언트와 서버가 각각 다르게 동작한다는 점을 이해하고, 적절한 비동기 처리 패턴을 적용하면 안정적인 시스템을 구축할 수 있습니다.
실무에서는:
- 클라이언트와 서버 양측에 타임아웃 설정
- 장시간 작업은 @Async로 백그라운드 처리
- 작업 상태를 추적할 수 있는 구조 설계
- 적절한 에러 핸들링과 로깅
이러한 원칙들을 지키면 타임아웃으로 인한 문제를 최소화할 수 있습니다.
반응형
'IT' 카테고리의 다른 글
| System.out.println()의 성능 이슈와 Logback 비교 (0) | 2026.01.15 |
|---|---|
| @Async 적용하기 (0) | 2026.01.14 |
| 실행계획과 인덱스 (0) | 2025.12.16 |
| 실서비스에서 커넥션 풀(hikari Cp) 제대로 이해하기 (0) | 2025.12.16 |
| Business Exception vs System Exception (0) | 2025.12.15 |
댓글