ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JDBC ❓
    🌱 spring 2023. 1. 13. 15:11

    인프런 김영한 님의 스프링 DB 1 강의를 듣고 정리한 글 (아직 듣지 않고 결제한 강의 들으면서 다시 정리하기)

     

    ❓JDBC


    애플리케이션을 개발 시 데이터는 대부분 데이터베이스에 저장한다.

     

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

     

    1. 커넥션 연결

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

    2. SQL 전달

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

    3. 결과 응답

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

     

    ⚠️ 문제 : 데이터베이스마다 커넥션 연결 방법, SQL 전달 방법, 결과 응답받는 방법이 모두 다르다.

    ⇒ 다른 종류의 DB로 변경시 애플리케이션 서버에 개발된 DB 사용 코드도 함께 변경

    ⇒ 개발자가 각각 DB마다 커넥션 연결, SQL 전달, 결과를 응답받는 방법을 새로 학습해야 함.

     

    이를 해결하기 위해 JDBC 자바 표준 등장

     

    JDBC 표준 인터페이스 ⭐

    💡 JDBC(Java Database Connectivity)
     - 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API다.
     - JDBC는 데이터베이스에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.

     

     

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

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

     

    📖 JDBC 드라이버

    • JDBC 표준 인터페이스를 각각의 DB 벤더에서 자신의 DB에 맞도록 구현해 라이브러리로 제공한다.
    • ex) MySQL DB에 접근할 수 있는 MySQL JDBC 드라이버
    • ex) Oracle DB에 접근할 수 있는 Oracle JDBC 드라이버

     

    문제 해결

    두 가지 문제 해결

    1. 데이터베이스를 다른 종류의 데이터베이스로 변경할 때 데이터베이스 사용 코드를 변경해야 하는 문제
      • 애플리케이션 로직은 JDBC 표준 인터페이스에 의존
      • 따라서 DB에 맞는 JDBC 구현 라이브러리만 변경하면 된다.
      • 다른 종류의 데이터베이스로 변경을 해도 사용코드를 그대로 유지할 수 있다.
    2. 개발자가 각각의 데이터베이스 커넥션 연결, SQL 전달, 결과를 응답받는 방법을 새로 학습해야 하는 문제
      • 개발자는 JDBC 표준 인터페이스 사용법만 학습하면 된다.

     

    ⚠️ 표준화 한계

    • 각각의 데이터베이스마다 SQL, 데이터타입 등의 일부 사용법이 다르다.
    • 따라서 데이터베이스를 변경하면 JDBC 코드는 변경하지 않아도 되지만 SQL은 해당 데이터베이스에 맞도록 변경해야 한다.
    • JPA를 사용하면 각각의 데이터베이스마다 다른 SQL을 정의해야 하는 문제도 해결할 수 있다.

     

    📌 SQL Mapper / ORM


    JDBC를 편리하게 사용하는 다양한 기술

    1. SQL Mapper
    2. ORM
    3. 등등

     

    JDBC를 직접 사용하는 방법

    • 애플리케이션 로직에서 SQL을 직접 JDBC로 전달
    • 매우 복잡

     

    SQL Mapper

    장점

    • SQL 응답 결과를 객체로 편리하게 변환
    • JDBC 반복 코드 제거

    단점

    • 개발자가 SQL를 직접 작성해야 한다.

    대표 기술

    1. Jdbc Template
    2. MyBatis

     

    ORM

    • ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다.
    • 개발자는 직접 SQL을 작성하지 않는다.
    • ORM이 개발자 대신 SQL을 동적으로 만들어 실행해준다.
    • 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해 준다.

     

    대표 기술

    1. JPA
    2. 하이버네이트
    3. 이클립스링크

     

    📖 JPA

    • 자바 진영의 ORM 표준 인터페이스
    • 이것을 구현한 것이 하이버네이트, 이클립스 링크 등의 구현체이다.

     

    SQL Mapper vs ORM

      SQL Mapper ORM
    SQL 직접 작성 직접 작성 ❌
    난이도 SQL만 작성하면 쉽다. 쉽지 않다.

     

    이 모든 기술들도 내부에서는 모두 JDBC를 사용한다.

    • JDBC가 어떤 식으로 동작하는지 기본 원리를 알아두어야 한다.

     

    〽️ 데이터베이스 연결


    데이터베이스에 연결하기 위한 필요 기본 정보

    public class ConnectionConst {
      public static final String URL = "jdbc:h2:tcp://localhost/~/jdbc-test";
      public static final String USERNAME = "sa";
    
      public static final String PASSWORD = "";
    }

     

    JDBC를 사용해 실제 데이터베이스에 연결하는 코드

    @Slf4j
    public class DBConnectionUtil {
    
      public static Connection getConnection() {
        try {
          Connection connection = DriverManager.getConnection(
              ConnectionConst.URL,
              ConnectionConst.USERNAME,
              ConnectionConst.PASSWORD);
    
          log.info("==== getConnection = {}, class = {}", connection, connection.getClass());
          return connection;
        } catch (SQLException e) {
          throw new IllegalStateException();
        }
      }
    }
    • DriverManager.getConnection
      • 데이터베이스 드라이버를 찾아 해당 드라이버가 제공하는 커넥션을 반환

     

    🔍 H2 드라이버를 사용하는 경우

    여기서 인터페이스 Connection의 구현체인 org.h2.jdbc.JdbcConnection 을 반환

    • H2 데이터베이스 드라이버가 제공하는 H2 전용 커넥션

     

    H2 드라이버의 Connection 코드(java.sql.Connection 인터페이스 구현)

    public class JdbcConnection extends TraceObject implements Connection, JdbcConnectionBackwardsCompat,
            CastDataProvider {
    
        private static final String NUM_SERVERS = "numServers";
        private static final String PREFIX_SERVER = "server";
    
        private static boolean keepOpenStackTrace;
    
        private final String url;
        private final String user;
    
        ...
    }

    ❗ 데이터베이스가 변경되어도 DriverManager.getConnection() 코드는 변함이 없다.

     

    ❓어떻게 DB에 따른 드라이버를 찾는 것일까

    JDBC 커넥션의 인터페이스 / 구현체

    • JDBC는 java.sql.Connection 표준 커넥션 인터페이스 정의
    • H2, MySQL 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한 구현체 제공

     

    DriverManager 커넥션 요청 흐름

     

    📖  DriverManager

    • JDBC가 제공
    • 라이브러리에 등록된 DB 드라이버를 관리, 커넥션을 획득하는 기능 제공


    📖  DriverManager.getConnection()

    • 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 호출
    • 라이브러리에 등록된 드라이버 목록을 자동으로 인식하고 순서대로 정보를 넘기면서 커넥션을 획득할 수 있는지 확인한다.
    • 정보 : URL, 이름, 비밀번호 등 접속에 필요한 정보
    • 각각의 드라이버는 URL 정보를 체크해 처리할 수 있는 요청인지 확인
    • 처리할 수 없다면 다음 드라이버에게 순서를 넘긴다.
    • 이렇게 찾은 커넥션 구현체를 반환

     

    ⌨️ CRUD


    JDBC를 사용해 회원(Member) 데이터를 데이터베이스에 관리하는 기능 개발

     

    Member

    @Getter
    @Setter
    public class Member {
    
      private String memberId;
      private int money;
      
      public Member() {}
      
      public Member(String memberId, int money) {
        this.memberId = memberId;
        this.money = money;
      }
    
      @Override
      public String toString() {
        final StringBuilder sb = new StringBuilder("Member{");
        sb.append("memberId='").append(memberId).append('\'');
        sb.append(", money=").append(money);
        sb.append('}');
        return sb.toString();
      }
    }

     

    등록

    MemberRepository

    /**
     * JDBC - DriverManager 사용한 코드
     */
    @Slf4j
    public class MemberRepositoryV0 {
    
      public Member save(Member member) throws SQLException {
    
        String sql = "insert into member(member_id, money) values(?, ?)";
        // 데이터베이스 연결을 위함
        Connection con = null;
        // PreparedStatement 를 통해 database에 쿼리를 날리낟.
        PreparedStatement pstmt = null;
        try {
          con = getConnection();
          // 커넥션이 prepareStatement 를 만들고 sql 을 넘긴다.
          pstmt = con.prepareStatement(sql);
          // SQL에 대한 파라미터 바인딩
          // 여기서 파라미터는 memberId, money 두 가지이다.
          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를 해주어야 함
          // 외부 리소스를 사용하는것이기 때문에 닫아주지 않으면 계속 유지가 될 수 있다.
          close(con, pstmt, null);
        }
      }
    
      // con, stmt, rs 를 close를 하는 과정
      // 각각에 try-catch를 사용하지 않으면 하나에서 exception 발생시 다른 것들을 close
      // 못해주는 상황이 발생할 수 있기 때문
      private void close(Connection con, Statement stmt, ResultSet rs) {
        if (rs != null) {
          try {
            rs.close();
          } catch (SQLException e) {
            log.info("resultSet close error", e);
          }
        }
        if (stmt != null) {
          try {
            stmt.close();
          } catch (SQLException e) {
            log.info("prepareStatement close error", e);
          }
        }
        if (con != null) {
          try {
            con.close();
          } catch (SQLException e) {
            log.info("connection close error", e);
          }
        }
      }
      
      // connection을 가져오는 코드
      private Connection getConnection() {
        return DBConnectionUtil.getConnection();
      }
    
    }
    
    • sql - 데이터베이스에 전달할 SQL 정의
    • con.prepareStatement(sql) - 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들 준비
    • pstmt.executeUpdate() : statement를 통해 준비된 SQL을 커넥션을 통해 실제 DB에 전달

     

    📖 리소스 정리(close)

    • 쿼리를 실행하고 나면 리소스를 정리해주어야 한다.
    • 리소스 정리 시 항상 역순으로 해야 한다.
    • 예외가 발생하든 하지 않든 항상 수행되어야 한다 ⇒ finally 구문에서 작성 ❗

     

    조회

    MemberRepository

    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";
        Connection con = null;
        PreparedStatement pstmt = null;
    
        // select 쿼리의 결과를 담고 있다.
        ResultSet rs = null;
        try {
          con = getConnection();
          pstmt = con.prepareStatement(sql);
          pstmt.setString(1, memberId);
    
          // pstmt.executeUpdate() : 데이터 변경시 사용
          // pstmt.executeQuery() : 데이터 조회시 사용, ResultSet 반환
          rs = pstmt.executeQuery();
    
          // rs.next()로 한 번은 호출을 해주어야 실제 데이터에 접근할 수 있다.
          // 한 번 호출 => 데이터가 있는지 없는지 확인
          // 첫 번째 데이터가 있다면 true 반환, 없다면 false 반환
          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);
        }
      }
    

    📖 Result Set

    • select 쿼리의 결과가 순서대로 들어간다.
    • ex) “SELECT member_id, money” → ‘member_id’, ‘money’라는 이름으로 데이터 저장
    • ResultSet 내부에 있는 커서(cursor)를 이동해 다음 데이터를 조회할 수 있다.
    • rs.next() : 커서가 다음으로 이동
    • 최초의 커서는 데이터를 가리키고 있지 않아 rs.next() 를 최초 한 번은 호출해야 한다.

     

    수정 / 삭제

    MemberRepository

    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);
    
          // 쿼리 실행 후 영향받은 row 수 반환
          int resultSize = pstmt.executeUpdate();
          log.info("resultSize={}", resultSize);
        } catch (SQLException e) {
          log.error("db error", e);
          throw e;
        } finally {
          close(con, pstmt, null);
        }
     }
    
    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);
          pstmt.executeUpdate();
        } catch (SQLException e) {
          log.error("db error", e);
          throw e;
        } finally {
          close(con, pstmt, null);
        }
     }
    

     

    참고자료

    https://www.inflearn.com/course/스프링-db-1

     

    '🌱 spring' 카테고리의 다른 글

    ThreadLocal  (0) 2023.01.18
    🌊 Connection Pool  (0) 2023.01.13
    Spring Interceptor에서 Request 데이터 처리  (1) 2022.12.06
    애플리케이션 컨텍스트와 빈팩토리  (0) 2022.11.16
    JPA - @ElementCollection  (0) 2022.11.02

    댓글

Designed by Tistory.