본문 바로가기
Study

JDBC 이해

by soeundid 2024. 6. 18.

JDBC 등장 이유

애플리케이션 서버와 DB의 일반적인 사용법

1. 커넥션 연결: 주로 TCP/IP를 사용해서 커넥션을 연결한다.

2. SQL 전달: 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.

3. 결과 응답: DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.

 

이러한 방식에는 2가지의 문제점이 있다.

1. 데이터베이스를 다른 종류의 데이터베이스로 변경하면 애플리케이션 서버에 개발된 데이터베이스 사용 코드도 함께 변경해야 한다.

2. 개발자가 각각의 데이터베이스마다 커넥션 연결, SQL 전달, 그리고 그 결과를 응답받는 방법을 새로 학습해야 한다.

 

이러한 문제를 해결하기 위해 JDBC 라는 자바 표준이 등장

 

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

표준 인터페이스로 정의해서 제공하는 3가지 기능

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

이 JDBC 인터페이스를 각각 자신의 DB에 맞도록 구현해서 라이브러리로 제공하는데, 이것을 JDBC 드라이버라고 한다.

 

JDBC 장점

1. JDBC는 표준 인터페이스에만 의존한다. 

- 다른 데이터베이스로 변경하고 싶으면 JDBC 구현 라이브러리만 변경하면 된다.

2. JDBC 표준 인터페이스 사용법만 학습하면 된다.

참고: JDBC 코드는 변경하지 않아도 되지만 SQL은 해당 데이터베이스에 맞도록 변경해야 한다.

 

JDBC DriverManager 연결 이해

JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB 드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.

1. 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection()을 호출한다.

2. DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다.

- URL: jdbc:h2:tcp://localhost:/~/test

  • URL이 jdbc:h2로 시작하면 h2 데이터베이스에 접근하기 위한 규칙이다.
  • URL이 jdbc:h2로 시작했는데 MySQL 드라이버가 먼저 실행되면 이 경우 본인이 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 넘어간다.

3. 찾은 커넥션 구현체가 클라이언트에 반환된다.

 

코드를 통해서 과정을 확인해 보자!

/**
 * JDBC - DriverManger 사용
 */
@Slf4j
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();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId()); //파라미터 바인딩
            pstmt.setInt(2, member.getMoney());
            pstmt.executeUpdate();  //실행 - insert 할 때, 숫자를 반환: insert건 수(영향을 받은 row 수)
            return member;
        }catch (SQLException e){
            log.error("db error", e);
            e.printStackTrace();
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }
    
        /**
     * Statement = sql을 그래도 넣는 것
     * PrepareStatement = 파라미터를 바인딩, Statement를 상속받음
     */
    private void close(Connection con, Statement stmt, ResultSet rs){
        if(rs != null){
            try {
                rs.close();
            } catch (SQLException e) {
                log.info("error", e);
            }
        }
        if(stmt != null){
            try {
                stmt.close();
            } catch (SQLException e) {
                log.info("error", e);   //여기서 에러가 발생하면 딱히 처리할 수 있는게 없음
            }
        }

        //외부 TCP connection을 사용하는 것이므로 닫아줘여함
        //위에서 SQLExceotion이 터져도 이 밑에 메소드에 영향을 주지 않는다.
        if(con != null){
            try {
                con.close();
            } catch (SQLException e) {
                log.info("error", e);
            }
        }
    }
}

 

주의 - 리소스 정리 해주기

쿼리를 실행하고 나면 리소스 정리를 꼭 해줘야 한다.
리소스를 정리할 때는 항상 역순으로 해야 한다.
따라서 예외가 발생하든 안 하든 항상 수행되어야 하므로 finally 구문에 주의해서 작성해야 한다.
리소스 정리를 하지 않는다면?
커넥션이 끊어지지 않고 계속 유지되는 문제가 발생할 수 있다. 리소스 누수가 발생하여 결과적으로 커넥션 부족으로 장애가 발생할 수 있다.

 

 

조회를 하는 방식에 대해서도 알아보자!

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);

            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.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, rs);  //이 순서대로 해제
        }
    }


rs = pstmt.executeQuery() 데이터를 변경할 때는 executeUpdate()를 사용하지만, 데이터를 조회할 때는 executeQuery()를 사용한다. executeQuery()는 결과를 ResultSet에 담아서 반환한다.

 

ResultSet

  • select 쿼리의 결과가 순서대로 들어간다
  • ResultSet 내부에 있는 cursor를 이동해서 다음 데이터를 조회할 수 있다.
  • rs.next()를 호출하면서 커서가 다음으로 이동한다.
    • 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 최초 한 번은 호출해야 데이터를 조회할 수 있다.
    • 결과가 true이면 커서의 이동 결과 데이터가 있다는 뜻이다.
    • false면 더 이상 커서가 가리키는 데이터가 없다는 뜻이다.