Rev Notebook

[Spring Boot] 4. 순수 JDBC 구현

by Rev_

JDBC란?

JDBC(Java Database Connectivity)자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다.

자바 애플리케이션이 데이터베이스 연결이 필요할 때 DriverManager.getConnection() 메소드들 가운데 하나를 사용하여 JDBC 연결을 만들게 된다. 쉽게 말해 데이터베이스와 연결해주는 역할을 한다.

 

과거에는 애플리케이션 서버가 직접 DB로 접근을 했다고 한다.

그렇기 때문에 DB마다 연결, SQL 전달, 응답 방식등이 모두 달랐다. 따라서 사용하던 DB를 다른 종류의 DB로 바꾸면 서버에 개발된 코드도 변경되어야 하는 불편함이 있었다.

위와 같은 문제로 개발된 것이 JDBC이다. 각 데이터베이스 벤더에서 JDBC 인터페이스에 맞게 구현하여 라이브러리로 제공한다. 이를 JDBC 드라이버라고 한다.

 

현재에는 JdbcTemplate, JPA등이 사용되고 있지만, 약 20년 전에는 순수 JDBC 코드로 개발을 했다고 한다.

이를 실습해보도록 하자.

 

1. build.gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2:'

build.gradle의 dependencies 항목에 위 두 줄의 추가가 필요하다.

build.gradle 파일은 프로젝트를 빌드 시키기 위한 폴더이다.

그리고 dependencies저장소에서 필요한 라이브러리를 사용하게 할 수 있다.

우리는 DB를 연결하기 위해 필요한 jdbc 드라이버 라이브러리를 불러오고,

DB와 연결할 때 데이터베이스가 제공하는 클라이언트가 필요한데, h2 데이터베이스의 라이브러리를 사용하기 위해 불러왔다.

 

implementation: 라이브러리 적용

runtimeOnly: 컴파일 시점에는 사용되지 않고, 실행 시점에 사용

 

2. application.properties 추가

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

application.properties에는 위 세 줄의 추가가 필요하다.

application.properties는 스프링 부트가 애플리케이션을 구동할 때 자동으로 로딩하는 파일이다.

스프링부트 2.4부터는 spring.datasource.username=sa 항목이 꼭 필요하다고 한다.

 

위 두 파일의 설정을 한다면 데이터베이스에 접근할 준비는 완료된 것이다.

 

3. JdbcMemberRepository 클래스 추가

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {
    // 먼저, DB를 사용하려면 데이터 소스라는 것이 필요하다.
    // 스프링이 dataSource라는 것을 만들어놓는다. 이것은 주입받을 수 있도록 해준다.
    private final DataSource dataSource;

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


    // save 하려면 쿼리 짜야함
    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)"; // sql문
        Connection conn = null; // 데이터베이스 커넥션
        PreparedStatement pstmt = null; // SQL DB에 전달
        ResultSet rs = null; // 결과를 받는 것

        try {
            conn = getConnection();
            // prepareStatement를 통해 sql을 넣음
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);

            // parameterIndex는 위 sql문 (?)과 매칭된다.
            pstmt.setString(1, member.getName());

            pstmt.executeUpdate(); // 실제 쿼리 전송
            rs = pstmt.getGeneratedKeys(); // DB에서 데이터를 꺼내줌

            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);

            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 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);
        }
    }

    @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);
        }
    }
    private Connection getConnection() {
        // getConnection을 쓸 때는 DataSourceUtils를 통해 가지고 와야함
        // getConnection: 실제 자바 프로그램과 데이터베이스를 네트워크 상에서 연결해주는 메소드
        // 연결에 성공하면 DB와 연결된 상태를 Connection 객체로 표현하여 반환
        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();
        }
    }
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

JDBC를 사용하는 멤버 레포지토리 클래스이다. 

쿼리를 직접 작성하여 날려주며, 코드가 길고 복잡하다.

 

4. SpringConfig 작성

package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;

@Configuration
public class SpringConfig {

    private DataSource dataSource;

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

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

    @Bean
    public MemberRepository memberRepository() {
        return new JdbcMemberRepository(dataSource);
    }
}

기존에 있던 MemoryMemberRepository 대신 JdbcMemberRepository를 사용해주면 된다.

@Autowired는 객체의 생성과 주입을 Spring에게 맡기는 것

 

그리고 H2 데이터베이스에 접속한 뒤 스프링 부트를 실행해주면 데이터가 DB에 저장된 것을 확인할 수 있다.

 

[References]

김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8#

 

블로그의 정보

Hi Rev

Rev_

활동하기