[Spring] DB 접근 기술 - 1

woozxn·2021년 7월 4일

Spring

목록 보기
6/8
post-thumbnail

🌕 H2 데이터베이스 설치

개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공

https://www.h2database.com 에서 다운로드, 설치!

H2 console을 실행 후 연결

오류 발생 시 주소창 localhost:8082로 수정

연결 후 ~/test.mv.db 파일이 생성되었는지 확인합니다.
이후 접속 시 jdbc:h2:tcp://localhost/~/test 로 접속해야합니다.
이전에는 파일로 H2에 접근하여 동시에 동작시 오류가 생길 수 있지만, 위와 같이 소켓을 통해 H2에 접근하면 여러 곳에서 접근이 가능해집니다.

콘솔 창에 sql을 작성하여 Member 테이블 생성

create table member(
	id bigint generated by default as identity,
	name varchar(255),
	primary key (id)
);

insert, select문으로 정상적으로 작동 확인.


🌕 순수 JDBC

이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게
고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.

build.gradle에 DB관련 라이브러리 추가

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

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

resources/application.properties

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

🌒 Jdbc 회원 리포지토리

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

🌒 스프링 설정 변경

DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.

hello.hellospring/SpringConfig.java

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

기존에는 서비스의 코드가 리포지토리를 의존하는 코드라면 리포지토리가 변경될 때 서비스 코드까지 변경해야 하는 경우가 생겼다면 이제는 스프링의 DI을 사용하여 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있습니다.

데이터를 DB에 저장하므로 스프링서버를 다시 실행해도 데이터가 안전하게 저장된다.

🌕 회원 서비스 스프링 통합 테스트

스프링 컨테이너DB를 연결하여 실행하는 통합 테스트를 진행하기 위해 다음 파일을 생성한다.

src\test\java\hello\hellospring\service\MemberServiceIntegrationTest.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("woojin");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복_회원_예약() {
        //given
        Member member1 = new Member();
        member1.setName("woojin");

        Member member2 = new Member();
        member2.setName("woojin");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");


        //then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

@SpringBootTest를 사용하면 스프링 컨테이너와 테스트를 같이 실행합니다.

@Transational은 테스트 시작 전 트랜잭션을 실행하고 실행 후 롤백을 수행하여 DB에 데이터가 반영되지 않도록 합니다.

단위테스트의 속도가 빠르므로 상황에 따라 적절히 사용한다.

🌕 스프링 JdbcTemplate

순수 Jdbc와 동일한 환경설정에서 진행한다.

스프링 JdbcTemplateMyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해주지만 SQL은 직접 작성해야한다

🌒 스프링 JdbcTemplate 회원 리포지토리

JdbcTemplate을 사용하기 위해 생성자에서 dataSource를 받아 jdbcTemplate을 생성할 때 이를 인자로 넘겨주고 jdbcTemplate 객체를 생성합니다. 이때 생성자가 하나이므로 @Autowired를 생략할 수 있습니다

save() 함수에서 jdbcTemplate을 사용하여 sql 쿼리 작성을 생략할 수 있고 설정을 통해 쿼리를 작성, 전송하여 생성된 key를 받고 반환된 id를 setting하여 회원 가입된 객체를 반환합니다.

나머지 함수는 중복되는 코드를 memberRowMapper() 함수로 작성하고 jdbcTemplate라이브러리를 사용해 sql과 함께 넘겨주어 sql 결과를 List 형태로 받습니다.
memberRowMapper() 함수는 이전에 중복되었던 코드 부분으로 회원 객체를 생성하고 반환된 결과 값을 세팅해 반환합니다.

JdbcTemplate의 사용방법 참조
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html

src\main\java\hello.hellospring\repository\JdbcTemplateMemberRepository.java

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?",memberRowMapper(), id);
        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where name = ?",memberRowMapper(), name);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }

    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

🌒 JdbcTemplate을 사용하도록 스프링 설정 변경

hello.hellospring/SpringConfig.java

    @Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }
profile
1a2a3a4a5a

0개의 댓글