SQL을 배우고 나면 자연스럽게 드는 생각이 있다. "그럼 Java 코드에서는 어떻게 데이터베이스에 접근하지?"
그 물음에 대한 답이 JDBC다.
JDBC(Java Database Connectivity)는 Java 애플리케이션이 데이터베이스와 통신할 수 있게 해주는 표준 API다. Java가 제공하는 인터페이스 집합이고, 실제 동작은 각 데이터베이스 벤더가 제공하는 드라이버(Driver) 가 담당한다.

MySQL을 쓰든, Oracle을 쓰든, PostgreSQL을 쓰든 코드 구조는 동일하다. 드라이버만 바꾸면 된다. 이게 JDBC가 인터페이스 기반으로 설계된 이유다.
JDBC로 데이터베이스를 다루는 흐름은 항상 일정하다.

MySQL에 접속해서 데이터를 조회하는 코드다.
import java.sql.*;
public class JdbcExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "1234";
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 드라이버 로드 (JDBC 4.0부터는 생략 가능)
Class.forName("com.mysql.cj.jdbc.Driver");
// 연결 획득
conn = DriverManager.getConnection(url, user, password);
// Statement 생성 및 SQL 실행
stmt = conn.createStatement();
rs = stmt.executeQuery("SELECT id, name FROM members");
// 결과 순회
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
System.out.println(id + " : " + name);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 자원 닫기 (역순으로)
try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
ResultSet은 SQL 결과를 행(row) 단위로 읽어오는 객체다. rs.next()를 호출할 때마다 다음 행으로 커서가 이동하고, 더 이상 행이 없으면 false를 반환한다.
위 예제에서 Statement를 사용했는데, 실제로는 PreparedStatement 를 쓰는 경우가 훨씬 많다.
// Statement — SQL을 문자열로 직접 이어붙임
String sql = "SELECT * FROM members WHERE name = '" + inputName + "'";
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
// PreparedStatement — ? 자리에 값을 따로 바인딩
String sql = "SELECT * FROM members WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, inputName);
rs = pstmt.executeQuery();
Statement는 SQL을 문자열로 직접 조합하기 때문에 입력값에 따라 SQL 구조 자체가 바뀔 수 있다. 이게 바로 SQL 인젝션(SQL Injection) 취약점이다. PreparedStatement는 값을 별도로 바인딩해서 이 문제를 원천 차단한다. 이 차이 하나만으로도 PreparedStatement를 기본으로 쓸 이유는 충분하다.

executeQuery()는 SELECT에만 쓴다. 데이터를 변경하는 쿼리는 executeUpdate()를 사용하고, 반환값은 영향받은 행(row) 수다.
String sql = "INSERT INTO members (name, age) VALUES (?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "김자바");
pstmt.setInt(2, 25);
int affected = pstmt.executeUpdate();
System.out.println("삽입된 행 수: " + affected);
finally 블록에서 자원을 하나씩 닫는 코드는 길고 실수하기 쉽다. Java 7부터는 try-with-resources 문법으로 이를 깔끔하게 처리할 수 있다.
String url = "jdbc:mysql://localhost:3306/mydb";
try (Connection conn = DriverManager.getConnection(url, "root", "1234");
PreparedStatement pstmt = conn.prepareStatement("SELECT id, name FROM members");
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
System.out.println(rs.getInt("id") + " : " + rs.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
try 블록이 끝나면 AutoCloseable을 구현한 객체들이 자동으로 닫힌다. Connection, Statement, ResultSet 모두 해당된다.
jdbc:mysql://localhost:3306/mydb 이 문자열이 처음엔 낯설게 느껴진다.
jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
│ │ │ │ │
│ │ │ │ └── 추가 옵션 (쿼리 파라미터)
│ │ │ └─────── 데이터베이스 이름
│ │ └──────────────── 포트 번호
│ │ 호스트 주소
│ └───────────────────────── 드라이버 종류 (mysql, oracle, postgresql 등)
└─────────────────────────────── JDBC 식별자
로컬 개발 환경에서는 localhost:3306이 기본이고, 운영 환경에서는 실제 서버 주소로 바꾼다.
JDBC 드라이버는 JDK에 포함되어 있지 않다. MySQL을 쓴다면 MySQL Connector/J를 별도로 추가해야 한다.
Maven 프로젝트라면 pom.xml에 의존성을 추가한다.
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
JDBC를 직접 쓰면 연결, 예외 처리, 자원 해제를 매번 반복해야 한다. SQL 한 줄을 실행하는데 코드가 20줄 이상 필요하기도 하다.
이런 반복을 줄이기 위해 등장한 것이 MyBatis나 JPA 같은 도구다. MyBatis는 SQL은 직접 작성하되 JDBC 반복 코드를 제거해주고, JPA는 SQL 자체를 거의 쓰지 않도록 객체와 테이블을 매핑해준다.
JDBC를 이해해두면 이런 도구들이 내부적으로 무슨 일을 하는지 파악하는 데 도움이 된다.
JDBC는 Java와 데이터베이스를 잇는 가장 기본적인 다리다. 드라이버를 로드하고, 연결을 열고, SQL을 실행하고, 자원을 닫는 이 흐름만 머릿속에 있으면 된다.