JDBC 이해
JDBC 등장이유
과거에 예로들면 MySQL DB, Oracle DB가 있다고 가정하자
애플리케이션 서버와 DB의 일반적인 사용법은
1. 커넥션 연결 : 주로 TCP/IP로 사용해서 커넥션 연결
2. SQL 전달 : 커넥션을 통해 SQL을 DB에 전달
3. 결과 응답 : DB는 SQL을 수행하고 결과 응답. 애플리케이션 서버는 응답결과를 활용
그런데, 각각의 DB마다 사용법이 다르기 때문에, DB 변경시 코드를 변경해야한다.
그리고 새로운 db 사용법을 학습해야 하는 시간이 필요하다.
그래서 나온게 JDBC 표준 인터페이스이다.
JDBC 표준 인터페이스

자바에서 DB에 접속할 수 있도록 하는 자바 API이다.
대표적으로 3가지 기능을 제공한다.
java.sql.Connection - 연결
java.sql.Statement - SQL을 담은 내용
java.sql.ResultSet - SQL 요청 응답
이제 JDBC를 이용해 표준 인터페이스만 사용해서 개발하면 된다. 그런데 인터페이스만 있다고해서 기능이 동작되지는 않고, 각각의 DB 벤더에서 자신의 DB에 맞도록 구현해서 라이브러리를 제공하는데, 이것을 JDBC 드라이버라고 한다.

JDBC를 사용함으로 코드 변경, 학습 없이 해결할 수 있다.
JDBC와 최신 데이터 접근 기술
JDBC를 편리하게 사용하는 다양한 기술이 존재하는데, 대표적으로 SQL Mapper, ORM 으로 나눌 수 있다.

- SQL 응답 결과를 객체로 편리하게 변환
- JDBC 반복 코드 제거
단점으로 개발자가 SQL을 직접 작성해야함

- ORM은 RDBMS 테이블과 매핑해주는 기술이다. 이 기술을 사용하면 반복적인 SQL을 직접 작성하지 않아도 된다.
단점으로는 어렵기때문에 학습시간이 많이 필요하다.
공통적으로 위 기술들도 다 JDBC를 사용한다. 따라서 JDBC의 작동 원리를 알아야 기술을 깊이있게 이해할 수 있다.
DB 연결
H2 DB를 연결해보자
package hello.jdbc.connection;
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 = "";
}
package hello.jdbc.connection;
import lombok.extern.slf4j.Slf4j;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import static hello.jdbc.connection.ConnectionConst.*;
@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 IllegalStateException(e);
}
}
}
DB에 연결하기 위해 DriverManager.getConnection()을 사용하면 된다.

JDBC는 java.sql.Connection 표준 커넥션 인터페이스를 정의한다.
H2 db 드라이버는 JDBC Connection 인터페이스를 구현한 org.h2.jdbc.JdbcConnection 구현체를 제공한다.
JDBC가 제공하는 DriverManager는 라이브러리에 등록된 DB드라이버들을 관리하고, 커넥션을 획득하는 기능을 제공한다.
1. 커넥션이 필요하면 DriverManager.getConnection() 호출
2. DriverManager는 라이브러리에 등록된 드라이버 목록을 자동으로 인식해서 순서대로 정보를 넘겨서 커넥션을 획득할 수 있는지 확인한다.
(URL: jdbc:h2:tcp://localhost/~/test, 이름, 패스워드 등 접속에 필요한 정보. 만약 다른 드라이버가 먼저 실행된다면 처리할수 없다는 결과 반환 후 다음 드라이버에게 순서 이동)
3. 찾은 클라이언트 구현체가 클라이언트에 반환
JDBC 개발 - 등록
// JDBC DriverManager 사용
@Slf4j
public class MemberRepositoryV0 {
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 {
close(con, pstmt, null);
}
}
private void close(Connection connection, 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 (connection != null) {
try {
connection.close();
} catch (SQLException e) {
log.info("error", e);
}
}
}
private Connection getConnection() {
return DBConnectionUtil.getConnection();
}
}
1. DBConnectionUtil - H2 연결 획득
2. prepareStatement - SQL 미리 준비
3. setString, setInt - 값 바인딩
4. executeUpdate() - DB 반영
5. return member - 현재 코드에선 void여도 무방하지만 API일관성을 위해 저장된 엔티티 리턴 (나중에 씀)
6. finally close - 커넥션, statement, resultset 정리
*리소스 정리는 중요하다
굳이 배운 이유: DB 접근 원리 이해를 위해서. Connection -> PreparedStatement -> ResultSet -> Close 흐름을 이해하기 위함
JDBC 개발 - 조회
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet resultSet = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
ResultSet 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(con, pstmt, resultSet);
}
}
조회할땐 pstmt.executeQuery()를 사용한다. 결과를 ResultSet에 담아서 반환한다.
ResultSet

ResultSet은 위와 같이 생긴 데이터 구조이다.
- select member_id, money라고 지정하면 member_id, money라는 데이터가 저장된다.
(select *는 테이블의 모든 컬럼을 다 지정)
- ResultSet 내부에 있는 cursor를 이동해서 다음 데이터를 조회할 수 있다.
- rs.next() : 이것을 호출하면 커서가 다음으로 이동한다. 참고로 최초의 커서는 데이터를 가리키고 있지 않기 때문에 next()를 호출해야 데이터를 조회할 수 있다.
- rs.getString() : 현재 커서가 가리키고 있는 위치으 member_id를 String 타입으로 반환한다.
- rs.getInt() : 마찬가지로 money를 int 타입으로 반환한다.
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
class MemberRepositoryV0Test {
MemberRepositoryV0 repository = new MemberRepositoryV0();
@Test
void crud() throws SQLException {
Member member = new Member("memberV2", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
log.info("member != findMember {}", member == findMember);
log.info("member equals findMember {}", member.equals(findMember));
assertThat(findMember).isEqualTo(member);
}
}

실행 결과, member != findMember이 false고, equals는 true이다.
이유는 DB에 있는 memverV2, 10000를 조회해서 member 객체를 만들어서 리턴한다. 따라서 두 레퍼런스가 다르다.
(findById를 할때마다 값을 꺼내서 새로운 인스턴스를 만드는 것)
JDBC 개발 - 수정, 삭제
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();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
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();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
등록, 조회와 크게 달라진 점은 없다. 같은 흐름으로 작동한다. sql문만 달라진 셈이다.
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
삭제한 데이터를 어떻게 테스트하냐?
NoSuchElementException이 발생하면 테스트가 성공한것으로 간주한다.
당연히 memberId가 삭제되었기 때문에 findById를 할 수가 없기때문이다.
'Spring' 카테고리의 다른 글
| 트랜잭션 (0) | 2025.10.02 |
|---|---|
| 커넥션 풀과 데이터소스 (0) | 2025.09.29 |
| 파일 업로드 (0) | 2025.09.23 |
| 스프링 타입 컨버터 (0) | 2025.09.21 |
| API 예외처리 (0) | 2025.09.20 |
