다음의 모든 내용은 김영한님의 스프링 입문 강의에서 가져온 것임을 밝힙니다.
스프링의 DB 접근 기술로는 크게 Jdbc와 Jpa가 있다!
이번 포스팅에서는 Jdbc를, 다음 포스팅에서 Jpa를 다루었다.
하지만 순수 Jdbc는 20년도 더 전의 이야기로... 너무나 복잡하고 반복 코드가 많으므로 현재 거의 사용되지 않는다.
대신 JdbcTemplate은 여전히 사용되고 있으니 이를 중심으로 알아보자!
먼저 로컬용 데이터베이스를 설치하자.
실무에서는 mySQL 계열을 많이 사용하나, 강의에서는 가벼운 H2를 사용했다.
실무에서도 로컬에서는 H2를 사용하고 운영시에만 mySQL 계열을 사용하는 경우도 많다!
다음 링크에 들어가서 1.4.200 버전을 설치했다.
cmd 창에서 해당 파일경로로 이동 후 H2 실행시키면, 다음과 같은 화면을 볼 수 있다.
$ cd C:\Program Files (x86)\H2\bin
$ h2.bat
처음의 디폴트값 그대로 연결
을 클릭하여 ~/test.mv.db 파일이 잘 생성되었다면, 이후부터는 jdbc:h2:tcp://localhost/~/test 으로 접속하면 된다.
Meber
테이블 생성다음의 SQL문을 사용하여, 이전의 회원관리 예제에서 사용되던 Member 테이블을 생성했다.
SQL문은 src 디렉토리 외부에 (ex. hello-spring 하위)
sql
패키지를 만들어서ddl.sql
파일로 저장하면, 추후 깃허브로 버전관리시 용이하다!
build.gradle
파일의 dependencies 부분에 다음 코드를 추가하면 된다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc' //Jdbc 관련 라이브러리
runtimeOnly 'com.h2database:h2' //H2 DB 관련 라이브러리
resources 하위의 application.properties
파일에 다음 코드를 추가하자.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
이전 포스팅의 단위 테스트 하나하나를 잘 만드는 게 훨씬 더 중요하다. 하지만 스프링 컨테이너와 DB까지 모두 통합한 통합테스트가 필요한 경우, 다음을 참고하자.
@SpringBootTest
: 스프링 컨테이너와 테스트를 함께 실행하도록 한다.
기존 MemberServiceTest 대신,
MemberServiceIntegrationTest.java를 생성하고 @SpringBootTest
애노테이션을 추가한다.
@Transactional
애노테이션
@Transactional
: 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고 테스트 완료 후에 항상 롤백한다. 즉 DB에 데이터가 남지 않으므로 DB를 지우는 코드가 필요 없고, 다음 테스트에 영향을 주지 않는다!
MemberServiceIntegrationTest.java
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
/**
@BeforeEach
public void beforeEach(){
//테스트 실행 전마다 리포지토리를 생성해서 MemberService에 넣어준다
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
**/
/**
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
**/
@Test
void 회원가입() throws Exception {
//given
Member member = new Member();
member.setName("홍길동");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
void 중복_회원_예외() throws Exception{
//given
Member member1 = new Member();
member1.setName("홍길동");
Member member2 = new Member();
member2.setName("홍길동");
//when
memberService.join(member1);
//중복 회원이 join하려 하면 IllegalStateException이 터져야 한다
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//then
}
}
JdbcTemplate: JDBC API에서의 반복 코드를 대부분 제거해준다! SQL은 직접 작성해야 한다.
이는 실무에서도 많이 쓰는 방식이다.
repository 디렉토리에 JdbcTemplate을 사용하는 JdbcTemplateMemberRepository
리포지토리를 생성하자.
이 리포지토리는 기존에 만들었던 MemberRepository를 implement하며,
다음의 코드를 추가하여 Jdbctemplate을 사용할 수 있다.
private final JdbcTemplate jdbcTemplate;
//@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource){
jdbcTemplate = new JdbcTemplate(dataSource);
}
[참고] 생성자가 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;
//@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 List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
@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 MemoryMemberRepository();
return new JdbcTemplateMemberRepository(dataSource); // Jdbc template 사용
}
}
앞서 만들었던 통합 테스트 진행시, 모두 성공적으로 체크표시가 떴다!