Persistent Connection
- Persistent Connection이란 네트워크 연결에서 클라이언트와 서버 간에 지속적인 연결을 유지하는 것을 의미한다.
- 이 연결 방식은 일반적으로 HTTP 프로토콜에서 사용되며, HTTP/1.1 이후의 버전에서는 기본적으로 지속 연결(Persistent Connection)이 활성화되어 있다.
Persistent Connection의 필요성
-
연결 오버헤드 감소: 각 요청마다 새로운 연결을 수립하는 것은 시간이 많이 걸리고 네트워크 리소스를 많이 소모한다. 따라서 지속적인 연결을 사용하면 이러한 연결 설정의 오버헤드를 줄일 수 있다.
-
성능 향상: 서버와 클라이언트 간의 연결을 지속적으로 유지함으로써 데이터를 보다 빠르게 교환할 수 있다. 이는 웹 페이지의 로딩 속도를 개선하고 사용자 경험을 향상시킨다.
-
리소스 사용 최적화: 연결을 유지함으로써 서버와 클라이언트는 자원을 보다 효율적으로 사용할 수 있다. 서버는 연결을 재활용할 수 있으며, 여러 요청에 대해 더 나은 자원 할당과 관리가 가능하다.
-
HTTP 파이프라이닝 지원: 지속적인 연결을 통해 HTTP 파이프라이닝이 가능해진다. 이는 클라이언트가 여러 요청을 연속적으로 보내고, 서버가 순차적으로 응답할 수 있게 함으로써 네트워크 지연 시간을 줄이고 처리량을 높일 수 있다.
Parallel Connection
- Parallel Connection은 동시에 여러 연결을 사용하여 데이터를 전송하는 방법이다.
- 이는 특히 웹 페이지가 다수의 리소스(예: 이미지, 스크립트, CSS 파일)를 필요로 할 때 유용하며 클라이언트는 여러 개의 연결을 동시에 열어 각각의 리소스를 병렬로 요청하고, 이를 통해 전체 페이지 로드 시간을 단축시킬 수 있다.
연관성
- Persistent Connection과 Parallel Connection은 서로 보완적인 관계에 있다.
- Persistent Connection은 연결을 유지하여 오버헤드를 줄이고, Parallel Connection은 여러 연결을 통해 리소스를 동시에 로드함으로써 성능을 극대화한다.
- 예를 들어, 웹 브라우저가 HTTP/1.1을 사용하여 웹 사이트에 접속할 경우, 지속적인 연결을 통해 서버와 통신하면서 동시에 여러 개의 병렬 연결을 열 수 있다.
- 이는 한 연결에서 처리할 수 있는 요청의 수가 제한적이기 때문에 특히 중요하며 결과적으로, 사용자는 더 빠른 페이지 로딩 속도를 경험할 수 있다.
- HTTP/2는 이 두 개념을 더 발전시켜 하나의 연결 내에서 여러 개의 메시지를 동시에 전송할 수 있는 멀티플렉싱 기능을 제공한다.
- 이는 병렬 연결의 필요성을 감소시키지만, 기본적으로 지속적인 연결의 이점은 그대로 유지한다.
- 이러한 연결 방식들의 적절한 조합은 웹 애플리케이션의 성능을 최적화하는 데 중요한 역할을 한다.
HTTP Keep-Alive
- Persistent Connection를 맺는 기법 중 하나로 이 기능은 HTTP 프로토콜에서 서버와 클라이언트 간의 연결을 지속적으로 유지함으로써 성능을 향상시키는 방법이다.
- 이 기능은 HTTP/1.1에서 기본적으로 활성화되어 있으며, 클라이언트와 서버가 연결을 끊지 않고 여러 HTTP 요청과 응답을 주고받을 수 있게 해준다.
- 반면 HTTP/1.0 connection 에서는 하나의 request에 응답할 때마다 connection을 close 하도록 지정되어있다.


