[스프링 입문] Section07. 스프링 DB 접근 기술[2] - 순수 JDBC

Euiyeon Park·2025년 6월 17일
0

갓영한 스프링🍀

목록 보기
6/12
post-thumbnail

JDBC란?

  • 자바 진영에서 데이터베이스에 접속할 수 있도록하는 API
  • 자바 애플리케이션에서 JDBC API를 사용해 DB에 접근
    (자바 언어로 DB 프로그래밍을 하기 위한 라이브러리)

환경 설정

  • build.gradle파일에 JDBC, h2 데이터베이스 관련 라이브러리 추가
    • 자바는 DB랑 붙으려면 JDBC 드라이버가 반드시 필요
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
  • DB접속 정보는 application.properties에 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
  • 회원을 저장하는 역할은 MeberRepository가 하지만
    구현은 메모리랑 할 지(MemoryMemberRepository),
  • DB랑 연동해서 JDBC가 할 지(JdbcMemberRepository)에 대한 차이

JDBC 레포지토리 구현

  • DateSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체
  • 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고,
    스프링 빈을 만들어 둠 → DI를 받을 수 있음

📂 JdbcMemberRepository

public class JdbcMemberRepository implements MemberRepository{
    // 📌 DB에 붙으려면 DataSource가 필요 -> 스프링에게 주입 받아야 함
    // 📌 application.properties의 내용을 스프링부트가 DataSource 생성 및 주입
    private final DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource){
        this.dataSource = dataSource;
    }

    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

            // parameterIndex 1은 SQL의 ?와 매칭
            pstmt.setString(1, member.getName());

            pstmt.executeUpdate();			// DB에 쿼리(insert)가 날라감
            rs = pstmt.getGeneratedKeys();	// 방금 생성된 KEY(1, 2, ..)를 반환

            if(rs.next()){
                member.setId(rs.getLong(1));
            } else{
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public Optional<Member> findById(Long id) {
        String sql = "select * from member where id = ?";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id);

            // 조회는 executeUpdate()❌, executeQuery⭕
            rs = pstmt.executeQuery();

            if(rs.next()){
                Member member= new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public Optional<Member> findByName(String name) {
        String sql = "select * from member where name = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, name);

            rs = pstmt.executeQuery();

            if(rs.next()){
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            }
            return Optional.empty();
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }

    @Override
    public List<Member> findAll() {
        String sql = "select * from member";

        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try{
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();

            List<Member> members = new ArrayList<>();
            while(rs.next()){
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                members.add(member);
            }
            return members;
        } catch (Exception e){
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
	
    // 📌 커넥션을 얻
    private Connection getConnection(){
        return DataSourceUtils.getConnection(dataSource);
    }
	
    // 📌 자원을 역순으로 해제**
    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs){
        try{
            if(rs!= null){
                rs.close();
            }
        } catch (SQLException e){
            e.printStackTrace();
        }
        try{
            if(pstmt != null){
                pstmt.close();
            }
        } catch (SQLException e){
            e.printStackTrace();
        }

        try{
            if(conn != null){
                close(conn);
            }
        } catch (SQLException e){
            e.printStackTrace();
        }
    }

    // 📌 dataSource.getConnection()❌, DataSourceUtils 권장 -> 트랜잭션 유지
    // 📌 데이터베이스와 연결된 커넥션을 얻음
    // 📌 진짜 데이터베이스와 연결되는 열린 소켓을 얻을 수 있음?**
    private void close(Connection conn) throws SQLException{
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

📂 SpringConfig

// 스프링이 application.properties를 보고 DataSource를 빈으로 자동 생성
@Configuration
public class SpringConfig {

    private DataSource dataSource;

	// DI, DataSource주입**

    @Autowired         
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
		// return new MemoryMemberRepository();		// 구현체 변경
        return new JdbcMemberRepository(dataSource);
    }
}

JDBC의 정형화된 패턴

  • JdbcMemberRepository는 JDBC를 직접 사용하는 레포지토리 구현의 전형적인 패턴

1. SQL 준비

String sql = "select * from member where id = ?";
  • 쿼리는 항상 String으로 선언
  • 쿼리 안에 ?를 넣고, 나중에 바인딩

2. Connection 획득

conn = getConnection();
  • DataSource로부터 커넥션을 가져옴
    • DataSource는 DB와 연결을 관리하는 객체
    • 내부적으로 Connection을 생성하고 풀링하는 역할
    • DS에서 커넥션을 가져온다는 건
      DB에 연결된 소켓 하나를 열고, 해당 연결을 Connection 객체로 받는 것
    • 이 커넥션을 이후 SQL 실행, 트랜잭션 처리 등에 사용됨
  • 스프링에서는 반드시 DataSourceUtils.getConnection() 사용해야 함
    • 트랜잭션 매니저와 연동되지 않으면 트랜잭션 동기화가 깨질 수 있음 (몬말일까?)

3. PreparedStatement 생성 및 파라미터 바인딩

pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
  • PreparedStatement를 통해 SQL 실행 준비
  • setXxx()로 파라미터 바인딩 (1은 ?의 인덱스)

4. 쿼리 실행

rs = pstmt.executeQuery(); 		// 조회
rs =  pstmt.executeUpdate(); 	// 삽입, 수정, 삭제

5. ResultSet을 통해 결과 매핑

if(rs.next()){
    Member member = new Member();
    member.setId(rs.getLong("id"));
    member.setName(rs.getString("name"));
  • ResultSet을 객체로 변환하는 단계 (Manual ORM)
  • 하나씩 수동으로 getter 호출

6. 예외 처리 및 감싸기

} catch (Exception e){
    throw new IllegalStateException(e);
}
  • 체크 예외인 SQLException을 런타임 예외로 감싸서 던짐
  • 스프링에서는 DataAccessException을 사용하는 경우도 많음

7. 리소스 해제

close(conn, pstmt, rs);
  • 반드시 역순으로 닫기: ResultSet → Statement → Connection

🍀 정리

  1. JdbcMemberRepository - Repository 계층
    • 데이터베이스에 직접 접근해 SQL쿼리를 실행하고 결과처리
    • 데이터베이스 연결을 위한 DataSource객체가 필요한데,
      생성자 주입 방식으로 SpringConfig에서 설정된 DataSource가 전달됨
  2. SpringConfig - 설정 및 의존성 주입 관리
    • 스프링 설정 클래스, 애플리케이션의 빈(Bean)을 관리
    • DI를 통해 객체 생성 및 관리 역할을 수행
    • MemberServiceMemberRepositoryDataSource 순서로 의존관계 주입
  3. application.properties - 설정파일
    • 데이터베이스 설정, 애플리케이션 이름, 정적 리소스 경로 등을 정의
    • 스프링부트는 이 파일을 기반으로 자동 설정(Auto Configuration)을 수행

🍀 스프링 전체 동작 과정 정리

  1. 애플리케이션 실행
  • application.properties를 기반으로 DataSource 객체가 빈으로 자동 생성
  1. SpringConfig 로드
  • JdbcMemberRepositoryMemberService가 빈으로 등록
  • 생성자 주입을 통해 의존 관계가 설정
  1. Repository 사용
  • MemberService에서 memberRepository를 호출하면,
    실제 데이터베이스와 연결된 JDBC 코드가 실행
  • SQL 쿼리를 통해 데이터 삽입, 조회가 이루어짐.
  1. 트랜잭션 및 커넥션 관리
  • DataSourceUtils를 통해 커넥션 풀을 관리하며,
    자원 누수 없이 트랜잭션을 유지

🍀 스프링과 다형성

MemoryMemberRepository에서 JdbcMemberRepository로 갈아끼우기

  • 인터페이스를 두고 구현체를 바꿔끼울수 있음 - 스프링 사용 이유(스프링 컨테이너가 지원)
  • DI를 통해 편리하게 기존 코드 변경없이 가능
  • 오직 애플리케이션을 설정하는 코드만 변경하면
    애플리케이션과 관련된 코드는 손댈게 하나도 없음 - OCP(개방 폐쇄 원칙)
profile
"개발자는 해결사이자 발견자이다✨" - Michael C. Feathers

0개의 댓글