framework/spring

1. JDBC의 이해

wooweee 2023. 9. 22. 11:53
728x90
  • spring boot db dependency
    • JDBC API
    • H2 Database
    • Lombok

  • test code에서 lombok 사용 시, dependency 추가
    • test code에서 @Slfj4 사용 하기 위함
testComileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

 

1. H2 db 설정

  • 개발, 테스트 용도로 사용하는 가벼운 db
  • sql 작성 화면 제공

 

  • 실행
    • terminal에서 다운로드 받은 h2 경로 이동
    • 권한 주기:  chmod 755 h2.sh 
    • 실행:  ./h2.sh

 

  • web browser 자동 실행 후 h2 db 정상 작동 방법

문제 화면
수정 화면

 

  • web browser h2 접속 방법
    1. jdbc:h2:~/test  (최초 한번)
      • 연결 시험을 호출하면 오류가 발생
      • 연결을 직접눌러주어야 한다. 
      •  ~/test.mv.db  파일 생성 확인 
        ~ 경로에서    $ ls -arlth    수행 하면 확인 가능
    2. 최초 생성 이후
      • jdbc:h2:tcp://localhost/~/test 으로 접속

 

2. JDBC 이해

  • app 개발시 중요 데이터는 대부분 db에 보관

 

2.1. 기본 작동 원리

 

  • app 과 db 일반적 사용 방법
    1. 커넥션 연결 : tcp/ip를 사용해서 connection 연결
    2. SQL 전달 : db가 이해할 수 있는 'SQL문'을 연결된 connection을 통해 db에 전달
    3. 결과 응답 : db에 정상적으로 'SQL문'이 수행 된 후, 결과를 app에게 응답 해준다.

 

2.2. server와 db 변경

  • sql회사 마다 connection, sql 전달, 결과 응답 방법이 다 다르다.
  • 관계형 db만 수십개 존재

 

  • 문제점
    1. 다른 종류의 db 변경시 코드도 함께 변경이 된다.
    2. 각각의 db마다 connection 연결, sql 전달, result 응답 받는 방법을 새로 학습 해야함

  • 해결방안
    • jdbc 자바 표준이 등장

 

3. JDBC 표준 인터페이스

  • 자바에서 db에 접속할 수 있도록 하는 자바 api
  • jdbc는 db에 data를 qoery하거나 update하는 방법을 제공

 

 

  • JDBC 표준 인터페이스
    • 하나의 인터페이스가 아니라 Connection, Statement, ResultSet이란 각각의 interface를 가지고 있는 것을 JDBC 표준 인터페이스라고 한다.

    • db 회상의 driver 또한 위 3가지 interface를 구현한 구현체들의 묶음을 말하는 것

 

  • 사용
    • 이제 db가 무엇이지에 관계 없이 jdbc 표준 인터페이스의 3가지의 method만 사용하면 된다.
    • 각각의 db 회사들은 jdbc 표준 인터페이스를 상속받아 구현한 구현체를 제공한다.

 

  • 구현체 == 드라이버
    • 각각의 db 회사들은 jdbc 표준 인터페이스를 상속받아 구현한 구현체를 제공한다.
      이 구현체들을 jdbc 드라이버라고 한다.

 

  • 문제점 해결
    1. db를 변경할 때 JDBC 구현 library만 변경하면 된다.
      -> db를 변경해도 app server의 사용 코드를 그대로 유지
    2. JDBC 표준 인터페이스 사용법만 학습하면 된다.

 

  • jdbc 표준화의 한계
    -> jdbc의 한계라기 보단 jdbc로 connect, preparedStatement, resultset을 공통화 했지만 sql문 자체가 통일이 안되는 한계가 있음을 의미
    • 각각의 db마다 sql, data type의 일부 사용법이 다르기 때문에 ansi sql이라는 표준이 있기는 하지만 일반적인 부분만 공통화 했기 때문에 한계가 존재한다.
    • 대표적 예로 실무에서 사용하는 페이징 sql은 각각의 db마다 사용법이 다르다
    • 결론
      • jdbc 코드는 변경하지 않아도 되지만 sql문은 해당 db에 맞도록 변경해야한다.
      • jpa 사용시 db마다 다른 sql의 정의 문제를 일부 해결 가능하다.

 

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

 

