JDBC

도토리·2023년 5월 4일
0

스프링 DB 접근

목록 보기
1/6

JDBC

애플리케이션 서버가 DB를 사용하는 방법

  1. 주로 TCP/IP 사용해서 커넥션 연결
  2. 연결된 커넥션 통해서 DB에 SQL 전달
  3. DB는 전달된 SQL 실행, 결과 응답

❗️ 문제 상황
각 DB마다 커넥션 연결하는 법, SQL 전달하는 법, 결과 응답받는 법이 전부 다르다.
(1) DB 종류를 변경하는 경우, 애플리케이션 코드(DB 사용 코드)를 변경해야 한다.
(2) 개발자가 각 DB마다 커넥션 연결, SQL 전달, 응답받는 방법을 새로 학습해야 한다.

✔️ 해결
JDBC(Java DB Connectivity) 도입

  • 자바에서 DB에 접속할 수 있도록 하는 자바 API
  • 위에서 언급한 3가지 기능을 인터페이스로 제공
java.sql.Connection
java.sql.Statement
java.sql.ResultSet

개발자는 이 인터페이스를 사용해서 애플리케이션을 개발하면 된다.

  • 각 DB vendor는 인터페이스를 자신의 DB에 맞도록 구현해서 라이브러리로 제공하는데, 이를 'JDBC 드라이버'라 부른다. EX) MySQL JDBC 드라이버, Oracle JDBC 드라이버
    즉, JDBC는 인터페이스(추상메소드), 드라이버는 구현클래스(구현메소드)




👨🏻‍💻 정리
문제1. DB 종류를 변경하는 경우, 애플리케이션 코드(DB 사용 코드)도 수정해야 한다.
-> 애플리케이션은 이제 JDBC에만 의존하기 때문에, DB 종류를 변경하더라도 애플리케이션 코드는 수정할 필요가 없다. JDBC 드라이버만 변경하면 된다.

문제2. 개발자는 DB 종류에 따라 커넥션 연결, SQL 전달, 결과 응답받는 방법을 새로 학습해야 한다.
-> 개발자는 JDBC 사용법만 학습하면 된다.

JDBC의 한계: 각 DB마다 데이터 타입(EX. MySQL의 varchar, Oracle의 varchar2), SQL(EX. 페이징 처리) 등 일부 사용법이 다르다. ANSI SQL(= 표준 SQL)이 존재하기는 하지만 일반적인 부분만 공통화했기 때문에 한계가 있다. 결국, DB 종류를 변경하면 SQL을 해당 DB에 맞도록 변경해야 한다.


최신 데이터 접근 기술

JDBC를 직접 사용하기 보다는 JDBC를 편리하게 사용하는 다양한 기술 존재

  • SQL mapper: JdbcTemplate, MyBatis
  • ORM 기술: JPA(hibernate)

※ 이러한 기술들도 내부적으로는 JDBC를 사용한다. 따라서 JDBC를 직접 사용하지 않더라도, JDBC의 내부 동작 원리는 반드시 알고 있어야 한다.


애플리케이션과 DB 연결하기

예제1

public Member save(Member member) throws SQLException {
    String sql = "insert into member(member_id, money) values(?, ?)";

    Connection conn = null;
    PreparedStatement pstmt = null;
    
    try {
        conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        pstmt = conn.prepareStatement(sql); //SQLException
        pstmt.setString(1, member.getMemberId());
        pstmt.setInt(2, member.getMoney());
        pstmt.executeUpdate();
        return member;
    } catch (SQLException e) {
        log.info("error", e);
        throw e;
    } finally {
        close(conn, pstmt, null);
    }
}

private void close(Connection conn, Statement stmt, ResultSet rs) throws SQLException {
    if (stmt != null) {
        try {
            stmt.close();
        } catch (SQLException e) {
            log.info("error", e);
            throw e;
        }
    }

    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            log.info("error", e);
            throw e;
        }
    }

    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            log.info("error", e);
            throw e;
        }
    }
}

Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
  • JDBC 드라이버를 찾은 후, DB와 커넥션을 맺고 그 결과(Connection) 반환
  • DB에 접속하기 위해서는 DB URL, USERNAME, PASSSWORD에 대한 정보가 필요하다.

