JDBC API
JDBC를 이용한 DB 연동
- JDBC Driver Class 로딩 (JDBC 4.0 이후 버전부터 생략 가능)
- Connection 생성
- Statement 생성
- SQL 실행
- 결과 집합 처리
- 자원 반납
Step 1. JDBC Driver Class 로딩
이 단계에서는 데이터베이스와 통신하기 위한 ‘드라이버’를 준비하는 작업을 합니다.
- 자바 애플리케이션이 “MySQL 드라이버를 쓰겠다”라고 등록(로딩)하면,
- 이후 DriverManager가 이 드라이버를 이용해 실제 DB와 연결을 맺을 수 있게 됩니다.
즉, 운전사를 고용해서 “이제 이 운전사로 DB에 갈 거야”라고 말해두는 단계라고 생각하면 됩니다.
위와 같은 작업은 진행하면, DBMS와 연결을 수립해주는 역할의 JDBC Driver Class 메모리에 로딩 및 등록이 된다.
명시적 로딩하는 방법으로는 Class.forName("com.mysql.cj.jdbc.Driver");
묵시적 로딩한는 방법으로는 DriverManager의 getConnection시에 자동 처리된다.
Step 2. Connection 생성
드라이버 클래스를 메모리에 묵시적.명시적으로 로딩을 하고 나서. DB랑 연결을 맺으려고 하는 과정이다.
이때 Connection은 DriverManager에게 "getConnection == 연결하나 해주세여~" 근데 url, user, password를 건네주면서.

DriverManager는 “URL을 보고 어느 DB 드라이버를 써야 하는지”를 결정하고, 해당 드라이버를 찾아준다. 그리고 실제로 “DB와 연결을 맺고 통신하는” 구체적인 작업은 DB 벤더(Oracle, MySQL 등)에서 제공하는 드라이버 클래스가 수행한다. DriverManager는 말 그대로 드라이버들을 관리하고, 적절한 드라이버를 찾아 연결 과정을 시작해 주는 매니저 역할을 할 뿐이다.
앞서 설명했던, 동그라미 역할을 하는 커넥션은 Java.spl.Connection 인터페이스다. mysqlconnetion이 만들어진다. getconnnection리턴 타입의 다형성. 지금은 커넥션 타임을 건네준다. 근데 만약에 mysql 과 연동이 되었다면 실제 리턴타입은 mysqlconnection이 될것이다. 그러면 mysqlconnection타입은 다형성 타입으로 connection타입에 담길수 있다. 그래서 우리는 connection타입으로 mysqlconnection타입을 다룰수 있게 되는 것이다.
Step 3. Statement 생성
옛날 전화기를 보면 밖 전봇대에서 전화기 줄을 빼와 전화기와 연결하게 되는데, 이때 줄이 connection이라고 생각하면 된다. 그렇다면 connection만 있으면 전화가 가능할까? 불가능 하다. 목소리를 보내는 수단인 전화기가 필요하다. 이때 전화기가 Statement라고 생각하면 이해하기 쉬울 것이다.

- 가장 기본이 되는 Statement
- JDBC로 SQL을 실행할 때 제일 먼저 배울 수 있는 ‘기본형’ 객체라고 보면 된다.
- 다른 Statement(PreparedStatement, CallableStatement)의 상위 클래스
- 다른 종류의 Statement들도 결국 이 Statement를 기반으로 만들어진 확장판이다.
- 생성할 때 구체적인 SQL이 정해져 있지 않음
- conn.createStatement()로 객체를 만들고, 그다음 stmt.executeQuery("SELECT ...") 같은 식으로 실행할 때마다 SQL을 넣어줘야 한다.
- 매 실행마다 SQL을 전송
- 즉, 실행할 때마다 우리가 직접 SQL 문자열을 전달해야 하는 것이다.
- SQL Injection 공격에 취약
- 사용자가 입력한 값을 문자열로 직접 이어붙여서 SQL을 만든다면, 악의적인 입력을 통해 SQL 구문이 변질될 수 있다(=SQL Injection).
- 그래서 실제로는 PreparedStatement 같은 걸 써서 미리 SQL을 준비하고 값만 안전하게 바인딩하는 방식을 많이 쓴다.
요약하자면, Statement는 가장 기본적이지만, 매번 SQL문을 직접 넣어야 하고 보안상 취약점이 있을 수 있어서, 실제 개발에선 PreparedStatement가 더 많이 사용되는 편이다.

