개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › Spring

JDBC

자바 코드는 어떻게 DB에 SQL을 보내는가

DB는 자바 프로그램과 따로 일하는 프로그램입니다. (MySQL 같은 DB는 아예 다른 프로그램이고, 이 실습의 H2는 자바 프로그램 안에서 돌지만 코드가 하는 일은 같습니다.) 자바 코드가 DB에 일을 시키려면 JDBC라는 약속대로 세 가지를 합니다. 먼저 DB와 연결(Connection)을 맺습니다. 그 연결로 SQL을 글자로 보냅니다. 돌아온 결과(ResultSet)를 한 행씩 읽습니다. 연결이 없으면 SQL을 보낼 수 없고, SQL 글자가 틀리면 DB가 오류로 답해서 자바에서는 SQLException(오류가 났다는 신호인 예외)이 됩니다. 이 실습은 member 테이블에서 id가 1인 회원의 name을 읽는 코드에 conn.close() 줄을 넣거나 SQL 글자를 바꿔 가며, DB가 받은 SQL과 자바가 출력한 값과 예외를 봅니다. (화면의 코드는 main 메서드 안에 있다고 보고 import 줄과 main 선언은 줄였습니다. 이 반복되는 코드를 대신 써 주는 JdbcTemplate과 JPA는 세부 동작 실습에서 다룹니다.)

실행으로 대조 · JDK 21과 H2 2.4.240에서 여섯 가지 조합을 컴파일해 새 자바 프로세스로 실행해 대조, 규칙은 Java SE 21 API 문서와 대조

이 실습에 나오는 개념

JDBC
자바 프로그램이 데이터베이스에 SQL을 보내고 결과를 받아 오는 가장 기본이 되는 표준 API다. 커넥션을 얻어 SQL 문장을 문자열로 넘기고, 물음표 자리에 값을 채워 실행한 뒤, 돌아온 결과(ResultSet)에서 컬럼 값을 하나씩 꺼낸다. SQL을 쓰는 일과 꺼낸 값을 객체의 필드에 옮겨 담는 일은 모두 개발자의 몫이다. JPA와 MyBatis도 내부에서는 JDBC로 SQL을 보낸다. 그래서 JDBC를 직접 쓰지 않더라도 그 위에서 어떤 일이 일어나는지 알아 두면 도움이 된다.
Connection과 DriverManager
Connection은 특정 DB와 맺은 연결(세션)을 나타내는 객체이다. SQL 문장은 이 연결 안에서 실행되고 결과도 이 연결로 돌아온다. 자바 코드는 DriverManager.getConnection(url)로 연결을 얻는데, url은 jdbc:부프로토콜:이름 꼴이고(예: jdbc:h2:mem:shop) DriverManager가 등록된 JDBC 드라이버 가운데 알맞은 것을 골라 연결을 맺는다. close()는 연결이 쥔 DB와 JDBC 자원을 바로 풀어 주고, 닫힌 연결에서 createStatement를 부르면 SQLException이 난다. 연결이 없으면 SQL을 보낼 수 없다.
Statement와 executeQuery
Statement는 값을 끼워 넣지 않은 SQL 문장을 DB에 보내 실행하는 데 쓰는 객체이고, conn.createStatement()로 만든다. executeQuery(sql)은 SELECT 같은 문장을 보내고 결과를 ResultSet 하나로 돌려주며, 돌려주는 값이 null인 일은 없다. DB 접근 오류가 나거나 닫힌 Statement에서 부르거나 ResultSet 하나를 만들지 않는 SQL을 보내면 SQLException이 난다. SQL은 자바에게 그냥 문자열이어서 글자가 틀려도 컴파일은 되고, DB가 읽는 실행 때에 오류로 알려진다. Statement를 만드는 것만으로는 SQL이 DB에 가지 않고 executeQuery를 부를 때 보내지는 것은 H2에서 실행해 확인했다.
ResultSet과 SQLException
ResultSet은 쿼리 결과인 행들의 표이고 현재 행을 가리키는 커서를 가진다. 커서는 처음에 첫 행의 앞에 있고, next()가 한 칸 앞으로 옮겨 그 자리에 행이 있으면 true, 더 없으면 false를 돌려준다. getString("name")처럼 현재 행의 열 값을 꺼내는 메서드는 현재 행이 있어야 하므로, next()가 false를 돌려준 뒤에 부르면 SQLException이 난다. 조건에 맞는 행이 없어 결과가 비는 것은 오류가 아니고 next()가 false라는 신호로 알려진다. SQLException은 DB 접근 오류나 그 밖의 오류를 알리는 예외(Exception의 하위 클래스이므로 checked 예외)이고, 설명 문자열과 SQLState, DB 제품마다 다른 오류 코드를 담는다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. DriverManager.getConnection("jdbc:h2:mem:shop"): 자바 프로그램이 DB에 연결을 맺습니다. 주소의 jdbc는 자바가 DB에 접속하는 약속(JDBC)이라는 뜻이고, h2는 DB의 종류, mem:shop은 메모리 안에 있는 shop이라는 DB입니다. 연결이 맺어지면 DB 쪽에 이 연결을 맡는 자리(세션)가 하나 생기고, 자바에는 그 연결을 가리키는 conn 객체가 생깁니다. member 테이블에는 (1, kim) 한 행이 들어 있습니다. 아직 SQL은 한 줄도 보내지 않았습니다.
  2. conn.createStatement(): conn 연결로 SQL을 보내는 도구(Statement)를 만들어 st에 담습니다. 도구를 만든 것뿐이라 DB에는 아직 SQL이 가지 않았습니다.
  3. st.executeQuery("SELECT name FROM member WHERE id = 1"): st가 SQL 글자를 DB에 보냅니다. DB는 받은 글자를 읽어 member 테이블에서 id가 1인 행을 찾아 결과를 만듭니다. 결과는 행이 1개인 표입니다. 자바는 이 결과를 ResultSet 객체 rs로 받습니다. ResultSet은 처음에 첫 행의 앞에 서 있어서 아직 어느 행도 읽지 않았습니다.
  4. rs.next(): 결과에서 다음 행으로 한 칸 옮기고, 옮긴 자리에 행이 있으면 true, 없으면 false 값을 돌려줍니다. 첫 행으로 옮겨 true 값이 found 변수에 담깁니다. next() 메서드를 부르기 전에 값을 읽으려 하면 읽을 행이 없다며 SQLException이 납니다.
  5. System.out.println(found): found 변수에 담긴 true 값을 화면에 출력합니다.
  6. rs.getString("name"): 현재 행에서 name 열의 값을 글자로 꺼내 kim 값을 출력합니다. main의 마지막 줄까지 실행되어 프로그램이 끝났고, DB가 받은 SQL은 한 문장, 예외는 없습니다.

