
0. 들어가기 전에
반성할 점
요즘 스터디와 코테런 등등의 활동으로 TIL을 쓰는 것이 좀 소홀해졌다. 남들이 보기에는 충분히 훌륭하다고 할 수도 있겠지만, 내용을 정리하는 것에 급급한 느낌이 너무 크다. 공부하는 것보다는 TIL 제출 자체에 초점을 맞추고 있는 듯한 느낌이 든다. TIL이 과제인 것과 별개로 내가 공부하자고 쓰는 것인데.. 🤔 이렇게 되면 주객전도이다. 이미지를 직접 만드는 편이다보니 더욱 그런 문제가 있는 거 같다(일러스트의 경우 오픈소스이다).
앞으로에 대해서
여태까지도 그러했지만, 기본적으로 내가 적고 있는 TIL은 수업 내용을 토대로한 보충 공부에 가깝다. 수업 내용을 복습하는 것도 중요하지만, 실습과 코드 시연만으로도 수업 시간은 적기 때문에 이론적인 보충은 TIL을 통해서 하고 있다. 다만 이 경우 기존에 적던 강의일지와 어느정도 겹치는 면이 없지 않아서, 이론적인 보충의 내용을 더 늘릴 것이다. 즉, 연장 수업이다.
이번 시간에는…
지난 TIL에서는 SQL의 작동 원리와 기본 문법을 다뤘다. 그런데 SQL만 알면 DB를 다룰 수 있을까? 아니다. 사실 Java로 웹이나 프로그램을 만들 때, 데이터베이스가 어떻게 연결되는지에 대해서는 많이 인지하고 있지 않다. 모든 코딩 수업이 비슷하듯이, 내부 구조가 어떻게 되는가 등보다 한 줄이라도 문법을 외우는 것이 더 코딩에 도움이 되기 때문이다. 다만 수업에서 설명해주신 것도 있고, 이론을 아는 것이 더욱 도움이 된다고 생각한다.
실제로 우리가 만드는 애플리케이션은 Java 같은 프로그래밍 언어로 작성되고, DB는 별도의 서버에서 돌아간다. 그러면 대체 이 둘은 어떻게 이어질까? 둘 사이를 연결해 주는 것이 바로 JDBC(Java Database Connectivity)다. 이번 포스트에서는 JDBC가 무엇인지, 어떻게 동작하는지, 그리고 꼭 알아두면 좋다는 사용 패턴까지 정리해 보려고 한다.
강의에서 드라이버 로딩, Connection, Statement 같은 코드를 따라 치다 보면 "왜 이렇게 해야 하지?"라는 의문이 들 수 있다. 나도 사실 처음에는 그냥 외우는 셈 치고 넘어갔는데, 문법만 외우기보다 JDBC의 구조와 원리를 이해해 두면 나중에 JPA나 MyBatis 같은 기술을 배울 때 훨씬 수월하다. 이번 TIL 역시 강의 내용 정리에 보충한 내용도 담았다.
1. JDBC란 무엇인가?
"JDBC"라는 말을 강의에서 처음 들었을 때, 대체 뭔가 싶었다. DB는 SQL로 다루는 거 아니었나? 그런데 Java 코드 안에서 Connection이라든가 Statement라든가 나오니까 헷갈렸다. 일단 알아둬야 할 것은, JDBC는 하나의 API다. Java Database Connectivity의 준말로, Java에서 데이터베이스에 접근하기 위한 표준 API라고 보면 된다. 1997년에 도입된 이래로, Oracle, MySQL, PostgreSQL 등 벤더마다 다른 DB를 Java에서 동일한 방식으로 다룰 수 있게 해 준다. 즉, "Java 프로그램 ↔ DB" 사이의 추상화 계층이라 할 수 있다. 우리가 프로그래밍 언어로 컴퓨터에 명령을 내리듯이, JDBC는 Java 프로그램이 데이터베이스에 "이렇게 해라"라고 말할 수 있게 해 주는 통로라고 보면 된다.

