나를 기록하다
article thumbnail
Published 2024. 3. 23. 00:40
[TIL-50/240322] JDBC TIL
반응형

 

Java, JDBC, RDBMS

JDBC

JDBC란?

  • Java Database Connectivity, 자바 프로그래밍 언어를 사용해 데이터베이스에 접근할 수 있도록 하는 자바 API
  • JDBC를 이용하여 DB에 접속, SQL 실행, 데이터를 가져오거나 삭제하는 등 데이터를 다룰 수 있음

 

JDBC가 등장하게 된 배경

  • DB 접근의 표준화를 위해서 등장
  • JDBC 등장 이전
    • DB마다 존재하는 고유한 API를 직접 사용
    • 이에 따라 개발자는 기존의 DB를 다른 DB로 교체해야하는 경우 DB에 맞게 기존의 코드도 모두 수정해야 했으며 심지어 각각의 DB를 사용하는 방법도 새로 학습해야 했음

→ JDBC의 표준 인터페이스 덕분에 개발자는 DB를 쉽게 변경할 수 있게 되었고 변경에 유연하게 대처할 수 있게 됨

 

JDBC를 알아야 하는 이유

  • JDBC는 매우 오래된 기술이며 사용 방법도 복잡함 → JDBC를 직접 사용하기보다는 DB 접근을 더 편리하게 하고 개발 생산성을 높이기 위한 기술인 SQL Mapper와 ORM(Object-Relational Mapping)을 주로 사용
  • SQL Mapper나 ORM은 JDBC 기반으로 동작하기에 JDBC를 이해해야 한다 !

 

JDBC 동작 흐름

JDBC 동작 흐름

  • JDBC API를 사용하기 위해서는 JDBC 드라이버를 먼저 로딩한 후 DB와 연결해야 함
  • JDBC 드라이버는 JDBC 인터페이스를 구현하는 구현체, 특정 DB 벤더(Oracle, MySQL, PostgreSQL 등)에 대한 연결과 DB에 대한 작업을 가능하게 해줌(JDBC 드라이버의 구현체를 이용해서 특정 벤더의 DB에 접근할 수 있음)
  • JDBC가 제공하는 DriverManager가 드라이버들을 관리하고 Connection을 획득하는 기능을 제공
  • 획득한 Connection을 통해서 DB에 SQL을 실행하고 결과를 응답받을 수 있음

JDBC 동작 과정

  • 동작 과정
    1. DriverManager를 통해 Connection 획득
    2. Connection을 통해 Statement 생성
    3. 질의 수행 및 Statement를 통해 ResultSet 생성
    4. ResultSet을 통해 데이터 획득
    5. 리소스 정리
import java.sql.*;