HTTP Keep-Alive의 동작 원리
- HTTP Keep-Alive를 사용할 때, 서버와 클라이언트는 TCP 연결을 초기 요청 이후에도 유지한다.
- 즉, 웹 페이지를 로드하기 위해 여러 리소스(이미지, 스크립트, CSS 등)가 필요할 경우, 각 리소스를 요청할 때마다 새로운 TCP 연결을 생성하고 종료하는 대신 이미 열려 있는 연결을 재사용한다.
HTTP Keep-Alive의 장점
- 시간 절약: 새로운 연결을 맺는 데 필요한 시간이 절약된다. TCP 연결 수립에는 일반적으로 "3-way handshake" 과정이 필요하며, 이는 추가적인 시간과 대역폭을 요구한다.
- 리소스 절약: 서버와 클라이언트는 연결을 재사용함으로써 CPU와 메모리 같은 자원을 보다 효율적으로 사용할 수 있다.
- 네트워크 오버헤드 감소: 연결의 잦은 수립과 종료로 인한 네트워크 트래픽이 줄어들어, 네트워크의 전반적인 부하가 감소한다.
- 웹 페이지 로딩 속도 개선: 연결이 이미 수립되어 있기 때문에, 리소스 요청과 응답이 빠르게 이루어질 수 있어 웹 페이지의 로딩 시간이 개선된다.
Client Keep-Alive 값 찾기 (Kotlin Spring)
- 서버의
keep-alive-timeout 및 max-keep-alive-requests 설정의 적절한 값을 결정하기 위해 서버에 대한 지속적인 요청을 보내어 이 설정들이 어떻게 동작하는지 테스트를 진행한다.
- 이를 위해, 클라이언트 측에서 HTTP 요청을 주기적으로 보내어 서버의 Keep-Alive 동작을 모니터링하는 테스트 코드를 작성
의존성 추가
- 먼저, WebClient가 포함된 build.gradle.kts 파일에 spring-boot-starter-webflux 의존성을 추가
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jetbrains.kotlin:kotlin-reflect")
}
WebClient를 사용한 테스트 클라이언트 구현
- 주기적으로 서버에 요청을 보내고, 응답을 콘솔에 출력하는 WebClient 코드 구현
package com.example.demo.client
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Flux
import java.time.Duration
@Component
class KeepAliveTester(private val webClient: WebClient.Builder) {
fun startRequests(url: String, interval: Long, totalRequests: Long) {
Flux.interval(Duration.ofSeconds(interval))
.take(totalRequests)
.flatMap {
webClient.build().get().uri(url).retrieve().bodyToMono(String::class.java)
}
.subscribe(
{ response -> println("Received response: $response") },
{ error -> println("Error during request: ${error.message}") },
{ println("Completed all requests") }
)
}
}
구성과 실행
- main 함수 또는 Spring Boot 애플리케이션의 초기화 섹션에서 이 클라이언트를 사용하여 요청을 시작
package com.example.demo
import com.example.demo.client.KeepAliveTester
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
@SpringBootApplication
class DemoApplication {
@Bean
fun run(keepAliveTester: KeepAliveTester) = ApplicationRunner {
keepAliveTester.startRequests("http://localhost:8080/message", 5, 20)
}
}
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
테스트 실행 방법
- URL 설정: startRequests 함수에서 첫 번째 인자로 테스트할 서버의 URL을 설정.
- 인터벌과 요청 수: 요청 간의 시간 간격(초)과 전송할 총 요청 수를 설정.
- 이를 통해 keep-alive-timeout 및 max-keep-alive-requests의 설정 값을 조정하여 가장 효율적인 값을 찾는다.
Server Keep-Alive 설정 (Kotlin Spring)
1. 설정 파일 구성
- application.yml 파일을 사용하여 설정
server:
port: 8080
netty:
idle-timeout: 60s
- application.properties 파일을 사용하여 설정
server.port=8080
server.netty.idle-timeout=60s
2. 서버 실행
- 애플리케이션을 실행한 후, Netty 서버가 포트 8080에서 리스닝하고 idle-timeout 설정이 적용되어 있는지 확인
3. 유휴 시간 확인
Netty 서버 설정이 올바르게 적용되었다면, 클라이언트 연결이 60초 동안 아무런 데이터를 주고받지 않으면 서버는 해당 연결을 자동으로 종료해야한다.
- curl 요청: curl을 사용하여 서버에 요청을 보내고 응답을 확인한 후, 60초 이상 동안 추가적인 요청을 보내지 않는다. 그 후 다시 요청을 보내 연결이 종료되었는지 확인합니다.
- 로깅: 서버에서 연결의 시작과 종료를 로깅하도록 설정하여, 연결이 어떻게 종료되는지 로그를 통해 확인한다.
Keep-Alive 미설정시 생길 수 있는 문제
1. 연결 시간 초과(Connection Timeout)
- 원인
- 각 HTTP 요청마다 새로운 TCP 연결을 수립해야 하므로, 연결 설정에 시간이 많이 소요된다.
- 서버가 많은 수의 동시 연결을 처리하려고 할 때, 연결 요청이 대기열에 머물러 있어 연결 시간 초과가 발생할 수 있다.
2. 연결 거부(Connection Refused)
- 원인
- 서버에 동시에 많은 TCP 연결이 시도될 때, 서버의 연결 대기열이 가득 차 연결 요청이 거부될 수 있다.
- 서버의 리소스(예: 메모리, 파일 디스크립터)가 부족하여 새로운 연결을 수락하지 못할 수도 있다.
- 원인
- TCP 연결의 빈번한 설정과 해제는 서버에 추가적인 CPU 및 메모리 부하를 일으킬 수 있다.
- 서버가 연결을 관리하느라 실제 데이터 처리보다 많은 리소스를 소모할 수 있다.
4. 네트워크 지연(Network Latency)
- 원인
- 각 요청에 대해 새로운 연결을 설정하려면 TCP의 3-way handshake가 완료되어야 하므로 추가 지연이 발생한다.
- 네트워크 대역폭이 불필요하게 TCP 연결 설정과 해제 패킷으로 소모된다.
5. 전체적인 통신 효율 감소
- 원인
- TCP 연결을 자주 끊고 재연결하는 과정은 네트워크 오버헤드를 증가시킨다.
- 서버와 클라이언트가 자주 연결 상태를 초기화해야 하므로 통신 프로세스의 효율성이 떨어진다.