🌱 spring

JDBC ❓

beomsic 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