JPA에서 em.persist(user)를 호출하면 내부에서 무슨 일이 벌어질까? Hibernate가 INSERT SQL을 생성하고, 결국 JDBC의 PreparedStatement로 실행 한다. JPA든 MyBatis든, 자바가 DB와 대화하는 최종 경로는 항상 JDBC다.

TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.

JDBC가 왜 필요한가 — DB 접근의 표준 인터페이스

JDBC(Java Database Connectivity) 란 자바에서 데이터베이스에 접근하기 위한 표준 API입니다.

왜 "표준"이 중요한지 생각해볼게요. 세상에는 MySQL, PostgreSQL, Oracle, MariaDB 등 수많은 데이터베이스가 있습니다. 만약 DB마다 접근 방식이 완전히 다르다면 어떻게 될까요?

  • MySQL용 코드, PostgreSQL용 코드, Oracle용 코드를 각각 따로 작성해야 합니다
  • DB를 교체하면 데이터 접근 코드를 전부 다시 짜야 합니다
  • 라이브러리마다 사용법이 달라서 학습 비용이 엄청나게 늘어납니다

JDBC는 이 문제를 해결합니다. "자바에서 DB에 접근하려면 이 인터페이스를 따라라" 라는 표준을 정해놓고, 각 DB 벤더가 그 표준에 맞는 드라이버를 제공하는 구조예요.

PLAINTEXT
[Java 애플리케이션]
        |
   [JDBC API]         ← 표준 인터페이스 (java.sql 패키지)
        |
 ┌──────┼──────┐
 |      |      |
[MySQL] [PostgreSQL] [Oracle]   ← 각 벤더의 JDBC 드라이버
 |      |      |
[MySQL DB] [PG DB] [Oracle DB]

이렇게 하면 애플리케이션 코드는 JDBC API만 사용하고, 드라이버만 바꾸면 다른 DB로 전환할 수 있어요. JDBC는 자바에서 DB에 접근하기 위한 표준 인터페이스 입니다. 애플리케이션 코드는 JDBC API만 사용하고, 드라이버만 교체하면 다른 DB로 전환할 수 있습니다.

JDBC 아키텍처 — Driver, Connection, Statement, ResultSet

JDBC의 핵심 구성요소 네 가지를 알아야 전체 흐름이 보입니다.

PLAINTEXT
[Java 앱] → [JDBC API] → [JDBC Driver] → [DB 서버]
             (java.sql)    (벤더 제공)      (TCP/IP)

JDBC를 사용하는 전체 흐름은 이렇습니다: (1) DriverManager에 드라이버 등록 → (2) Connection 획득 → (3) Statement 생성 → (4) SQL 실행 → (5) ResultSet에서 데이터 추출 → (6) 자원 해제.

각 구성요소의 역할을 정리하면 다음과 같습니다.

구성요소역할핵심 인터페이스
DriverDB와의 실제 통신을 담당하는 드라이버java.sql.Driver
ConnectionDB와의 연결 세션을 나타냄java.sql.Connection
StatementSQL 문을 DB로 전송하고 실행java.sql.Statement
ResultSetSQL 실행 결과를 담는 커서java.sql.ResultSet

이 네 가지가 JDBC의 뼈대입니다. 이제 코드로 하나씩 살펴볼게요.

기본 CRUD — DriverManager로 시작하기

가장 기본적인 JDBC 사용법을 살펴볼게요. MySQL을 기준으로 작성했지만, 드라이버와 URL만 바꾸면 다른 DB에서도 동일하게 동작합니다.

연결하기

JAVA
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class JdbcBasic {
    public static void main(String[] args) {
        // JDBC URL 형식: jdbc:DB종류://호스트:포트/DB이름
        String url = "jdbc:mysql://localhost:3306/mydb";
        String user = "root";
        String password = "1234";

        try {
            // DriverManager를 통해 커넥션 획득
            Connection conn = DriverManager.getConnection(url, user, password);
            System.out.println("DB 연결 성공!");

            // 사용 후 반드시 닫아야 한다
            conn.close();
        } catch (SQLException e) {
            System.out.println("DB 연결 실패: " + e.getMessage());
        }
    }
}