우리가 Connection, Statement, ResultSet 같은 인터페이스를 사용하면, 실제로는 각 DB 벤더가 제공하는 JDBC 드라이버가 그에 맞는 네이티브 통신(DB마다 정해진 통신 방식, 예: MySQL 프로토콜)으로 변환해 준다. 비유를 하면, 우리는 "편지 보내라"라고만 말하고, 우체부(드라이버)가 실제로 어느 집(어느 DB)에 어떻게 배달할지를 처리하는 셈이다. 그래서 코드는 그대로 두고 드라이버만 바꾸면 다른 DB로 전환할 수 있다.
정리하면 JDBC API는 java.sql 패키지에 정의된 인터페이스들(Connection, Statement, ResultSet 등)이고, JDBC 드라이버는 각 DB 벤더가 이 API를 구현한 라이브러리다. MySQL Connector/J, Oracle JDBC Driver 같은 것들이 그 예라고 보면 된다.
2. JDBC 드라이버의 종류
JDBC 드라이버라는 말을 들었을 때, "그게 뭔데?" 싶을 수 있다. 드라이버는 구현 방식에 따라 Type 1부터 Type 4까지 네 가지로 나뉜다. 사용 빈도는 Type 4가 가장 높은 편이다.

Type 1은 JDBC-ODBC 브리지라고 해서, 예전에 쓰이던 방식이고 지금은 레거시에 가깝다. Type 2는 네이티브 API를 부분적으로 사용하는데, DB 클라이언트 라이브러리를 따로 설치해야 하는 부담이 있다. Type 3은 순수 Java로 된 미들웨어를 거쳐서 DB에 접근하는 방식이다. Type 4는 순수 Java로, DB 프로토콜을 직접 사용하는 방식이다. 100% Java로 작성되어 있어서 DB 벤더가 제공하는 JAR 하나만 프로젝트에 라이브러리로 추가하면(classpath에 넣으면) 된다. 별도로 네이티브 라이브러리를 깔 필요가 없어서 가장 쓰기 편하다.
MySQL의 경우 Maven이라면 mysql-connector-j(예전엔 mysql-connector-java라고 불렀다)를 의존성에 추가하면 된다. 예시는 다음과 같다.
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
이렇게 한 번 넣어 두면, 우리는 JDBC API만 쓰고 실제 통신은 드라이버가 알아서 처리해 준다고 보면 된다. "Type 4만 쓰면 되나?"라고 할 수 있는데, 대부분 그렇게 해도 된다. 다른 타입은 레거시나 특수한 경우에나 쓰인다.
3. JDBC 기본 흐름: 연결, 실행, 결과 처리, 종료
JDBC로 DB를 다루는 기본 순서는 항상 비슷하다. 연결(Connection) → SQL 실행(Statement 또는 PreparedStatement) → 결과 처리(ResultSet) → 리소스 정리(close). 이 네 단계로 기억해 두면 된다.

드라이버 로딩과 연결
과거에는 Class.forName("com.mysql.cj.jdbc.Driver"); 로 드라이버를 명시적으로 로딩했다. 그런데 JDBC 4.0(Java 6) 이후부터는 DriverManager가 classpath에 있는 드라이버를 자동으로 찾기 때문에 이 줄을 생략해도 된다. 다만 "왜 예전 코드에는 이게 있지?" 할 때를 위해, 동작 원리를 알기 위해 한 번쯤은 눈에 익혀 두는 것이 좋다.
연결할 때는 DriverManager.getConnection(url, user, password) 를 쓴다. url은 DB마다 형식이 다르다. MySQL은 jdbc:mysql://호스트:포트/DB이름?옵션 형태고, Oracle은 jdbc:oracle:thin:@호스트:포트:SID 같은 형태로 사용한다. 예를 들어 로컬 MySQL의 mydb라는 데이터베이스에 접속한다면 다음과 같다.
String url = "jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul";
String user = "root";
String password = "password";
Connection conn = DriverManager.getConnection(url, user, password);
이렇게 하면 conn이라는 연결 객체를 얻을 수 있다. 이 연결이 있어야 그다음에 SQL을 보낼 수 있다. 주소를 알지 못하면 편지를 전달해 줄 수 없듯이, Connection이 없으면 Java 쪽에서 DB에게 아무 요청도 할 수 없다. 즉, Connection은 "우리와 DB 사이에 맺어진 전화선" 같은 것이다.
SQL 실행과 결과 조회
연결된 Connection에서 Statement 또는 PreparedStatement를 얻어서 SQL을 실행한다. SELECT처럼 결과가 오는 경우에는 ResultSet으로 받는다.
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name FROM users");
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
System.out.println(id + ", " + name);
}
보다시피 rs.next()는 "다음 행으로 이동해라"라는 뜻이다. 행이 있으면 true, 없으면 false를 반환한다. getInt, getString 같은 메서드는 컬럼 이름이나 인덱스(1부터 시작한다!)로 값을 꺼낸다. 처음에는 next()를 안 하고 getXXX를 부르면 안 된다. 반드시 next()로 한 번 이동한 뒤에 그 행의 값을 읽어야 한다고 기억해 두자.
리소스 정리
여기서 중요한 건, Connection, Statement, ResultSet은 리소스이므로 반드시 close 해 줘야 한다는 점이다. close를 안 하면 커넥션이 계속 쌓여서 서버 리소스가 고갈될 수 있다. try-with-resources를 쓰면 자동으로 닫혀서 편하다.

