SPRING 입문 [ 코드로 배우는 스프링 부트 ]

[스프링 입문] 섹션 6.2 스프링 DB 접근 기술 (순수 JDBC)

wlalsu_u 2023. 1. 17. 16:15

6.2.1  순수 JDBC - 드라이버 세팅

 

 

앞서 섹션 5에서 DB를 메모리에 저장하면, 서버를 내렸을 때 정보가 모두 손실되는 문제가 있었다.

 

따라서, 어플리케이션에서 DB 를 연동하여,

회원 정보가 데이터베이스에 저장되도록 하고자 한다.

 

 

그중에서 첫 번째로, 고전적인 방식인 순수 JDBC 기술을 살펴보자.

 

 

 

먼저, build.gradle 파일Jdbc 와 H2 데이터 베이스 관련 라이브러리를 추가해야 한다.

 

다음 코드를 dependencies 안에 추가로 작성해준다.

 

 

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

 

 

- Jdbc : Java는 db와 연동하려면 기본적으로 jdbc 드라이버를 꼭 필요로 한다.

 

- H2 : db와 연동할 때, 데이터베이스가 제공하는 클라이언트가 필요하다.

 

 

 

 

다음으로, db와 연동하기 위해서는 접속 정보를 넣어주어야 한다.

 

스프링 부트가 db와 연동하는 일을 모두 해주므로, 우리는 경로만 넣어주면 된다.

 

 

 

 

 

src  >  main  >  resources  >  application.properties 에 아래의 코드를 작성한다.

 

 

h2 데이터베이스에 접근할 것이므로, h2 드라이버를 넣어준다.

 

 

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

 

 

 

 


 

 

6.2.2  순수 JDBC - Jdbc Repository 연결

 

 

Jdbc API 를 사용하여 개발을 진행한다.

 

 

기존에 우리는 MemoryMemberRepository 에서 인터페이스를 구현하여 회원을 저장하였다.

이는 인터페이스 구현을 메모리에 하는 방식이었다. 

 

 

하지만 구현을 Jdbc 로 하기 위해,

Jdbc 리포지토리로 바꿔 연결하는 작업을 수행하고자 한다.

 

 

 

먼저, repository 폴더 아래에 JdbcMemberRepository 파일을 아래와 같이 작성한다.

 

 

 

private final DataSource dataSource;

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

 

 

 

- db 와 연동하기 위해서는 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);
        }
    }

 

 

 

String sql = "insert into member(name) values(?)";

 

: 나중에 파라미터를 받아야 하므로 ? 로 작성한다.

 

 

ResultSet rs = null;

 

: resultset 는 결과를 받는다.

 

 

conn = getConnection();

 

: getConnection 을 통해, 먼저 커넥션을 가져온다.

 

 

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

 

: 앞서 작성한 sql 을 넣는다.

: RETURN_GENERATED_KEYS 를 통해 DB 에 회원 정보를 넣을 때, id 값을 얻을 수 있다.

 

 

pstmt.setString(1, member.getName());

 

: 파라미터 값으로 인덱스 1번을 넣으면, 앞선 sql 의 ?와 매칭된다.

: member 객체로 넘어온 이름 값을 넣어준다.

 

 

pstmt.executeUpdate();

 

: 업데이트를 해주어, DB에 값이 넘어가도록 한다.

 

 

rs = pstmt.getGeneratedKeys();

 

: 앞선 RETURN_GENERATED_KEYS 와 매칭된다.

 

 

if (rs.next()) { } else { }

 

: ResultSet에 값이 있는 경우,  getLong을 통해 값을 꺼내고, 세팅한다.

: 값이 없는 경우, 조회 실패 메시지가 뜨도록 한다.

 

 

 

 

private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }

 

 

 

- db 컬렉션을 사용하면, 외부 네트워크와 연결된 것이므로, 사용을 완료한 후 리소스를 모두 반환해야 한다.

 

 

 

 

localhost8080 를 실행시켜 확인해보자.

 

다음과 같이 이름이 Java인 회원을 등록한다.

 

 

 

 

회원 목록을 확인해보면, 제대로 db에 저장되는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

전체 코드는 다음과 같다.

 

 

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

 

 

 

+ 추가 )
처음에 DataSourceUtils 에서 오류가 발생했는데,
spring 프로젝트 파일을 C 드라이브로 옮기니 제대로 실행되었다.

 

 

 

 


 

 

6.2.3  순수 JDBC - Configuration

 

 

 

지금까지는 MemoryMemberRepository 를 통해 메모리에 회원 정보를 저장하였다.

 

따라서, SpringConfig 에서 MemoryMemberRepository 를 스프링 빈에 등록하도록 하였다.

 

 

 

 

하지만 Jdbc 를 리포지토리로 사용하기 위해 SpringConfig 파일을 아래와 같이 변경한다.

 

 

 

private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
    this.dataSource = dataSource;
}

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

 

 

 

- dataSource 객체를 통해 데이터베이스 커넥션을 가능하도록 만들고, return 값에 주입한다.

 

- 스프링 부트가 데이터베이스 커넥션을 바탕으로,

  dataSource 를 제공하고, 자체적으로 빈도 생성해준다.

 

 

 

이렇게 코드를 완성하면, 회원 정보가 db 에 저장되므로,

서버를 껐다가 다시 켜도 회원 정보가 보존되는 것을 볼 수 있다.

 

 

 

 

+ ) H2 데이터베이스를 실행한 상태에서 서버를 실행해야한다.

 

 

 

단순히 JdbcMemberRepository 를 만들고 인터페이스를 확장함으로서,
회원 정보 저장을 메모리에서 Jdbc 로 바꿀 수 있다.
(다른 코드 수정이 필요 없다)

 

 

 

 


 

 

6.2.3  객체 지향 설계의 장점 (다형성)

 

 

앞서, MemberRepository 인터페이스를 구현하는

MemoryMemberRepository 와 JdbcMemberRepository 를 작성하였다.

 

 

기존에는 memberService 가 MemoryMemberRepository(메모리) 와 연결되어 있었지만,

단순히 SpringConfig 를 수정하여, JdbcMemberRepoisotry(Jdbc) 와 연결되도록 하였다.

 

 

 

이를 개방 폐쇄 원칙(OCP, Open-Closed Principle)이라고 한다.

 

 

<개방 폐쇠 원칙(OCP, Open-Closed Principle)>
확장에는 열려 있지만 , 수정/변경에는 닫혀 있다

 

 

우리는 단순히 JdbcMemberRepository 를 만들고, 인터페이스를 확장함으로서,

다른 코드 변경 없이 구현 클래스를 변경할 수 있다.

 

 

 

스프링의 DI (Dependencies Injection)를 통해 기존 코드 변경 없이,
간단한 설정만으로 구현 클래스를 변경할 수 있는 것이다.

 

 

 

 

 

 

 

 

김영한 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 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/dashboard