자바가 관계형 데이터베이스와 이야기하는 표준 인터페이스(java.sql 패키지). 1997년 JDK 1.1부터 있었다.
"요즘도 쓰나요"에 대한 정확한 답
"안 쓴다"가 아니라 "직접 호출하지 않는다" 가 맞다.
애플리케이션 코드
↓
JPA/Hibernate · MyBatis · jOOQ · JdbcTemplate · Spring Data JDBC
↓
━━━━━━━━━━━ JDBC (java.sql) ━━━━━━━━━━━ ← 전부 이 위에서 돈다
↓
커넥션 풀 (HikariCP)
↓
DB 드라이버 (postgresql, mysql-connector, ...)
↓
데이터베이스
위의 모든 도구가 내부에서 JDBC를 호출한다. 커넥션 풀도 JDBC 스펙 위의 구현이다. 그래서 N+1이든 커넥션 고갈이든, 사고가 나면 원인은 늘 이 층에 있다.
네 개의 핵심 객체
try (Connection con = dataSource.getConnection(); // ① 연결을 빌린다
PreparedStatement ps = con.prepareStatement(sql)) { // ② SQL을 준비한다
ps.setLong(1, userId); // ③ 파라미터를 바인딩
try (ResultSet rs = ps.executeQuery()) { // ④ 결과를 커서로 받는다
while (rs.next()) { ... }
}
} // close() = 풀로 반납 (물리 연결을 닫는 게 아니다)
여기서 가장 자주 헷갈리는 지점 — 풀에서 빌린 Connection에 close()를 부르면 물리 연결이 닫히는 게 아니라 풀로 반납된다. 반납하지 않으면 커넥션 누수가 되고, 쌓이면 풀이 고갈된다.
Statement vs PreparedStatement
// ❌ 문자열을 이어 붙이면 SQL 인젝션에 뚫린다
stmt.executeQuery("SELECT * FROM users WHERE name = '" + input + "'");
// ✅ 파라미터를 값으로 넘기면 SQL 구조가 고정된다 + DB가 실행 계획을 재사용
ps = con.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, input);
PreparedStatement는 SQL 뼈대를 먼저 DB에 보내 파싱시켜 두고 값만 나중에 채운다. 그래서 값이 SQL 문법으로 해석될 여지가 없고(인젝션 차단), 같은 쿼리를 반복할 때 실행 계획을 재사용해 빠르다.
가상 스레드 시대의 재평가
"JDBC는 블로킹이라 확장이 안 된다"가 오랫동안 리액티브를 택할 이유였다. 가상 스레드가 블로킹 대기 비용을 없애고 Java 24의 JEP 491이 pinning까지 지우면서, 그 근거가 사라졌다. 대신 새 주의점이 생겼다 — 커넥션 풀이 다음 병목으로 드러난다.
면접 함정
- ❌ "JDBC는 레거시" → 모든 상위 도구의 토대다.
- ❌ "
close()하면 연결이 닫힌다" → 풀 환경에서는 반납이다.
대량 처리 — 왕복을 줄인다
// 조회: 기본 fetch size가 작아(드라이버마다 다르나 흔히 10) 왕복이 잦다
ps.setFetchSize(1000); // 한 번에 1,000행씩 가져온다 (메모리와의 균형)
// 쓰기: 한 건씩 보내면 매번 네트워크 왕복이다
for (var u : users) {
ps.setString(1, u.name());
ps.addBatch(); // 모아 두었다가
}
ps.executeBatch(); // 한 번에 보낸다
INSERT 10,000건
건별 실행 왕복 10,000회
배치 1,000 왕복 10회 → 대개 수십 배 차이
JPA를 쓰더라도 결국 이 층에서 벌어지는 일이라, 배치 설정을 안 켜면 JPA의 saveAll()도 건별 INSERT로 나간다.