DriverManager.getConnection()을 호출하면 내부적으로 이런 일이 벌어집니다.

  1. 등록된 드라이버 목록에서 URL에 맞는 드라이버를 찾습니다
  2. 해당 드라이버가 DB 서버에 TCP 연결을 맺습니다
  3. 인증(사용자, 비밀번호)을 수행합니다
  4. 연결된 Connection 객체를 반환합니다

JDBC 4.0(Java 6) 이후로는 Class.forName("com.mysql.cj.jdbc.Driver") 같은 드라이버 로딩 코드를 쓸 필요가 없어요. 클래스패스에 드라이버 JAR만 있으면 자동으로 로딩됩니다.

데이터 조회 (SELECT)

JAVA
import java.sql.*;

public class SelectExample {
    public static void main(String[] args) throws SQLException {
        String url = "jdbc:mysql://localhost:3306/mydb";
        Connection conn = DriverManager.getConnection(url, "root", "1234");

        // Statement 생성
        Statement stmt = conn.createStatement();

        // SQL 실행 → ResultSet 반환
        ResultSet rs = stmt.executeQuery("SELECT id, name, email FROM users");

        // ResultSet 순회
        while (rs.next()) {
            int id = rs.getInt("id");           // 컬럼명으로 조회
            String name = rs.getString("name");
            String email = rs.getString("email");
            System.out.println(id + " | " + name + " | " + email);
        }

        // 역순으로 닫기 (열린 순서의 반대)
        rs.close();
        stmt.close();
        conn.close();
    }
}

데이터 삽입/수정/삭제 (INSERT, UPDATE, DELETE)

JAVA
Statement stmt = conn.createStatement();
// executeUpdate()는 영향받은 행 수를 반환한다
int rows = stmt.executeUpdate(
    "INSERT INTO users (name, email) VALUES ('홍길동', 'hong@email.com')"
);
System.out.println(rows + "행 삽입됨");

executeQuery()는 SELECT에, executeUpdate()는 INSERT/UPDATE/DELETE에 사용합니다.

메서드용도반환 타입
executeQuery()SELECTResultSet
executeUpdate()INSERT, UPDATE, DELETE, DDLint (영향받은 행 수)
execute()모든 SQLboolean (ResultSet 여부)

PreparedStatement — SQL Injection 방어

위에서 본 Statement에는 치명적인 문제가 있습니다. 사용자 입력을 SQL에 직접 넣으면 SQL Injection 공격에 노출돼요.

JAVA
// ❌ 절대 이렇게 하면 안 된다
String userInput = "'; DROP TABLE users; --";
String sql = "SELECT * FROM users WHERE name = '" + userInput + "'";
stmt.executeQuery(sql);
// 실행되는 SQL: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
// → users 테이블이 삭제된다!

PreparedStatement 는 이 문제를 구조적으로 해결합니다. SQL 구문과 파라미터를 분리해서 처리하기 때문에, 사용자 입력이 절대로 SQL 구문의 일부로 해석되지 않아요.

JAVA
// ✅ PreparedStatement 사용 — 안전하다
String sql = "SELECT * FROM users WHERE name = ? AND email = ?";

// ?는 플레이스홀더 — 나중에 값을 바인딩한다
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userName);  // 첫 번째 ?에 값 바인딩 (1부터 시작)
pstmt.setString(2, userEmail); // 두 번째 ?에 값 바인딩

ResultSet rs = pstmt.executeQuery();

PreparedStatement의 장점을 정리하면 다음과 같습니다.

  • **SQL Injection 방어 **: 파라미터가 값으로만 처리되어 SQL 구문 조작이 불가능합니다
  • ** 성능 향상 **: SQL을 미리 컴파일(precompile)하므로, 같은 구문을 반복 실행할 때 더 빠릅니다
  • ** 가독성 **: 문자열 연결 대신 ?로 파라미터 위치가 명확해요
  • ** 타입 안전성 **: setInt(), setString() 등 타입별 메서드로 바인딩합니다

Statement와 PreparedStatement의 핵심 차이는 SQL Injection 방어 와 프리컴파일 입니다. 실무에서 Statement를 직접 쓸 이유는 없어요.

