Spring/Spring 김영한

[Spring DB 1편] (1) Jdbc의 이해

민돌v 2022. 9. 8. 10:12
728x90

 

 

인프런 김영한 - 스프링 DB 1 강의를 듣고 정리한 글 입니다.

 

jdbc 의 등장배경과 사용방법, jdbc 연결방법에 대해서 다룹니다.

 

목차

  1. jdbc 란
  2. jdbc의 역사?
  3.  JDBC 와 최신 데이터 접근 기술
  4.  데이터베이스 JDBC 커넥션
  5. JDBC DriverManager 이해
  6.  JDBC 를 사용한 간단한 CRUD

 


 

 

1. jdbc 란

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

 


 

2. jdbc 등장이유 

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

 

클라이언트가 서버(어플리케이션)에 요청을 보내면, 데이터를 저장하거나 조회하는 것을 아래처럼 데이터베이스를 사용하여 처리합니다.

  1. 커넥션 연결: 주로 TCP/IP를 사용해서 커넥션을 연결한다.
  2. SQL 전달: 애플리케이션 서버는 DB가 이해할 수 있는 SQL을 연결된 커넥션을 통해 DB에 전달한다.
  3. 결과 응답: DB는 전달된 SQL을 수행하고 그 결과를 응답한다. 애플리케이션 서버는 응답 결과를 활용한다.

 

물론 이 모든건 옛날 이야기입니다..ㅎ

 

이러한 방법의 문제점은, 각각의 데이터베이스(mysql, redis, mongoDb 등등) 사용방법이 다르고, 접근 방법이 달라 데이터베이스를 변경할 때마다 데이터베이스 접근 코드함께 변경되어야 한다는 점입니다.

 

➡️ jdbc가 나온 이유 : 이러한 문제의 해결을 위해 JDBC 라는 자바에서 데이터베이스 접근 표준 인터페이스가 나왔습니다. 🔥

대표적으로 다음 3가지 기능을 표준 인터페이스로 정의해서 제공합니다.

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

 

DB 드라이버란

JDBC 는 표준 "인터페이스" 이기 때문에, JDBC 만으로는 아무것도 할 수 없습니다.

구현체가 필요하고, 각각 DB 회사에서 JDBC 인터페이스의 구현체로 만들어 라이브러리로 제공하는게 JDBC 라이브러리 입니다.

  • 예를 들어서 MySQL DB에 접근할 수 있는 것은 MySQL JDBC 드라이버라 하고,
  • Oracle DB에 접근할 수 있는 것은 Oracle JDBC 드라이버라 합니다

 

각각 데이터베이스에 맞는 드라이버 구현체를 선택

 


JDBC 장점 및 이점

jdbc 가 등장하여서 2가지 문제가 해결되었습니다.

1. 데이터 베이스 변경시, 불필요한 어플리케이션의 코드도 변경되어야 했던 문제

  • 데이터베이스 연결 로직을 jdbc 인터페이스에 의존하게 됨으로써, 다른 데이터베이스 종료로 변경하여도, jdbc 구현 라이브러리만 변경해주면 됩니다.
  • 어플리케이션의 비지니스 로직은 영향을 받지 않습니다.

 

2. 데이터베이스마다 달랐던 커넥션 연결, sql 전달, 응답 방법을 jdbc 표준 라이브러리가 동일하게 제공하기 때문에, 따로 학습하지 않아도 됩니다.

 


JDBC 한계

📌 jdbc 표준 인터페이스 등장으로 일반적인 부분은 공통화 하였지만, 각각 데이터베이스마다 제공하는 기능과 특별한 양식들은 다르기 때문에 결국, 한계가 있었다고 합니다.

  • ex) 페이징 SQL - 각 db 마다 사용방법이 다름

 

👏🏻 즉, 데이터베이스 연결 코드는 JDBC 를 사용함으로 써, 변경하지 않아도 되었지만 SQL 문은 해당 데이터베이스에 맞게 수정해주어야 하는 한계점이 존재합니다.

-> 현재 이 부분은 "JPA" 로 상당부분을 해결합니다!

 

 

 


 

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

JDBC의 한계를 극복하기 위해서 대표적으로  SQL Mapper 혹은 ORM기술이 등장했습니다. (mybatis vs jpa??)

 

sql mapper

SQL Mapper

  1. 장점: JDBC를 편리하게 사용하도록 도와준다.
    1. JDBC의 반복 코드를 제거해준다.
    2. SQL 응답 결과를 객체로 편리하게 변환해준다.
  2.  단점: 개발자가 SQL을 직접 작성해야한다. (어디까지나 ORM 에 비해서)

대표 기술: 스프링 JdbcTemplate, MyBatis

 

orm

