본문 바로가기

DB

JDBC란? (SQL Mapper, ORM)

JDBC 란

Spring 환경에서 개발을 하면서 JDBC라는 단어는 많이 들어봤지만 굳이 JDBC에 대해 알려고 하지 않았다. 그래서 지금까지 묻어두었던 JDBC를 한번 알아보려 한다.

우리는 알게 모르게 JDBC를 많이 사용하고 있다. 그치만 JDBC를 직접 사용하는 경우는 드물 것이다. 아마도 JPA나 MyBatis와 같은 ORM, Sql Mapper와 같은 기술들을 사용하면서 이 기술들에서 JDBC 인터페이스를 호출하는 경우가 많을 것이다.

 

JDBC란 무엇이고 왜 필요한 것일까?

이전에는 서버와 데이터베이스 간의 통신, 쿼리 요청, 결과 응답을 수행하는 과정에 대해서 표준화된 인터페이스가 존재하지 않았다. 따라서 애플리케이션 개발자가 데이터베이스에 접속(TCP/IP)하는 코드, 쿼리를 요청하는 코드, 결과를 가져와서 이를 매핑하는 코드 등 모든 것을 작성해야 했다.

  • 이러한 환경에서 만약 데이터베이스가 변경된다면 어떻게 될까?
    각각의 데이터베이스마다 접속하는 코드, 쿼리를 요청, 응답하는 코드가 다르기 때문에 다시 해당 데이터베이스에 맞는 코드로 수정해줘야 한다.
    뿐만 아니라 각각의 DB마다 다른 코드들을 다 학습해야하는 문제도 생기게 되었다. 이러한 문제를 해결하기 위해 JDBC라는 표준화된 인터페이스가 등장했다.
  • 이런 표준화된 인터페이스인 JDBC가 등장하고나서 우리는(애플리케이션 개발자는) JDBC의 인터페이스를 가지고 연결, 쿼리 요청, 응답을 작성하면 각 벤더들이 제공하는 JDBC 인터페이스 구현체를 호출할 수 있게 되었다.

 

JDBC 등장 이후

  • 이렇게 JDBC의 등장으로 2가지 문제가 해결되었다.
    1. 데이터베이스의 종류가 변경되면 애플리케이션의 코드가 각각 수정되어야하는 문제
      • 애플리케이션 로직은 JDBC 표준 인터페이스만 의존하기 때문에 데이터베이스가 변경되더라도 코드가 변하지 않는다.
    2. 개발자가 각각의 데이터베이스에 대한 연결, 쿼리 요청, 결과 응답을 받는 방법을 학습해야하는 문제
      • JDBC 표준 인터페이스만 학습하면 됨

 

JDBC와 최신 데이터 접근 기술

앞에서 설명했듯이 JDBC는 오래된 기술이고 이를 직접 사용하는 경우는 잘 없을 것이다. 대신 JDBC를 편리하게 사용할 수 있게 하는 두 가지 기술이 있다.

  • SQL Mapper
  • ORM

 

SQL Mapper

  • 장점: JDBC를 편리하게 사용하도록 도와준다.
    • SQL 응답 결과를 객체로 편리하게 변환해준다.
    • JDBC의 반복 코드를 제거해준다.
  • 단점: 개발자가 SQL을 직접 작성해야한다. 대표 기술: 스프링 JdbcTemplate, MyBatis

 

ORM

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

 

SQL Mapper vs ORM

  • 각각의 기술은 각각의 장단점이 있다.
    • SQL Mapper의 경우는 Sql을 작성할 줄만 알면 쉽게 배우고 사용할 수 있다.
    • ORM 기술은 SQl을 작성하지 않아도 되기 때문에 개발 생산성이 높아진다. 하지만 배우기 어렵고 깊이있게 공부해야한다.
  • 두 기술 모두 JDBC를 내부적으로 사용하고 있다. JDBC에 대해 깊이있게 이해한다면 내부에 문제가 생겼을 때 근본적인 원인을 찾을 수 있을 것이다.

 

JDBC 사용법

JDBC는 애플리케이션에서 DB에 접근하기 위해 사용하는 표준 인터페이스라고 배웠다. 이를 사용하는 방법을 살펴보자.

 

