9 min read

<DB> JDBC

필자가 JDBC를 처음 사용해본것은 학교 교과목 중 데이터베이스라는 수업에서 사용해보았습니다.당시만 하더라도 저도 JPA를 공부하고 있던터라 “그냥 CRUD만 코드 따라쳐서 해봐야지”하고 간단하게 실습 후 넘어갔던 적이 있습니다.하지만 JPA를 사용해 어플리케이션을 만들고 동시에 다양한 에러들을 접하면서 느낀것이 하나 있습니다.결국 JPA도 JDBC라는 큰 틀위에서 돌아가는 구조임을 알게되었습니다.즉,트러블슈팅을 하기 위해서 JDBC가 로우 레벨에서 어떻게 동작하는지는 무조건 알아야겠구나 싶었고 지금과 같이 공부를 하고 포스팅을 합니다.

다시한번 말씀드리지만 순수 JDBC를 이용해서 개발하는 회사는 없다고 보시면 됩니다.다들 JPA 또는 MyBatis라도 사용해서 개발을 합니다.하지만 이와 같은 기술의 기저에는 JDBC가 있으며 기술을 사용하며 에러를 만날 때 조금이나마 쉽게 해결하기 위해 JDBC가 필요하다고 생각합니다.

JDBC 등장 배경

이글을 읽으시는 대대수가 알고 있듯이,애플리케이션이 동작할때 중요한 데이터는 모두 데이터베이스에 저장됩니다.즉,백엔드 엔지니어가 개발하는 애플리케이션 서버가 데이터베이스와 통신을 하며 사용자에게 필요한 모든 정보들을 저장하고 가져오고 삭제하고 변경하는 동작을 수행합니다.여기서 서버가 데이터베이스를 사용하기위해 필요한 일련의 절차가 존재합니다.

  1. 커넥션 연결 : 주로 TCP/IP 연결을 통해 연결합니다.
  2. SQL 전달 : 서버는 DB가 이해할수 있는 SQL을 연결된 커넥션을 통해 전달합니다.
  3. 결과 응답 : DB는 전달된 SQL을 수행후,그 결과를 리턴합니다.

간략하게 그림으로 나타내면 아래와 같습니다.아래에서 DB는 MySQL을 예로 들었습니다.

그러면 과연 RDB가 MySQL에서 Oracle로 변경된다면 MySQL과 동일한 커넥션 연결 방법으로 통신할수 있을까요? 또한 SQL의 문법 구조도 동일할까요?그렇지 않습니다.그래서 DB의 회사가 바뀔때마다 아래과 같은 문제에 개발자들은 직면하게 되었습니다.

  1. DB와 통신하고 비지니스로직을 수행하는 코드를 변경해야한다.
  2. 개발자가 디비 회사마다 서로다른 방식을 배우고 학습해야한다.

위와 같은 두가지의 문제를 돌파하기 위해 JDBC가 등장했습니다.

JDBC(Java DataBase Connectivity)

앞에서 DB를 제공하는 회사마다 커넥션 연결 및 디비에 대한 코드를 작성하는 방법이 달라서 개발자들이 고생을 하는 구조라고 말씀드렸습니다. 자바에서는 이러한 문제를 표준 인터페이스를 만듦으로서 해결하였습니다.즉, 각각의 업체들을 이러한 표준에 맞추어 라이브러리를 제공을하고 개발자들은 라이브러리만 다운 받으면 기존의 기능을 모두 사용할 수 있는 것입니다.여기서 라이브러리라 함은 익숙하게 듣는 DB 드라이버입니다.즉,표준 인터페이스에 대한 구현체라고 생각하시면 됩니다.

여기서 말하는 표준 인터페이스가 바로 JDBC입니다.

정리를 해보면 JDBC를 사용하는 법만 알면 더 이상 DB 벤더에 상관없이 자유롭게 RDB를 사용할수 있게 된것입니다.예를 들자면 중구난방으로 섞여있던 코드들을 깔끔하게 한 곳으로 모아 표준화 시킨것이라고 생각하면됩니다.

하지만 이러한 JDBC도 세월 앞에서는 장사가 없습니다.당시에는 혁신이였지만 시간이 지남으로써 단점이 발견되고 이러한 단점을 보완하기 위해 새로운 기술들이 등장합니다.자바 진영에서도 SQL Mapper와 ORM 기술을 적용시킨 기술이 등장합니다.

최신 데이터 접근 기술

지금부터 JDBC을 보완한 기술들에 대해 간략이 알아보겠습니다.하지만 들어가기 앞서 아래의 나열될 기술은 모두 JDBC를 근간에 두고 동작하는것을 알고 계시면 좋을 것 같습니다.

