728x90
JDBC(Java Database Connectivity)를 사용하여 데이터베이스 CRUD(Create, Read, Update, Delete) 작업을 수행하는 예제
🧩 1. SimpleCrudRepository 인터페이스
public interface SimpleCrudRepository {
Member save(Member member) throws SQLException;
Optional<Member> findById(Integer id) throws SQLException;
void update(Member member) throws SQLException;
void remove(Integer id) throws SQLException;
}
✅ 설명
- DB에 접근해 CRUD를 수행하는 기능을 인터페이스로 추상화한 것이다.
- save, findById, update, remove 메서드를 정의해서, 나중에 실제 구현체에서 어떤 DB를 쓰든 이 구조만 맞추면 된다.
- 예: JDBC든, JPA든, MyBatis든 이 인터페이스를 구현하면 교체가 가능!
- 데이터베이스 CRUD 연산을 위한 기본 인터페이스를 정의합니다.
- DAO(Data Access Object) 패턴을 구현하는 기초가 됩니다.
🍦상세설명
- save(): 새 Member 객체를 데이터베이스에 저장하고, 생성된 ID가 포함된 객체를 반환
- findById(): ID로 Member를 검색하고 Optional 타입으로 반환 (데이터가 없을 수 있으므로)
- update(): 기존 Member 정보를 업데이트
- remove(): ID로 Member를 삭제
- 모든 메서드가 SQLException을 던질 수 있음을 명시 (JDBC 작업 중 발생할 수 있는 예외)
🔧2. simpleJdbcCrudRepository 클래스
package io.shi.db.dao;
import io.shi.db.member.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.Optional;
@RequiredArgsConstructor
public class SimpleJdbcCrudRepository implements SimpleCrudRepository {
private final DataSource dataSource;
private Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
private void closeConnection(Connection connection, Statement statement, ResultSet resultSet) {
JdbcUtils.closeConnection(connection);
JdbcUtils.closeStatement(statement);
JdbcUtils.closeResultSet(resultSet);
}
@Override
public Member save(Member member) throws SQLException{
String sql = "INSERT INTO member (username, password) VALUES (?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getUsername());
pstmt.setString(2, member.getPassword());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if( rs.next() ){ //가져왔으면 이제 꺼내야됨
int idx = rs.getInt(1);//왜 int로 가져오냐면 member에 있는 member의 타입이 int이니까
member.setMemberId(idx);
}
return member;
} catch (SQLException e) {
throw e;
} finally {
closeConnection(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Integer id) throws SQLException {
String sql = "SELECT * FROM member WHERE member_id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
connection = getConnection(); //커넥션 꺼내오기
preparedStatement = connection.prepareStatement(sql); //여기서는 두번째 인자 안줘도 됨 -> 어차피 한줄 다 가져올꺼라
preparedStatement.setInt(1, id); //1번째 바인딩 해야하는 곳에다가 id를 넣어서 만들어줘
resultSet = preparedStatement.executeQuery(); //결과를 받아서 넣어오자
if(resultSet.next()){ //결과를 받아 왔을 수 있도 있고 안 받아왔을 수도 있다
//받아왔으면 여기가 실행이 됨
Member findMember = new Member(
resultSet.getInt("member_id"),
resultSet.getString("username"),
resultSet.getString("password")
);
return Optional.of(findMember);
} else{
return Optional.empty(); //비어있는 옵셔널을 반환을 해줘야 함
}
} catch (SQLException e) {
throw e;
} finally {
closeConnection(connection, preparedStatement, resultSet);
}
}
@Override
public void update(Member member) {
}
@Override
public void remove(Integer id) {
}
}
✅ 설명
- 위에서 정의한 인터페이스를 JDBC 기반으로 구현한 클래스이다.
- JDBC를 사용하여, 데이터베이스와의 실제 연결 및 쿼리 실행을 담당합니다.
- DataSource를 통해 DB 커넥션을 가져오고, JDBC API (Connection, PreparedStatement, ResultSet)를 이용해 SQL을 실행한다.
- save는 INSERT 쿼리로 DB에 멤버 추가 후, 생성된 PK 값을 받아서 member 객체에 세팅함.
- findById는 SELECT 쿼리로 멤버를 조회하고, 결과가 있으면 Member 객체로 감싸서 반환함.
💡 주요 포인트
- PreparedStatement: SQL Injection 방지를 위한 바인딩 처리.
- JdbcUtils: Spring에서 제공하는 리소스 자동 닫기 유틸.
- Optional<Member>: null 처리 방식을 Optional로 대체 → 안정적인 코드
주요 속성
- dataSource: DB 연결을 위한 데이터소스 객체
- @RequiredArgsConstructor: Lombok 어노테이션으로, final 필드에 대한 생성자를 자동 생성
주요 메서드 설명
- 유틸리티 메서드:
- getConnection(): DataSource에서 DB 연결을 가져옴
- closeConnection(): 연결, 명령문, 결과 집합을 안전하게 닫음 (JdbcUtils 사용)
2. save() 메서드:
@Override
public Member save(Member member) throws SQLException{
String sql = "INSERT INTO member (username, password) VALUES (?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try{
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getUsername());
pstmt.setString(2, member.getPassword());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if( rs.next() ){ //가져왔으면 이제 꺼내야됨
int idx = rs.getInt(1);//왜 int로 가져오냐면 member에 있는 member의 타입이 int이니까
member.setMemberId(idx);
}
return member;
} catch (SQLException e) {
throw e;
} finally {
closeConnection(conn, pstmt, rs);
}
}
- SQL 쿼리를 준비하고 파라미터를 설정하여 Member 데이터를 삽입
- Statement.RETURN_GENERATED_KEYS를 사용하여 자동 생성된 ID를 가져옴
- 가져온 ID를 Member 객체에 설정하고 반환
- try-catch-finally 구조로 예외처리 및 리소스 정리
3. findById() 메서드 :
@Override
public Optional<Member> findById(Integer id) throws SQLException {
String sql = "SELECT * FROM member WHERE member_id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try{
connection = getConnection(); //커넥션 꺼내오기
preparedStatement = connection.prepareStatement(sql); //여기서는 두번째 인자 안줘도 됨 -> 어차피 한줄 다 가져올꺼라
preparedStatement.setInt(1, id); //1번째 바인딩 해야하는 곳에다가 id를 넣어서 만들어줘
resultSet = preparedStatement.executeQuery(); //결과를 받아서 넣어오자
if(resultSet.next()){ //결과를 받아 왔을 수 있도 있고 안 받아왔을 수도 있다
//받아왔으면 여기가 실행이 됨
Member findMember = new Member(
resultSet.getInt("member_id"),
resultSet.getString("username"),
resultSet.getString("password")
);
return Optional.of(findMember);
} else{
return Optional.empty(); //비어있는 옵셔널을 반환을 해줘야 함
}
} catch (SQLException e) {
throw e;
} finally {
closeConnection(connection, preparedStatement, resultSet);
}
}
-
- ID에 해당하는 Member 정보를 조회
- 결과가 있으면 Member 객체로 매핑하여 Optional로 감싸 반환
- 결과가 없으면 빈 Optional 반환
- 마찬가지로 try-catch-finally로 자원 관리
4. update() 및 remove() 메서드:
- 아직 구현되지 않은 상태 (비어있음)
3. SimpleJdbcCrudRepositoryTests 클래스
package io.shi.db.query;
import com.zaxxer.hikari.HikariDataSource;
import io.shi.db.dao.SimpleCrudRepository;
import io.shi.db.dao.SimpleJdbcCrudRepository;
import io.shi.db.member.Member;
import io.shi.db.util.ConnectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@Slf4j
class SimpleJdbcCrudRepositoryTests {
//구현체보다 인터페이스를 통해 테스트하는게 좋다(상위파일로 하는게 좋음)
SimpleCrudRepository repository;
@BeforeEach
void init(){
//추상화되어있는 데이터 소스를 이용하면 풀에 있는거 뽑아올 수도 있고 폴이 아닌것도 뽑아올 수도 있다.
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(ConnectionUtil.MysqlDbConnectionConstant.URL);
dataSource.setUsername(ConnectionUtil.MysqlDbConnectionConstant.USERNAME);
dataSource.setPassword(ConnectionUtil.MysqlDbConnectionConstant.PASSWORD);
repository = new SimpleJdbcCrudRepository(dataSource); //이때는 구현체를 넣는게 맞음(왜냐면 해당 조건에 대한 테스트를 해야되니까)
}
@Test
@DisplayName("save Test-추가 insert")
void save_test() throws Exception {
//랜덤으로 user이름을 만들겠다.
String randomUsrStr = "USER_" + ((int)(Math.random() * 1_000_000));
log.info("randomUsrStr = {}", randomUsrStr);
Member saveRequest = new Member(0, randomUsrStr, randomUsrStr);//맴버 인스턴스 하나 생성
Member savedMember = repository.save(saveRequest);
log.info("savedMember = {}", savedMember); //중급방법
assertThat(savedMember.getMemberId()).isNotEqualTo(0); //고수방법
}
@Test
@DisplayName("read test - 조회하는 테스트(성공)")
void read_test_ok() throws Exception {
int availableIdx = 1;
Optional<Member> memberOptional = repository.findById(availableIdx);//where문에 1이 들어가서 테스트를 하는 것
boolean result = memberOptional.isPresent();
assertThat(result).isTrue();
Member findMember = memberOptional.get();
assertThat(findMember).isNotNull();
assertThat(findMember.getMemberId()).isEqualTo(availableIdx);
log.info("findMember = {}", findMember);
}
@Test
@DisplayName("read test - 조회하는 테스트(실패)")
void read_test_ng() throws Exception {
int unavailableIdx = 9999;
Optional<Member> memberOptional = repository.findById(unavailableIdx);
boolean result = memberOptional.isPresent();
assertThat(result).isFalse();
memberOptional.get();
}
}
✅ 설명
- SimpleJdbcCrudRepository가 제대로 작동하는지 Junit으로 테스트하는 클래스.
- @BeforeEach: 테스트 실행 전에 DB 연결과 repository 세팅.
- save_test: 임의의 username/password로 DB insert 테스트.
- read_test_ok: 존재하는 ID로 조회 테스트 (예: ID 1번)
- read_test_ng: 존재하지 않는 ID로 조회 시 빈 값 반환 확인.
⚙️주요 속성 및 메서드
1. 테스트 준비 :
SimpleCrudRepository repository;
@BeforeEach
void init() {
HikariDataSource dataSource = new HikariDataSource();
// 데이터소스 설정...
repository = new SimpleJdbcCrudRepository(dataSource);
}
- 테스트 전에 HikariCP 데이터소스를 생성하고 리포지토리 인스턴스를 초기화
- @BeforeEach: 각 테스트 메서드 실행 전에 실행됨
2. save 테스트 :
@Test
@DisplayName("save Test-추가 insert")
void save_test() throws Exception {
//랜덤으로 user이름을 만들겠다.
String randomUsrStr = "USER_" + ((int)(Math.random() * 1_000_000));
log.info("randomUsrStr = {}", randomUsrStr);
Member saveRequest = new Member(0, randomUsrStr, randomUsrStr);//맴버 인스턴스 하나 생성
Member savedMember = repository.save(saveRequest);
log.info("savedMember = {}", savedMember); //중급방법
assertThat(savedMember.getMemberId()).isNotEqualTo(0); //고수방법
}
- 랜덤한 사용자명으로 회원 생성 테스트
- 저장 후 생성된 ID가 0이 아닌지 확인
3. findById 성공 테스트 :
@Test
@DisplayName("read test - 조회하는 테스트(성공)")
void read_test_ok() throws Exception {
int availableIdx = 1;
Optional<Member> memberOptional = repository.findById(availableIdx);//where문에 1이 들어가서 테스트를 하는 것
boolean result = memberOptional.isPresent();
assertThat(result).isTrue();
Member findMember = memberOptional.get();
assertThat(findMember).isNotNull();
assertThat(findMember.getMemberId()).isEqualTo(availableIdx);
log.info("findMember = {}", findMember);
}
- ID 1을 가진 회원이 조회되는지 테스트
- Optional에 값이 존재하는지, 해당 ID가 맞는지 확인
4. findById 실패 테스트 :
@Test
@DisplayName("read test - 조회하는 테스트(실패)")
void read_test_ng() throws Exception {
int unavailableIdx = 9999;
Optional<Member> memberOptional = repository.findById(unavailableIdx);
boolean result = memberOptional.isPresent();
assertThat(result).isFalse();
memberOptional.get();
}
- 존재하지 않는 ID로 조회 시 빈 Optional이 반환되는지 테스트
- 참고: 마지막 줄 memberOptional.get()은 실제로는 NoSuchElementException을 발생시키므로 테스트가 실패할 것
📊 전체 흐름도 요약
[Test 코드]
↓
SimpleCrudRepository 인터페이스 호출
↓
SimpleJdbcCrudRepository (JDBC로 직접 SQL 실행)
↓
DataSource → Connection 얻기
↓
SQL 실행 (INSERT or SELECT 등)
↓
결과 반환 or 예외 처리
💫전체 작동 방식
- 아키텍처 구조:
- 인터페이스(SimpleCrudRepository)로 CRUD 메서드 정의
- 구현 클래스(SimpleJdbcCrudRepository)에서 JDBC를 이용한 실제 구현
- 테스트 클래스에서 기능 검증
- 데이터 흐름:
- 테스트 클래스에서 요청 객체 생성 → 리포지토리 메서드 호출
- 리포지토리에서 DB 연결 → SQL 실행 → 결과 처리
- 결과를 엔티티 객체(Member)로 변환하여 반환
- 사용된 기술:
- JDBC: 자바에서 DB 연결 및 쿼리 실행
- HikariCP: 효율적인 DB 커넥션 풀 관리
- JUnit5: 테스트 프레임워크
- AssertJ: 테스트 어설션 라이브러리
- Lombok: 반복 코드 감소
⭐️JPA와의 연관성
이 코드는 JPA를 사용하기 전 JDBC의 기본을 이해하기 위한 것으로 보입니다. JPA는 이런 JDBC 코드를 추상화하여:
- SQL 쿼리를 직접 작성하지 않아도 됨
- 객체와 테이블 매핑을 어노테이션으로 정의
- 영속성 컨텍스트를 통한 객체 관리
- 지연 로딩, 캐싱 등 다양한 기능 제공
🔚 마무리
이번 실습을 통해,
- JDBC를 직접 다루는 방법
- SQL을 자바 코드로 실행하고 결과를 매핑하는 방법
- 인터페이스와 구현 클래스를 분리해 테스트하기 쉬운 구조로 만드는 법 을 배울 수 있었다!
이제 JPA를 학습할 때, 내부에서 JDBC가 어떻게 동작하는지 머릿속에 그림이 그려질 것 같다.
아직은 어렵지만 하나씩 쌓아가자 😄
728x90
'프로그래밍 > Spring' 카테고리의 다른 글
MyBatis란? (0) | 2025.04.09 |
---|---|
🌟 트랜잭션(Transaction) 이란? (0) | 2025.04.09 |
@GeneratedValue란? (0) | 2025.04.07 |
🌙JPA 매핑 어노테이션 가이드 (0) | 2025.04.07 |
커밋, 롤백, 트랜잭션, flush()과 커밋의 차이점 (0) | 2025.04.07 |