Connection 연결하기

public static final String URL = "jdbc:h2:tcp://localhost/~/test";  
public static final String USERNAME = "sa";  
public static final String PASSWORD = "";

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();  
    }  
}
  • DriverManager.getConnection 함수를 호출하면 DriverManager는 라이브러리에 등록된 DB 목록을 순차적으로 접근하며 연결을 시도한다.
  • 이때 URL을 분석하여 각 DB에 맞는 패턴인지 확인하여 이를 처리할 수 있으면 연결을 요청하게 된다.

 

JDBC를 이용한 개발

  • save
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());  
        int count = pstmt.executeUpdate(); //영향받은 row 수를 반환  
        return member;  
    } catch (SQLException e) {  
        log.error("DB error");  
        throw e;  
    } finally {  
        close(con, pstmt, null);  
    }  
}

private void close(Connection con, Statement stmt, ResultSet rs) {  
    if (rs != null) {  
        try {  
            rs.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);  
        }  
    }  
}
  • 코드를 한줄한줄 살펴보면서 의미를 해석해보자.
  • 먼저 jdbc를 직접 사용할 때에는 sql을 작성해 줘야한다. 위의 코드에서는 insert into ~ values 쿼리를 작성했다.
  • 그 후 DriverManager를 통해 Connection을 연결하고 커넥션 객체를 통해서 preparedStatement 객체를 생성한다. 이 [[preparedStatement]]는 setString, setInt 등의 메서드를 통해서 파라미터를 바인딩 할 수 있다.
  • 마지막으로 preparedStatement.executeUpdate() 메서드를 호출하여 쿼리를 실행한다.
    • executeUpdate() 메서드는 insert, update, delete 시에 사용한다. (select는 executeQuery() 메서드를 사용한다.)
  • finally 구문에서는 얻었던 resource를 반환하는 close() 메서드를 호출하는데 자원을 얻었던 순서의 역순으로 반환해야한다.
  • findById
public Member findById(String memberId) throws SQLException {  
    String sql = "select * from Member where member_id = ?";  
    Connection con = null;  
    PreparedStatement pstmt = null;  
    ResultSet rs = null;  
    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 member_id=" + memberId);  
        }  
    } catch (SQLException e) {  
        log.error("error", e);  
        throw e;  
    } finally {  
        close(con, pstmt, rs);  
    }  
}
  • 앞에서와 마찬가지로 sql, connection, preparedStatement를 생성한다.
  • 조회 시에는 ResultSet이라는 객체를 통해 쿼리의 결과를 받아올 수 있는데 이 ResultSet은 Cursor 방식으로 데이터를 접근할 수 있다.
    • 위의 코드에서 볼 수 있듯이 데이터의 시작값은 rs(resultSet)의 next() 를 호출했을 때 가져올 수 있다.
    • preparedStatement와 비슷하게 rs.getString(), rs.getInt() 등의 메서드로 결과를 조회해 올 수 가있다.
    • rs의 값이 여러개일 경우 while(rs.next()) 메서드를 통해 데이터를 순차적으로 조회할 수 있다.

 

  • update
public void update(String memberId, int money) throws SQLException {  
    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(); //영향받은 row 수를 반환  
        log.info("resultSize={}", resultSize);  
    } catch (SQLException e) {  
        log.error("DB error");  
        throw e;  
    } finally {  
        close(con, pstmt, null);  
    }  
}
  • 업데이트 역시 쿼리를 작성하여 preparedStatement.executeUpdate() 메서드로 실행시킨다.
  • 실행의 결과로 리턴되는 값(resultSize)은 영향받은 row의 수다. 위의 예시에서는 하나의 레코드를 업데이트 했으므로 resultSize가 1이다.
  • delete
public void delete(String memberId) throws SQLException {  
    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(); //영향받은 row 수를 반환  
    } catch (SQLException e) {  
        log.error("DB error");  
        throw e;  
    } finally {  
        close(con, pstmt, null);  
    }  
}

Reference

  • 스프링 DB 1편 - 데이터 접근 핵심 원리 by 김영한