본문 바로가기

Web Sever 개발과 CS 기초/스프링

스프링 DB 사용을 위한 JDBC에 대한 이해와 사용 방법

관련 내용

해당 프로젝트 깃허브 

 

<Spring DB를 사용하기 위한 JDBC, Connection 기초>

  • (현재 글) JDBC 기술이 어떤 역할을 하는 지, 커넥션은 어떻게 획득하는 지, SQL 쿼리와 JDBC 사용법은 어떻게 되는지에 대한 설명

 [백엔드/스프링] - 스프링 DB 사용을 위한 JDBC에 대한 이해와 사용 방법

  • 커넥션 풀에 대한 이해와, DataSource 인터페이스를 사용한, 커넥션 얻어오기 통일화 방법

[백엔드/스프링] - ConnectionPool에 대한 이해와 DataSource 인터페이스로 커넥션을 획득하는 방법 통일하기

개요 목적

Spring과 데이터 베이스 사용하기 위해 ORM기술인 JPA를 가장 많이 사용한다. 이번 시간에는 JPA 기술의 기반이 되는 Java JDBC 기술에 대해서 알아보겠다.

JDBC 기술이 어떤 역할을 하는 지, 커넥션은 어떻게 획득하는 지, SQL 쿼리와 JDBC 사용법은 어떻게 되는 지 순차적으로 알아보자.

개발 환경

  • SpringBoot(gradle) -2.7.7
  • h2 - 2.1.4.214

JDBC의 필요성

DB에 데이터를 저장, 조회를 위해 서버에서 데이터베이스와 통신 할 때 다음 과정을 거친다.

  • 커넥션 연결: 주로 TCP/IP를 사용해서 커넥션을 연결한다.
  • SQL 전달: DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
  • 결과 응답: DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다

그러나 문제는, 데이터 베이스마다 커넥션 연결, SQL 전달, 결과 응답 하는 방법들이 다르다는 것이다.

→그렇게 되면, 백엔드 서버에서 데이터 베이스를 변경하면, 코드도 함께 변경해야 하고, 새로운 데이터 베이스 사용 방법을 새로 익혀야 하는 문제 발생.

그래서 탄생한 것이, JDBC 표준 인터페이스이다.

JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다.

JDBC는 대표적인 3가지 기능을 표준 인터페이스로 정의해서 제공한다.

  • java.sql.Connection - 연결
  • java.sql.Statement - SQL을 담은 내용
  • java.sql.ResultSet - SQL 요청 응답

개발자는 어떤 DB를 사용하든, 위 인터페이스만 사용해서 코드를 구현하면 된다. 하지만 인터페이스는 틀이고 실질적인 구현 코드가 필요하다. 그래서 DB 회사에서 자신의 DB와 잘 통신할 수 있도록, 인터페이스를 활용해 실제 코드를 구현해서 제공한다. 이것을 JDBC 드라이버라고 한다. MySQL 드라이버Oracle DB 드라이버가 그 예이다.

JDBC에 DB에 맞는 드라이버를 연결하는 방법은, 아래 JDBC 데이터 베이스 연결 원리에서 확인 할 수 있다.

그러나 JDBC의 한계 점도 있다.

각각의 데이터 베이스마다, SQL, 데이터 타입, 페이징 방법 등은 다르다.

JDBC 데이터 베이스 Connection 획득 방법

JDBC에서 제공하는 DriverManager.getConnection에 DB가 작동하는 주소와 유저 이름, 비밀번호를 입력하면, URL 규칙에 따라서, 라이브러리에 있는 해당 데이터베이스 드라이버를 찾고, 드라이버가 작동해서 실제 데이터 베이스와 커넥션을 맺고 그 결과를 반환해준다.

public class ConnectionUtil {

    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            log.info("get connection={}, class={}", connection,connection.getClass());
						return connection;
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }
}
public abstract class ConnectionConst {
    public static final String URL = "jdbc:h2:tcp://localhost/~/test";
    public static final String USERNAME = "sa";
    public static final String PASSWORD = "";
}

위 정보를 사용해서 Connection을 얻으면, class=class org.h2.jdbc.JdbcConnection 정보를 얻을 수 있다. 이것은 H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션이다. 물론 이 커넥션은 JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스를 구현하고 있다.

//**org.h2.jdbc.JdbcConnection 일부 코드 Connection을 구현해서 작성된 것을 알 수 있다.**
package org.h2.jdbc;
public class JdbcConnection extends TraceObject implements Connection, JdbcConnectionBackwardsCompat,
        CastDataProvider {
...
@Override
    public Statement createStatement() throws SQLException {
		...}
]

 

