minghxx.blog
  • [Spring] 스프링 DB 1편 1) JDBC 이해
    2023년 11월 11일 09시 09분 51초에 업로드 된 글입니다.
    작성자: 민발자
    728x90

    스프링 DB 1편 데이터 접근 핵심 원리

    Session 1 JDBC 이해

    1. 프로젝트 생성

    1) 프로젝트 생성

    https://start.spring.io/

    스프링 부트 3.x 버전부턴 자바 17 이상을 사용

     

    2) 프로젝트 설정

    build.gradle에 추가

    //테스트에서 lombok 사용
    testCompileOnly 'org.projectlombok:lombok' 
    testAnnotationProcessor 'org.projectlombok:lombok'

    2. H2 데이터베이스 설정

    1) H2 데이터 베이스 설치

    https://www.h2database.com/html/main.html

    스프링 부트 버전에 맞춰서 다운로드


    3. JDBC 이해

    1) 애플리케이션 서버와 DB

    애플리케이션을 개발할 때 중요한 데이터는 대부분 데이터베이스에 보관

    클라이언트가 애플리케이션 서버를 통해 데이터를 저장하거나 조회하면 다음 과정을 통해 데이터 베이스를 사용

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

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

    3. 결과 응답 : DB는 전달된 SQL을 수행하고 그 결과를 응답. 서버는 응답 결과를 활용

     

    문제는 각각 데이터베이스마다 위 방법이 다르다는 점이다. 

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

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

     

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

     

    2) JDBC 표준 인터페이스

    JDBC : 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API, JDBC는 데이터베이스에서 자료를 쿼리 하거나 업데이트하는 방법을 제공

    - 대표적인 3가지 기능

    표준 인터페이스로 정의해서 제공

    java.sql.Connection : 연결

    java.sql.Statement : SQL을 담은 내용

    java.sql.ResultSet : SQL 요청 응답 결과

     

    3) JDBC 드라이버

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

     

    4) JDBC 등장으로 2가지 문제 해결

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

    ▶ 애플리케이션 로직은 이제 JDBC 표준 인터페이스에만 의존

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

    데이터베이스를 변경해도 애플리케이션 서버의 사용 코드를 그대로 유지할 수 있다.

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

    개발자는 JDBC 표준 인터페이스 사용법만 학습하면 된다

     

    5) 표준화의 한계

    각각 데이터베이스 SQL은 여전히 다르다. ANSI SQL 표준이 있지만 일반적인 부분만 공통화

    데이터베이스를 변경하면 JDBC 코드를 변경하지 않아도 되지만 SQL은 해당 데이터베이스에 맞도록 변경해야 함

    JPA 사용하면 SQL 문제도 해결 가능


    4. JDBC와 최신 데이터 접근 기술

    1) JDBC 직접 사용

    JDBC는 오래된 기술이고 사용하는 방법도 복잡

    최근에는 JDBC를 직접 사용하기보다는 편리하게 사용하는 다양한 기술이 존재 대표적으로 SQL Mapper, ORM 기술이 있다.

     

    2) SQL Mapper

    JDBC를 편리하게 사용하도록 도와준다.

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

    개발자가 SQL을 직접 작성해야 하는 단점이 있다.

    JdbcTemplate, MyBatis 

     

    3) ORM 기술

    객체를 관계형 데이터베이스 테이블과 매핑해 주는 기술.

    반복적인 SQL을 직접 작성하지 않고, ORM 기술이 대신 SQL을 동적으로 만들어 실행

    각각 데이터베이스마다 다른 SQL을 사용하는 무제도 중간에서 해결

    JPA, 하이버네이크, 이클립스링크

    JPA는 자바 진영의 ORM 표준 인터페이스고, 이것을 구현한 하이버네이트와 이클립스 링크 등 구현 기술이 있다.

     

    4) SQL Mapper vs ORM

    SQL Mapper는 SQL만 직접 작성하면 나머지 번거로운 일은 SQL Mapper가 대신해결

    ORM 기술은 SQL 자체를 작성하지 않아 개발 생산성이 높아지나 실무에서 사용하기 위해 깊이 있는 학습이 필요

    내부에서는 모두 JDBC를 사용


    5. 데이터베이스 연결

    1) ConnectionConst

    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 = "";
    }

    데이터베이스에 접속하는데 필요한 기본 정보를 편하게 사용할 수 있게 상수로 만듦

     

    2) DBConnectionUtil

    @Slf4j
    public class DBConnectionUtil {
      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);
          }
       } 
    }

    JDBC가 제공하는 DriverManager.getConnection(...)을 사용해 데이터베이스 연결

    ▶ 라이브러리에 있는 데이터베이스 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환해 줌

    H2 데이터베이스 드라이버가 작동해서 실제 데이터베이스와 커넥션을 맺고 그 결과를 반환해 준다.

     

    3) 연결 결과

    DBConnectionUtil - get connection=conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class org.h2.jdbc.JdbcConnection

    class=class org.h2.jdbc.JdbcConnection

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

    JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스 구현체

     

    4) JDBC DriverManager 연결 이해

    JDBC는 Connection 표준 커넥션 인터페이스를 정의

    H2 데이터베이스 드라이버는 JDBC Connection 인터페이스를 구현한 org.h2.jdbc.JdbcConnection 구현체를 제공

    5) DriverManager 커넥션 요청 흐름

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

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

    2. DriverManager는 라이브러리에 등록된 드라이버 목록 자동 인식해 드라이버들에게 정보를 넘겨 커넥션을 획득할 수 있는지 확인해 커넥션 구현체가 클라이언트에 반환


    6. JDBC 개발 - 등록

    1) Connection 획득

    // 커넥션 획득
    private Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }

    이전에 만들어둔 DBConnectionUtil를 통해 데이터베이스 커넥션 획득

     

    2) SQL 전달

    // SQL 전달
    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();
          return member;
      } catch (SQLException e) {
        log.error("db error", e);
        throw e;
      } finally {
        close(con, pstmt, null);
      }
    }

    sql : 데이터베이스에 전달할 sql 정의

    con.prepareStatement(sql) : sql과 파라미터로 전달할 데이터들을 준비

    pstmt.setXxxx() : sql에 값을 바인딩

    pstmt.executeUpdate() : Statement를 통해 준비된 sql을 커넥션을 통해 데이터베이스에 전달, 영향받은 DB row 수를 반환한다.

     

    3) 리소스 정리

    // 리소스 정리
    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);
            }
        }
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                log.info("error", e);
            }
        } 
    }

    리소스를 정리하지 않으면 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생(리소스 누수), 커넥션 부족으로 장애 발생 가능

    리소스는 항상 역순으로 진행하며 예외가 발생하든, 하지 않든 항상 수행되어야 하므로 finally 구문에 작성!


    7. JDBC 개발 - 조회 

    1) 조회기능

    String sql = "select * from member where member_id = ?";
    
    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.info("db error", e);
        throw e;
    } finally {
        close(con, pstmt, rs);
    }

     

    pstmt.executeQuery() : 데이터를 조회할 때 사용, 결과를 ResultSet에 담아서 반환

     

    2) ResultSet

    select 쿼리의 결과가 순서대로 들어간다. 

    ResultSet 내부에 있는 커서를 이동해서 데이터를 조회할 수 있다.

    rs.next() : 커서가 다음으로 이동, 최초의 커서는 데이터를 가리키고 있지 않기 때문에 최초 한 번은 호출해야 데이터를 조회할 수 있다.

    rs.next() → true : 커서의 이동 결과 데이터가 존재

    rs.next()  false : 커서의 이동 결과 데이터가 없음

    rs.getString("member_id") : 현재 커서가 가리키고 있는 위치의 member_id 데이터를 String 타입으로 반환

     

    3) 테스트

    @Test
    void crud() throws SQLException {
      Member member = new Member("memberV0", 10000);
    
      //findById
      Member findMember = repository.findById(member.getMemberId());
      log.info("findMember={}", findMember);
      assertThat(findMember).isEqualTo(member);
    }

    log.info("findMember={}", findMember) : member 객체의 참조값이 아니라 실제 데이터가 보이는 이유는 롬복의 @Data가 toString()을 오버라이딩해서 보여주기 때문

    isEqualTo() : member와 fineMember가 equal 값이 true인 이유는 롬복의 @Data가 해당 객체의 모든 필드를 사용하도록 equals()를 오버라이딩 하기 때문


    8. JDBC 개발 - 수정, 삭제

    1) 수정기능 

    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);
      int resultSize = pstmt.executeUpdate();
      log.info("resultSize={}", resultSize);
    } catch (SQLException e) {
      log.error("db error", e);
      throw e;
    } finally {
      close(con, pstmt, null);
    }

    pstmt.executeUpdate()를 실행하고 영향받는 row 수를 반환

     

    2) 수정기능 테스트

    //update: money: 10000 -> 20000
    repository.update(member.getMemberId(), 20000);
    Member updatedMember = repository.findById(member.getMemberId());
    assertThat(updatedMember.getMoney()).isEqualTo(20000);

     

    3) 삭제기능 

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

     

    4) 삭제기능 테스트

    //delete
    repository.delete(member.getMemberId());
    assertThatThrownBy(() -> repository.findById(member.getMemberId()))
            .isInstanceOf(NoSuchElementException.class);

    삭제 후 findById()를 통해 조회 회원이 삭제되어 없으므로 NoSuchElementException 예외가 발생한다.

    assertThatThrownBy는 해당 예외가 발생하면 성공

    현재 삭제 후 조회되는 회원이 없어 예외가 발생하게 되고 검증에 성공한다.

     

     

     

    728x90
    댓글