public class Jdbc {
    public static void main(String[] args) {
        // 0. 데이터베이스 연결 정보 설정
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String username = "myusername";
        String password = "mypassword";

        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            // 1. DriverManger를 통해 Connection 획득
            connection = DriverManager.getConnection(url, username, password);

            // 2. Connection을 통해 Statement 생성
            statement = connection.createStatement();
            String sql = "SELECT * FROM users";

            // 3. 질의 수행 및 Statement를 통해 ResultSet 생성
            resultSet = statement.executeQuery(sql);

            // 4. ResultSet을 통해 데이터 획득
            while (resultSet.next()) {
                int id = resultSet.getInt("id");
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");

                System.out.println("ID: " + id + ", Name: " + name + ", Age: " + age);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5. 리소스 정리
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (statement != null) {
                    statement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}
JDBC 드라이버 관련
DriverManager를 사용하기 전에 Class.forName("com.mysql.jdbc.Driver"); 을 통해 해당 드라이버를 불러와야 하는데 JDBC 4.0 버전 이후부터는 JDBC 드라이버 자동 로딩이 도입되어 생략이 가능

 

커넥션 풀과 DataSource

  • 위의 과정에서 DriverManager를 통해 DB 커넥션을 획득함
  • JDBC 드라이버가 DB와 연결을 맺고 부가 정보를 DB에 전달한 후 연결 응답을 받으려면 커넥션을 생성해 넘겨줌
  • 커넥션을 사용하고 나면 해당 커넥션을 종료시킴

DriverManger 사용시

DriverManger

  • 이렇게 매번 커넥션을 생성하기 위해서는 네트워크와 연결하고 서버의 자원을 사용해야 함
  • SQL을 실행할 때마다 커넥션을 획득해야 함 → 연결하는 데에 추가적인 시간이 걸리고 매번 리소스를 사용하게 됨
  • 이러한 문제점을 해결하기 위해 커넥션 풀(Connection Pool)을 사용

커넥션 풀 사용시

Connection Pool

  • DB와의 연결이 필요할 때마다 매번 새로운 커넥션을 생성하는 것 대신 미리 생성된 커넥션을 커넥션 풀에 보관하고 필요할 때 커넥션을 꺼내서 사용하는 것
  • 커넥션 풀을 통해 커넥션을 사용하고 나면 커넥션을 종료하지 않고 커넥션 풀에 반납
  • 이처럼 커넥션을 재사용하게 되면 리소스를 효율적으로 활용할 수 있으며 성능을 높일 수 있음

커넥션을 획득할 때 DriverManager를 통해서 커넥션을 획득하거나 커넥션 풀을 통해서 커넥션을 획득하는 것과 같이 여러 방법이 존재한다. 그래서 DataSource 인터페이스를 통해서 커넥션을 획득하는 방법을 추상화한다. 이 말은 즉, 어떤 방식으로 커넥션을 획득하는 것과 상관없이 DataSource 인터페이스를 통해 일관된 방식으로 DB와 통신할 수 있는 것이다. 또한 인터페이스를 구현한 구현체를 쉽게 교체할 수 있어 애플리케이션의 유연성을 높일 수 있다.

 

PreparedStatement 이용하기

  • 자바에서 쿼리문을 사용할 때 기본적으로 java.sql 패키지 내의 Statement를 사용
  • Statement는 SQL문을 실행할 때 사용하는 인터페이스
  • Statment 종류(3가지)
    1. Statement
    2. PreparedStatement
    3. CallableStatement
  • CallableStatement는 PL/SQL문을 호출할 때 사용한다고 했지만 성능상 이슈로 인해 거의 사용하지 않음
    → Statement나 PreparedStatement를 사용
  • 실무 환경에서는 Statement는 쓰지 않고 PreparedStatement만 사용함

 

Statement의 동작 방식

실행 과정

  1. 구문 분석(Parsing) 및 정규화(Normalization)
    • Query 문법 확인 및 DB, 테이블 존재 여부 확인
  2. 컴파일(Compliation)
    • Query 컴파일
  3. Query 최적화(Query Optimization Phase)
    • Query 실행 방법의 최적 계획 선택
  4. 캐시(Cache)
    • Query 최적화 단계에서 선택도니 계획이 캐시에 저장되어 동일한 Query 실행 시, 1 ~ 3 단계를 실행하지 않고 캐시를 통해 찾음
  5. 실행(Execution Phase)
    • Query가 실행된 값이 담긴 객체(ResultSet)를 사용자에게 반환

 

PreparedStatement

PreparedStatement

  • Statement는 매번 Query를 실행할 때마다, 1번에서 5번 과정을 반복
  • PreparedStatement는 최초 Query 실행 시, 1번에서 5번까지의 과정을 반복, 두 번째 Query 실행부터는 1번부터 3번까지의 과정을 생략하고 4번부터 시작함 → 성능 향상
// Statement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
Statement stmt = conn.createStatement();
stmt.executeUpdate("Update Member Set Name = 'prao' Where id = 10");
stmt.executeUpdate("Update Member Set Name = 'oarp' Where id = 11");

// PreparedStatement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
String sql = "Update Member Set Name = ? Where id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "prao");
pstmt.setInt(2, 10);
pstmt.executeUpdate();
pstmt.setString(1, "oarp");
pstmt.setInt(2, 11);
pstmt.executeUpdate();

 

비교

  • Statement, PreparedStatement 방식 둘 다 Query를 컴파일하고 최적화한 다음 실행되기 전에 Caching
  • Statement는 첫 수행한 Query와 완전히 일치하는 Query를 요청하는 경우에만 캐싱한 데이터를 재활용 가능
    • 첫 번째 Query와 일치하지 않는 별도의 Query를 실행하게 되면 컴파일을 다시 해야함
  • PreparedStatement 방식은 Statement 방식과는 달리 placeHolder (?)를 이용하여 파라미터를 바인딩
    • 기존에 컴파일된 Query를 재활용
    • PreparedStatement 방식을 사용하면 DB의 부하를 낮추고 속도를 더 높일 수 있음
  • 파라미터를 Query문 안에 직접 넣어주는 Statment 방식은 가독성도 좋지 못하고 코딩하기에도 번거로움
  • PreparedStatement 방식은 파라미터 바인딩을 통해 가독성도 챙기고 손쉽게 코딩 가능

 

SQL Injection 방지(보안성)

// 기존 Statement 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
Statement stmt = conn.createStatement();
stmt.executeQuery("SELECT * FROM Member WHERE id = '" + id + "' AND pwd = '" + pwd + "'");
  • 로그인 시, 사용자에게 입력받은 값을 WHERE 조건절에 넣어 조회하는 Query
  • 만약 다음과 같이 공격자가 파라미터 값을 악의적으로 조작해서 보낼 수 있음
String userInput = "test123' OR '1' = '1";
  • 위의 경우, userInput 값이 'test123' OR '1' = '1' 로 설정되면 아래와 같은 쿼리가 생성
SELECT * FROM Users WHERE username = 'test123' OR '1' = '1';
  • 이는 항상 참이 되므로 모든 사용자의 데이터가 반환될 수 있음
  • 이처럼 공격자가 악의적으로 파라미터 값을 조작하여 사용자 정보 탈취 가능
// PreparedStatment 방식
Connection conn = DriverManager.getConnection(url, id, pwd);
String sql = "Select * from Member Where id = ? and pwd = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
pstmt.setString(2, pwd);
pstmt.executeQuery();
  • PreparedStatement 방식은 사용자에게서 받은 파라미터 값이 악의적으로 조작되었다 하더라도 파라미터 바인딩을 통해 SQL Injection을 방지할 수 있다.
  • 정확히 pstmt.setString() 메소드 내부에서 사용되는 javaEncode() 메소드가 SQL Injection을 방지해줌
// javaEncode() 메소드
public static void javaEncode(String s, StringBuilder buff, boolean forSQL) {
    int length = s.length();
    for (int i = 0; i < length; i++) {
        char c = s.charAt(i);
        switch (c) {
            case '\t':
                buff.append("\\t");
                break;
            case '\n':
                buff.append("\\n");
                break;
            case '\f':
                buff.append("\\f");
                break;
            case '\'':
                buff.append('\'');
                break;
                ...
        }
    }
}

이번 주에 학습했던 MySQL 데이터베이스와 SQL을 실제로 활용하여 Java와 DB를 연결해주는 JDBC 설정을 학습했다. 물론 JDBC 설정은 공식처럼 복사 - 붙여넣기로 사용할 수도 있지만, 이렇게 원리를 알고 왜 PreparedStatement를 쓰는지, Statement의 동작과정, JDBC의 역할 등을 공부하며 실제 프로그램이 동작할 때 어떻게 데이터를 주고 받는지 배울 수 있었다.

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...