SQL Mapper 또는 ORM을 사용해도 결국 모든 것은 JDBC 위에서 동작한다.
JDBC의 필요성

애플리케이션 서버와 DB의 일반적인 사용법은 다음과 같다.
- TCP/IP를 사용하여 커넥션 연결
- 커넥션을 통해 DB에 맞는 SQL 전달
- DB는 전달받은 SQL을 수행하고 결과를 응답
문제는 각각의 데이터베이스마다 커넥션을 연결하는 방법, SQL 전달 방법, 결과를 응답 받는 방법이 모두 다르다
- 데이터베이스를 변경하면 애플리케이션 서버에 개발된 데이터베이스 관련 코드도 전부 변경해야한다
- 개발자가 각각의 데이터베이스마다 커넥션 연결, SQL 전달, 결과 응답 받는 방법을 새로 학습해야한다
이 문제를 해결하기 위해 JDBC라는 자바 표준이 등장한다.
JDBC 표준 인터페이스
JDBC(Java Database Connectivity)는 자바에서 데이터베이스에 접속할 수 있도록 도와주는 자바 API이다.

- java.sql.Connection - 연결
- java.sql.Statement - SQL 문을 데이터베이스에 전달하고 실행하는 역할
- java.sql.ResultSet - SQL 요청 응답
개발자는 표준 인터페이스만 사용해서 개발하면 된다.
실제 구현체는 각각의 벤더에서 벤더의 DB에 맞도록 구현해서 라이브러리로 제공한다.
이를 JDBC 드라이버라고 한다.
표준화의 한계
각각의 데이터베이스마다 SQL, 데이터 타입 등 일부 사용법이 다르다.
ANSI SQL이라는 표준이 있기는 하지만 일반적인 부분만 공통화 했기 때문에 한계가 있다.
대표적으로 페이징 SQL은 DB마다 각각 다르다.
결국 데이터베이스 변경시 JDBC 코드는 유지되지만 SQL은 해당 데이터베이스에 맞게 변경해야한다.
데이터베이스 연결
public abstract class ConnectionConst {
public static final String URL = "jdbc:mysql://your_ip:3306/course";
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
}
@Slf4j
public class DbConnectionUtil {
public static Connection getConnection() {
try {
// 라이브러리에 있는 DB 드라이버를 찾아 해당 드라이버가 제공하는 커넥션 반환
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get conection={}, class={}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new IllegalStateException(e);
}
}
}
get conection=com.mysql.cj.jdbc.ConnectionImpl@2d72f75e, class=class com.mysql.cj.jdbc.ConnectionImpl
MySQL 드라이버가 제공하는 mysql 전용 커넥션을 확인할 수 있다.
ConnectionImpl을 직접 보면 JDBC 표준 커넥션 인터페이스인 java.sql.Connection 인터페이스를 구현하고 있다.
Insert
public MemberDto save(MemberDto memberDto) throws SQLException {
String sql = "insert into db1_member(member_id, money) value(?,?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
// ?을 통한 파라미터 바인딩
// 사용자 입력을 문자열 처리하여 SQL injection 방지
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberDto.getId());
pstmt.setInt(2, memberDto.getMoney());
// Statement를 통해 준비된 SQL을 커넥션을 통해 DB에 전달
// 영향받은 DB row수 반환
// 데이터 변경시 사용
pstmt.executeUpdate();
return memberDto;
} catch (Exception e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
리소스 정리
private void close(Connection con, 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.info("error", e);
}
}
if (con != null) {
try {
con.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
- 리소스 정리는 역순으로 진행한다.
- Connection을 통해 Statement를 만들었기 때문에 Statement를 먼저 종료하고 난 후 Connection을 종료한다.
- 예외 발생과 상관없이 항상 리소스 정리는 수행되어야 하므로 finally에서 한다.
- 리소스를 정리하지 않으면 커넥션이 끊어지지 않고 계속 유지되는 문제가 발생한다.
- 이를 리소스 누수라고 하며 결과적으로 커넥션 부족 현상이 발생한다.
Select
public MemberDto findById(String id) throws SQLException {
String sql = "select * from db1_member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, id);
// 데이터 조회에 사용
rs = pstmt.executeQuery();
if (rs.next()) {
MemberDto memberDto = new MemberDto();
memberDto.setId(rs.getString("member_id"));
memberDto.setMoney(rs.getInt("money"));
return memberDto;
} else {
throw new NoSuchElementException();
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
ResultSet
- select 쿼리의 결과가 순서대로 들어간다.
- 내부에 있는 cursor를 이동해서 다음 데이터를 조회한다.
- 최초의 cursor는 데이터를 가리키고 있지 않기 때문에 rs.next()를 한번은 호출해야 데이터 조회가 가능하다.
- rs.next()가 true이면 데이터가 존재하고 false이면 더이상 cursor가 가리키는 데이터가 없다는 뜻이다.
- 위의 예제에서는 한 명을 찾으므로 if로 했지만 여러 회원을 조회할 경우 while을 사용한다.
'springboot > data access' 카테고리의 다른 글
| Spring의 트랜잭션 추상화로 JDBC 기반 트랜잭션 처리의 문제점 해결하기 (0) | 2025.04.27 |
|---|---|
| 커넥션 풀과 DataSource (1) | 2025.03.22 |