김영한T 스프링 입문강의 - [섹션6] 스프링 DB 접근 기술 (JDBC, Jdbc Template)
실제 db와 연동을 해보기 위해, h2 db를 설치해주었다.
https://www.h2database.com/html/download-archive.html
Archive Downloads
www.h2database.com
최근에 나온 버전은 일부기능이 정상작동하지 않는다고 하여, 1.4.200 버전을 다운받아주었다.
설치 후, h2 콘솔에 들어간 후에
연결 버튼을 눌렀을 때 에러없이 정상적으로 들어가지고, 본인의 홈디렉토리에 test.mv 파일이 생겼다면 성공이다.
그 이후부터는 JDBC URL 자리에 jdbc:h2:tcp://localhost/~/test 이렇게 접속해준다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key(id)
);
들어가서 table을 생성하는 sql문을 작성한다.
Member 테이블이 생긴것을 확인할 수 있다.
<순수 JDBC>
스프링과 db를 연결해주기 위해서 적어주어야하는 코드들이다.
build.gradle 안의 dependencies에 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
다음은 application.properties에 추가해준다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
원래는 비밀번호 등을 적어주는데, h2 db는 생략한다.
예전에 JDBC API로 직접 코딩하였었다. 순수 JDBC는 지금은 사용하지 않는 방법이지만, 이해를 위해 정리하고 넘어간다.
MemberRepository를 상속받는 JdbcMemberRepository 클래스를 만든 후에(생략),
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
이렇게 기존에 적었던 Configuration 코드를
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new JdbcMemberRepository();
}
}
이렇게 저장소를 JdbcMemberRepository를 반환하게만 바꿔주면 설정이 끝난다.
이게 자바의 매우 큰 장점!!
작성을 마친 후 스프링 테스트를 진행할 때, 테스트를 진행할 때마다 db에 정보가 쌓이게 된다.
그니까, 이미 db에 "spring" 이름의 회원이 저장되어있다면, 다음 테스트시 "spring"이름으로 저장하지 못하게 된다.
그래서 @AfterEach를 사용해서 deleteAll()을 만들고...하는 작업을 해도...괜찮지만
스프링이 제공해주는 기능이 있다!
@Transactional 애노테이션을 테스트케이스에 달면, 테스트를 끝내면 db에 넣었던 데이터들이 깔끔하게 지워준다.
이렇게 하면 동일한 테스트를 여러번 실행할 수 있다!
@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception{
//Given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//Then
Member findmember = memberRepository.findById(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findmember.getName());
}
@Test
public void 중복_회원_예외() throws Exception{
//Given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,
()->memberService.join(member2));
//예외 발생해야함
Assertions.assertThat(e.getMessage().equals("이미 존재하는 회원입니다."));
}
}
통합 테스트코드다.
<Jdbc Template>
JdbcTemplate 는 Jdbc Api의 반복코드를 대부분 제거해주지만, SQL은 직접 작성해야한다.
순수 jdbc와 동일한 환경설정을 해준다.
저장소 클래스를 하나 만들어주고,
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
...
JdbcTemplate은 DataSource를 필요로 한다. DataSource는 스프링 빈으로 등록되어 있어야 한다.
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
...
그래서 이렇게 스프링빈에 넣어주고,
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.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());
//결과를 memberRowMapper로 묶어서 반환
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 new RowMapper<Member>(){
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}
}
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
return new JdbcTemplateMemberRepository(dataSource);
}
Configuration에서 다시 조립해준다.