Spring Boot는 기본적으로 HikariCP를 내장된 커넥션 풀로 지원하며, 이를 통해 데이터베이스 연결을 효율적으로 관리할 수 있습니다. 이번 포스팅에서는 Spring Boot 환경에서 HikariCP의 설정을 최적화하여 TPS 변화에 유연하게 대응하는 방법을 알아보겠습니다.

HikariCP 설정 예시#

Spring Boot에서 application.yml 또는 application.properties 파일을 통해 HikariCP 설정을 정의할 수 있습니다. 이번 예시에서는 다음과 같은 설정을 적용하였습니다.

1
2
3
4
5
6
7
spring:
datasource:
hikari:
minimum-idle: 10
maximum-pool-size: 10
idle-timeout: 30000
connection-timeout: 20000
  • minimum-idle: 최소 유휴 커넥션 수입니다. 초기 설정 시 최소한의 커넥션(여기서는 10개)만 유지하여, TPS가 낮을 때 리소스를 절약할 수 있습니다.
  • maximum-pool-size: 커넥션 풀의 최대 크기입니다. TPS가 높아질 때 최대 10개의 커넥션까지 생성하여 요청을 처리할 수 있게 설정합니다.
  • idle-timeout: 지정된 시간(밀리초) 동안 유휴 상태인 커넥션이 있을 경우 풀에서 제거합니다. 트래픽이 낮아질 때 자동으로 풀 크기를 줄이는 데 기여합니다.
  • connection-timeout: 커넥션을 얻기 위해 대기하는 최대 시간입니다. 이 시간 내에 커넥션을 확보하지 못하면 예외가 발생합니다.

TPS 변화에 따른 커넥션 풀의 동작#

  1. TPS가 낮은 경우: Spring Boot 애플리케이션이 유휴 상태이거나 트래픽이 적은 경우, HikariCP는 최소 커넥션(minimum-idle)만 유지하여 리소스 사용을 최적화합니다.
  2. TPS가 높아질 경우: TPS가 증가하여 커넥션이 필요한 상황이 되면, HikariCP는 최대 커넥션(maximum-pool-size)까지 확장하여 대량의 요청을 처리할 수 있게 합니다. 이를 통해 성능 저하 없이 안정적으로 트래픽을 소화할 수 있습니다.
  3. TPS가 다시 낮아지는 경우: TPS가 다시 낮아지면 HikariCP는 idle-timeout에 따라 불필요한 커넥션을 풀에서 제거하고, minimum-idle만 유지하여 리소스를 절약합니다.

이 케이스는 MySQL Connection Pool에서 minimum-idlemaximum-pool-size를 동일하게 설정한 상황에서, TPS가 200대에 도달할 때 발생하는 성능 문제를 다루고 있습니다. 그래프와 로그를 바탕으로 아래와 같이 분석할 수 있습니다.

TPS가 높아지는 상황에서의 커넥션 풀 동작 분석#

Spring Boot 애플리케이션에서 TPS가 높아질 때, HikariCP의 커넥션 풀이 어떻게 반응하고 성능에 어떤 영향을 미치는지 살펴보겠습니다. 이 테스트는 minimum-idle: 10maximum-pool-size: 10 설정을 사용해, 커넥션 풀의 확장성과 한계점을 확인하는 데 중점을 두었습니다.

애플리케이션은 지속적으로 증가하는 사용자 요청을 처리하며, TPS가 증가함에 따라 커넥션 풀이 최대에 도달하는 시점에서 성능 지연과 요청 실패가 발생하는 과정을 시각적으로 분석했습니다.

상황 설명#

