JDBC는 DB와 어떻게 통신하는가: Connection부터 PreparedStatement까지
과거 JDBC 0628·0629 학습 노트를 하나로 합치고, 현재 관점에서 JDBC의 실행 흐름과 자원 관리를 다시 정리합니다.
과거 Tistory의 JDBC - 0628, JDBC - 0629 두 학습 노트를 하나로 통합해 다시 작성했다.
처음 JDBC를 배울 때는 순서를 외웠다.
드라이버 준비 → Connection 생성 → SQL 실행 → 결과 처리 → 연결 종료.
지금 다시 보면 중요한 것은 순서 자체보다 각 객체가 어떤 책임을 가지는지 이해하는 것이다.
JDBC의 역할
JDBC(Java Database Connectivity)는 Java 애플리케이션이 관계형 DBMS와 통신하기 위한 표준 API다.
애플리케이션은 JDBC API를 사용하고, 실제 DBMS별 통신은 JDBC Driver가 담당한다.
flowchart LR
A[Java Application] --> B[JDBC API]
B --> C[JDBC Driver]
C --> D[(DBMS)]
그래서 애플리케이션 코드가 MySQL, Oracle, PostgreSQL의 네트워크 프로토콜을 직접 구현할 필요가 없다.
1. Connection
Connection은 데이터베이스와의 세션을 표현한다.
1
2
3
4
5
6
7
8
9
String url = "jdbc:oracle:thin:@localhost:1521:xe";
String user = "shopping";
String password = "password";
try (Connection connection =
DriverManager.getConnection(url, user, password)) {
// SQL 실행
}
예전 노트에는 Class.forName으로 Oracle Driver를 명시적으로 로딩하는 코드가 있었다.
1
Class.forName("oracle.jdbc.driver.OracleDriver");
과거에는 흔한 방식이었지만 JDBC 4 이후에는 JDBC Driver가 Service Provider 메커니즘을 통해 자동 등록될 수 있기 때문에 일반적인 애플리케이션 코드에서 명시적 Class.forName 호출이 필수는 아니다.
2. PreparedStatement
문자열을 이어 붙여 SQL을 만드는 것보다 PreparedStatement를 사용하는 편이 좋다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String sql = """
SELECT member_id, name
FROM member
WHERE member_id = ?
""";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, 10L);
try (ResultSet rs = statement.executeQuery()) {
while (rs.next()) {
long memberId = rs.getLong("member_id");
String name = rs.getString("name");
}
}
}
PreparedStatement의 중요한 장점은 파라미터 값을 SQL 구조와 분리할 수 있다는 점이다.
따라서 사용자 입력을 문자열 연결로 SQL에 직접 붙이는 것보다 안전하고 코드도 읽기 쉽다.
3. executeQuery와 executeUpdate
조회와 변경 작업은 실행 방식이 다르다.
SELECT
1
ResultSet rs = statement.executeQuery();
조회 결과를 ResultSet으로 받는다.
INSERT / UPDATE / DELETE
1
int affectedRows = statement.executeUpdate();
변경된 행의 개수를 반환한다.
4. ResultSet
ResultSet은 조회 결과를 가리키는 Cursor 형태의 객체다.
1
2
3
while (rs.next()) {
System.out.println(rs.getString("name"));
}
처음부터 모든 행이 현재 위치에 있는 것이 아니라 next()로 한 행씩 이동하면서 값을 읽는다.
대량 조회를 다룰 때 이 특성이 중요해진다. ORM이나 MyBatis를 사용하더라도 결국 하위 계층에서는 DB Driver와 ResultSet의 처리 방식이 성능과 메모리 사용에 영향을 줄 수 있다.
5. 자원 정리
초기에는 finally에서 close를 하나씩 호출하는 코드를 많이 작성했다.
현재 Java에서는 AutoCloseable을 이용하는 try-with-resources가 훨씬 명확하다.
1
2
3
4
5
6
7
8
9
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet rs = statement.executeQuery()
) {
while (rs.next()) {
// result mapping
}
}
블록을 벗어나면 역순으로 자원이 닫히므로 예외 상황에서도 자원 누수를 줄일 수 있다.
6. 실제 Spring 애플리케이션에서는
Spring을 사용하면 보통 DriverManager를 직접 호출하지 않는다.
DataSource와 Connection Pool을 사용하고, JdbcTemplate·MyBatis·JPA 같은 상위 추상화를 사용한다.
그렇다고 JDBC를 몰라도 된다는 뜻은 아니다.
오히려 다음 문제를 이해할 때 JDBC 지식이 다시 필요하다.
- Connection Pool 고갈
- Transaction 범위
- 대량 조회 시 Fetch Size
- Cursor 기반 스트리밍
- Statement Timeout
- ResultSet 메모리 사용
- DB Driver별 동작 차이
예전에는 순서를 외웠고, 지금은 경계를 본다
처음 JDBC를 배울 때는 네 단계로 기억했다.
- 연결
- SQL 준비
- 실행
- 종료
현재는 그 사이의 책임을 더 중요하게 본다.
Connection은 DB 세션, PreparedStatement는 실행할 SQL과 파라미터, ResultSet은 조회 결과의 순차 접근, Transaction은 여러 SQL을 하나의 작업 단위로 묶는 경계다.
이 기본 구조를 알고 있으면 MyBatis나 JPA에서 문제가 발생했을 때 한 단계 아래로 내려가 원인을 추적하기 쉬워진다.
참고
- Oracle Java JDBC PreparedStatement Tutorial: https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html