4.1. sql Mapper

 

 

 

 

  • sql Mapper
    • 장점
      • jdbc를 편리하게 사용하도록 해준다. 
        = jdbc의 connect, statement, resultset + close 의 무한 반복 단계를 편리하게 해줌
      • sql 응답 결과를 객체로 편리하게 변환
      • jdbc의 반복 코드를 제거
    • 단점
      • 개발자가 sql을 직접 작성해야한다.
    • 대표 기술
      1. jdbcTemplate
      2. MyBatis

 

4.2. ORM 기술

 

  • ORM
    • 관계형 db table과 mapping 해주는 기술
    • 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 sql을 동적으로 만들어 실행
    • 대표기술
      • JPA interface
        1. 하이버네이트 구현체
        2. 이클립스링크 구현체

 

4.3. 정리

  • sql mapper 이든 ORM 이든 내부에서는 모두 jdbc를 사용한다.
  • jdbc가 어떻게 동작하는지 기본 원리 정도는 알아야 한다.

 

 

5. 데이터베이스 연결

 

5.1. Connection 방법

 

  • h2 db 서버를 먼저 실행
  • db connect를 위한 정보 abstract class - 객체로 생성하지 못하도록 하기 위함
package hello.jdbc.connection;

// 상수를 사용하기 위한 class로 객체 생성을 막기 위해 abstract class로 사용
public abstract class ConnectionConst {
    public static final String URL = "jdbc:h2:tcp://localhost/~/test"; // h2 db 접근 규약
    public static final String USERNAME = "sa";
    public static final String PASSWORD = "";
}

 

  • JDBC를 사용해서 실제 db에 연결하는 코드 작성
package hello.jdbc.connection;

import static hello.jdbc.connection.ConnectionConst.*;

@Slf4j
public class DBConnectionUtil {
    // jdbc가 제공하는 Connection class
    public static Connection getConnection(){
        try{
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD); // h2 Connection 구현체 가지고 온다. // org.h2.jdbc.JdbcConnection
            log.info("get connection={}, class={}", connection, connection.getClass());
            return connection;
        }catch (SQLException e){
            throw new IllegalStateException(e); // checked exception을 runtime으로 가지고 온다 정도로 이해
        }
    }

}

 

  • db 연결: JDBC에서 제공하는 DriverManger.getConnection(..) 사용
  • org.h2.jdbc.JdbcConnection
    • H2 db driver가 제공하는 h2 전용 Connection
    • 해당 Connection은 JDBC 표준 connection인 java.sql.Connection interface를 구현한 구현체 

 

5.2. JDBC Drivermanager 연결 이해

  • 어떻게 jdbc가 app server의 h2 driver를 찾은 건지 대한 내용

 

 

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

 

  • 해당 db driver 찾는 방법
    1. 애플리케이션 로직에서 커넥션이 필요하면  DriverManager.getConnection() 을 호출한다. 
    2. DriverManager 는 라이브러리에 등록된 드라이버 목록을 자동으로 인식
      • 이 드라이버들에게 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인
      • 정보 : url, username, password
    3. 이렇게 찾은 커넥션 구현체가 클라이언트에 반환
      • H2 커넥션은 JDBC가 제공하는  java.sql.Connection  인터페이스를 구현

 

6. JDBC 개발

 

  • 요약
    • 앞서 만든 db 드라이버 connection method를 이용해서 connection 연결
    • 해당 connection을 통해서 메서드를 이용해서 사용할 sql 쿼리문을 작성하고 preparedStatement에 저장
    • prepared statement의 setTYPE() method를 이용해서 ?에 들어갈 값 지정
    • pstmt의 executeUpdate(), executeQuery()를 통해서 실제 db에 넣는 과정 수행
    • close하기