PreparedStatement로 INSERT 예제

JAVA
// INSERT — ?에 값을 바인딩하면 된다
String insertSql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(insertSql);
pstmt.setString(1, "김자바");
pstmt.setString(2, "kim@java.com");
pstmt.setInt(3, 25);
int rows = pstmt.executeUpdate(); // 영향받은 행 수 반환

UPDATE, DELETE도 동일한 패턴이에요. SQL만 바꾸고 ?에 값을 바인딩하면 됩니다.

트랜잭션 — setAutoCommit, commit, rollback

트랜잭션이란 "전부 성공하거나, 전부 실패하거나" 를 보장하는 작업 단위입니다. 대표적인 예가 계좌 이체예요.

PLAINTEXT
A 계좌에서 10만원 출금 → B 계좌에 10만원 입금

출금은 성공했는데 입금이 실패하면? 10만원이 증발합니다. 이걸 막으려면 두 작업을 하나의 트랜잭션으로 묶어야 해요.

JDBC는 기본적으로 Auto Commit 모드입니다. 즉, SQL 문 하나하나가 실행될 때마다 자동으로 커밋돼요. 수동으로 트랜잭션을 관리하려면 Auto Commit을 꺼야 합니다.

JAVA
Connection conn = DriverManager.getConnection(url, user, password);

try {
    // 1. Auto Commit 끄기
    conn.setAutoCommit(false);

    PreparedStatement withdraw = conn.prepareStatement(
        "UPDATE accounts SET balance = balance - ? WHERE id = ?"
    );
    withdraw.setInt(1, 100000);  // 10만원
    withdraw.setInt(2, 1);       // A 계좌
    withdraw.executeUpdate();

    PreparedStatement deposit = conn.prepareStatement(
        "UPDATE accounts SET balance = balance + ? WHERE id = ?"
    );
    deposit.setInt(1, 100000);   // 10만원
    deposit.setInt(2, 2);        // B 계좌
    deposit.executeUpdate();

    // 2. 두 작업 모두 성공하면 커밋
    conn.commit();
    System.out.println("이체 성공");

} catch (SQLException e) {
    // 3. 하나라도 실패하면 롤백
    conn.rollback();
    System.out.println("이체 실패, 롤백 완료: " + e.getMessage());

} finally {
    // Auto Commit 복원
    conn.setAutoCommit(true);
    conn.close();
}

추가로 Savepoint를 사용하면 트랜잭션 중간에 부분 롤백도 가능합니다. conn.setSavepoint("name")으로 저장점을 만들고, conn.rollback(savepoint)로 해당 지점까지만 되돌릴 수 있어요.

ResultSet 다루기

ResultSet은 SQL 실행 결과를 행 단위로 순회하는 커서입니다. 처음에는 첫 번째 행 ** 앞 **을 가리키고 있어서, next()를 호출해야 첫 번째 행으로 이동해요.

PLAINTEXT
커서 위치:  [시작] → [행1] → [행2] → [행3] → [끝]

         최초 위치

데이터 타입 매핑

Java 타입과 SQL 타입 사이에는 매핑 규칙이 있습니다.

SQL 타입Java 타입getter 메서드
INT, INTEGERintgetInt()
BIGINTlonggetLong()
VARCHAR, CHARStringgetString()
DOUBLE, FLOATdoublegetDouble()
BOOLEANbooleangetBoolean()
DATEjava.sql.DategetDate()
TIMESTAMPjava.sql.TimestampgetTimestamp()
BLOBbyte[]getBytes()
JAVA
ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    // 컬럼명으로 조회 (권장 — 가독성이 좋다)
    int id = rs.getInt("id");
    String name = rs.getString("name");

    // 컬럼 인덱스로 조회 (1부터 시작)
    int id2 = rs.getInt(1);
    String name2 = rs.getString(2);

    // NULL 처리 — wasNull() 사용
    int age = rs.getInt("age");
    if (rs.wasNull()) {
        System.out.println("age는 NULL입니다");
    }
}

컬럼명으로 조회하는 것이 인덱스보다 권장됩니다. 컬럼 순서가 바뀌어도 코드가 깨지지 않기 때문이에요.