적절한 JDBC 드라이버 찾는 과정

  • DriverManger는 라이브러리에 등록된 드라이버 목록을 자동 인식하고, 각 드라이버에게 순서대로 URL(+ USERNAME, PASSWORD) 정보를 넘겨서 커넥션을 획득할 수 있는지 확인
  • 예를 들어, URL이 jdbc:h2:tcp://localhost/~/test인 경우 URL이 'jdbc:h2'로 시작하는데, 이는 H2 DB에 접근하기 위한 규칙이다.
    ⅰ) MySQL 드라이버: 본인이 처리할 수 없다는 결과 반환, 다음 드라이버에게 순서가 넘어간다.
    ⅱ) H2 드라이버: 본인이 처리할 수 있으므로, 실제 DB와 커넥션을 맺고, JDBCConnection(H2 드라이버의 Connection 구현체) 반환
  • JDBC 드라이버는 내가 직접 지정하는 것이 아니라, DriverManger 통해서 드라이버가 자동 선택되는 것이다.

String sql = "insert into member(member_id, money) values(?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());

pstmt.executeUpdate();
  • prepareStatement(): DB에 전달할 SQL, 파라미터로 전달할 데이터 준비하기
  • executeUpdate(): 커넥션 통해서 SQL을 실제 DB에 전달
    참고로, int를 반환하는데, 영향받은 DB row 수를 의미한다.
  • PreparedStatement: Statement의 자식, ?를 통한 파라미터 바인딩 기능 제공
  • 참고로, SQL injection 공격을 예방하려면, 문자열 더하기 연산을 통해 SQL을 만드는 것이 아니라, PreparedStatement 통한 파라미터 바인딩 방식으로 SQL을 만들어야 한다. 전자의 경우 해커가 SQL문을 집어넣을 수 있지만, 후자의 경우 집어 넣은 SQL이 단순 데이터 취급되기 때문이다.

pstmt.close();
conn.close();
  • 쿼리 실행한 후에는 반드시 리소스를 정리해야 하는데, 리소스 정리는 역순으로 한다.
  • 리소스를 정리하지 않으면 DB와의 커넥션이 끊어지지 않고 계속 유지되는, 리소스 누수(resource leak) 문제가 발생하는데, 커넥션 부족으로 장애가 발생할 수 있다.
  • 리소스 정리는 SQLException 발생 여부에 관계없이 수행되어야 하기 때문에, finally 구문에 작성한다.

예제2

public Member findById(String memberId) throws SQLException {

    String sql = "select * from member where member_id = ?";

    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;

    try {
        conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);

        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, memberId);
        rs = pstmt.executeQuery();

        if (rs.next()) {
            Member member = new Member();
            member.setMemberId(rs.getString("member_id"));
            member.setMoney(rs.getInt("money"));
            return member;
        } else {
            throw new NoSuchElementException("member not found memberId = " + memberId);
        }
        
    } catch (SQLException e) {
        log.info("error", e);
        throw e;
    } finally {
        close(conn, pstmt,  rs);
    }
}

DB 데이터 변경(= insert, delete, update)할 때는 executeUpdate(), 조회할 때는 executeQuery()를 사용한다. executeQuery()는 select의 결과를 ResultSet에 담아서 반환한다.

ResultSet의 구조

  • ResultSet 내부에는 cursor가 존재하고, 최초의 커서는 데이터를 가리키고 있지 않다. 따라서 rs.next()를 최초 1번은 호출해야 데이터를 조회할 수 있다.
  • rs.next()를 호출하면, cursor가 다음으로 이동한다. rs.next()의 결과가 true면 cursor의 이동 결과, 데이터가 있다는 뜻이다.
  • rs.getString("member_id"): 현재 cursor가 가리키고 있는 위치의 member_id 데이터를 String 타입으로 반환한다.

JdbcUtils의 도움을 받아서 편하게 close()하자.

close() 메소드를 다음과 같이 수정하자.

private void close(Connection conn, Statement stmt, ResultSet rs) throws SQLException {
    JdbcUtils.closeResultSet(rs);
    JdbcUtils.closeStatement(stmt);
    JdbcUtils.closeConnection(conn);
}

JdbcUtils를 사용하기 위해서는 spring-boot-starter-jdbc 라이브러리를 추가해야 한다.

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

0개의 댓글