try (Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT id, name FROM users")) {
while (rs.next()) {
System.out.println(rs.getInt("id") + ", " + rs.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
try 옆의 괄호 안에 선언한 것들은 try 블록이 끝나면 자동으로 close가 호출된다. 그러니까 우리가 finally에서 일일이 close() 쓰지 않아도 된다. 이걸 안 쓰면 나중에 "갑자기 DB 연결이 안 돼요" 하는 상황을 만나기 쉽다.
4. Statement vs PreparedStatement
SQL을 실행하는 방법에는 Statement와 PreparedStatement 두 가지가 있다. "둘 다 SQL 보내는 거 아니야?" 싶을 수 있는데, PreparedStatement 쪽 사용 빈도가 높은 편이다. 이유는 두 가지다. 첫째는 SQL 인젝션을 막을 수 있어서, 둘째는 성능 때문이다.

Statement는 SQL 문자열을 그대로 붙여서 실행한다. 사용자 입력을 그대로 넣으면 SQL 인젝션(사용자 입력이 SQL 문장에 그대로 붙어서 악의적인 쿼리가 실행될 수 있는 공격)이라는 보안 문제에 노출된다. 예를 들어 사용자가 이름을 입력하는 칸에 '; DROP TABLE users; -- 같은 문자열을 넣으면, 그게 그대로 SQL에 붙어서 실행될 수 있다. 그러면 테이블이 삭제되는 대참사가 난다.

String name = request.getParameter("name"); // 사용자가 "'; DROP TABLE users; --" 입력
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
stmt.executeQuery(sql); // 의도치 않은 SQL이 실행됨
이렇게 문자열을 이어 붙이는 방식은 위험하다. 그리고 같은 형태의 SQL을 반복 실행할 때마다 DB가 매번 파싱을 하기 때문에 성능도 불리하다.
PreparedStatement의 장점
PreparedStatement는 SQL에 물음표(?)를 두고, 값을 나중에 바인딩(? 자리에 넣어 주는 것)하는 방식이다. 전달된 값은 이스케이프·따옴표 처리가 되어서 SQL 인젝션으로 쓰일 수 없고, 같은 SQL은 한 번 파싱한 뒤 재사용할 수 있어서 성능도 좋다.
String sql = "SELECT * FROM users WHERE name = ? AND status = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, userName);
pstmt.setString(2, "ACTIVE");
ResultSet rs = pstmt.executeQuery();
// ...
}
? 의 순서는 1부터 시작한다. setString(1, userName) 은 첫 번째 ? 자리에 userName을 넣는다는 뜻이다. INSERT나 UPDATE, DELETE는 executeQuery가 아니라 executeUpdate()를 쓴다. 영향받은 행 개수가 int로 반환된다.
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "홍길동");
pstmt.setString(2, "hong@example.com");
int rows = pstmt.executeUpdate(); // 1 이 반환됨
}
한 마디로, Statement보다 PreparedStatement를 쓰는 경우가 많다.
5. ResultSet 다루기
SELECT 결과는 ResultSet으로 받는다. ResultSet은 커서가 한 행씩 이동한다고 생각하면 된다. 엑셀에서 셀을 하나씩 내려가면서 읽는 것과 비슷하다.