String sql = "insert into member(member_id, money) values(? , ?)";
 
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;

con = getConnection();
pstmt = con.prepareStatement(sql);

pstmt.setString(1, memberId);
pstmt.setInt(1, memberId);

rs = pstmt.executeQuery();
int = pstmt.executeUpdate();

// try - catch - finally
finally - close()

 

  • domain = DTO = VO
package hello.jdbc.domain;

import lombok.Data;

@Data
public class Member {
    private String memberId;
    private int money;
    public Member(){}
    public Member(String memberId, int money){
        this.memberId = memberId;
        this.money = money;
    }
}

 

 

6.1. JDBC 개발 - 등록

 

package hello.jdbc.repository;

/**
 * JDBC - DriverManager 사용
 */
@Slf4j
public class MemberRepositoryV0 {

    // 1. 등록
    public Member save(Member member) throws SQLException {
        //  sql : 데이터베이스에 전달할 SQL을 정의
        String sql = "insert into member(member_id, money) values(? , ?)";

        Connection con = null;
        PreparedStatement pstmt = null; // sql params를 바인딩 받는 class

        try {
            con = getConnection(); // 1. connection 연결

            pstmt = con.prepareStatement(sql); // 2. db랑 소통
            pstmt.setString(1, member.getMemberId()); // sql string values의 첫번째 ?
            pstmt.setInt(2, member.getMoney()); // sql string values의 두번째 ?
            
            // dcl에 따라서 실행 메서드 명이 다르다.
            pstmt.executeUpdate(); // 등록

            return member;

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null); // close하는 과정에서 예외발생시 뒤에것도 close안되니깐 method로 분리
        }

    }


    // h2 db Connection
    private Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }

    // close() 예외 처리
    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);
            }
        }

    }
}

 

  1. 커넥션 획득
    • getConnection() : 이전에 만들어둔  DBConnectionUtil 를 통해서 db connection 획득

  2. SQL 전달
    • PreparedStatement 는  Statement 의 자식 타입인데,  ? 를 통한 파라미터 바인딩을 가능하게 한다.
    • 참고로 SQL Injection 공격을 예방하려면  PreparedStatement 를 통한 파라미터 바인딩 방식을 사용
    •  sql : 데이터베이스에 전달할 SQL을 정의
    • con.prepareStatement(sql) : 데이터베이스에 전달할 SQL과 파라미터로 전달할 데이터들을 준비
      1. sql

      2.  pstmt.setString(1, member.getMemberId()) : SQL의 첫번째  ? 에 값을 지정
        문자이므로 setString 을 사용
      3.  pstmt.setInt(2, member.getMoney()) : SQL의 두번째  ? 에 값을 지정
        Int 형 숫자이므로 setInt 를 지정
    • pstmt.executeUpdate()
      • Statement 를 통해 준비된 SQL을 커넥션을 통해 실제 데이터베이스에 전달
      • int를 반환하는데 영향받은 DB row 수를 반환

 

 

  • 리소스 정리
    • 쿼리를 실행하고 나면 리소스를 정리해야 한다. 여기서는  Connection ,  PreparedStatement를 사용했다. 리소스를 정할 때는 항상 역순

    •  참고로 여기서 사용하지 않은  ResultSet은 결과를 조회할 때 사용

    • 리소스 정리는 꼭! 해주어야 한다.

    •  예외가 발생하든, 하지 않든 항상 수행되어야 하므로  finally 구문에 주의해서 작성해야한다.

    • 이 부분을 놓치게 되면 커넥션이 끊어지지 않고 계속 유지되는 문제가발생
      이런 것을 리소스 누수라고 하는데, 결과적으로 커넥션 부족으로 장애가 발생

 

 

 

6.2. JDBC 개발 - 조회

package hello.jdbc.repository;

/**
 * JDBC - DriverManager 사용
 */
@Slf4j
public class MemberRepositoryV0 {

