Java Spring Framework

기존에 memory 영역에 저장한 data를 h2 DB를 이용해서 DB에 저장해보자.

Settings for H2

1. Installing H2 DB

H2DB

2. Run DB

h2/bin/h2.batch

3. Configure Gradle Settings

jdbc, h2 관련 dependency를 추가해준다.

build.gradle

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

4. Configure h2 Connection Settings

h2 연결 관련 정보를 설정해준다. 이렇게 하므로써 DataSource를 spring bean로 등록 해줄 수 있다.

resources/applcation.properties

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

JDBCMemoryRepository

기존 jdbc 방식으로 구현한 MemoryRepository를 먼저 알아보자, JDBC만을 이용하는 방식은 너무 구식의 방식이다. Connection, PrepareStatement, ResultSet, 등과 같은 객체들이 중복해서 쓰이며, Connection 설정도 까다롭다.

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 JDBCMemberRepositry implements MemberRepository {
    private final DataSource dataSource;
    //Autowired 설정을 이용해서 DI를 설정한다.
    @Autowired
    public JDBCMemberRepositry(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);
        }
    }
    //DataSourceUtil를 이용해서 DB Connection 관리를 해서 무분별한 DB Connection 생성을 방지한다.
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }
    //이렇듯 JDBC 방식에서는 resource 할당/해제 관련된 부분을 신경써서 해야한다.
    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);
    }
}

Spring Configuration

MemoryRepositry를 interface로 생성하여 이를 구현하는 class들을 만들었다. 그래서 DB, Memory 접근 방식에 따라서 다른 구현체를 사용하게 된다. 구현체가 바뀌게 되었으니 DI 설정 또한 다시 해줘야한다. 하지만, Mannual Configuration 방식을 이용해서 DI 설정을 해줬으므로 코드 수정의 여지는 적다.

SpringConfig

package hello.hellospring;

import hello.hellospring.repository.JDBCMemberRepositry;
import hello.hellospring.repository.JDBCTemplateMemberRepositry;
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;
import javax.swing.*;

@Configuration
public class SpringConfig {
    private DataSource dataSource;

    public SpringConfig(DataSource dataSource){
        this.dataSource=dataSource;
    }
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository(){
        //return  new MemoryMemberRepository();
        return new JDBCMemberRepositry(dataSource);
       
    }
}

바뀐 부분

private DataSource dataSource;
    //DB Connection을 관리하기 위해 DataSource 생성
    public SpringConfig(DataSource dataSource){
        this.dataSource=dataSource;
    }
//기존 Memory 방식에거 H2DB 접근 방식을 이용하기 위해 구현클래스 변경
public MemberRepository memberRepository(){
        //return  new MemoryMemberRepository();
        return new JDBCMemberRepositry(dataSource);
       
    }

위 2가지 부분만 바뀌게 되었다.

이를 보면 Open-Close Principle를 준수하는 객체프로그래밍이다. Memory에서 DB 접근 방식으로 변경하기 위해 최소한의 코드 수정으로 이를 가능케 하고 있다.

JDBCTemplateMemoryRepository

순수 JDBC 방식만으로 개발하는 것은 너무 복잡하고 비효율적이다. 그래서 JDBC Template를 이용하는데, 이를 통해 중복되는 코드를 줄일 수 있다.

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 org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.*;

public class JDBCTemplateMemberRepositry implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JDBCTemplateMemberRepositry(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;
        };
    }
}

Spring-Integrated Test

기존에는 junit 기반의 단위테스트만을 수행했다. 이번에는 스프링을 포함한 통합테스트를 수행해보자.

MemberServiceIntegrationTest

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

|Annotataion|Description| |———|————-| |SpringBootTest|스프링부트 기반의 통합 테스트 수행| |Transactional|테스트 진행시 테스트 실행 전과 테스트 실행 후의 상태 유지를 위해 rollback 자동 수행|

References

link: inflearn

link:jdbc_template

link: jdbc_template

댓글남기기