리소스 관리 — try-with-resources

JDBC를 쓸 때 가장 실수하기 쉬운 부분이 ** 리소스 해제 **입니다. Connection, Statement, ResultSet 모두 사용 후 반드시 닫아야 해요. 안 닫으면 어떻게 될까요?

  • **Connection 누수 **: DB 커넥션이 계속 쌓여서 결국 DB가 새 연결을 거부합니다
  • ** 메모리 누수 **: GC가 수거하지 못하는 네이티브 리소스가 쌓입니다
  • ** 장애 **: 서비스 전체가 DB에 접근하지 못하는 상황이 발생합니다

Java 7 이전에는 finally 블록에서 일일이 close()를 호출해야 했는데, close 자체도 예외를 던질 수 있어서 코드가 매우 지저분해졌어요. try-with-resources(Java 7+)가 이 문제를 깔끔하게 해결합니다.

JAVA
// Connection, Statement, ResultSet 모두 AutoCloseable을 구현한다
try (
    Connection conn = DriverManager.getConnection(url, user, password);
    PreparedStatement pstmt = conn.prepareStatement(
        "SELECT * FROM users WHERE id = ?"
    )
) {
    pstmt.setInt(1, 1);

    // ResultSet도 try-with-resources 안에서 관리
    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
    }
} catch (SQLException e) {
    e.printStackTrace();
}
// → try 블록을 벗어나면 자동으로 close() 호출
// → 역순으로 닫힌다: ResultSet → PreparedStatement → Connection

try-with-resources를 사용하면 리소스 누수를 구조적으로 방지할 수 있습니다. JDBC 코드를 작성할 때는 반드시 이 패턴을 사용하세요.

커넥션 풀 — 왜 필요한가

여기까지 배운 방식에는 근본적인 성능 문제가 있습니다. 매 요청마다 DriverManager.getConnection()을 호출하면 다음 과정이 반복돼요.

PLAINTEXT
[요청 도착]
    → TCP 3-way handshake (네트워크 왕복)
    → DB 인증 (사용자/비밀번호 검증)
    → 커넥션 객체 생성
    → SQL 실행
    → 커넥션 종료 (TCP 연결 해제)
[응답 반환]

커넥션을 하나 만드는 데 보통 ** 수십 밀리초 **가 걸립니다. 웹 서비스에서 초당 1,000개 요청이 들어온다면? 매번 커넥션을 새로 만들고 버리면 DB가 버티지 못해요.

** 커넥션 풀(Connection Pool)**은 이 문제를 해결합니다.

PLAINTEXT
┌─────────────── 커넥션 풀 ───────────────┐
│                                          │
│  [Conn1: 사용중] [Conn2: 대기] [Conn3: 대기] │
│  [Conn4: 사용중] [Conn5: 대기]              │
│                                          │
└──────────────────────────────────────────┘

1. 애플리케이션 시작 시 일정 수의 커넥션을 미리 만들어둔다
2. 요청이 오면 풀에서 대기 중인 커넥션을 꺼내 쓴다
3. 사용이 끝나면 커넥션을 풀에 반환한다 (닫지 않는다!)
4. 대기 중인 커넥션이 없으면 새로 만들거나 대기한다

커넥션 풀의 이점을 정리하면 다음과 같습니다.

  • ** 성능 향상 **: 커넥션 생성/종료 비용을 줄여줍니다
  • ** 자원 관리 **: 최대 커넥션 수를 제한하여 DB 과부하를 방지합니다
  • ** 안정성 **: 커넥션 유효성 검사, 타임아웃 관리 등을 자동으로 처리해요

HikariCP — 가장 빠른 커넥션 풀

Spring Boot의 기본 커넥션 풀이 HikariCP 입니다. "Light(빛)"이라는 일본어에서 이름을 따왔고, 실제로 가장 빠른 커넥션 풀로 알려져 있어요.