ORM 기술

  • ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다.
  • 이 기술 덕분에 개발자는 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 SQL을 동적으로 만들어 실행해준다.
  • 추가로 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해준다.
    • 대표 기술: JPA, 하이버네이트, 이클립스링크


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

 

🔥 하지만, 2가지 기술 모두 base low level 에서는 JDBC를 사용합니다!

 


 

4. 데이터베이스 JDBC 커넥션

 

jdbc 커넥션을 얻어서, h2 데이터베이스에 연결해보겠습니다.

실행전, h2 디비를 백그라운드로 실행시켜야 코드가 동작합니다.

 

ConnectionConst.class

  • h2에 연결하기위한 상수 정보만을 담은 추상클래스를 만들어줍니다.
  • 아래의 변수들을 이용해 정보를 호출할건데, 지금부터 아래의 코드 모두 ConnectionConst 를 전역패키지로 import 해 변수만을 불러서 이용할 예정입니다.
  • 추상클래스는 상수와 추상메소드만을 가질수 있고 인스턴스화 하지 않고 상수에 접근할 수 있습니다.
//상수 데이터만 담은 class - 객체 생성하지 않기위해 추상클래스로
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 = "";
}

 

DBConnectionUtil.class

  • JDBC를 이용해서, 실제 h2 에 연결할 커넥션을 얻어오는 코드 입니다.
  • JAVA 에서 데이터베이스에 연결하려면 JDBC가 제공하는 DriverManager.getConnection(..) 메소드를 사용해서 얻어와야합니다.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;


//DB 연결
@Slf4j
public class DBConnectionUtil {
    public static Connection geConnection(){
        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);
        }
    }
}

 

🙌🏻 그럼 위에서 말한 것 처럼, DB 가 달라지는걸 어디서 어떻게 신경쓰길래, 사용자는 JDBC 만 사용하면 디비 변경 체크를 하지 않아도 될까요?

  • 여기서 사용된 DriverManager 가 External Libraries 에 있는 현재 다운되어진 라이브러리 중 디비 라이브러리를 찾아옵니다.
  • 그래서 DriverManager.getConnection() 은 데이터베이스 드라이브러를 찾아 해당 드라이버가 제공하는 커넥션을 반환해주는 것 입니다.

 


 

5. JDBC DriverManager 이해

  • JDBC DriverManager란, JDBC 드라이버 세트를 관리하기 위한 기본 서비스입니다.
  • DriverManager 초기화는 느리게 수행되며 스레드 컨텍스트 클래스 로더를 사용하여 서비스 공급자를 찾습니다. 
  • getConnection 메소드가 호출되면 DriverManager 는 초기화 시 로드된 드라이버와 현재 애플리케이션과 동일한 클래스 로더를 사용하여 명시적으로 로드된 드라이버 중에서 적절한 드라이버를 찾으려고 시도합니다.

 

📌 쉽게 정리하자면, JDBC 인터페이스에서 커넥션을 얻기위한 메소드이고, 이 커넥션은 각각 DB 의 구현체를 의미합니다.

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

 

 

getConnection 과정

(다수의 디비 라이브러리에서 맞는 디비 구현체를 가져오는 과정!!)

  1. 애플리케이션 로직에서 커넥션이 필요하면 DriverManager.getConnection() 을 호출한다.
  2.  DriverManager 는 라이브러리에 등록된 드라이버 목록을 자동으로 인식한다. 이 드라이버들에게
    1. 순서대로 다음 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
    2. URL: ) jdbc:h2:tcp://localhost/~/test
    3. 이름, 비밀번호 등 접속에 필요한 추가 정보
    4. 여기서 각각의 드라이버는 URL 정보를 체크해서 본인이 처리할 수 있는 요청인지 확인한다.
    5. 예를 들어서 URLjdbc:h2 로 시작하면 이것은 h2 데이터베이스에 접근하기 위한 규칙이다.
    6. 따라서 H2 드라이버는 본인이 처리할 수 있으므로 실제 데이터베이스에 연결해서 커넥션을 획득하고 이 커넥션을 클라이언트에 반환한다.
    7. 반면에 URLjdbc:h2 로 시작했는데 MySQL 드라이버가 먼저 실행되면 이 경우 본인이 처리할 수 없다는 결과를 반환하게 되고, 다음 드라이버에게 순서가 넘어간다.
  3.  이렇게 찾은 커넥션 구현체가 클라이언트에 반환된다.

 

 

 


 

6. JDBC 를 사용한 간단한 CRUD

JDBC를 사용하여 DriverManager로 직접 커넥셕을 얻어와 디비에 연결해 쿼리를 날려보는 로직입니다.

 

단순한

Member.class

@Data
public class Member {
    private String memberId;
    private int money;
}

 

 

MemberRepositoryV0.class

