한둥둥 2022. 9. 28. 11:11

이번 시간에는 진짜 애플리케이션에서 DB에 연동을 해가지고 애플리케이션에서 저장하는게 기존에 처럼 메모리에 저장되어 프로그램을 내렸다 올리면 데이터들이 사라지는 것이 아닌 데이터베이스에 저장이 되어서 데이터가 남도록 만들어줄 것이다. 

Selet Query, Insert Query를 통해서 데이터베이스에 데이터를 넣고 빼는 방식을 한번 해볼 것이다. 

지금부터 내가 작성하는 글은 걍 옛날에 이런식으로 프로그래밍 했다 정도만 알도록 하자. 

필요하면 필요할 때 찾아보면서 하도록 하자. 

가장 먼저 해야 할 일은 JDBC build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가하자.

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

 buildgradle은 dependencies에 두개의 코드를 추가 해주자. 자바는 기본적으로 DataBase를 사용하기 위해서는 jdbc가 반드시 있어야 한다. 이것을 가지고 서로를 연동하는 것이다. 

runtimeOnly가 뭐냐면 DB랑 붙을 때 데이터베이스가 제공하는 클라이언트가 필요하다. 그래서 이 코드를 보면 h2Database클라이언트가 내가 만든 프로젝트에는 필요하기에 추가해준 모습이다. 

DB에 붙으려면 접속 정보 같은 것을 넣어야 한다. 이것을 옛날에는 개발자가 어렵게 다 설정을 하고 그랬는데 요즘에는 SpringBoot가 알아서 다해준다. 그래서 요즘에는 경로만 넣으면 됨. 

 

application.properties에서 나는 아래의 코드를 추가해주었다. 

spring.datasource.url = jdbc:h2:tcp://localhost/~/test

이 때 spring.datasource.url은 

위에 사진에 보이는 JDBC URL을 사용하였다. 

build.gradle에 가서 우리는 코끼리 모양을 클릭하여 dependencies에 추가한 것들을 다운 받아 주자. 

원래는 여기서 아이디, 패스워드까지 입력해야 하는데 h2 DataBase는 생략을 해도 된다. 

이렇게 까지하면 Spring이 DB랑 연결하는데 필요한 작업들 전부 다 해준 것이다. 

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

이렇게 Properties에서 Setting을 해주었다. 

build run을 돌려준 모습이다. 이제부터 JDBC API를 가지고 개발을 시작해보자. 

그런데 어디서 부터 개발을 시작해야 하는가? 

기존에는 memoryMemberRepository를 만들었다. 이거를 우리가 인터페이스로 만들었기 때문에 이것의 구현체를 만들면 된다. 

우선적으로 우리는 JdbcMemberRepository를 만들자. 

회원을 저장하는 역할을 MemberRepository가 딱 역할을 하지만 구현을 메모리에 할꺼야 아니면 DB랑 연결해서 JDBC랑 연동 할꺼야 이 두개의 차이가 있다. 

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 {
        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();// getConnection을 통해서 Connection가지고옴 
            pstmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
//두번째 인자에 RETURN_GENERATED_KEY라는게 있다. 이것이 뭐냐면 우리가 DB에 insert하면 1,2번 id값을 얻을 수 있었다. 이것을 하기 위해 사용한다.
            pstmt.setString(1, member.getName());// setString에서 1 , member.getName()을 하면 물음표랑 매칭이되서 member.getName()을 넣는다. 
            pstmt.executeUpdate();//pstmt에다가 executeUpdate하면 실제 쿼리가 날라감. 
            rs = pstmt.getGeneratedKeys();// 위에 있는 RETURN_GENERATED_KEY랑 매칭되서 할 수 있는 거임. 
        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);
// 자원들 쓴거 무조건 Release해주어야함 닫아주어야함. 안그러면 데이터베이스 Connection계속 쌓이다가 장애가 올 수도 있음. 
        }
}
    @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();//조회는 executeUpdate()가 아닌 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);
    }
}

implements Member Repository를 해준다. 이후에 option + Enter를 누르면 MemberRepository의 기본 메서드들을 만들 수  있다. 먼저 db랑 붙으려면 DataSource가 필요하다. DataSource <- 이 친구를 나중에 주입받아야 한다.. 어떻게 주입 받냐면 스프링에게 주입을 받아야 한다. 

우리가 Setting을 했었는데 그러면 어떻게 되냐면 데이터 소스라는 접속 정보를 만들어 놓고 Spring을 통해서 DataSource를 주입 받으면 된다. 그 후 dataSource.getConnection()을 통해서 데이터 커넥션을 얻어올 수 있다. 이렇게 되면 데이터베이스와 연결되는 socket이 열린다. 

이런식으로 쭉 다 작성을 save를 해주면 db에 우리가 적어준 쿼리가 날라가도록 만들었다.  

 

이렇게 까지 해준 후 Spring config 파일도 다시 재 작성해주어야 한다. 

package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
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();
    }
}

Spring Boot가 dataSource를 보고 Setting에서 데이터베이스를 확인한 후 자체적으로 빈을 생성 해준다. 

꼭 ./h2.sh 실행 해놓고 돌려야 된다. 

 

계속해서 에러가 나길래 아래 코드를 붙여 넣었더니 정상작동을 하였다. 

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

회원목록으로 들어가니깐 DB에 있던 것들이 화면에 출력되는 것을 확인할 수 있다. 이게 바로 나의 첫 스프링 페이지이다. 데이터베이스에 접근한 코드가 정말 잘 돌아가고 있다. 

 

우리가 스프링을 사용하는 이유는 다형성 때문이다. 그니깐 인터페이스를 하나 두고 구현체를 만들어서 바꿔 낄 수가 있다. 스프링은 이것을 굉장히 편리하게 사용할 수 있다. 

과거에는 MemoryMemberRepository에서 JdbcMemberRepository로 바꿔주면 MemberServices도 수정해야 했다. 원래는 MemoryMemberRepository를 의존하는 것이 JdbcMemberRepository를 의존하게 하는 것으로 바뀐 것이다. 

하지만 지금은 assembly code 조립하는 코드만 딱 손대면 나머지 실제 애플리케이션 관련된 코드는 하나도 건들일 필요가 없다.

MemberService는 사실 MemberRepository를 의존하고 있다. 과거에는 위에 처럼 다 손봐야 했다. 

하지만 지금 그냥 JdbcMemberRepository만 바꿔 껴주면 된다. 

- OCP(Open- Closed Principle)

확장에는 열려있고, 수정, 변경에는 닫혀있다.(인터페이스 기반의 다형성이라는 개념을 잘 활용하여 애플리케이션 전체를 수정할 필요가 없는 것이다.)