16. 순수 JDBC

Bummy·2023년 4월 29일
0
post-thumbnail

지난번 설치한 H2 데이터베이스를 순수 JDBC를 활용해서 프로젝트에 연결해

볼 것이다.

환경 설정

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	runtimeOnly 'com.h2database:h2'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  • build.gradle 파일에 JDBC, H2 데이터베이스 관련 라이브러리를 추가해준다.

application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
  • 스프링 부트 데이터베이스 연결 설정 추가를 위해 properties 파일에 데이터베이스 url, driver-class-name을 작성해준다.
💡 스프링부트 2.4부터는 spring.datasource.username = sa 를 꼭 추가해주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다.

JDBC 리포지토리 구현

JDBC를 위한 리포지토리를 작성해준다.

JdbcMemberRepository.java


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 {
    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);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            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() {
        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);
    }
}
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
  • 먼저 DataSource라는 것을 주입받는 것을 확인할 수 있는데 DataSource는 세팅에서 application.properties에서 추가해준 내용으로 스프링이 미리 생성해둔 것이다.
  • DataSource를 통해 DB와의 커넥션을 얻을 수 있으며 이 커넥션에 직접 SQL문을 보내서 데이터를 저장하고 얻어올 수 있게 된다.

Save의 동작원리

  1. DataSource로 부터 getConnection을 통해 connection을 얻어온다.
  2. connection에서 prepareStatement안에 SQL문을 넣는다.
  3. setString으로 값을 세팅해준다. 이 값은 SQL문의 ? 와 매칭된다.
  4. excuteUpdate를 통해 쿼리가 DB로 전송된다.
  5. 값이 있다면 pk값을 가져와서 member id로 세팅한다.
  6. 동작이 끝나고 나면 연결을 종료해주어야한다.

→ 중복되는 부분도 많고 예외처리도 많이 신경 써줘야해서 더 개선된 것이 JDBC Template이다.

💡 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 개발자들이 사용했던 방식이라 확인해보면 좋지만 지금은 이렇게 사용하는 경우가 거의 없기에 코드만 확인하고 넘어가도록 한다.

SpringConfig.java

package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.sevice.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 MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}
  • DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다.
  • 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다.
💡 필자는 기존에 있던 메모리에 저장하는 리포지토리를 주석처리하고 새롭게 작성한 JDBC 리포지토리 연결하기 위해 SpringConfig.java에 한줄만 추가해주었다. → 이것이 객체 지향 프로그래밍의 장점!

구현 클래스 이미지

스프링 설정 이미지

  • 개방-폐쇄 원칙(OCP, Open-Closed Principle)
    • 확장에는 열려 있고, 수정, 변경에는 닫혀 있다.
  • 스프링의 DI(Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
  • 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 남아 있는 것을 확인할 수 있었다.

0개의 댓글