실행 결과

코드

// member 테이블에는 (id 1, name kim) 한 행이 들어 있다
// 연결: 주소가 가리키는 DB와 연결을 맺는다
Connection conn = DriverManager.getConnection("jdbc:h2:mem:shop");
// 도구: 이 연결로 SQL을 보내는 객체
Statement st = conn.createStatement();
// 보내기: SQL 글자를 DB에 보내고, 결과를 받는다
ResultSet rs = st.executeQuery("SELECT name FROM member WHERE id = 1");
// 읽기: 다음 행으로 옮긴다. 행이 있으면 true
boolean found = rs.next();
System.out.println(found);
System.out.println(rs.getString("name"));

이 모형의 전제

JDK 21.0.12.1과 H2 2.4.240(메모리 DB)에서 실행해 대조했고, MySQL에서의 실행 대조는 하지 않았습니다. 여섯 가지 조합을 모두, 화면의 코드를 main 메서드가 든 Main.java에 넣어(import java.sql.*와 main 선언만 더했습니다) javax.tools로 컴파일하고, 조합마다 새 자바 프로세스에서 실행했습니다. 실행 전에 같은 프로세스에서 member 테이블(id INT 기본 키, name VARCHAR(20))과 (1, kim) 한 행을 만들어 두었는데, 화면에서는 이 준비 코드를 줄였습니다. DB는 자바 프로세스 안에서 도는 H2라서 실제로는 같은 프로세스에 있고, MySQL처럼 따로 떨어진 프로그램이 아닙니다. 연결과 SQL 전송과 결과 읽기의 차례는 같지만, 그 사이를 오가는 것이 네트워크 통신이라는 점은 이 실습에서 확인하지 않았습니다. 대조한 것은 결과 표의 세 항목, 곧 DB가 받은 SQL, 출력된 값, 예외입니다. DB가 받은 SQL은 H2의 INFORMATION_SCHEMA.QUERY_STATISTICS(실행 기록)에 그 SELECT가 남았거나, 예외가 DB 엔진의 파서(org.h2.command.Parser)에서 났고 오류 메시지가 그 SQL을 인용한 경우로 판정했습니다. 닫힌 연결에서는 두 가지 모두 없었고, 예외가 JdbcConnection.checkClosed에서 났습니다. 예외의 종류는 H2의 SQLState와 오류 코드(42102 테이블을 찾을 수 없음, 2000 읽을 데이터가 없음, 90007 이미 닫힌 객체)를 화면의 한국어로 옮긴 것이고, 이 코드는 H2의 것이어서 MySQL에서는 다를 수 있습니다. 세 예외는 모두 SQLException의 하위 클래스입니다. 단계 문장이 기대는 사실로 연결을 맺으면 DB 쪽 세션이 하나 늘고 닫으면 줄어드는 것, createStatement만으로는 DB의 실행 기록에 SELECT가 남지 않고 executeQuery 뒤에는 남는 것, next() 메서드를 부르기 전에 getString을 하면 SQLException이 나는 것, next() 메서드를 한 번 더 부르면 false인 것도 실행으로 확인했습니다. 세션 수에는 실행 전에 준비 코드가 연 연결 하나가 들어 있습니다. 실행 기록(QUERY_STATISTICS)은 한 번 읽은 뒤에는 새 기록이 보이지 않아서(원인은 확인하지 않았습니다) 시점마다 DB를 따로 만들어 한 번씩만 읽었습니다. 화면의 코드는 짧게 하려고 연결과 ResultSet을 닫는 줄과 예외를 처리하는 코드를 적지 않았습니다. 예외를 처리하지 않으므로 예외가 난 줄에서 프로그램이 멈추고 아래 줄이 실행되지 않는 것은 출력에 뒤 줄의 값이 없는 것으로 확인했습니다. 문서는 Java SE 21 API 문서를 직접 열어 확인했습니다. java.sql 패키지 설명에서 DriverManager로 연결을 맺고 Statement로 SQL을 보내고 ResultSet으로 결과를 받는다는 것, Connection의 설명에서 SQL이 연결 안에서 실행되고 결과가 돌아온다는 것과 createStatement가 닫힌 연결에서 SQLException을 던진다는 것, Statement.executeQuery가 ResultSet을 돌려주고 닫힌 Statement에서 SQLException을 던진다는 것, ResultSet의 설명에서 커서가 처음에 첫 행의 앞에 있고 next가 한 칸 옮기며 행이 없으면 false 값을 돌려준다는 것과 현재 행이 필요한 메서드가 그때 SQLException을 던진다는 것, SQLException의 설명에서 SQLState와 오류 코드를 담는 예외라는 것, DriverManager.getConnection의 URL이 jdbc:부프로토콜:이름 꼴이라는 것을 따랐습니다. H2 문서(features 페이지)에서는 jdbc:h2:mem:이름이 같은 이름으로 같은 JVM 안에서 하나의 메모리 DB를 가리킨다는 것과 마지막 연결이 닫히면 메모리 DB가 사라진다는 것을 확인했습니다. 닫은 연결로 만든 Statement를 쓰면 H2가 오류를 내는 것은 문서가 정한 것이 아니라 이 H2에서 관찰한 동작입니다(Statement 문서는 닫힌 Statement에서 예외를 던진다고만 적고 있습니다). 이 밖에 PreparedStatement와 SQL에 값을 끼워 넣는 방법, 데이터를 바꾸는 SQL(executeUpdate), 트랜잭션, 연결을 닫는 올바른 방법(try-with-resources)은 다루지 않았고, 반복되는 코드를 줄여 주는 JdbcTemplate은 "세부 동작 실습: JdbcTemplate과 예외 변환", 연결을 매번 새로 맺지 않고 빌려 쓰는 방식은 "커넥션 풀과 OSIV", 객체와 테이블을 이어 주는 JPA는 "JPA가 푸는 문제"에서 다룹니다.

앞 실습: 설정 파일 · 다음 실습: @Transactional

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요