이 이미지는 PreparedStatement를 사용해서 SQL을 실행하는 과정을 설명하고 있다. 핵심은 ‘?’ 플레이스홀더를 이용해 SQL을 미리 준비해 두고, 실제 값은 나중에 대입하는 방식이라는 것이다.
- String sql = "SELECT * FROM emp WHERE empno = ?";
- 실행할 SQL 문에서 바뀌는 부분(empno)은 ?로 표시해 둔다.
- PreparedStatement stmt = conn.prepareStatement(sql);
- Connection 객체에서 PreparedStatement를 생성하면서, 어떤 SQL을 쓸 건지(“?” 포함)를 미리 등록해.
- stmt.setInt(1, empNo);
- SQL 문 안의 첫 번째 ? 자리에 empNo라는 정수 값을 대입한다는 의미다.
이렇게 하면, DB가 SQL을 미리 컴파일해 두기 때문에 반복 실행 시 성능도 좋아지고, 문자열 연결로 인한 SQL Injection 위험도 크게 줄어든다.
- 즉, ‘Statement’와 달리 ‘PreparedStatement’는 같은 SQL 틀을 여러 번 실행할 때 최적화되고, ‘?’ 자리에 안전하게 값을 바인딩해 줄 수 있다.

CallableStatement는 DB에 저장된 프로시저(Stored Procedure)를 호출하기 위한 Statement다
- 예: CallableStatement cstmt = conn.prepareCall("{CALL processReturn(?, ?)}");
- 여기서 {CALL processReturn(?, ?)} 부분이 실제 DB 프로시저를 호출하는 SQL이다.
- cstmt.setXXX(...) 메서드로 프로시저에 넘길 파라미터들을 설정한 다음, cstmt.execute()로 실행한다.
이 방식은 DB 내부에 이미 작성되어 있는 로직(Stored Procedure)을 재활용할 수 있고, 일반적으로 속도가 빠르고 효율적이지만, DBMS에 따라 사용 방법이나 문법이 조금씩 달라질 수 있다는 점도 알아두면 좋다.
Step 4. SQL 실행
int rowCnt = stmt.executeUpdate(sql);
ResultSet rs = stmt.executeQuery(sql);
boolean hasResult = stmt.execute(sql);
- 실행할 때마다 SQL문을 직접 넘겨야 한다.
- 예) stmt.executeQuery("SELECT * FROM table WHERE col='value'");
- 간단한 작업에는 괜찮지만, 사용자 입력을 문자열로 이어붙이면 SQL Injection 위험이 있다.
int rowCnt = pstmt.executeUpdate();
ResultSet rs = pstmt.executeQuery();
boolean hasResult = pstmt.execute();
- SQL을 미리 준비(prepareStatement("SELECT * FROM table WHERE col=?"))해 두고, 실행 시에는 SQL을 다시 안 넘겨도 된다.
- ?(플레이스홀더)에 값을 바인딩(pstmt.setString(1, "value"))하므로, SQL Injection 위험이 훨씬 줄어들게 된다.
- 같은 SQL을 여러 번 실행할 때, DB가 미리 컴파일된 SQL을 재사용하므로 성능도 좋아질 수 있다고 한다.
주의해야 할 점
- SQL 종류별 메서드 선택
- executeUpdate()는 INSERT, UPDATE, DELETE처럼 데이터를 변경하는 SQL에 쓰고,
- executeQuery()는 SELECT처럼 데이터를 조회하는 SQL에 사용된다.
- execute()는 좀 더 범용적이지만, 결과 처리 방식이 복잡할 수 있다.
- SQL Injection 보안
- Statement는 문자열을 그대로 넘기기 때문에, 사용자 입력이 포함된 SQL을 만들 땐 매우 조심해야 된다.
- PreparedStatement는 ?에 값만 바인딩하므로, Injection 공격을 예방하는 데 훨씬 안전하다.
- 반복 실행 시 성능
- PreparedStatement는 한 번 SQL을 준비(컴파일)해 두고 여러 번 실행할 수 있다.
- 같은 쿼리를 반복한다면, Statement보다 빠른 경우가 많다고한다.
정리하자면, Statement는 간단하지만 보안상 취약하고, PreparedStatement는 좀 더 안전하고 반복 실행 시 효율적이다. 그래서 실무에서는 대부분 PreparedStatement를 권장한다고 한다.
JDBC에서 SQL을 실행하는 3가지 대표적인 메서드와 그 리턴 타입
- executeQuery( )
- 주로 SELECT 문을 실행할 때 사용.
- 실행 결과로 ResultSet(조회된 데이터의 집합)을 반환
ResultSet rs = stmt.executeQuery("SELECT * FROM table");
- executeUpdate( )
- INSERT, UPDATE, DELETE 같은 데이터 변경 쿼리에 사용.
- 실행 결과로 int 값을 반환하는데, 이는 “몇 행(row)이 변경되었는지”를 알려준다
int rowsAffected = stmt.executeUpdate("UPDATE table SET col='value' WHERE id=1");
- execute( )
- 모든 종류의 SQL을 실행할 수 있다
- 실행 결과가 ResultSet이면 true, 아니면 false를 반환
- true면 stmt.getResultSet()으로 조회 결과를 받아오고,
- false면 stmt.getUpdateCount()로 변경된 행 수를 확인할 수 있다
- 여러 결과(SELECT 후 UPDATE 등)가 동시에 나오는 복합 쿼리를 다뤄야 할 때 주로 사용
Step 5. 결과 집합 처리

