JDBC란?

서버란·2024년 11월 3일

웹 어플리케이션

목록 보기
3/4

JDBC(Java Database Connectivity)는 Java에서 관계형 데이터베이스에 접근하고 조작하기 위한 API입니다. Java 애플리케이션이 DBMS(Database Management System) 종류와 관계없이 일관된 방식으로 데이터베이스에 연결하고 쿼리할 수 있게 해줍니다.

JDBC 구조

JDBC는 Java 애플리케이션이 데이터베이스에 접근할 수 있도록 지원하는 여러 컴포넌트로 구성되어 있습니다.

  • JDBC API: Java 애플리케이션과 DBMS 간의 통신을 위한 API입니다.
  • JDBC Driver: 각 DBMS에 맞는 드라이버가 필요합니다. 이 드라이버는 JDBC API가 해당 DBMS와 통신할 수 있게 합니다.
  • JDBC Driver Manager: Java 애플리케이션이 여러 종류의 DBMS와 연결할 수 있도록 드라이버를 관리하고 로드하는 역할을 합니다.

JDBC 아키텍처

JDBC는 Two-Tier와 Three-Tier 두 가지 아키텍처로 사용할 수 있습니다.

  1. Two-Tier 아키텍처:
  • Client Layer와 Server Layer로 구성되며, Java 애플리케이션이 DBMS와 직접 통신하는 구조입니다.
  • JDBC 드라이버를 사용하여 데이터베이스와 직접 연결하고 쿼리를 실행하여 결과를 받습니다.
  • 클라이언트가 직접 DBMS와 통신하므로 확장성이 제한적입니다. 이 구조는 클라이언트-서버 아키텍처라고도 불립니다.
  1. Three-Tier 아키텍처:
  • 미들웨어 계층이 추가되어 비즈니스 로직을 처리하고, 클라이언트는 미들웨어를 통해 간접적으로 DBMS와 통신합니다.
  • 사용자의 요청은 미들웨어(예: Tomcat과 같은 애플리케이션 서버)를 통해 전달됩니다. 미들웨어가 DBMS와 통신하고 결과를 받아 다시 클라이언트에 전송합니다.
  • 이 구조는 성능을 높이고 애플리케이션 배포를 단순화하는 장점이 있습니다.

JDBC 드라이버 로딩

JDBC 드라이버는 Class.forName()을 통해 로드되며, 이는 Java의 Reflection API를 사용합니다. 직접 드라이버 클래스를 인스턴스화하지 않는 이유는 애플리케이션과 드라이버 간의 결합을 줄이기 위함입니다. 또한, 드라이버 로딩은 전체 애플리케이션에서 한 번만 수행하는 것이 좋습니다.

Statement와 PreparedStatement

데이터베이스 쿼리 실행 시 두 가지 주요 방식이 있습니다:

  1. Statement: 일반적인 SQL 쿼리를 실행할 때 사용합니다.
  • executeQuery(): SELECT 쿼리를 실행하며, ResultSet 객체로 결과를 반환합니다.
  • executeUpdate(): INSERT, UPDATE, DELETE와 같은 DDL을 실행하며, 성공 여부를 나타내는 int 값을 반환합니다.
  1. PreparedStatement: 자주 사용되는 SQL 문을 캐싱하여 성능을 향상시키고, SQL Injection 공격을 방어할 수 있는 특성을 갖고 있습니다.
  • SQL 쿼리가 미리 컴파일되고 캐시되어 이후의 실행에서 재사용되기 때문에 성능이 향상됩니다.
    • 매개변수를 통해 데이터 삽입이 가능하므로, SQL Injection을 방지할 수 있습니다.
      배치 실행을 지원하여 여러 쿼리를 한 번에 실행할 수 있습니다.
  1. PreparedStatement의 메모리 효율성:
  • PreparedStatement는 쿼리를 미리 컴파일하여 캐시에 저장합니다. 같은 SQL 쿼리를 반복적으로 실행할 때 매번 컴파일하지 않고, 캐시에 저장된 쿼리를 재사용할 수 있습니다.
  • 덕분에 대량의 반복 작업에서 메모리와 CPU 리소스를 절약할 수 있습니다. 캐싱된 쿼리는 데이터베이스 서버의 메모리를 효율적으로 사용하므로 성능도 향상됩니다.
  1. Statement의 메모리 사용량:
  • Statement는 매번 새로운 SQL 쿼리를 실행할 때마다 새롭게 컴파일합니다.
    따라서 반복적인 쿼리 실행 시 SQL 문이 재사용되지 않아 메모리와 CPU 사용량이 증가할 수 있으며, 이는 서버의 메모리 부담을 높이고 성능 저하로 이어질 수 있습니다.
  1. 실제 사용 시 차이점:
  • Statement는 단일 실행의 쿼리에는 적합하지만, 같은 쿼리를 여러 번 실행할 경우 메모리 효율이 떨어집니다.
  • 반면, PreparedStatement는 동일한 구조의 쿼리를 반복적으로 실행할 때 더 적은 메모리와 CPU 자원을 사용하여 대규모 데이터 작업에 효율적입니다.