작동 과정은 URL = "jdbc:h2”로 시작하는 것이 h2데이터 베이스에 접근하기 위한 규칙이다. DriverManager가 규칙을 발견하면, H2 드라이버에게 권한을 넘긴 후 실제 데이터 베이스와 연결해서 커넥션을 흭득하고 반환한다.

JDBC와 SQL 쿼리를 사용한 개발

JDBC와 SQL 쿼리를 사용해서, 실제 DB와 데이터를 주고 받는 방법에 대해서 알아보자.

이 글에서는 JDBC가 작동하는 핵심 코드과 설명만 있고, 종료 절차, 데이터베이스 스키마 등 자세한 정보는 깃허브를 확인하면 된다.

저장하기

KIMHWANG\jdbc\src\test\java\hello\jdbc\repository\MemberRepositoryV0Test.java

public class MemberRepositoryV0 {

		public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values(?,?)";
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            //con은 h2드라이버에서 흭득한 connection이다.
            //h2 전용 커넥션으로 PreparedStatement 객체를 만들어 준다.
            //그렇게 되면 h2 드라이버가 구현한 PreparedStatement사용할 수 있는 것이다.
            pstmt = con.prepareStatement(sql);
            
            //sql를 설정한 pstmt에 파라미터를 지정하고, excuteUpdate()를 통해서, DB에 값 추가 요청을 한다.
            //DB 통신이 성공한다면, 에러 발생 없이, 해당 메소드가 종료된다.
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();
            return member;
        }catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
}

PreparedStatement

는 Statement 의 자식 타입인데, ? 를 통한 파라미터 바인딩을 가능하게 해준다.

참고로 SQL Injection 공격을 예방하려면 PreparedStatement 를 통한 파라미터 바인딩 방식을 사용해야 한다

pstmt.setString 등의 set 메소드를 통해서, 원하는 형의 데이터를 파라미터 바인딩 한다.

pstmt.executeUpdate()를 통해서, 준비 된 SQL을 커넥션을 통해 데이터 베이스에 전달한다. 성공하면 영향 받은 DB row수를 int로 반환한다.

조회하기

KIMHWANG\jdbc\src\test\java\hello\jdbc\repository\MemberRepositoryV0Test.java

public class MemberRepositoryV0 {

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            //pstmt executeUpdate가 아니라, excuteQuery를 통해서, 조회 결과를 ResultSet으로 반환받는다.
            rs = pstmt.executeQuery();
            //결과가 담겨있는 resultSet에서 rs.get 메소드를 통해서 정보를 가져온다.
            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.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, rs);
        }
    }
}

ResultSet

는 조회하려는 모든 DB의 컬럼을 커서에 지정해서 저장하고 있다. 예를 들어서, 조회하려는 컬럼이 2개이면 3개의 커서를 가지게 된다.(첫 번째 커서는 데이터를 가리키지 않고, 조회된 컬럼이 있을 경우 데이터가 존재한다는 알림 역할을 한다.) 만약 조회한 데이터가 없다면, rs에는 0개의 커서가 존재한다.

처음 rs.next()를 하게 되면 false가 반환 되면 조회한 데이터가 없다는 것을 알 수 있고, true를 반환하게 되면 member1이 있는 커서로 위치하게 된다. 해당 커서에서 rs.getString("member_id") 나 rs.getInt("money")를 통해서 member1의 정보를 얻을 수 있는 것이다. member2의 정보를 얻고 싶다면, re.next()를 한번 더 해서 커서의 위치를 움직이면 된다.

findById() 에서는 회원 하나를 조회하는 것이 목적이다. 따라서 조회 결과가 항상 1건이므로 while 대신에 if 를 사용한다. 여러 개의 데이터를 얻을 때는 while을 지정해서 그 값들을 List에 담아준다.

수정

KIMHWANG\jdbc\src\test\java\hello\jdbc\repository\MemberRepositoryV0Test.java

public class MemberRepositoryV0 {
   

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id=?";
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            //update 방법도 insert와 유사하다. sql 쿼리에 원하는 값을 파라미터 바인딩 후에
            //excuteUpdate를 통해서 DB에 sql 쿼리를 보낸다.
            //resultSize에는 영향 받은 컬럼의 수를 반환한다.
            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
}

삭제

KIMHWANG\jdbc\src\test\java\hello\jdbc\repository\MemberRepositoryV0Test.java

public class MemberRepositoryV0 {

    public void delete(String memberId) throws SQLException {
        String sql = "delete from member where member_id=?";
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            //sql 쿼리만 다르고 방식은 insert update와 같다.
            pstmt.executeUpdate();
        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
}

<Reference>

인프런 김영한 DB 강의