JDBC
애플리케이션에서 DB에 접근하기 위해 아래의 과정을 거친다.
첫 번째로 TCP/IP로 연결을 확립하고 두 번째로 DB 서버에 SQL을 전달한다. 마지막으로 DB 서버는 SQL 수행 결과를 응답한다. 이 과정을 위해 DB 사에서 전용 라이브러리를 제공하며 프로그래머는 해당 라이브러리로 위 과정을 구현한다. 하지만 DB의 종류는 무수히 많으며 DB에 따라 DB 접근 코드도 달라진다. DB 접근 코드를 바꾸지 않고도 DB를 바꿀 수 있는 방법이 없을까? 이를 위해 DB 접근 과정을 표준화한 JDBC가 등장했다.
JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다. 즉 DB 접근 과정을 표준화한 인터페이스이다.
자바 개발자는 JDBC 표준 인터페이스로 DB 접근 코드를 작성하며 DB 사는 JDBC 표준 인터페이스의 구현체인 JDBC 드라이버를 제공한다. 따라서 DB가 바뀌어도 애플리케이션은 JDBC 인터페이스에만 의존하기 때문에 DB 접근 코드는 변경되지 않는다.
하지만 JDBC에는 한계가 있다. DB마다 데이터 타입, SQL 구문이 다르기에 JDBC 코드는 변경하지 않아도 되지만 SQL은 변경되어야 한다. JPA로 SQL 변경 문제를 대부분 해결할 수 있다.
최신 DB 접근 기술
JDBC
JDBC는 오래된 기술이며 사용 방법도 복잡하다. 최근에는 직접 JDBC를 사용하지 않고 JDBC를 편리하게 사용할 수 있는 다양한 기술을 사용한다. 대표적으로 SQL Mapper와 ORM 기술로 나눌 수 있다.
SQL Mapper
SQL Mapper은 SQL을 실행하고 실행 결과를 객체에 맵핑하는 기술이다. 대표적인 기술로 스프링의 JdbcTemplate와 MyBatis가 있다.
장점
- 쉽게 사용할 수 있다.
- SQL 응답 결과를 객체로 변환해준다.
- JDBC의 반복 코드를 제거해준다.
단점
- 개발자가 SQL을 직접 작성한다.
- DB 변경 시 작성한 SQL도 변경되어야 한다.
ORM(Object Relational Mapping) 기술
ORM은 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술이다. 이 기술 덕분에 개발자는 반복적인 SQL을 직접 작성하지 않고, ORM 기술이 개발자 대신에 SQL을 동적으로 만들어 실행해준다. 추가로 각각의 데이터베이스마다 다른 SQL을 사용하는 문제도 중간에서 해결해준다. 대표적인 기술로 JPA, 하이버네이트, 이클립스링크가 있다.
위 기술들은 모두 내부에서 JDBC를 사용한다.
JDBC 연결
DB에 접근하려면 우선 DB에 연결해야한다. JDBC의 DriverManager.getConnection 메서드는 라이브러리에 있는 JDBC 드라이버를 찾아서 해당 드라이버가 제공하는 커넥션을 반환한다. 만약 H2 DB와 연결한다면 H2 JDBC 드라이버가 작동해 실제 H2 DB와 연결을 맺고 커넥션(H2 Connection)을 반환한다. JDBC로 SQL을 실행하려면 커넥션 객체가 꼭 필요하다.
아래는 예시 코드이다.
@Slf4j
public class 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 e) {
throw new IllegalArgumentException(e);
}
}
}
JDBC 개발
JDBC 등록
Statement 객체로 SQL을 수행하고 수행 결과를 받을 수 있다. Statement 객체를 생성하려면 Connetion 객체가 필요하다. PreparedStatement는 Statement를 상속받고 있으며 ?를 통한 파라미터 바인딩을 가능하게 한다. SQL Injection 공격을 예방하기 위해 PreparedStatement의 파라미터 바인딩 기능을 사용하는 것이 좋다. 아래는 JDBC로 회원(member) 데이터를 DB에 추가하는 코드이다.
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?,?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DBConnectionUtil.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);
}
}
JDBC를 통한 수정, 삭제도 이와 비슷하다.
JDBC 리소스 정리
SQL을 다 수행했으면 관련 리소스(Connection, Statement, ResultSet)를 정리해야 한다. 정리하지 않으면 사용하지 않는 리소스가 계속 존재하는 리소스 누수가 발생한다. Connection, Statement, ResultSet 순으로 리소스를 생성하기에 리소스를 반환할 때는 반대의 순서로 반환한다.
private void close(Connection conn, 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.error("error", e);
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
log.error("error", e);
}
}
}
JDBC를 사용하면 try catch 구문이 반복되어 가독성이 안좋다.
JDBC 조회
ResultSet 객체로 SQL 조회 결과를 받을 수 있다. ResultSet 객체를 생성하려면 Statement 객체가 필요하다. ResultSet는 마치 파일 포인터를 움직여 파일의 데이터를 읽어오는 것처럼 동작한다.
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 = DBConnectionUtil.getConnection();
pstmt = conn.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 memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, rs);
}
}
아래는 전체 코드이다.
package hello.jdbc.repository;
import hello.jdbc.connection.DBConnectionUtil;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import java.sql.*;
import java.util.NoSuchElementException;
@Slf4j
public class MemberRepositoryV0 {
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?,?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DBConnectionUtil.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);
}
}
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 = DBConnectionUtil.getConnection();
pstmt = conn.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 memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, rs);
}
}
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 = DBConnectionUtil.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = DBConnectionUtil.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
private void close(Connection conn, 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.error("error", e);
}
}
if(conn != null) {
try {
conn.close();
} catch (SQLException e) {
log.error("error", e);
}
}
}
}