DataSource·커넥션 풀
HikariCP 연결 풀의 크기·획득 시간·상태 초기화를 연결 고갈 실험으로 확인하고 DB 처리 한도에 맞춥니다.
데이터베이스 연결은 TCP 연결, 인증, 세션 초기화 비용이 있어 쿼리마다 새로 만들면 비용이 큽니다.
연결 풀은 미리 만든 제한된 연결을 애플리케이션 작업이 빌려 쓰게 합니다. 처리량을 무조건 늘리는 캐시가 아니라 DB가 동시에 감당할 작업 수를 애플리케이션에 배분하는 백프레셔 경계입니다.
DataSource와 구성 분리
리포지토리가 DriverManager와 URL·인증 정보를 직접 알면 환경과 풀 정책이 코드에 박힙니다.
DataSource를 주입받으면 테스트와 운영 환경이 같은 JDBC 코드를 쓰면서 연결 생성 정책만 바꿀 수 있습니다. Spring Boot는 클래스 경로와 속성을 바탕으로 HikariCP DataSource를 구성합니다.
package board.jdbc;
import java.sql.SQLException;
import java.time.LocalDate;
import javax.sql.DataSource;
public final class PostCountQuery {
private static final String SQL = """
select count(*)
from posts
where member_id = ? and published_on = ?
""";
private final DataSource dataSource;
public PostCountQuery(DataSource dataSource) {
this.dataSource = dataSource;
}
public int count(long memberId, LocalDate publishedOn)
throws SQLException {
try (var connection = dataSource.getConnection();
var statement = connection.prepareStatement(SQL)) {
statement.setLong(1, memberId);
statement.setObject(2, publishedOn);
try (var result = statement.executeQuery()) {
if (!result.next()) {
throw new SQLException("count row missing");
}
return result.getInt(1);
}
}
}
}풀의 Connection은 대개 논리 프록시입니다. close()는 물리 소켓을 닫는 대신 현재 대여를 끝내고 연결을 풀에 반환합니다.
따라서 풀을 써도 try-with-resources를 그대로 사용합니다. “풀이 알아서 회수한다”며 닫지 않으면 활성 연결이 최대치에 도달합니다.
package board.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
class PostCountQueryTest {
@Test
void query가_정확한_count를_읽고_connection을_반환한다()
throws Exception {
var config = new HikariConfig();
config.setJdbcUrl(
"jdbc:h2:mem:post_count;MODE=PostgreSQL;DB_CLOSE_DELAY=-1");
config.setUsername("sa");
config.setMaximumPoolSize(1);
config.setMinimumIdle(0);
try (var dataSource = new HikariDataSource(config)) {
new ResourceDatabasePopulator(
new ClassPathResource("schema.sql")).execute(dataSource);
try (var connection = dataSource.getConnection();
var statement = connection.createStatement()) {
statement.execute("""
insert into members(
id, email, password_hash, name,
active, daily_character_limit)
values (
41, '[email protected]', 'hash', '회원41',
true, 10000),
(42, '[email protected]', 'hash', '회원42',
true, 10000)
""");
statement.execute("""
insert into posts(
member_id, title, content, published_on,
client_request_id, created_at, version)
values
(41, 'A', '본문 A', date '2026-08-29',
'count_req_A1', timestamp with time zone
'2026-08-29 00:00:00+00', 0),
(41, 'B', '본문 B', date '2026-08-29',
'count_req_B2', timestamp with time zone
'2026-08-29 00:00:01+00', 0),
(42, 'C', '본문 C', date '2026-08-29',
'count_req_C3', timestamp with time zone
'2026-08-29 00:00:02+00', 0)
""");
}
var query = new PostCountQuery(dataSource);
assertThat(query.count(
41L, LocalDate.of(2026, 8, 29))).isEqualTo(2);
assertThat(query.count(
41L, LocalDate.of(2026, 8, 30))).isZero();
assertThat(dataSource.getHikariPoolMXBean()
.getActiveConnections()).isZero();
}
}
}연결 풀 크기
요청 스레드가 200개라고 연결도 200개 만들지는 않습니다.
DB CPU, 디스크 지연 시간, 다른 애플리케이션의 연결, 쿼리 종류와 목표 지연 시간을 함께 봅니다. 너무 큰 풀은 DB의 컨텍스트 전환과 잠금 경합을 늘리고 장애 때 더 많은 쿼리를 몰아넣습니다.
이 장의 유일한 application.properties는 앞에서 구성한 H2 PostgreSQL 호환 모드와 다음 Hikari 정책을 함께 사용합니다.
| 속성 | 예제 값 | 결정 기준 |
|---|---|---|
maximum-pool-size | 12 | DB가 이 애플리케이션에 배정한 동시 연결 예산 |
minimum-idle | 2 | 낮은 트래픽에서 유지할 준비 연결 수 |
connection-timeout | 2000 ms | 요청이 연결을 기다릴 최대 시간 |
validation-timeout | 1000 ms | 연결 유효성 검사의 상한, 획득 시간보다 짧게 |
max-lifetime | 1740000 ms | DB·네트워크 강제 종료보다 먼저 교체 |
keepalive-time | 120000 ms | 수명보다 짧은 유휴 연결 확인 주기 |
예제 숫자를 그대로 복사하지 않고 인프라 유휴 타임아웃과 DB 연결 예산을 확인합니다. HikariCP 설정 관계를 위반하면 시작 경고가 나므로 무시하지 않습니다.
변화는 활성·유휴·대기 메트릭을 보고 조정합니다.
연결 대기 타임아웃
풀이 모두 사용 중이면 다음 스레드는 연결이 반환되기를 기다립니다.
획득 시간이 너무 길면 웹 스레드가 함께 쌓입니다. 제한된 타임아웃으로 빠르게 실패시키고 업스트림 재시도와 대기열을 통제합니다.
연결 풀은 제한된 연결 예산으로 요청에 백프레셔를 건다
연결 1개 점유·다음 획득 timeout·held close·재획득 성공의 실제 풀 상태를 보여 준다.
CONTRACT / IMPLEMENTATION / REVIEW SCOPE
풀의 연결 1개를 점유하면 다음 획득은 제한 시간 뒤 실패한다
실제 고갈 fixture는 maximumPoolSize=1, connectionTimeout=250ms입니다. 점유한 연결을 반환한 뒤 다시 빌릴 수 있습니다. 이 획득 timeout은 이미 실행 중인 SQL을 취소하는 설정이 아닙니다.
POOL / BACKPRESSURE
동시에 빌리는 연결 예산을 제한
repository는 DataSource를 주입받아 연결 생성 정책과 분리됩니다. 풀의 close는 대여를 끝내고 재사용 가능한 물리 연결을 돌려줍니다. FIFO 순서나 요청 스레드 수만큼의 연결을 보장하지 않습니다.
ACTUAL EXHAUSTION FIXTURE
max 1 · active 1 · timeout 250ms
제공된 test는 SQLTransientConnectionException과 200ms 이상·2,000ms 미만 경과, 실패 뒤 awaiting=0을 확인합니다. 점유 연결을 finally에서 닫은 뒤 다시 획득해 isValid(1)=true, 종료 후 active=0을 확인합니다.
ACTUAL QUERY FIXTURE
회원·날짜를 바인딩하고 연결 반환
공유 schema에 회원41의 해당 날짜2행과 회원42의1행을 넣습니다. 회원41의 count=2, 다음 날짜 count=0, query 뒤 active connection=0을 확인합니다.
ACTUAL RESET FIXTURE
같은 H2 session의 autoCommit=true
max1/minIdle1 풀에서 첫 session의 autoCommit을 false로 바꾸고 명시적으로 rollback·close합니다. 다시 빌린 session ID가 같고 autoCommit=true인지 확인합니다. 모든 vendor 세션 변수·임시 테이블 초기화의 증거는 아닙니다.
APPLICATION CONFIG / DIFFERENT VALUES
운영 설정 예제와 고갈 fixture를 구분
본문 application.properties의 예제는 max12/minIdle2, 획득2,000ms/검증1,000ms입니다. 실제 고갈 실험의 max1/250ms와 다릅니다. DB 예산·메트릭·점유 시간을 보고 환경에 맞게 결정합니다.
COMPLETION / ADDITIONAL
반환 전에 commit 또는 rollback을 소유
활성 transaction을 Connection.close에 넘겼을 때 JDBC 결과는 구현 정의입니다. 누수 로그가 연결을 강제로 안전하게 반환하지 않습니다. 외부 I/O 분리와 outbox worker는 추가 설계이며 이 reset test에 포함되지 않습니다.
연결 하나의 풀에서 held connection을 유지하면 다음 getConnection이 SQLTransientConnectionException으로 실패한다. held를 close한 뒤 다시 획득하면 유효한 연결을 얻는다. 별도 reset fixture는 같은 H2 session의 autoCommit 복원을 확인한다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 연결 하나의 풀에서 held connection을 유지하면 다음 getConnection이 SQLTransientConnectionException으로 실패한다. held를 close한 뒤 다시 획득하면 유효한 연결을 얻는다. 별도 reset fixture는 같은 H2 session의 autoCommit 복원을 확인한다.
- 원래 가로 도표의 두 흐름을 유지한다. 풀 max 1 held 점유→다음 획득 대기 timeout 250 ms→일시적 획득 실패. held.close→다시 획득→isValid 1 true. 그림의 두 흐름 사이를 단일 진행 화살표로 합치지 않으며 원래 fixture는 하나를 점유한 뒤 실패하고 반환 후 복구하는 순서다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 연결 풀은 제한된 연결 예산으로 요청에 백프레셔를 건다 |
| 15 | 원래 가로 도표의 두 흐름을 유지한다. 풀 max 1 held 점유→다음 획득 대기 timeout 250 ms→일시적 획득 실패. held.close→다시 획득→isValid 1 true. 그림의 두 흐름 사이를 단일 진행 화살표로 합치지 않으며 원래 fixture는 하나를 점유한 뒤 실패하고 반환 후 복구하는 순서다. |
| 21 | 제공된 test는 SQLTransientConnectionException과 200 ms 이상·2,000 ms 미만 경과, 실패 뒤 awaiting=0을 확인합니다. 점유 연결을 finally에서 닫은 뒤 다시 획득해 isValid(1)=true, 종료 후 active=0을 확인합니다. |
| 24 | 공유 schema에 회원 41의 해당 날짜 2행과 회원 42의 1행을 넣습니다. 회원 41의 count=2, 다음 날짜 count=0, query 뒤 active connection=0을 확인합니다. |
| 27 | max 1/minIdle 1 풀에서 첫 session의 autoCommit을 false로 바꾸고 명시적으로 rollback·close합니다. 다시 빌린 session ID가 같고 autoCommit=true인지 확인합니다. 모든 vendor 세션 변수·임시 테이블 초기화의 증거는 아닙니다. |
| 30 | 본문 application.properties의 예제는 max 12/minIdle 2, 획득 2,000 ms/검증 1,000 ms입니다. 실제 고갈 실험의 max 1/250 ms와 다릅니다. DB 예산·메트릭·점유 시간을 보고 환경에 맞게 결정합니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 연결 하나를 보유 capacity 1인 pool의 연결을 held가 유지합니다. | capacity: 1 · held: 연결 보유 · 빈 연결: 0 | 유일한 연결이 사용 중 상태가 됩니다. |
| 추가 획득 timeout 연결을 반환하지 않은 동안 다음 getConnection이 실패합니다. | 추가 획득: SQLTransientConnectionException · held: 계속 보유 | 대기한 획득이 실패로 끝납니다. |
| 반환 뒤 재획득 held.close 뒤 다시 getConnection을 호출하면 유효한 연결을 얻습니다. | held: close · 재획득: 유효 연결 · pool: 반환 후 사용 가능 | 연결 반환이 새 획득을 가능하게 합니다. |
연결 풀은 제한된 연결 예산으로 요청에 백프레셔를 건다
연결 1개 점유·다음 획득 timeout·held close·재획득 성공의 실제 풀 상태를 보여 준다.
CONTRACT / IMPLEMENTATION / REVIEW SCOPE
풀의 연결 1개를 점유하면 다음 획득은 제한 시간 뒤 실패한다
실제 고갈 fixture는 maximumPoolSize=1, connectionTimeout=250ms입니다. 점유한 연결을 반환한 뒤 다시 빌릴 수 있습니다. 이 획득 timeout은 이미 실행 중인 SQL을 취소하는 설정이 아닙니다.
POOL / BACKPRESSURE
동시에 빌리는 연결 예산을 제한
repository는 DataSource를 주입받아 연결 생성 정책과 분리됩니다. 풀의 close는 대여를 끝내고 재사용 가능한 물리 연결을 돌려줍니다. FIFO 순서나 요청 스레드 수만큼의 연결을 보장하지 않습니다.
ACTUAL EXHAUSTION FIXTURE
max 1 · active 1 · timeout 250ms
제공된 test는 SQLTransientConnectionException과 200ms 이상·2,000ms 미만 경과, 실패 뒤 awaiting=0을 확인합니다. 점유 연결을 finally에서 닫은 뒤 다시 획득해 isValid(1)=true, 종료 후 active=0을 확인합니다.
ACTUAL QUERY FIXTURE
회원·날짜를 바인딩하고 연결 반환
공유 schema에 회원41의 해당 날짜2행과 회원42의1행을 넣습니다. 회원41의 count=2, 다음 날짜 count=0, query 뒤 active connection=0을 확인합니다.
ACTUAL RESET FIXTURE
같은 H2 session의 autoCommit=true
max1/minIdle1 풀에서 첫 session의 autoCommit을 false로 바꾸고 명시적으로 rollback·close합니다. 다시 빌린 session ID가 같고 autoCommit=true인지 확인합니다. 모든 vendor 세션 변수·임시 테이블 초기화의 증거는 아닙니다.
APPLICATION CONFIG / DIFFERENT VALUES
운영 설정 예제와 고갈 fixture를 구분
본문 application.properties의 예제는 max12/minIdle2, 획득2,000ms/검증1,000ms입니다. 실제 고갈 실험의 max1/250ms와 다릅니다. DB 예산·메트릭·점유 시간을 보고 환경에 맞게 결정합니다.
COMPLETION / ADDITIONAL
반환 전에 commit 또는 rollback을 소유
활성 transaction을 Connection.close에 넘겼을 때 JDBC 결과는 구현 정의입니다. 누수 로그가 연결을 강제로 안전하게 반환하지 않습니다. 외부 I/O 분리와 outbox worker는 추가 설계이며 이 reset test에 포함되지 않습니다.
연결 하나의 풀에서 held connection을 유지하면 다음 getConnection이 SQLTransientConnectionException으로 실패한다. held를 close한 뒤 다시 획득하면 유효한 연결을 얻는다. 별도 reset fixture는 같은 H2 session의 autoCommit 복원을 확인한다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 연결 하나의 풀에서 held connection을 유지하면 다음 getConnection이 SQLTransientConnectionException으로 실패한다. held를 close한 뒤 다시 획득하면 유효한 연결을 얻는다. 별도 reset fixture는 같은 H2 session의 autoCommit 복원을 확인한다.
- 원래 가로 도표의 두 흐름을 유지한다. 풀 max 1 held 점유→다음 획득 대기 timeout 250 ms→일시적 획득 실패. held.close→다시 획득→isValid 1 true. 그림의 두 흐름 사이를 단일 진행 화살표로 합치지 않으며 원래 fixture는 하나를 점유한 뒤 실패하고 반환 후 복구하는 순서다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 연결 풀은 제한된 연결 예산으로 요청에 백프레셔를 건다 |
| 15 | 원래 가로 도표의 두 흐름을 유지한다. 풀 max 1 held 점유→다음 획득 대기 timeout 250 ms→일시적 획득 실패. held.close→다시 획득→isValid 1 true. 그림의 두 흐름 사이를 단일 진행 화살표로 합치지 않으며 원래 fixture는 하나를 점유한 뒤 실패하고 반환 후 복구하는 순서다. |
| 21 | 제공된 test는 SQLTransientConnectionException과 200 ms 이상·2,000 ms 미만 경과, 실패 뒤 awaiting=0을 확인합니다. 점유 연결을 finally에서 닫은 뒤 다시 획득해 isValid(1)=true, 종료 후 active=0을 확인합니다. |
| 24 | 공유 schema에 회원 41의 해당 날짜 2행과 회원 42의 1행을 넣습니다. 회원 41의 count=2, 다음 날짜 count=0, query 뒤 active connection=0을 확인합니다. |
| 27 | max 1/minIdle 1 풀에서 첫 session의 autoCommit을 false로 바꾸고 명시적으로 rollback·close합니다. 다시 빌린 session ID가 같고 autoCommit=true인지 확인합니다. 모든 vendor 세션 변수·임시 테이블 초기화의 증거는 아닙니다. |
| 30 | 본문 application.properties의 예제는 max 12/minIdle 2, 획득 2,000 ms/검증 1,000 ms입니다. 실제 고갈 실험의 max 1/250 ms와 다릅니다. DB 예산·메트릭·점유 시간을 보고 환경에 맞게 결정합니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 연결 하나를 보유 capacity 1인 pool의 연결을 held가 유지합니다. | capacity: 1 · held: 연결 보유 · 빈 연결: 0 | 유일한 연결이 사용 중 상태가 됩니다. |
| 추가 획득 timeout 연결을 반환하지 않은 동안 다음 getConnection이 실패합니다. | 추가 획득: SQLTransientConnectionException · held: 계속 보유 | 대기한 획득이 실패로 끝납니다. |
| 반환 뒤 재획득 held.close 뒤 다시 getConnection을 호출하면 유효한 연결을 얻습니다. | held: close · 재획득: 유효 연결 · pool: 반환 후 사용 가능 | 연결 반환이 새 획득을 가능하게 합니다. |
package board.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.sql.Connection;
import java.sql.SQLTransientConnectionException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
class PoolExhaustionTest {
@Test
void 최대_연결이_사용_중이면_timeout으로_실패하고_반환_뒤_회복한다()
throws Exception {
var config = new HikariConfig();
config.setJdbcUrl(
"jdbc:h2:mem:pool_exhaustion;MODE=PostgreSQL;DB_CLOSE_DELAY=-1");
config.setUsername("sa");
config.setMaximumPoolSize(1);
config.setMinimumIdle(0);
config.setConnectionTimeout(250);
try (var dataSource = new HikariDataSource(config)) {
Connection held = dataSource.getConnection();
try {
assertThat(dataSource.getHikariPoolMXBean()
.getActiveConnections()).isEqualTo(1);
long started = System.nanoTime();
assertThrows(
SQLTransientConnectionException.class,
dataSource::getConnection);
long elapsedMillis =
(System.nanoTime() - started) / 1_000_000;
assertThat(elapsedMillis)
.isGreaterThanOrEqualTo(200)
.isLessThan(2_000);
assertThat(dataSource.getHikariPoolMXBean()
.getThreadsAwaitingConnection()).isZero();
} finally {
held.close();
}
try (var recovered = dataSource.getConnection()) {
assertThat(recovered.isValid(1)).isTrue();
}
assertThat(dataSource.getHikariPoolMXBean()
.getActiveConnections()).isZero();
}
}
}벽시계 검증은 CI 부하에 민감하므로 좁은 상한을 두지 않습니다. 핵심 oracle은 무한 대기하지 않고 일시적 획득 실패가 발생하며 점유 연결을 반환한 뒤 다시 획득할 수 있다는 점입니다.
누수 탐지와 메트릭
Hikari 누수 탐지 임계값을 넘게 연결을 보유하면 획득 스택을 로그할 수 있습니다. 긴 정상 트랜잭션도 누수처럼 보일 수 있으므로 업무 목표 시간보다 높게 둡니다.
누수 로그가 연결을 강제로 안전하게 반환해 주지는 않습니다. 활성 연결이 최댓값에 도달하고 대기가 늘면 느린 SQL, 잠금 대기, 트랜잭션 안의 외부 호출을 먼저 확인합니다.
| 신호 | 가능한 원인 | 다음 확인 |
|---|---|---|
| 활성=최댓값 | 긴 쿼리·트랜잭션 | 느린 SQL·잠금 대기 |
| 대기 증가 | 용량 초과 | 요청 비율·획득 타임아웃 |
| 유휴=최댓값 | 과도한 풀 | DB 연결 예산 |
| 검증 실패 | 네트워크·DB 재시작 | 수명·연결 유지 정책 |
풀만 키우면 DB 부담이 커질 수 있습니다. JDBC 4 드라이버라면 별도 검증 쿼리보다 Connection.isValid()를 우선 사용합니다.
연결 상태 초기화
연결을 풀에 돌려줄 때 자동 커밋, 읽기 전용, 격리, 카탈로그 같은 추적 가능한 상태가 다음 사용자에게 새지 않아야 합니다.
Hikari 프록시는 바뀐 JDBC 상태를 풀 기본값으로 복원합니다. 하지만 공급자 세션 변수와 임시 테이블까지 일반화하지 않습니다. 트랜잭션 소유자는 닫기 전에 커밋 또는 롤백을 명시해야 합니다.
package board.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import java.sql.Connection;
import java.sql.SQLException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.Test;
class PoolStateResetTest {
@Test
void 같은_물리_session을_다시_빌려도_autoCommit은_복원된다()
throws Exception {
var config = new HikariConfig();
config.setJdbcUrl(
"jdbc:h2:mem:pool_reset;MODE=PostgreSQL;DB_CLOSE_DELAY=-1");
config.setUsername("sa");
config.setMaximumPoolSize(1);
config.setMinimumIdle(1);
try (var dataSource = new HikariDataSource(config)) {
long firstSession;
try (Connection first = dataSource.getConnection()) {
firstSession = sessionId(first);
first.setAutoCommit(false);
first.rollback();
}
try (Connection second = dataSource.getConnection()) {
assertThat(sessionId(second)).isEqualTo(firstSession);
assertThat(second.getAutoCommit()).isTrue();
}
}
}
private long sessionId(Connection connection) throws SQLException {
try (var statement = connection.createStatement();
var result = statement.executeQuery("select session_id()")) {
result.next();
return result.getLong(1);
}
}
}JDBC Connection.close()에 활성 트랜잭션을 넘겼을 때의 결과는 인터페이스 수준에서 구현 정의입니다. Hikari의 정리 동작을 안전망으로 이해하되 애플리케이션의 정상 제어 흐름으로 사용하지 않습니다.
연습 문제
풀 최댓값이 2일 때 두 트랜잭션이 외부 HTTP 호출을 기다리며 연결을 점유하고 세 번째 요청이 타임아웃나는 상황을 재현하세요.
외부 호출을 트랜잭션 밖으로 옮긴 설계와 비교해 활성·대기·지연 시간 변화를 설명합니다.
실행 전에 연결 점유 시간을 계산하기 풀에 연결은 2개뿐이고, A와 B는 시각 0ms에 각각 하나를 빌려 1,000ms까지 돌려주지 않습니다. C는 100ms에 획득을 요청하며 대기 한도는 250ms입니다. 새 연결 생성·다른 반환·재시도는 없다고 가정합니다. C가 실패하는 가정 시각과 그때 활성·대기 요청 수를 구하세요. A가 대신 200ms에 반환한다면 C가 획득할 수 있는지도 판단하세요.
풀 점유와 대기 구간의 풀이
첫 조건에서 C의 대기 마감은 100+250=350 ms입니다. 그 전에 반환이 없어 C는 획득하지 못합니다. 실패 직전에는 활성 연결 2개와 획득 대기 1개이며, C가 실패하여 대기에서 빠진 뒤에는 활성 2개·대기 0개입니다. A가 200 ms에 반환하는 변형에서는 C가 기다린 100 ms가 한도보다 짧아 그 연결을 획득할 수 있습니다. A·B·C만 존재하므로 다른 대기 요청의 우선순위는 고려하지 않습니다.
실제 HikariCP 테스트의 경과 시간은 스케줄링과 실행 환경의 영향을 받습니다. 위 시각은 점유와 대기 조건을 계산하는 모델이며 정확한 350 ms 실측을 주장하지 않습니다. 외부 HTTP 대기를 DB 트랜잭션 밖으로 옮기면 연결 점유 구간을 줄일 수 있지만 DB와 원격 서버의 원자적 커밋을 얻는 것은 아닙니다.
해설 보기
DB 변경 전에 외부 데이터를 먼저 읽을 수 있는지, 커밋할 DB 상태와 전송 의도를 함께 저장한 뒤 워커로 보낼 수 있는지 분류합니다.
package board.application;
import java.time.Instant;
import java.util.Objects;
public record PostCreatedEvent(
long postId,
long memberId,
Instant occurredAt,
String clientRequestId
) {
public PostCreatedEvent {
if (postId <= 0) {
throw new IllegalArgumentException("postId must be positive");
}
if (memberId <= 0) {
throw new IllegalArgumentException("memberId must be positive");
}
Objects.requireNonNull(occurredAt, "occurredAt");
if (clientRequestId == null
|| !clientRequestId.matches("[A-Za-z0-9_-]{8,64}")) {
throw new IllegalArgumentException("invalid clientRequestId");
}
}
}게시글 행과 PostCreatedEvent를 표현한 아웃박스 행을 같은 DB 트랜잭션에 저장하고, 워커가 커밋된 이벤트를 전송합니다. 원자성이 필요하다고 연결을 잡은 채 느린 네트워크를 기다리는 것은 분산 트랜잭션을 제공하지 않습니다.
다음 문서에서는 풀 연결 하나 안에서 커밋과 롤백이 만드는 원자성과 READ_COMMITTED 교차 세션 가시성을 두 DB 연결로 확인합니다. 장애 뒤 지속성은 그 관찰 범위에 포함하지 않습니다.