    // 2. 조회
    public Member findById(String memberId) throws SQLException {
        String sql = "select * from member where member_id = ?";

        Connection con = null; // db와 연결
        PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
        ResultSet rs = null; // select query 문의 결과를 담고 있는 통

        try {
            con = getConnection();

            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);
            
            // dcl에 따라서 실행 메서드 명이 다르다.
            // 조회시에는 Query() 사용,  등록시 update 사용
            rs = pstmt.executeQuery(); // 조회
            
            // db 존재 여부 확인
            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);
        }
    }

    // h2 db Connection
       생략
    // close() 예외 처리
       생략
}

 

  • JDBC 표준 인터페이스 마지막 - ResultSet

 

 

  • ResultSet
    • 데이터 구조 - table 모양
    • 보통 select 쿼리의 결과가 순서대로 들어간다. 
    •  ResultSet  내부에 있는 커서( cursor )를 이동해서 다음 데이터를 조회
    • rs.next() : 이것을 호출하면 커서가 다음으로 이동한다.
      • 참고로 최초의 커서는 데이터를 가리키고 있지않기 때문에  rs.next()를 최초 한번은 호출해야 데이터를 조회할 수 있다. 
      • rs.next() 의 결과가  true 면 커서의 이동 결과 데이터가 있다는 뜻
      • rs.next() 의 결과가  false 면 더이상 커서가 가리키는 데이터가 없다는 뜻
    • rs.getString("member_id") : 현재 커서가 가리키고 있는 위치의  member_id  데이터를  String 타입으로 반환
    • rs.getInt("money") : 현재 커서가 가리키고 있는 위치의  money  데이터를  int  타입으로 반환
    • 조회하는 개수가 가늠이 안될 경우에는 while문으로 조회를 수행

 

6.3. JDBC 개발 - 수정

package hello.jdbc.repository;

/**
 * JDBC - DriverManager 사용
 */
@Slf4j
public class MemberRepositoryV0 {
    // 3. 수정
    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id = ?";

        Connection con = null; // db와 연결
        PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행

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


    // h2 db Connection
       생략
    // close() 예외 처리
       생략
}

 

6.4. JDBC 개발 - 제거

 

package hello.jdbc.repository;

/**
 * JDBC - DriverManager 사용
 */
@Slf4j
public class MemberRepositoryV0 {
    // 4. 삭제
    public void delete(String memberId) throws SQLException {
        String sql = "delete from member where member_id = ?";

        Connection con = null; // db와 연결
        PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행

        try {
            con = getConnection();

            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            int resultSize = pstmt.executeUpdate();
            log.info("resultSize={}", resultSize);

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            close(con, pstmt, null);
        }
    }


    // h2 db Connection
       생략
    // close() 예외 처리
       생략
}

 

 

6.5. test 코드

package hello.jdbc.repository;

import static org.assertj.core.api.Assertions.*;

@Slf4j
class MemberRepositoryV0Test {

    MemberRepositoryV0 repository = new MemberRepositoryV0();
    @Test
    void crud() throws SQLException {
        // save
        Member member = new Member("memberV1", 10000);
        repository.save(member);
        // findById
        Member findMember = repository.findById(member.getMemberId());
        log.info("findMember={}", findMember);
        log.info("findMember == member {}", findMember == member); // false
        log.info("findMember.equals(member) {}", findMember.equals(member)); // true
        // 다른 객체 member이지만 equals가 되는 이유는 @Data lombok 기능에서 equals를 오버라이딩 했기 때문
        // -> 마냥 좋은 방법은 아닌 듯. effective java에서 equals overriding을 지양했다.
        assertThat(findMember).isEqualTo(member);

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

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

 

  • Test 코드는 반복적으로 수행 될 수 있는 코드가 좋은 코드이다.
  • 위와 같이 crud 가 모두 수행되는 code로 반복이 잘 수행되지만 중간에 예외가 발생하게 되고, 예외를 처리 후 다시 test code 수행시 수동으로 변경하는 과정이 생긴다.
  • 이를 해결하기 위해서 transaction을 사용한다.