JAVA
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class HikariExample {
    public static void main(String[] args) throws Exception {
        // HikariCP 설정
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("root");
        config.setPassword("1234");

        // 풀 설정
        config.setMaximumPoolSize(10);      // 최대 커넥션 수
        config.setMinimumIdle(5);           // 최소 유휴 커넥션 수
        config.setConnectionTimeout(30000); // 커넥션 획득 대기 시간 (ms)
        config.setIdleTimeout(600000);      // 유휴 커넥션 유지 시간 (ms)
        config.setMaxLifetime(1800000);     // 커넥션 최대 생존 시간 (ms)

        // DataSource 생성
        HikariDataSource ds = new HikariDataSource(config);

        // 풀에서 커넥션을 꺼내 사용
        try (Connection conn = ds.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(
                 "SELECT * FROM users WHERE id = ?"
             )) {
            pstmt.setInt(1, 1);
            try (ResultSet rs = pstmt.executeQuery()) {
                while (rs.next()) {
                    System.out.println(rs.getString("name"));
                }
            }
        }
        // try-with-resources로 닫으면 풀에 반환된다 (실제로 닫히지 않음)

        // 애플리케이션 종료 시 풀 닫기
        ds.close();
    }
}

HikariCP 주요 설정 값

설정기본값설명
maximumPoolSize10풀의 최대 커넥션 수
minimumIdlemaximumPoolSize유지할 최소 유휴 커넥션 수
connectionTimeout30000 (30초)커넥션 획득 대기 최대 시간
idleTimeout600000 (10분)유휴 커넥션이 풀에서 제거되기까지의 시간
maxLifetime1800000 (30분)커넥션의 최대 생존 시간

maximumPoolSize를 너무 크게 잡으면 DB에 부하가 걸리고, 너무 작게 잡으면 대기 시간이 길어집니다. HikariCP 공식 문서에서는 다음 공식을 권장해요.

PLAINTEXT
최적 풀 사이즈 ≈ (CPU 코어 수 × 2) + 유효 디스크 수

예를 들어 4코어 서버에 디스크 1개면 (4 × 2) + 1 = 9개 정도가 적당합니다. 물론 실제로는 부하 테스트를 통해 조정해야 해요.

DataSource — DriverManager를 대체하는 표준

지금까지 두 가지 방법으로 커넥션을 얻었습니다.

JAVA
// 1. DriverManager 방식 — 매번 새 커넥션 생성
Connection conn = DriverManager.getConnection(url, user, password);

// 2. DataSource 방식 — 커넥션 풀에서 꺼내기
Connection conn = dataSource.getConnection();

실무에서는 항상 DataSource를 사용합니다. 그 이유는 다음과 같아요.

DriverManagerDataSource
커넥션 풀지원하지 않음지원
분산 트랜잭션지원하지 않음지원 가능
설정 변경코드 수정 필요외부 설정으로 변경 가능
성능매번 새 커넥션 생성커넥션 재사용
사용 위치학습/테스트용실무/프로덕션

Spring에서는 application.yml에 설정하면 HikariCP DataSource가 자동으로 구성됩니다.

YAML
# Spring Boot application.yml 설정 예시
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000

JPA와의 관계 — JDBC 위에 쌓인 추상화 계층

JPA와 JDBC의 관계를 한 장의 그림으로 정리하면 이렇습니다.

PLAINTEXT
┌──────────────────────────────────┐
│         애플리케이션 코드           │
│   em.persist(user)               │
└────────────┬─────────────────────┘

┌────────────▼─────────────────────┐
│         JPA (표준 명세)            │
│   - EntityManager                │
│   - JPQL                         │
└────────────┬─────────────────────┘

┌────────────▼─────────────────────┐
│       Hibernate (JPA 구현체)      │
│   - Session                      │
│   - HQL                          │
│   - 영속성 컨텍스트                │
└────────────┬─────────────────────┘
             │ SQL 생성
┌────────────▼─────────────────────┐
│         JDBC API                  │
│   - Connection                   │
│   - PreparedStatement            │
│   - ResultSet                    │
└────────────┬─────────────────────┘

┌────────────▼─────────────────────┐
│       JDBC Driver                │
└────────────┬─────────────────────┘

┌────────────▼─────────────────────┐
│       데이터베이스                  │
└──────────────────────────────────┘

