JDBC

등장 배경

jdbc_structure

웹 어플리케이션을 개발할 경우, 어플리케이션에서 사용되는 중요한 데이터는 DB에 보관된다. 따라서, WAS 서버에서는 DB로부터 정보를 받거나, 정보를 저장하는 일을 수행해야한다.

일반적으로, 서버와 DB는 아래 3가지 기능을 수행하게 된다.

was_db_communication

  1. 커넥션 연결은 TCP/IP를 통해 WAS 와 DB 간에 논리적인 연결을 만드는 것을 의미한다.
  2. SQL 전달: 비즈니스 로직에 맞춰 필요한 데이터를 반환, 저장하기 위해 SQL를 DB에 전달한다.
  3. 결과 응답: SQL 실행 결과를 받아와서 WAS에 전달하는 역할을 한다.

위와 같은 기능을 수행하게 되는데, 만약 MySQL DB에서 Oracle DB로 변경되면 어떻게 될까? 위의 기능은 현재 MySQL DB에 맞춰 설계되어 있기 때문에 모든 부분을 수정해야한다.

DB 마다, 연결 방식, 결과 응답 방식이 모두 다르므로, DB가 변경되는 경우 DB와 연관된 모든 코드를 수정해야하는 문제가 발생하게 된다. 이를 해결하기 위해 도입되는 것이 바로 JDBC 이다.

jdbc_diagram

JDBC는 표준 인터페이스 형태로 제공되며, 각각의 DB에 맞는 드라이버가 해당 인터페이스를 각각의 DB에 맞게 구현하고 있다.

즉, JDBC 드라이버만 있으면 해당 DB에 맞는 라이브러리를 제공하게 된다.

한계점

JDBC 이후로 많은 부분이 표준화 되어 편리해졌지만, JDBC 특유의 반복되는 구문들이 많이 존재하고, DB마다 조금씩 다른, 데이터타입, SQL로 인해, DB가 변경되면 해당 DB에 맞게 SQL를 설계해야한다.

최신 DB 기술

위의 한계점을 가진 JDBC를 직접적으로 사용하기 보다, JDBC를 더욱 간편하게 활요하는 기술들이 있다.

SQL Mapper

sql_mapper

  • 장점
    • JDBC를 편리하게 사용하도록 상당 부분이 Mapping 되어 있다
    • 반복되는 JDBC 코드를 제거해준다.
  • 단점
    • SQL를 직접 작성해야하는 문제가 있다.
  • 종류
    • Jdbc Template, MyBatis

JPA

jpa

  • 장점
    • ORM 기반으로 DB 테이블 정보와 Spring의 객체 정보가 매핑되어 있다. 객체 형태로 데이터를 주고 받을 수 있다.
    • 직접적으로 SQL을 설계해야하는 경우 매우 적어진다.(상당 부분이 JPA에서 제공된다.)
  • 단점
    • Learning Curve가 상당히 높은 매우 방다한 양의 학문이다.
  • 종류
    • JPA, Hibernate, EclipseLink

SQL Mapper, JPA를 이용해서 JDBC를 직접 사용하는 방식보다 더욱 효과적으로 DB와 송수신할 수 있다. SQL Mapper, JPA 내부에는 JDBC가 동작하고 있다.

Database Connection

DB와의 연결은 DriverManager을 통해 이루어진다. 그렇기 때문에, MySQL, Oracle, H2 등 각각의 DriverManager가 존재하는데, 사용하고자 하는 DB에 맞는 driver을 라이브러리로 등록한다.

Connection Const abstract 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

public static Connection getConnection(){
    try{
        Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        log.info("get Connection={}, class={}", connection, connection.getClass());
        return connection;
    }
    catch(SQLException exception){
        throw new IllegalStateException();
    }
}

Test

@Test
void getConnection() {
    Connection connection = DBConnectionUtil.getConnection();
    assertThat(connection).isNotNull();
}

DriverManager을 이용해서 DB Connection을 할당한다.

DriverManager을 통해 getConnection 메소드를 실행하게 되면 인자에 전달된 값을 이용해서 해당하는 DB의 Driver을 찾아서 DB Connection을 반환하게 된다.

위의 경우, jdbc:h2 와 같은 형태로 URL이 설정되어 있어, H2 Driver가 실행되어 H2 Connection이 할당된다. 웹 어플리케이션에 등록된 다른 Driver의 경우 URL 정보가 매칭이 되지 않아, 처리되지 않고 다음 driver을 검사하도록 흐름(제어)를 넘긴다.

driver_manager

JDBC CRUD

schema.sql

drop table member if exists cascade;
create table member (
    member_id varchar(10),
    money integer not null default 0,
    primary key (member_id)
);

Member class

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

MemberRepository