SQL Mapper

흔히 JdbcTemplate,MyBatis 등이 SQL Mapper를 활용한 대표적인 자바 진영의 기술들입니다.이 기술은 SQL을 작성하기만 하면 나머지 번거로운 부분을 모두 SQL Mapper가 알아서 처리해주는 동작을 수행합니다.즉,SQL 작성에 대해 익숙하신 분들은 굉장히 편리하게 사용가능합니다.하지만 SQL을 무조건 작성해야하는 단점이 있습니다.

ORM

ORM은 객체와 관계형 데이터베이스의 테이블을 매칭시켜주는 기술입니다.즉,RDB를 객체지향적으로 사용할 수 있게 만들어주는 기술이라고 생각하셔도 됩니다.이 기술을 통해 개발자는 반복적인 SQL 작성 대신 ORM이 동적으로 SQL을 작성하도록 할 수 있습니다.또한 DB 벤더마다 다른 SQL 문법의 문제도 해결해줍니다.

하지만 JPA가 고도화된 기술을 제공하기 때문에 러닝 커브가 만만치 않다는 단점이 있습니다.비유를 하자면 JPA는 1종 특수이고 Mybatis는 2종 보통 같은 느낌이라고 보셔도 됩니다.

JDBC 사용하기

이제부터 간단하게 JDBC를 통해 save 쿼리를 DB에 날려봅시다.

참고로 H2 데이터베이스를 활용해 실습을 진행합니다.
test라는 스키마를 생성하시고 연결해주시면 됩니다.

Member라는 객체를 디비에 CRUD하는 로직을 만들어봅시다.우선 Member 엔티티의 스펙은 아래와 같습니다.

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

먼저 save부터 만들어봅시다.우선 sql을 실제 자바 스트링으로 초기화 시켜줍니다.이후 Connection을 얻기 위한 인스턴스 con을 만들고 쿼리에 객체를 넣어주기 위한 인스턴스 pstmt도 생성해줍니다.이후 try문을 통해 커넥션을 getConnection()을 통해 얻고 실제 sql문의 ? 자리에 들어가야할 객체의 필드값을 바인딩 시켜줍니다.그리고 무조건 항상 finally 블럭에서 저희가 사용했던 리소스 con,stmt를 closing 시키기 위한 메서드도 호출해줍니다.

참고로 해당 메서드는 MemberRepsitory 클래스를 임의로 생성해서 해당 클래스 내부에 선언 및 정의해주어야 합니다.

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 {
            /*
            * Resource 정리를 위한 closing 메서드
            * 항상 역순으로! con -> pstmt -> pstmt -> con
            * */
            closing(con,pstmt,null);
        }
    }
  • getConnection 메서드입니다.
private Connection getConnection() {
        return DBConnectionUtil.getConnection();
    }
  • closing 메서드입니다.
private void closing(Connection con, Statement stmt, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                log.error("ResultSet Error ", e);
            }
        }

        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                log.error("Statement Error ", e);
            }
        }
        if (con != null) {
            try {
                con.close();
            } catch (SQLException e) {
                log.error("Connection Error ", e);
            }
        }

    }
  • DBConnectionUtil 클래스입니다
import lombok.extern.slf4j.Slf4j;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

@Slf4j
public class DBConnectionUtil {
    public static Connection getConnection() {
        try {
            Connection connection = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD);
            log.info("Get connection={},class={}", connection, connection.getClass());
            return connection;
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        }
    }
}

  • ConnectionConst 클래스입니다.H2 데이터베이스와 연결할 때 필요한 정보들을 자바 스트링으로 상수화 시켜서 관리하는 클래스입니다.
package hello.jdbc.connection;

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

  • 위와 같이 진행한 후 간단하게 Junit5 테스트를 통해 정상적으로 작동하는지 확인 가능합니다.
import hello.jdbc.donmain.Member;

import org.junit.jupiter.api.Test;

import java.sql.SQLException;
import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;


class MemberRepositoryTest {

    MemberRepositoryV0 repository = new MemberRepositoryV0();

    @Test
    void save() throws SQLException {
        //create - save
        Member member = new Member("memberV3", 10000);
        repository.save(member);
    }
}

실행시켜보면 아래와 같이 실제 테스트가 통과하고 H2 디비에도 Member가 insert가 정상적으로 된것을 확인 할 수 있습니다.

나머지 update, delete,read의 경우 직접 해보시면 JDBC를 이해하는데 큰 도움이 되실껍니다.

Ref : 김영한 - 스프링 DB 1