# 스프링 DB 접근 기술
## H2 데이터베이스 설치
H2 Database Engine (redirect)
H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo
www.h2database.com
H2 데이터 베이스 실행 및 접속
윈도우의 경우 h2 > bin 의 h2.bat 실행 하면 다음과 같은 웹 화면 표시 (실행한 cmd의 경우 종료하면 연결 종료됨)
그리고 최초에 데이터베이스 파일 생성을 위해 JDBC URL 에 아래와 같이 입력 (jdbc:h2:~/test) 후 연결 클릭.
연결이 완료된 후 C:\Users\user 에 test.mv.db 파일 생성되었는지 확인.
이후 접속 시에는 JDBC URL에 아래와 같이 입력(jdbc:h2:tcp://localhost/~/test)하여 접속 진행.
member 테이블 생성
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
아래와 같이 insert 문으로 데이터 입력 후 select 문 사용하여 조회 가능.
ddl 문의 경우 아래와 같이 별도의 디렉토리 생성하여 관리하는게 좋음.
## 순수 JDBC
build.gradle
build.gradle 에 아래와 같이 jdbc, h2 관련 dependencies 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.properties
application.properties 에 아래와 같이 db 접속정보 설정.
spring.datasource.url =jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name =org.h2.Driver
JdbcMemberRepository 생성
기존에 생성했던 MemoryMemberRepository (인터페이스) 를 구현체로 생성.
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 connection = null ;
PreparedStatement pstmt = null ;
ResultSet resultSet = null ;
try {
connection = getConnection();
pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1 , member.getName());
pstmt.executeUpdate();
resultSet = pstmt.getGeneratedKeys();
if (resultSet.next()) {
member.setId(resultSet.getLong(1 ));
} else {
throw new SQLException("회원 ID 조회 실패." );
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(connection, pstmt, resultSet);
}
}
@Override
public Optional<Member> findById (Long id) {
String sql = "select * from member where id = ?" ;
Connection connection = null ;
PreparedStatement pstmt = null ;
ResultSet resultSet = null ;
try {
connection = getConnection();
pstmt = connection.prepareStatement(sql);
pstmt.setLong(1 , id);
resultSet = pstmt.executeQuery();
if (resultSet.next()) {
Member member = new Member();
member.setId(resultSet.getLong("id" ));
member.setName(resultSet.getString("name" ));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(connection, pstmt, resultSet);
}
}
@Override
public List<Member> findAll () {
String sql = "select * from member" ;
Connection connection = null ;
PreparedStatement pstmt = null ;
ResultSet resultSet = null ;
try {
connection = getConnection();
pstmt = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
resultSet = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while (resultSet.next()) {
Member member = new Member();
member.setId(resultSet.getLong("id" ));
member.setName(resultSet.getString("name" ));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(connection, pstmt, resultSet);
}
}
@Override
public Optional<Member> findByName (String name) {
String sql = "select * from member where name = ?" ;
Connection connection = null ;
PreparedStatement pstmt = null ;
ResultSet resultSet = null ;
try {
connection = getConnection();
pstmt = connection.prepareStatement(sql);
pstmt.setString(1 , name);
resultSet = pstmt.executeQuery();
if (resultSet.next()) {
Member member = new Member();
member.setId(resultSet.getLong("id" ));
member.setName(resultSet.getString("name" ));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(connection, pstmt, resultSet);
}
}
private Connection getConnection () {
return DataSourceUtils.getConnection(dataSource);
}
private void close (Connection connection, PreparedStatement pstmt, ResultSet resultSet) {
try {
if (resultSet != null ) {
resultSet.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null ) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (connection != null ) {
close(connection);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close (Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
SpringConfig 설정 변경
기존에 작업 수행 시 메모리에 저장되도록 Repository 를 MemoryMemberRepository 로 설정 함.
아래와 같이 입력하여 위에서 만든 JdbcMemberRepository 로 변경.
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
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 final 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(dataSource);
}
}
개방 폐쇄의 원칙(OCP:Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다. (위 코드는 개발 폐쇄의 원칙이 지켜진 것.)
스프링의 DI (Dependencied Injection) 활용 시, 위와 같이 기존 소스 코드를 전혀 손대지 않고 설정만으로 구현 클래스 변경이 가능하다.
실행 결과
h2 데이터베이스 실행 된 상태에서 서버 재 시작 후 http://localhost:포트번호 입력하여 메인화면 진입.
회원 등록 테스트 진행 후 서버 재 시작하여 다시 확인 시 데이터가 유지되는것을 확인할 수 있다.
# 스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트 진행.
MemberServiceIntegrationTest
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.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
Member member = new Member();
member.setName("hello" );
Long saveId = memberService.join(member);
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
Member member1 = new Member();
member1.setName("spring1" );
Member member2 = new Member();
member2.setName("spring1" );
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원 입니다." );
}
}
@SpringBootTest
테스트 케이스에 해당 에노테이션 추가 시, 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
테스트 케이스에 해당 에노테이션 추가 시, 테스트 실행 전 트랜잭션을 걸고 시작하고, 테스트 완료 후 항상 ROLLBACK 한다.
롤백으로 인해 DB에 테스트 데이터가 남지 않아서, 테스트로 입력한 데이터에 대한 별도의 delete 로직을 추가하지 않아도 테스트를 반복 실행 할 수 있다.
테스트 케이스에 붙었을 때만 항상 롤백하도록 동작 함.
## 스프링 JdbcTemplate (실무에서 많이 사용됨)
순수 Jdbc 와 동일한 환경설정을 하면 된다.
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복적인 코드(중복)를 대부분 제거해준다. 하지만, SQL은 직접 작성해야 한다.
JdbcTemplateMemberRepository
생성자가 1개만 존재하는 경우 @Autowired 생략 가능.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
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.sql.ResultSet;
import java.sql.SQLException;
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;
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;
};
}
}
SpringConfig 설정
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
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 final DataSource dataSource;
@Autowired
public SpringConfig (DataSource dataSource) {
this .dataSource = dataSource;
}
@Bean
public MemberService memberService () {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository () {
return new JdbcTemplateMemberRepository(dataSource);
}
}