//DB Connection을 생성하는 메소드
private Connection getConnection(){
      return DBConnectionUtil.getConnection();
}
//할당받은 Connection, Statement, ResultSet와 같은 자원을 반환한다.
private void close(Connection conn, Statement statement, ResultSet resultSet){
      if (resultSet != null) {
          try {
              resultSet.close();
          } catch (SQLException e) {
              log.info("error", e);
          }
      }
      if (statement != null) {
          try {
              statement.close();
          } catch (SQLException e) {
              log.info("error", e);
          }
      }
      if (conn != null) {
          try {
              conn.close();
          } catch (SQLException e) {
              log.info("error", e);
          }
      }
  }

할당 받은 자원에 대해서는 반드시 자원을 반환해야한다. 또한, 반환 과정에서 SQLException 에러가 발생할 수 있으므로, Try-Catch 구문을 예외처리를 반드시 해줘야한다.

Create

save

public Member save(Member member) throws SQLException {
    String sql = "INSERT INTO MEMBER VALUES(?,?)";

    Connection conn = null;
    PreparedStatement pstmt = null;

    try{
        conn = getConnection();
        pstmt = conn.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(conn, pstmt, null);
    }
}

PrepareStatement을 이용해서 parameter 기반의 statement을 구성할 수 있도록 한다 –> SQL Injection 방지의 효과를 가짐

pstmt.executeUpdate를 통해 create/update/delete와 같은 수정이 있는 sql 쿼리문을 수행할 수 있다.

finally안에 close 메소드를 포함해서 반드시 할당받은 자원을 해제할 수 있도록 한다.

Test

MemberRepositoryV0 repository = new MemberRepositoryV0();

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

Read

findById

public Member findById(String memberId) throws SQLException {
    String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID =?";

    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;

    try{
        conn = getConnection();
        pstmt = conn.prepareStatement(sql);
        pstmt.setString(1, memberId);

        rs=pstmt.executeQuery();

        if (rs.next()) {
            Member member = new Member();
            member.setMemberId(rs.getString(1));
            member.setMoney(rs.getInt(2));
            return member;
        }
        else
            throw new NoSuchElementException("member not found memberId=" + memberId);

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

executeQuery를 통해 query문을 수행한다. 이때, Query문에 대한 응답을 저장하는 ResultSet을 할당받아서 query 결과를 저장한다.

ResultSet의 경우 cursor를 이용해서 데이터에 접근하게 되는데, 처음에는 데이터를 가르키지 않은 상태이다. 그래서, 다음 데이터를 접근하기 위해 rs.next()를 이용한다.

Test

@Test
void crud() throws SQLException {
    //save
    Member member = new Member("memberV0", 10000);
    repository.save(member);

    //findById
    Member findMember = repository.findById(member.getMemberId());
    log.info("findMember={}", findMember);
    assertThat(findMember).isEqualTo(member);
}

이전에 저장한 값을 findById를 통해 검색해서 비교한 결과 동등한 값임을 확인할 수 있고, 저장 및 조회 기능이 정상적으로 작동됨을 확인할 수 있다.

Update

update

public void update(String memberId,int money) throws SQLException {
    String sql = "UPDATE MEMBER SET MONEY=? WHERE MEMBER_ID =?";

    Connection conn = null;
    PreparedStatement pstmt = null;

    try{
        conn = getConnection();
        pstmt = conn.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(conn, pstmt, null);
    }
}

Create 부분과 비교해서, SQL을 제외한 부분은 모두 동일한 것을 알 수 있다.

Test

//정보 수정
Member savedMember = memberRepositoryV0.save(member);
memberRepositoryV0.update(savedMember.getMemberId(), 20000);
Member updatedMember = memberRepositoryV0.findById(savedMember.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);

기존에 저장한 Member 객체의 money를 10000 -> 20000을 수정했고, 이를 다시 DB에 조회를 한 결과 수정 사항이 반영된 것을 확인할 수 있다.

Delete

delete

public void delete(String memberId) throws SQLException {
    String sql = "DELETE FROM MEMBER WHERE MEMBER_ID =?";

    Connection conn = null;
    PreparedStatement pstmt = null;

    try{
        conn = getConnection();
        pstmt = conn.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(conn, pstmt, null);
    }
}

Test

//정보 제거
Member savedMember = memberRepositoryV0.save(member);
memberRepositoryV0.delete(savedMember.getMemberId());
Assertions.assertThatThrownBy(() -> memberRepositoryV0.findById(savedMember.getMemberId())).isInstanceOf(NoSuchElementException.class);

기존에 저장한 member을 제거하고, 이를 조회했을때, NoSuchElementException 에러가 발생된 것을 보아, 삭제가 정상적으로 됬음을 확인할 수 있다.

References

link: inflearn

link:springdb

댓글남기기