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 필드에 대한 생성자를 자동 생성

 

주요 메서드 설명

  1. 유틸리티 메서드:
    • 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 예외 처리

 

 

 

💫전체 작동 방식

  1. 아키텍처 구조:
    • 인터페이스(SimpleCrudRepository)로 CRUD 메서드 정의
    • 구현 클래스(SimpleJdbcCrudRepository)에서 JDBC를 이용한 실제 구현
    • 테스트 클래스에서 기능 검증
  2. 데이터 흐름:
    • 테스트 클래스에서 요청 객체 생성 → 리포지토리 메서드 호출
    • 리포지토리에서 DB 연결 → SQL 실행 → 결과 처리
    • 결과를 엔티티 객체(Member)로 변환하여 반환
  3. 사용된 기술:
    • JDBC: 자바에서 DB 연결 및 쿼리 실행
    • HikariCP: 효율적인 DB 커넥션 풀 관리
    • JUnit5: 테스트 프레임워크
    • AssertJ: 테스트 어설션 라이브러리
    • Lombok: 반복 코드 감소

 

 


⭐️JPA와의 연관성

이 코드는 JPA를 사용하기 전 JDBC의 기본을 이해하기 위한 것으로 보입니다. JPA는 이런 JDBC 코드를 추상화하여:

  1. SQL 쿼리를 직접 작성하지 않아도 됨
  2. 객체와 테이블 매핑을 어노테이션으로 정의
  3. 영속성 컨텍스트를 통한 객체 관리
  4. 지연 로딩, 캐싱 등 다양한 기능 제공

 

 


🔚 마무리

이번 실습을 통해,

  • 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