next()는 다음 행으로 이동하고, 있으면 true, 없으면 false를 반환한다. getXXX(컬럼명) 또는 getXXX(인덱스)로 해당 타입으로 값을 읽는다. 인덱스는 1부터 시작한다. getObject()는 어떤 타입이든 Object로 가져올 때 쓴다.
ResultSet의 기본 동작은 앞으로만 이동 가능한(forward-only), 읽기만 가능한(read-only)이다. 한 번 next()로 지나간 행은 다시 돌아가서 읽을 수 없다. 스크롤 가능하거나 갱신 가능한 ResultSet이 필요하면 createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY) 같은 옵션을 쓰면 되지만, 일반적인 조회에서는 기본으로 충분하다. 처음에는 "왜 인덱스가 1부터지?" 싶을 수 있는데, SQL 쪽 관례가 1부터라서 JDBC도 그렇게 맞춰 두었다고 보면 된다.
6. 트랜잭션과 예외 처리
여러 SQL을 "하나의 작업 단위"로 묶으려면 트랜잭션을 사용한다. 예를 들어 계좌 이체를 한다고 하면, A 계좌에서 돈을 빼고 B 계좌에 돈을 넣는 두 작업이 하나의 트랜잭션이다. 둘 중 하나만 성공하고 하나가 실패하면 안 되니까, 둘 다 성공하거나 둘 다 실패하도록 묶어서 관리하는 것이다.
기본적으로 Connection은 auto-commit이 true라서, SQL을 한 번 실행할 때마다 자동으로 확정(커밋)된다. 트랜잭션을 제어하려면 auto-commit을 끄고, 성공하면 commit(), 실패하면 rollback() 하면 된다.

conn.setAutoCommit(false);
try {
// 여러 번의 pstmt.executeUpdate() ...
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
}
예외는 SQLException으로 던져진다. 에러 코드나 SQLState를 활용해서 재시도할지, 로깅할지, 사용자에게 뭘 보여줄지 구분할 수 있다. 지난 TIL에서 SQL 쪽 트랜잭션(COMMIT, ROLLBACK)을 다룬 적이 있는데, JDBC에서는 Connection의 setAutoCommit, commit, rollback으로 그걸 제어한다고 보면 된다. 트랜잭션이 보장해야 할 ACID(원자성, 일관성, 고립성, 지속성)는 DB가 담당하고, 우리는 "지금부터 하나의 단위로 묶을게" "확정할게" "취소할게"만 알려 주는 셈이다.
7. Connection Pool
매 요청마다 DriverManager.getConnection()으로 새 연결을 만들면 비용이 크다. TCP 연결 생성, 인증, 리소스 할당이 매번 반복되기 때문이다. 그래서 Connection Pool을 쓰면 연결 비용을 줄일 수 있다.