핵심은 JPA도 결국 내부에서 JDBC를 사용한다 는 것이다.

  • em.persist(user) → Hibernate가 INSERT SQL을 생성 → JDBC PreparedStatement로 실행
  • em.find(User.class, 1) → Hibernate가 SELECT SQL을 생성 → JDBC ResultSet에서 데이터 추출
  • 트랜잭션 → JDBC의 setAutoCommit(false), commit(), rollback()

그래서 JPA에서 문제가 발생하면 결국 JDBC 레벨까지 내려가서 디버깅해야 할 때가 있다. 슬로우 쿼리 분석, 커넥션 풀 튜닝, 트랜잭션 격리 수준 설정 등은 JDBC를 이해해야 제대로 할 수 있다.

MyBatis도 마찬가지다. SQL을 직접 작성하지만, 실행은 JDBC를 통해 이루어진다. 결국 JPA든 MyBatis든 Spring JDBC Template이든, 전부 JDBC 위에 쌓인 추상화일 뿐이다.

정리 테이블

개념핵심 정리
JDBC자바에서 DB에 접근하기 위한 표준 인터페이스 (java.sql 패키지)
DriverDB 벤더가 제공하는 JDBC 구현체, 실제 통신 담당
ConnectionDB와의 연결 세션, TCP 연결 포함
StatementSQL 문을 DB로 전송 — 직접 사용은 비권장
PreparedStatementSQL 프리컴파일 + 파라미터 바인딩으로 SQL Injection 방어
ResultSetSELECT 결과를 순회하는 커서, next()로 이동
** 트랜잭션**setAutoCommit(false) → 작업 → commit()/rollback()
try-with-resourcesJDBC 리소스 자동 해제, Connection 누수 방지
** 커넥션 풀**커넥션을 미리 만들어두고 재사용, HikariCP가 사실상 표준
DataSourceDriverManager를 대체하는 커넥션 제공 인터페이스, 풀 기능 포함
JPA/HibernateJDBC 위에 쌓인 ORM 추상화 계층, 내부에서 JDBC 사용

주의할 점

Connection 누수는 서비스 전체를 멈춘다

JDBC 리소스(Connection, Statement, ResultSet)를 닫지 않으면 커넥션이 계속 쌓여서 결국 DB가 새 연결을 거부한다. try-with-resources를 반드시 사용 해야 한다. finally 블록에서 수동으로 닫는 방식은 close() 자체가 예외를 던질 수 있어서 코드가 복잡해진다.

maximumPoolSize를 너무 크게 잡으면 역효과

커넥션 풀 크기를 무작정 늘리면 DB에 부하가 집중된다. HikariCP 공식 문서의 권장 공식은 (CPU 코어 수 x 2) + 유효 디스크 수다. 4코어 서버에 디스크 1개면 약 9개가 적당하다. 실제로는 부하 테스트를 통해 조정해야 한다.

Statement 대신 PreparedStatement를 써야 하는 이유

Statement로 사용자 입력을 SQL에 직접 넣으면 SQL Injection에 노출된다. SELECT * FROM users WHERE name = ' + userInput + ' 형태의 코드에 '; DROP TABLE users; --를 넣으면 테이블이 삭제된다. PreparedStatement는 SQL 구문과 파라미터를 분리하므로 이 공격을 구조적으로 차단 한다.

정리

개념핵심 정리
JDBC자바에서 DB에 접근하기 위한 표준 인터페이스 (java.sql 패키지)
DriverDB 벤더가 제공하는 JDBC 구현체, 실제 통신 담당
ConnectionDB와의 연결 세션, TCP 연결 포함
PreparedStatementSQL 프리컴파일 + 파라미터 바인딩으로 SQL Injection 방어
ResultSetSELECT 결과를 순회하는 커서, next()로 이동
** 트랜잭션**setAutoCommit(false) -> 작업 -> commit()/rollback()
try-with-resourcesJDBC 리소스 자동 해제, Connection 누수 방지
** 커넥션 풀**커넥션을 미리 만들어두고 재사용, HikariCP가 사실상 표준
DataSourceDriverManager를 대체하는 커넥션 제공 인터페이스, 풀 기능 포함
JPA/HibernateJDBC 위에 쌓인 ORM 추상화 계층, 내부에서 JDBC 사용
댓글 로딩 중...