따라서, 반복적이거나 대량 작업에서는 PreparedStatement를 사용하는 것이 메모리와 성능 관리 측면에서 더욱 유리합니다.

ResultSet

ResultSet 객체는 쿼리 결과를 나타내며, 커서(cursor)를 통해 결과 집합의 현재 행을 가리킵니다. 초기에는 결과 집합의 첫 번째 행 이전을 가리키고 있으며, next() 메서드를 통해 다음 행으로 이동할 수 있습니다. ResultSet은 행 단위로 데이터를 처리할 수 있는 다양한 메서드를 제공합니다.

JDBC 트랜잭션 처리

JDBC에서는 데이터베이스 트랜잭션을 제어하기 위한 기능도 제공합니다. 트랜잭션은 여러 데이터베이스 작업을 한 단위로 묶어 관리할 수 있으며, 자동 커밋 모드를 비활성화하고 commit()이나 rollback() 메서드를 사용하여 트랜잭션을 직접 관리할 수 있습니다. 트랜잭션 관리는 데이터 일관성을 유지하고 오류 발생 시 복구하기 위해 중요합니다.

    • setAutoCommit(false): 자동 커밋을 비활성화하여 트랜잭션을 수동으로 제어할 수 있게 합니다.
      commit(): 트랜잭션을 완료하고 변경사항을 커밋합니다.
  • rollback(): 오류 발생 시 트랜잭션을 롤백하여 이전 상태로 되돌립니다.

Q1 Java에서 트랜잭션을 수동으로 제어할 때 자주 발생하는 오류와 그 해결 방법에는 무엇이 있을까요?

트랜잭션 수동 제어 시 발생할 수 있는 오류와 해결 방법
JDBC에서 트랜잭션을 수동으로 제어하려면 자동 커밋을 비활성화하고 commit()과 rollback()을 사용하여 작업을 완료해야 합니다. 하지만 잘못된 트랜잭션 처리 방식은 데이터 일관성 문제를 초래할 수 있으므로 주의가 필요합니다.

  1. 커밋/롤백 누락:
  • 문제: 트랜잭션이 종료되지 않고 방치되면 다른 사용자가 동일한 데이터를 접근하는 데 문제가 생길 수 있습니다. commit()이나 rollback()을 호출하지 않으면 트랜잭션이 영구히 종료되지 않는 상태가 될 수 있습니다.

  • 해결 방법: 트랜잭션 작업이 끝나면 반드시 commit()이나 rollback()을 호출해야 합니다. 일반적으로 try-catch-finally 블록을 사용해 예외 발생 시 자동으로 롤백하고, 작업 종료 후 커밋이 되도록 처리합니다.

Connection conn = null;
try {
    conn = DriverManager.getConnection(DB_URL, USER, PASS);
    conn.setAutoCommit(false); // 트랜잭션 시작
    
    // 트랜잭션 처리 작업
    
    conn.commit(); // 작업 완료 후 커밋
} catch (SQLException e) {
    if (conn != null) {
        try {
            conn.rollback(); // 예외 발생 시 롤백
        } catch (SQLException rollbackEx) {
            rollbackEx.printStackTrace();
        }
    }
    e.printStackTrace();
} finally {
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException closeEx) {
            closeEx.printStackTrace();
        }
    }
}
  1. 중복 트랜잭션 시작:
  • 문제: 이미 트랜잭션이 진행 중인 상태에서 또 다른 트랜잭션을 시작하면 데이터 불일치가 발생할 수 있습니다.
  • 해결 방법: 트랜잭션이 중첩되지 않도록 주의하고, 이미 트랜잭션이 진행 중인지 확인하는 로직을 추가하는 것이 좋습니다.
  1. 연결 누수(Connection Leak):
  • 문제: 트랜잭션이 완료된 후 연결을 해제하지 않으면 자원이 낭비되고, 과도한 연결이 누적될 경우 서버가 다운될 위험이 있습니다.
  • 해결 방법: 트랜잭션 작업이 끝난 후 반드시 연결을 닫아주는 것이 중요합니다. finally 블록을 활용하여 연결을 해제하도록 합니다.

Q2 JDBC의 각 드라이버 타입(Type 1 ~ Type 4)마다 장단점과 사용 사례는 무엇인가요?

