이 포스트는 김영한 이사님의 스프링 입문 강의를 듣고 작성하였습니다.
JdbcTemplate은 jdbc의 반복적인 코드를 제거해준다. 하지만 SQL문은 직접 작성해주어야 한다. 참고로 JdbcTemplate은 실무에서도 많이 쓰인다고 한다.
package memberpractice.memberpractice.repository;
import memberpractice.memberpractice.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
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) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return null;
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
다른 구현체들과 같이 MemberRepository인터페이스를 상속받아 구현한다. Jdbc와 동일하게 DataSource를 주입받아야 한다. 참고로 생성자가 하나일 경우 스프링 빈으로 등록된 것에 대해서는 @AutoWired어노테이션을 생략해도 된다.
package memberpractice.memberpractice.repository;
import memberpractice.memberpractice.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
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) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper());
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
//추가
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
코드의 밑단에 보면 RowMapper라는 것을 찾아볼 수 있다. 값을 조회하는 쿼리를 날려서 얻은 결과 값에 대해서 RowMapper를 통해서 맵핑을 시켜주어야 한다. RowMapper안을 들여다보면 rs, 즉 ResultSet으로 값을 받아오는 것을 확인할 수 있다.
그 후 findById에서는 List로 가져온 데이터를 스트림으로 변환하여 값을 return하게 된다. 이 부분에 대해선 앞서 다뤘었기 때문에 생략한다.
기존에 우리가 jdbc로 구현한 findById와 비교하면 코드의 양이 확연하게 줄어든 것을 확인할 수 있다.
package memberpractice.memberpractice.repository;
import memberpractice.memberpractice.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.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());
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
save를 살펴보면 신기하게 SQL문이 없다. 대신에 SimpleJdbcInsert라는 것을 사용한 것을 확인할 수 있다.
대략적인 원리로는 테이블 명과 pk로 사용할 값, 저장할 값을 지정하면 쿼리문을 직접 만들어준다고 한다. 이에 대해서는 공식문서를 비롯한 다른 자료를 참고하여도 된다.
package memberpractice.memberpractice.repository;
import memberpractice.memberpractice.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.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;
};
}
}
findByName과 findAll은 쿼리문만 수정하면 된다.
우리가 새롭게 구현한 JdbcTemplateMemberRepository로 SpingConfig를 수정해주자.
package memberpractice.memberpractice;
import memberpractice.memberpractice.repository.JdbcMemberRepository;
import memberpractice.memberpractice.repository.JdbcTemplateMemberRepository;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.repository.MemoryMemberRepository;
import memberpractice.memberpractice.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 MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
}
통합 테스트가 정상적으로 실행된 것을 확인할 수 있다.
이렇게 해서 JdbcTemplate을 이용하여 MemberRepository를 구현하고 통합 테스트까지 진행해 보았다. 여기서 다루진 않았지만 강의 마지막에 사소한 오류가 발생하여 김영한 이사님이 테스트 코드의 중요성을 더욱 강조하셨었다.
이렇게 강의를 들을 때 이외에 실제 프로젝트를 할 때도 테스트 코드를 작성하는 것에 대해서 잘 연습을 해두어야겠다.