📌 커넥션을 얻어 쿼리를 날리는 Repository입니다. 매우 길어 과정을 요약하자면

  1. 커넥션을 얻는다.
  2. 커넥셩을 닫을 객체를 null 로 초기화하는 이유는, SQLException 은 Checked Exception 이기 때문에, try catch 로 처리해주기 위함이다.
  3. PreparedStatement 객체를 이용해 쿼리를 디비에 날린다.
  4. 모든 처리가 완료되었다면, 연결된 커넥션을 순차적으로 역순으로 끊어주어야한다.
  5. ResultSet 은 디비에서 불러온 결과를 저장해서 cursor 를 이용해 값을 조회하는 역할을 한다. (조회 저장소다)

 

@Slf4j
public class MemberRepositoryV0 {

    public Member save(Member member) throws SQLException {
        String sql = "insert into member(member_id, money) values (?, ?)";

        Connection connection = null;
        PreparedStatement preparedStatement = null; // 이걸로 쿼리를 날림?

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);

            //파라미터 바인딩
            preparedStatement.setString(1, member.getMemberId());
            preparedStatement.setInt(2, member.getMoney());
            preparedStatement.executeUpdate();
            return member;

        } catch (SQLException e) {
            log.error("db error", e);
            throw e;
        } finally {
            //db 연결을 끊어줘야함
            preparedStatement.close();
            //여기서 익셉션이 터지면 connection 이 안끊어짐 그래서 다 묵어야함

            connection.close();


            //요래 해야함
            close(connection, preparedStatement, null);
        }
    }

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

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);

            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Member member = new Member();
                member.setMemberId(resultSet.getString("member_id"));
                member.setMoney(resultSet.getInt("money"));
                return member;
            }else{
                throw new NoSuchElementException("member not found memberId : " + memberId);
            }
        } catch (SQLException e) {
            log.error("error : ", e);
            throw e;
        }finally {
            close(connection,preparedStatement,resultSet);
        }
    }

    public void update(String memberId, int money) throws SQLException {
        String sql = "update member set money=? where member_id=?";


        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setString(2, memberId);

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

        } catch (SQLException e) {
            log.error("error : ", e);
            throw e;
        }finally {
            close(connection,preparedStatement,null);
        }
    }

    public void delete(String memberId) throws SQLException {
        String sql = "delete from member where member_id=?";


        Connection connection = null;
        PreparedStatement preparedStatement = null;

        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);

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

        } catch (SQLException e) {
            log.error("error : ", e);
            throw e;
        }finally {
            close(connection,preparedStatement,null);
        }
    }
    private void close(Connection con, Statement stmt, ResultSet resultSet) {

        //이렇게 해야 Exception 이 터져도 catch 로 잡기 때문에 각각의 close 가 다른 close 에 영향을 주지 않음
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                log.error("error", e);
            }
        }
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                log.error("error", e);
            }
        }
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                log.error("error", e);
            }
        }
    }

    private static Connection getConnection() {
        return DBConnectionUtil.geConnection();
    }
}

 

 

Connection 얻어온 Repository TEST 코드

MemberRepositoryV0Test.class

@Slf4j
class MemberRepositoryV0Test {

    MemberRepositoryV0 repositoryV0 = new MemberRepositoryV0();

    @Test
    void crud() throws SQLException {

        //save
        Member memberV0 = new Member("memberV0", 100000);
        repositoryV0.save(memberV0);

        //findById
        Member findMember = repositoryV0.findById(memberV0.getMemberId());
        log.info("findMember = {}", findMember);

        //2개는 다른 인스턴스지만, isEqualTo 가  equals() 를 호출해 값만 비교 하기 때문에 true
        //@Data 롬복은, 모든 상태 값을 비교할수 있는 equals 와 고유한 hashCode 를 자동으로 만들어준다.
        assertThat(findMember).isEqualTo(memberV0);


        //update
        int updateMoney = 200000;
        repositoryV0.update(memberV0.getMemberId(), updateMoney);
        Member updateMember = repositoryV0.findById(memberV0.getMemberId());
        assertThat(updateMember.getMoney()).isEqualTo(updateMoney);


        //delete
        repositoryV0.delete(memberV0.getMemberId());
        assertThatThrownBy(() -> repositoryV0.findById(memberV0.getMemberId()))
            .isInstanceOf(NoSuchElementException.class);
    }
}

 

 

 


 

여기까지 자바 표준 데이터베이스 인터페이스인 JDBC 를 사용하여 DB 에 직접 연결하여 crud 를 해본 포스팅이였습니다.

jsp & servlet 을 학교에서 배울때가 생각났던 시간이였고,,

그만큼 이 강의를 들으면서 학교에서는 얼마나 옛날 기술을 알려줬던건가.. 하는 생각이 들어씀다

 

아무튼 유익해!

 

 


참고

반응형