[백엔드 첫 걸음] 스프링 DB 접근 기술 - (회원 관리 예제)

khyojun·2022년 8월 2일
0

Spring 시작

목록 보기
14/15
post-thumbnail

1. H2 데이터베이스 사용

이번 실습에서는 최대한 개발이나 테스트 용도로 가볍고 편리하며 웹 화면도 제공을 하는 H2 데이터베이스를 사용하기로 하였다.

다음과 같은 사이트에서 설치를 할 수 있었다.
https://www.h2database.com

장점•단점

H2를 썼을 때의 장점

  1. 가볍고 설치가 쉽고, 관리가 편하다.
  2. 프로젝트 초기에 DB에 대한 고민을 조금 덜어줄 수 있다.
  3. Unit Test를 할 때 용이하다.
    • 하지만 이 부분은 Mock을 이용하는 것이 더 좋을 것 같긴 하다.
    • 왜냐하면 통합테스트의 경우엔 H2를 쓰지 않는 것이 좋다고 생각하기 때문이다. (아래 내용 참고)

운영에서 H2를 썼을 때의 단점

  1. 대규모 프로젝트에서는 안정성과 성능이 부족
  2. 백업, 복구 등에 대한 기능 부족

운영이 아닌 경우에만, H2를 썼을 때의 단점

  1. H2에서는 지원되지만, 사용하는 DB에서는 지원하지 않는 기능이 있을 수 있음.
  2. 기능이 지원되지 않을 수도 있지만, 문법이 다를 수도 있음(호환성 모드가 100% 모두 지원하는 것은 아닌 듯)
    이를 H2가 아닌 운영환경(Dev, Real) DB에 배포해보거나, 쿼리를 날려서 확인해봐야 알 수 있음
  3. 멀티 DB를 사용하는 경우, 부분만 H2를 사용하면 JPA Create DDL을 이용하기 어려울 수 있고, 잘못 이용하는 경우 원하지 않은 데이터 유실이 될 수 있음
  4. 운영에서 H2를 사용하지 않은 경우, 통합 테스트를 H2를 사용하는 것이 옳은 방식인지에 대한 고민
  5. 실제 DB로는 테스트하지 않았는데, 이러면 통합 테스트의 의미가 있는지
  6. 유닛 테스트 시에 용이하다고 하는데, 그럼 차라리 Mock을 사용하는 것은 어떤지

다음과 같은 특징들이 있다는 것을 알 수 있었는데 일단 여러가지 실습에서는 h2를 이용하는것이 가장 직관적일 수 있다는 생각을 가지게 되지만 실제로 나중에 프로젝트들을 진행하게 되면서는 다른 DB들을 사용해야되겠다는 느낌이 많이 들게 되었습니다.

2. 순수 JDBC

잠깐 JDBC?

  • JDBC: Java Database Connectivity
  • 자바에서 DB 프로그래밍을 하기 위해 사용되어지는 API

==> 데이터베이스의 종류에 상관이없다.

일단은 용어만 정리하고 더 자세한 내용은 후에 다루도록 하겠다.

환경설정

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가하는 작업

📂 build.gradle

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

다음과 같은 2문장을 dependencies에 추가를 하여 진행을 하게 된다.

스프링 부트 데이터베이스 연결 설정 추가

📂 resources/application.properties

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

또한 이런 부분들도 작성을 반드시!! 해줘야한다. 하나라도 되지 않을 경우 애초에 h2데이터베이스에 연결이 되어지지 않아 진행이 되지 않을 수 있다.(username부분은 그냥 지나가도 되지 않나 싶다가 계속 문제가 생겨버렸었다.)

JDBC 리포지토리 구현

실습하는 강의에서도 듣긴 하였지만 이런 형태의 JDBC API는 거의 고대의 개발자분들이 사용하셨던 방법이였다고 한다. 그래서 코드도 상당히 길고 반복되는 부분들도 많고 그렇다. 뒤의 강의들을 보게 되면은 흠..... 이게 진짜 방식이 옛날의 방식이었구나라는 생각이 상당히 많이 들긴 하였지만 역사를 모르는 민족에게 미래는 없다는 것처럼 예전의 방식도 봐야 지금의 방식을 사용하게되면서도 불평없이 참 다행이라는 생각을 가지고 개발을 해나갈 수 있지 않을까? 라는 마음을 가지고 보게 되었다.

JDBC 회원 리포지토리

📂 java/repository/JdbcMemberRepository

package com.example.demo_practice.repository;

import com.example.demo_practice.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);
    }
}

음.... 뭔가 보기만해도 너무나도 길긴 하다.

이 코드에서 봐야할 부분들은 많지만 핵심적인 부분들을 보게 된다면 close부분 그리고 try catch 를 잘 처리해야하는 부분들이 있다.

close부분은 왜 중요한가! close를 해주지 않는다? 그렇다면 정말정말정말정말 강조하고도 모자를만큼 나중에 계속 DB에 쌓이게 되어서 문제가 크게 발생된다고 한다. 이 부분은 진짜 돈과 관련하여 중요한 문제이기때문에 나중에도 이런 코드들을 제작하게 되었을때는 close같은 역할의 구문들을 반드시 확인하도록 해야겠다.

그리고 try catch를 아주 잘 사용해야한다. 여러 exception들이 많이 발생하기 때문이다!

스프링 설정 변경

📂 java/SpringConfig.java

package com.example.demo_practice;

import com.example.demo_practice.repository.JdbcMemberRepository;
import com.example.demo_practice.repository.MemberRepository;
import com.example.demo_practice.repository.MemoryMemberRepository;
import com.example.demo_practice.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

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는 데이터베이스 커넥션을 획득할 때 사용되는 객체이다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어주기때문에 DI를 받을 수가 있다.

자 이제 새롭게 생성한 순수 JDBC에게로 넘겨줄 차례다. 이전에 있었던 MemoryMemberRepository를 return하지말고 새로운 JdbcMemberRepository를 return을 한다. 여기서! dataSource를 파라미터로 넘겨줘야하는데 이 부분은 스프링에서 자동적으로 연결이되어져있는 dataSource를 찾아내 연결을 시켜준다고한다.(너무 좋은기능!)

그림으로는 다음과 같이 과정이 진행되어짐을 알 수가 있다.

결론

이번에 순수 JDBC파트를 하면서 이런 부분들이 진행이 되고 참고를 해야겠다는 생각이 많이 들었다.

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

알게 된 점

  1. JDBC
  2. h2 데이터베이스

참고

김영한 - 스프링 입문 강의
h2관련
JDBC관련

profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글