Connection Pool은 미리 여러 개의 Connection을 만들어 두고, 애플리케이션이 요청할 때 빌려 주고(getConnection), 사용이 끝나면 반환(close라고 부르지만 실제로는 풀에 돌려줌)하는 방식이다. 도서관에서 책을 빌리듯이, 연결을 "빌려 쓰고" "반납"하는 것이다. 매번 새로 연결을 만들면 TCP 핸드셰이크나 DB 인증 같은 비용이 반복되니까, 미리 만들어 둔 걸 재사용하는 편이 훨씬 빠르다. HikariCP, Apache DBCP, Tomcat JDBC Pool 등이 널리 쓰이고, Spring Boot 기본이 HikariCP다. 설정(예: application.properties)에 maximum-pool-size, minimum-idle 같은 값만 넣어 주면 풀이 동작한다.
JDBC를 직접 쓸 때는 Pool에서 Connection을 얻는 방식만 다를 뿐, 그다음 단계(PreparedStatement, ResultSet 사용하고 close)는 똑같다. Pool을 쓰면 conn.close()는 실제로 연결을 끊지 않고 풀에 반환하는 동작으로 처리된다는 점만 기억하면 된다.
8. JDBC와 JPA, MyBatis
과정 중에에 스프링을 배우면 JPA나 MyBatis를 쓰게 될 가능성이 크다. "그럼 JDBC는 안 쓰는 거 아니야?" 싶을 수 있다. 사실 이미 능숙하게 JPA만 쓰는 사람들도 JDBC가 뭔지 정의부터 말하라고 하면 확실히 모를 수도 있다. 그런데 JPA나 MyBatis도 결국 내부적으로 JDBC를 사용한다. JDBC 위에 편의 기능을 올려 놓은 것이라 보면 된다.
JPA는 객체와 테이블을 매핑해서 객체만 다루면 알아서 SQL을 만들어 준다고 하고, MyBatis는 XML이나 어노테이션으로 SQL을 관리해서 SQL은 우리가 쓰되 반복 코드는 줄여 준다고 한다. 둘 다 "Connection을 누가 맺어 주지?", "PreparedStatement는 어디서 쓰이지?" 같은 걸 우리가 직접 쓰진 않을 뿐, 라이브러리가 대신 해 준다. 그래서 JDBC를 모른 채 JPA만 쓰면 "N+1 문제가 뭔데?", "왜 지금 쿼리가 이렇게 나가지?" 할 때 원인을 찾기 힘들다. 반대로 JDBC에서 연결·실행·결과·닫기 흐름을 알고 있으면, "아, JPA도 결국 이 순서로 DB랑 통신하는구나" 하고 추론할 수 있다.
MyBatis도 마찬가지다. SqlSession이 내부적으로 Connection을 갖고 있고, mapper에서 작성한 SQL이 PreparedStatement로 실행된다고 보면 된다. JDBC의 Connection, PreparedStatement, ResultSet 같은 개념을 알아 두면, JPA가 "영속성 컨텍스트"라든가 MyBatis가 "SqlSession"이라든가 할 때 "아, 이게 결국 연결하고 쿼리 날리는 거구나" 하고 이해하기 쉬워진다. 이론을 익히지 않으면 나중에 "왜 이렇게 동작하지?"라고 할 때 답을 찾기 어렵다. 그래서 지금 JDBC 정리해 둔 게 나중에 프레임워크 배울 때 발이 되어 줄 수 있다.

9. 정리하며
이번에는 JDBC에 대해서 정리해 보았다. 정리하면 다음과 같다.
- JDBC는 Java에서 DB를 다루기 위한 표준 API이고, 각 DB별 드라이버(Type 4)가 이를 구현한다.
- 기본 흐름은 Connection → Statement/PreparedStatement → ResultSet → close 이다.
- PreparedStatement를 사용해서 SQL 인젝션을 막고, 성능과 안정성을 확보하자.
- try-with-resources로 Connection, Statement, ResultSet을 반드시 닫아서 리소스 누수를 방지하자.
- Connection Pool(예: HikariCP)을 쓰면 연결 비용을 줄일 수 있다.
JDBC는 JPA, MyBatis 같은 프레임워크의 기반이 된다. 원리를 이해해 두면 나중에 "왜 이렇게 동작하지?"라는 질문에 스스로 답하기 쉬워진다. 다음 TIL에서는 알고리즘을 배운다. 아직 헷갈려하는 개념을 특별보충할 수 있으면 하겠지만, 아마 알고리즘 편으로 볼 거 같다.
'부트캠프 일지 > 멀티캠퍼스 TIL' 카테고리의 다른 글
| [Java 풀스택 개발자] 알고리즘 편#2: 알고리즘 방법론 (0) | 2026.03.09 |
|---|---|
| [Java 풀스택 개발자] 알고리즘 편: 무엇이 좋은 알고리즘일까? (0) | 2026.03.03 |
| [Java 풀스택 개발자] SQL편: SQL의 작동원리와 기본 문법 (0) | 2026.02.10 |
| [Java 풀스택 개발자] Java의 용어와 개념: 낯선 Java를 친숙하게 만들어보자! (0) | 2026.02.02 |
| [Java 풀스택 개발자] React의 언어체계 (0) | 2026.01.26 |