그림은 SELECT 쿼리로 데이터를 조회하고, 그 결과(=ResultSet)를 자바 코드에서 반복문으로 하나씩 꺼내 쓰는 과정이다.
- SQL문 작성: "SELECT empno, ename FROM emp"
- 쿼리 실행: stmt.executeQuery(sql) → 결과가 ResultSet 객체로 반환
- 반복문으로 결과 처리:
- while (rs.next()) { ... }
- rs.getInt("empno")처럼 컬럼값을 꺼내 EmpDTO 등에 담음
즉, DB에서 조회한 레코드(행)들을 한 줄씩 읽어서 자바 객체로 만들거나, 필요한 데이터를 꺼내 쓰는 모습

이 그림은 SELECT 쿼리 결과를 ResultSet으로 받아서 하나씩 꺼내 쓰는 과정
- ResultSet은 커서(cursor) 기반
- 커서는 처음에 “첫 번째 행 전(before first row)” 위치에 있다.
- 그래서 rs.next()를 호출해야만 다음 행(첫 번째 행)으로 이동하고, 해당 행의 데이터를 읽을 수 있다.
- 만약 쿼리 결과가 하나도 없으면, rs.next()가 바로 false를 반환해서 반복문에 들어가지 않는다.
- Connection이 닫히면 ResultSet도 사용 불가
- ResultSet은 DB 커넥션을 통해 데이터를 받아오는 구조라서,
- Connection을 닫으면 더 이상 ResultSet에서 데이터를 읽을 수 없다.
- rs.next()가 true를 반환할 때만 행 데이터를 꺼낼 수 있음
- while (rs.next()) { ... } 구조로, 한 행씩 순회하면서 rs.getInt("empno"), rs.getString("ename")처럼 컬럼값을 가져와.
- 컬럼 이름 대신 인덱스(1, 2, 3, …)로도 꺼낼 수 있다.
- rs.next()가 false가 되는 순간(즉, 더 이상 데이터가 없으면) 반복문이 종료
- SELECT 결과가 여러 컬럼이라면
- 각 행에 대해 getXXX 메서드를 여러 번 호출해서, 필요한 모든 컬럼 값을 읽을 수 있다.
정리하자면,
- ResultSet은 쿼리 결과를 “커서”로 한 줄씩 탐색하는 구조이며,
- Connection이 살아 있는 동안만 데이터를 가져올 수 있고,
- 처음엔 커서가 ‘첫 행 이전’에 있어서 rs.next()를 호출해야 실제 행을 가리키게 된다는 점이 핵심
Step 6. 자원 반납
다 사용했으니깐 반납하자. 사용한 모든 자원에 대한 반납 처리

이 그림은 JDBC 리소스(객체) 정리를 어떻게 해야 하는지 보여주는 예시다.
ResultSet → Statement → Connection 순서로 닫는다.
보통 if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); 이런 식으로 작성한다. 닫을 때도 null 체크를 해서, 객체가 있을 때만 닫도록 한다.
Statement를 닫으면 그 Statement에서 사용 중이던 ResultSet도 자동으로 닫힌다.
이미지에 있는 노란 박스의 설명은 “When a Statement object is closed, its current ResultSet object, if one exists, is also closed.” 라고 한다.
즉, stmt.close()를 호출하면 해당 Statement가 생성한 ResultSet도 함께 닫히기 때문에, 굳이 rs.close()를 먼저 안 해도 문제가 생기지 않는다. 하지만 명시적으로 rs.close()를 해 주면, 코드가 좀 더 명확해지고 혹시 모를 리소스 누수를 방지하기 좋다.
모든 리소스를 확실히 닫아야 DB와의 연결이 끊기고 자원도 해제된다.
Connection을 닫지 않으면 DB 연결이 계속 살아있어서 서버 자원을 점유한다. Statement나 ResultSet도 마찬가지로, 닫지 않으면 내부적으로 쿼리 실행 자원이 해제되지 않는다.
정리하면, ResultSet, Statement, Connection은 꼭 모두 닫는다.
권장 순서는 ResultSet → Statement → Connection이다. Statement를 닫으면 ResultSet이 자동으로 닫히긴 하지만, 명시적으로 닫는 습관을 들이는 것이 좋다.