JDBC 드라이버 타입별 특징과 사용 사례
JDBC 드라이버는 네 가지 유형으로 나뉘며, 각각의 드라이버는 특정 환경에 적합한 장단점을 갖고 있습니다.

  1. Type 1: JDBC-ODBC 브리지 드라이버
  • 특징: JDBC와 ODBC(Open Database Connectivity) 드라이버 간의 브릿지 역할을 수행합니다.
  • 장점: 다양한 ODBC 호환 데이터베이스와 쉽게 연결할 수 있습니다.
  • 단점: ODBC 드라이버가 필요하고, 성능이 느리며 플랫폼 종속적입니다.
  • 사용 사례: 주로 실험용이나 프로토타입 제작에 사용되며, 실제 환경에서는 잘 사용되지 않습니다.
  1. Type 2: 네이티브 API 드라이버
  • 특징: 각 DBMS의 네이티브 API를 호출하여 JDBC를 지원하는 드라이버입니다.
  • 장점: 네이티브 호출로 인해 성능이 빠릅니다.
  • 단점: 플랫폼에 종속적이며, 데이터베이스 클라이언트 소프트웨어가 필요합니다.
  • 사용 사례: 성능이 중요하고 특정 플랫폼에 종속되는 환경에서 사용됩니다.
  1. Type 3: 네트워크 프로토콜 드라이버
  • 특징: 미들웨어 서버를 통해 네트워크 프로토콜을 사용하는 드라이버입니다.
  • 장점: DBMS와 독립적이며, 클라이언트가 플랫폼 종속성을 가지지 않습니다.
  • 단점: 미들웨어가 추가되어야 하므로 설정이 복잡할 수 있습니다.
  • 사용 사례: 네트워크 환경에서 다양한 DBMS와의 연결이 필요할 때 적합합니다.
  1. Type 4: 순수 Java 드라이버
  • 특징: 순수 Java로 작성되어 있으며, DBMS 네이티브 프로토콜을 직접 사용합니다.
  • 장점: 플랫폼 독립적이며, 설정이 간편하고 성능이 좋습니다.
  • 단점: 특정 DBMS용 드라이버를 사용해야 하므로, 데이터베이스가 변경되면 드라이버도 변경해야 할 수 있습니다.
  • 사용 사례: 웹 애플리케이션 및 크로스 플랫폼 애플리케이션에서 가장 많이 사용되는 드라이버입니다.

Q3 PreparedStatement를 사용해 데이터베이스에서 성능 최적화할 수 있는 구체적인 예는 어떤 것이 있을까요?

PreparedStatement는 SQL 쿼리를 미리 컴파일하고 캐시하기 때문에 성능을 크게 개선할 수 있습니다. 특히 반복적인 쿼리 작업에서 유용하며, 대량 데이터를 처리할 때 효과적입니다.

예시: 배치 작업을 통한 대량 데이터 삽입
PreparedStatement의 배치 기능을 활용하면 대량의 데이터를 효율적으로 삽입할 수 있습니다. 배치 기능을 사용하면 여러 SQL 문을 한 번에 보내므로 네트워크 왕복 횟수가 줄어 성능이 향상됩니다.

Connection conn = null;
PreparedStatement pstmt = null;
try {
    conn = DriverManager.getConnection(DB_URL, USER, PASS);
    conn.setAutoCommit(false); // 자동 커밋 비활성화

    String sql = "INSERT INTO Employees (id, name, position) VALUES (?, ?, ?)";
    pstmt = conn.prepareStatement(sql);

    for (int i = 1; i <= 1000; i++) {
        pstmt.setInt(1, i);
        pstmt.setString(2, "Employee" + i);
        pstmt.setString(3, "Position" + i);
        pstmt.addBatch(); // 배치에 추가

        if (i % 100 == 0) { // 100개마다 실행
            pstmt.executeBatch();
            conn.commit(); // 커밋
        }
    }
    pstmt.executeBatch(); // 남은 배치 실행
    conn.commit(); // 마지막 커밋
} catch (SQLException e) {
    if (conn != null) {
        try {
            conn.rollback(); // 오류 시 롤백
        } catch (SQLException rollbackEx) {
            rollbackEx.printStackTrace();
        }
    }
    e.printStackTrace();
} finally {
    if (pstmt != null) {
        try {
            pstmt.close();
        } catch (SQLException closeEx) {
            closeEx.printStackTrace();
        }
    }
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException closeEx) {
            closeEx.printStackTrace();
        }
    }
}
  • 배치 실행: 반복적인 삽입 작업을 배치로 모아서 한 번에 처리합니다.
  • 성능 향상: 네트워크 호출 횟수가 줄어 성능이 크게 향상됩니다.
  • 예외 처리: 배치 실행 중 오류가 발생하면 rollback()으로 모든 작업을 되돌려 안정성을 확보합니다.
profile
백엔드에서 서버엔지니어가 된 사람

0개의 댓글