다음 코드는 Spring Boot와 Kotlin 환경에서 설정된 컨트롤러와 서비스 로직입니다. 컨트롤러에서는 SampleServicegetMember() 메서드를 호출하며, 이 메서드는 1~100 사이의 랜덤 ID로 Member 엔티티를 PK를 기반으로 조회한 후, 1초의 지연 시간을 둔 뒤 커넥션 풀의 현재 상태를 로깅합니다.

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping
class SampleController(
private val SampleService: SampleService
) {

@GetMapping("/api/v1/members")
fun sample(): Member {
// 1 ~ 100 사이의 랜덤으로 member 조회
return SampleService.getMember()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
class SampleService(
private val dataSource: DataSource,
private val memberRepository: MemberRepository
) {
private val log = LoggerFactory.getLogger(javaClass)!!

@Transactional
fun getMember(): Member {
val findById = memberRepository.findById(Random.nextInt(1, 100).toLong()).get()
runBlocking { delay(1000) }
val targetDataSource = dataSource.unwrap(HikariDataSource::class.java)
val hikariDataSource = targetDataSource as HikariDataSource
val hikariPoolMXBean = hikariDataSource.hikariPoolMXBean
val hikariConfigMXBean = hikariDataSource.hikariConfigMXBean
val log =
"""
totalConnections : ${hikariPoolMXBean.totalConnections}
activeConnections : ${hikariPoolMXBean.activeConnections}
idleConnections : ${hikariPoolMXBean.idleConnections}
threadsAwaitingConnection : ${hikariPoolMXBean.threadsAwaitingConnection}
maxLifetime : ${hikariConfigMXBean.maxLifetime}
maximumPoolSize : ${hikariConfigMXBean.maximumPoolSize}
minimumIdle : ${hikariConfigMXBean.minimumIdle}
connectionTimeout : ${hikariConfigMXBean.connectionTimeout}
validationTimeout : ${hikariConfigMXBean.validationTimeout}
idleTimeout : ${hikariConfigMXBean.idleTimeout}
""".trimIndent()
this.log.info(log)
return findById
}
}

이 코드는 지연을 위해 1초 동안 대기한 후, HikariCP 커넥션 풀의 상태를 로깅하여 현재 커넥션 풀 상황을 모니터링할 수 있게 합니다.

성능 테스트 결과 (위 이미지 설명)#

위 이미지는 커넥션 풀 설정이 minimum-idle: 10, maximum-pool-size: 10으로 설정된 상황에서, TPS가 증가함에 따라 성능이 어떻게 변화하는지를 시각화한 결과입니다.

  • Total Requests per Second:
    • 이 그래프는 초당 요청 처리량(RPS, 초록색 라인)과 실패한 요청(Failures, 빨간색 라인)을 보여줍니다.
    • TPS가 점진적으로 증가하여 초당 12 요청 수준에 도달했을 때, 실패한 요청이 발생하기 시작했습니다. 이는 커넥션 풀이 최대 용량인 10에 도달하여 더 이상 추가 요청을 처리하지 못하는 상황을 나타냅니다.
    • 이후 TPS는 유지되지만, 실패한 요청이 지속적으로 발생하면서 커넥션 풀의 제한에 따른 성능 저하가 명확히 드러납니다. 이후 TPS는 응답 지연으로 인해 더 이상 올라가지 않습니다.
  • Response Times:
    • 응답 시간 그래프에서는 50th 퍼센타일(주황색 라인)과 95th 퍼센타일(보라색 라인)의 응답 시간이 시간이 지남에 따라 증가하는 모습이 보입니다.
    • 특히 TPS가 증가함에 따라 95th 퍼센타일 응답 시간은 약 20,000ms 이상으로 치솟아, 사용자 요청이 큰 지연을 겪고 있음을 나타냅니다.
    • 이는 커넥션 풀이 가득 차서 새로운 요청이 대기 상태로 전환되었기 때문이며, 트래픽 증가와 함께 시스템의 성능 한계에 도달했음을 보여줍니다.
  • Number of Users:
    • 사용자의 수가 점진적으로 증가하며 시스템에 부하를 가하고 있습니다. 사용자가 약 300명 이상일 때부터 시스템은 커넥션 풀이 한계에 도달하여, 그 이후로는 성능 저하가 본격적으로 발생합니다.
    • 커넥션 풀 크기를 초과하는 사용자 요청은 실패하거나 긴 대기 시간을 초래하게 되며, 이는 응답 시간 증가와 TPS 유지의 원인이 됩니다.

이 이미지에서는 커넥션 풀이 최대 용량에 도달함에 따라, 시스템이 추가적인 요청을 감당하지 못하고 지연 시간과 실패율이 증가하는 과정을 시각적으로 확인할 수 있습니다.

로그 분석#

필드 설명
activeConnections 10 - 현재 활성 상태인 모든 커넥션이 사용 중입니다.
idleConnections 0 - 유휴 상태의 커넥션은 없습니다.
threadsAwaitingConnection 84 - 84개의 스레드가 커넥션을 기다리고 있습니다.
maxLifetime 1800000 (밀리초) - 커넥션의 최대 수명입니다.
maximumPoolSize 10 - 최대 커넥션 풀 크기가 10으로 설정되어 있습니다.
minimumIdle 10 - 최소 유휴 커넥션이 10으로 설정되어 있습니다.
connectionTimeout 30000 (밀리초) - 커넥션을 얻기 위해 대기할 수 있는 최대 시간입니다.
validationTimeout 5000 (밀리초) - 커넥션 유효성 검사를 위한 시간입니다.
idleTimeout 600000 (밀리초) - 유휴 커넥션을 유지하는 최대 시간입니다.

이 로그는 커넥션 풀이 한계에 도달하여 더 이상 커넥션을 확장할 수 없고, 여러 스레드가 커넥션을 기다리면서 성능 저하가 발생하고 있음을 보여줍니다.

문제 원인#

  1. 커넥션 풀 크기 제한: 현재 maximum-pool-size가 10으로 설정되어 있어 초당 12개 이상의 요청(TPS)을 처리하기에는 커넥션 풀 크기가 부족합니다. 모든 커넥션이 이미 사용 중이기 때문에, 추가적인 요청이 들어오면 커넥션을 기다리게 되고, 이로 인해 요청 실패가 발생하거나 응답 시간이 길어집니다.
  2. 스레드 대기: 커넥션 풀이 최대 용량에 도달하면서 threadsAwaitingConnection 수가 증가하게 됩니다. 이는 커넥션을 얻지 못한 요청이 대기 상태로 전환되는 상황을 나타내며, TPS가 증가할 때 시스템이 추가 요청을 즉각적으로 처리하지 못하고 성능 저하를 초래하는 주요 원인이 됩니다.
  3. connectionTimeout 설정: 현재 connectionTimeout이 30000밀리초(30초)로 설정되어 있어, 커넥션을 기다리는 요청은 최대 30초까지 대기할 수 있습니다. 그러나 이 대기 시간이 길어질수록 전체 응답 시간이 증가하게 되며, 대기 중인 요청이 많아지면 TPS가 상승하기 어려워지고 응답 지연으로 인한 성능 저하가 발생할 수 있습니다.

이러한 문제들은 TPS가 높아질수록 커넥션 풀의 제한으로 인해 전체적인 성능 저하가 발생하게 되는 이유입니다.

해결 방안#

  1. maximum-pool-size 증가: 현재의 TPS 수요를 충족하기 위해 maximum-pool-size 값을 늘려야 합니다. 예를 들어, 10 이상으로 설정하여 커넥션 풀이 더 많은 요청을 처리할 수 있도록 하면, 요청 대기 시간과 실패를 줄일 수 있습니다.
  2. 동적 커넥션 관리: HikariCP의 특성을 활용해 minimum-idlemaximum-pool-size를 적절히 조정하여 트래픽 변화에 유연하게 대응할 수 있도록 합니다. TPS가 높아질 때는 커넥션 풀이 자동으로 확장되도록 하고, TPS가 감소할 때는 최소한의 커넥션만 유지해 리소스를 절약하도록 설정하는 것이 좋습니다.
  3. 모니터링 및 지속적인 튜닝: 커넥션 풀의 상태를 지속적으로 모니터링하여, 트래픽 패턴에 맞게 적절히 튜닝하는 것이 필요합니다. 정기적인 모니터링을 통해 TPS와 응답 시간 변화를 관찰하고, 필요에 따라 maximum-pool-size, connectionTimeout 등의 설정을 조정하여 최적의 성능을 유지할 수 있습니다.

이러한 방안들은 트래픽 변동에 따라 유연하게 커넥션 풀을 관리하고, 시스템 성능을 최적화하는 데 도움이 됩니다.

성능 테스트 결과 분석: maximum-pool-size를 200으로 조정한 경우#

1
2
3
4
5
spring:
datasource:
hikari:
maximum-pool-size: 200 # 최대 커넥션 수
minimum-idle: 10 # 최소 유휴 커넥션 수

위와 같이 maximum-pool-size를 200, minimum-idle을 10으로 설정하여 테스트를 진행한 결과, 커넥션 풀이 충분히 확장 가능해지면서 시스템 성능이 크게 개선되었습니다. 주요 개선 사항은 다음과 같습니다.

  • Total Requests per Second (RPS):
    • RPS가 점진적으로 증가하여 높은 TPS를 안정적으로 처리할 수 있게 되었습니다. 초당 요청 처리량이 약 150까지 증가했음에도 불구하고, 실패한 요청(Failures/s)은 발생하지 않았습니다.
    • 이는 커넥션 풀이 충분히 확장되어, 모든 요청이 처리되는 동안 커넥션 부족으로 인한 대기 시간이 발생하지 않았음을 의미합니다.
  • Response Times:
    • 응답 시간 그래프에서 50th 및 95th 퍼센타일 응답 시간이 비교적 안정적인 수준을 유지하고 있습니다.
    • 95th 퍼센타일 응답 시간은 약 3,000ms 이하로, 50th 퍼센타일은 약 1,000ms 내외로 유지되었습니다. 이는 고TPS 상황에서도 일관된 응답 속도를 제공할 수 있음을 보여줍니다.
    • 이전 설정에서 발생했던 응답 시간의 급격한 증가가 해소되어, 사용자 경험이 크게 개선되었습니다.
  • Failures/s 비율:
    • 요청 실패율이 0으로 유지되었습니다. maximum-pool-size를 200으로 설정한 덕분에, connectionTimeout으로 인해 대기 상태에서 실패하는 요청이 없었습니다.
    • 이로써 고TPS 상황에서도 안정적인 서비스가 가능해졌으며, 대량의 동시 요청을 처리하는 데 적합한 환경이 조성되었습니다.

로그 분석#

필드 설명
totalConnections 152 - 현재 총 152개의 커넥션이 생성되었습니다.
activeConnections 152 - 모든 커넥션이 활성 상태로 사용 중입니다.
idleConnections 0 - 유휴 상태의 커넥션은 없습니다.
threadsAwaitingConnection 48 - 48개의 스레드가 커넥션을 기다리고 있습니다.
maximumPoolSize 200 - 최대 커넥션 풀 크기가 200으로 설정되었습니다.
minimumIdle 10 - 최소 유휴 커넥션이 10으로 설정되어 있어, 초기에 모든 커넥션이 생성된 상태입니다.

이 설정을 통해 커넥션 풀은 트래픽이 적을 때에는 최소한의 자원만 사용하고, 트래픽이 증가할 때에는 최대 200개의 커넥션까지 확장하여 요청을 처리할 수 있습니다. 로그에서 볼 수 있듯이 TPS가 높은 상황에서도 커넥션 풀이 충분히 확장되었고, 전체적인 시스템 성능에는 큰 영향을 미치지 않았습니다. 이를 통해 시스템은 높은 TPS 환경에서도 안정적이고 일관된 성능을 제공할 수 있음을 확인할 수 있습니다.

성능 테스트 결과 분석: TPS 감소 상황에서의 커넥션 풀 동작#

아래 설정으로 TPS가 낮아진 상황에서 성능 테스트를 진행했습니다.

1
2
3
4
5
6
7
spring:
datasource:
hikari:
maximum-pool-size: 200 # 최대 커넥션 수
minimum-idle: 10 # 최소 유휴 커넥션 수
max-lifetime: 300000 # 커넥션이 유지될 최대 시간 (밀리초)
idle-timeout: 250000 # 유휴 커넥션이 유지될 최대 시간 (밀리초)
  • max-lifetime: 기본값은 1800000ms (30분)입니다. 각 커넥션이 일정 시간 동안만 유지된 후 새 커넥션으로 교체되도록 하여, 오래된 커넥션으로 인해 발생할 수 있는 문제를 방지합니다. 설정된 시간(예: 300,000ms = 5분) 이후에 커넥션은 제거되고 새로운 커넥션으로 대체됩니다.
  • idle-timeout: 기본값은 600000ms (10분)입니다. 유휴 상태의 커넥션이 설정된 시간 동안 사용되지 않으면 커넥션 풀에서 제거됩니다. 트래픽이 감소한 상황에서는 유휴 커넥션이 줄어들어 리소스가 절약됩니다. 위 설정에서는 250,000ms 동안 유휴 상태인 커넥션을 유지하고 이후에 해제하도록 설정했습니다.

테스트 결과 분석#

  • Total Requests per Second (RPS):
    • TPS가 낮아지며, 초당 요청 처리량이 약 5 수준으로 안정화되었습니다.
    • 요청 실패(Failures/s)가 발생하지 않았으며, 모든 요청이 성공적으로 처리되었습니다.
    • 이는 트래픽이 줄어들면서 커넥션 풀이 유휴 상태로 돌아가고 있음을 의미합니다.
  • Response Times:
    • 50th 및 95th 퍼센타일 응답 시간 모두 약 1,000ms 내외로 일정하게 유지되고 있습니다.
    • 응답 시간의 변동이 크지 않고 안정적인 수준을 보여, TPS가 낮아진 상황에서도 일관된 성능을 제공하고 있습니다.
  • Number of Users:
    • 테스트에서 사용자 수가 10명으로 일정하게 유지되고 있으며, TPS가 낮은 상태로 안정화되었습니다.

로그 분석#

필드
totalConnections 15 - 현재 총 15개의 커넥션이 생성되었습니다. 트래픽 감소에 따라 필요 없는 커넥션이 자동으로 해제되며, 풀 크기가 줄어든 상태입니다.
activeConnections 6 - 현재 활성 상태로 사용 중인 커넥션이 6개입니다.
idleConnections 9 - 최소 유휴 커넥션 설정(minimumIdle)이 10이므로, 트래픽이 줄어든 상황에서 9개의 유휴 커넥션이 유지됩니다.
threadsAwaitingConnection 0 - 대기 중인 스레드가 없어 모든 요청이 즉시 처리되고 있습니다.
maximumPoolSize 200 - 최대 커넥션 풀 크기가 200으로 설정되어 있지만, 현재 트래픽 수준에서는 전체를 사용할 필요 없이 적정 개수의 커넥션만 유지하고 있습니다.
minimumIdle 10 - 최소 유휴 커넥션 수가 10으로 설정되어, 트래픽이 적을 때도 최소한의 커넥션을 유지해 자원을 절약합니다.
maxLifetime 300000 (밀리초) - 각 커넥션이 5분(300초) 동안 유지된 후 자동으로 갱신되도록 설정되어 있어, 오래된 커넥션으로 인한 문제를 방지하고 안정적인 연결 상태를 보장합니다.
idleTimeout 250000 (밀리초) - 유휴 상태의 커넥션이 4분 10초 동안 사용되지 않으면 풀에서 해제됩니다. 이를 통해 트래픽이 감소한 상황에서는 유휴 커넥션을 줄여 리소스를 절약할 수 있습니다.

이 테스트 결과는 TPS가 줄어들면 totalConnections도 함께 감소하며 리소스가 효율적으로 관리되는 모습을 보여줍니다. minimum-idle 설정을 통해 커넥션 풀이 최소 10개의 유휴 커넥션을 유지하고, idleTimeout이 설정된 시간 동안 유휴 상태인 커넥션을 자동으로 해제하여 불필요한 자원 낭비를 방지합니다. 또한 maxLifetime 설정으로 각 커넥션의 유지 시간을 제한하여 일정 시간이 지나면 커넥션이 새롭게 갱신되도록 함으로써 오래된 커넥션으로 인한 문제를 예방합니다. 이를 통해 시스템은 트래픽 변화에 유연하게 대응하며 안정적으로 자원을 관리할 수 있는 구조를 갖추게 됩니다.

결론#

HikariCP를 사용한 커넥션 풀 설정은 트래픽 변화에 유연하게 대응할 수 있으며, 성능 최적화를 위해 중요한 도구가 됩니다. 적절한 maximum-pool-sizeminimum-idle 설정을 통해 고TPS 환경에서도 안정적이고 일관된 응답 시간을 제공할 수 있으며, 리소스를 효율적으로 관리하여 비용 절감 효과도 기대할 수 있습니다. 이를 통해 Spring Boot 애플리케이션은 다양한 트래픽 상황에서 성능을 최적화하며, 사용자 경험을 크게 개선할 수 있습니다.