Spring JDBC, JDBC Template 예제
요즘에는 보통 MyBatis나 JPA를 이용해서 개발하지만 옛날에는 JDBC로 개발했던 적이 있었다.
JDBC는 JSP로 웹 개발할 때 사용해서 익숙하지만 JDBC Template는 처음 보는 것이어서 두 개의 예제를 비교해본다.
JDBC
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
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 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);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
// 생략
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
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);
}
}
insert문, 두 개의 select문을 살펴본다. findByName() 메서드는 findById() 메서드와 로직이 똑같기 때문에 생략하였다. 사실상 위 코드는 반복되는 내용이 많기 때문에 깊게 살펴볼 필요는 없다.
순수 JDBC라고 아주 오래전에 사용한 것이라는데 학부 프로젝트 때는 MyBatis 이런 건 전혀 모르고 그냥 JDBC를 사용했었다. 혹시 나도 아주 오래전 사람?!
JSP에서 JDBC를 연결할 때는 DataSource가 필요 없었지만 스프링에서는 javax.sql.DataSource를 사용해야 한다.
DataSource가 데이터베이스와의 커넥션을 관리하기 때문에 DB와 Connection 하기 위해 스프링에게서 DataSource를 주입받아야 한다.
코드 가장 아래의 getConnection() 메서드와 close() 메서드를 먼저 살펴보면 Connection이 DataSource와 관련되어 있음을 알 수 있다. 스프링에서는 DataSourceUtils를 사용해 Connection을 얻어야 한다. 데이터 이동 시 같은 Connection이 아니라, 각각의 데이터에 따라 다른 Connection을 사용하면 트랜잭션에 걸려 데이터 전달이 안 될 수도 있다. 그래서 같은 Connection으로 유지하기 위해 DataSourceUtils를 사용하고, getConnection() 메서드의 인자 값으로 DataSource를 넣어준다.
close() 메서드는 두 개의 메서드가 오버로딩 되어있다. 우선 인자 값을 넣지 않는 close() 메서드는 전체적인 연결을 close 하고, 인자 값이 한 개 들어가는 close() 메서드는 Connection을 끊는 메서드로, 여기서도 DataSourceUtils를 사용하고 있다.
데이터베이스 접속 정보는 application.properties에 저장한다. 예시로 H2 Database와 연결한다고 가정하면 application.properties 내용은 아래와 같다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
설정 내용을 보면 spring.datasource라고 되어있는 걸 확인할 수 있다.
JDBC template 사용 시에도 application.propertis에 저장된 데이터베이스 접속 정보를 사용한다.
JDBC Template
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
...
while(rs.next()) {}
...
close();
순수 JDBC 방법 사용 시 이처럼 반복되는 코드들이 많이 존재했다. 이런 반복적인 코드를 줄이기 위해 JDBCTemplate을 사용한다.
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.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) {
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(), 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;
};
}
}
JdbcTemplate은 JDBC에서의 DataSource처럼 직접적으로 주입받지 못하기 때문에 JdbcTemplate을 사용해야 한다. 그래서 new JdbcTemplate(dataSource)로 JdbcTemplate 객체를 생성하였다.
// JDBC
@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);
}
}
// JdbcTemplate
@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;
}
save() 메서드를 비교했을 때 JDBC API 사용보다 JdbcTemplate 사용 시 코드량이 확 줄었음을 볼 수 있다.
JdbcTemplate도 SQL 쿼리를 직접 작성해야 하는 건 똑같지만 중복되는 코드를 줄일 수 있다는 점에서 굉장히 편리하다.
save() 메서드에서는 insert를 하므로 SimpleJdbcInsert(JdbcTemplate) 객체를 생성하여 사용한다.
withTableName()에 insert 할 테이블명을 넣고, usingGeneratedKeyColumns를 사용하여 id 값은 직접 입력하는 값이 아닌 자동으로 등록되는 값임을 알려준다.
Map을 사용하여 key 값에는 칼럼명을 넣고, value 값에 insert 할 데이터 값을 넣는다.
select 쿼리 사용은 insert를 하는 것보다 훨씬 더 편리하다. JdbcTemplate.query() 메서드만 사용하면 된다. 해당 메서드의 반환 타입은 List 타입이다.
query() 메서드의 첫 번째 인자 값으로는 sql문이 들어가고, 두 번째 인자 값으로는 RowMapper가 들어간다. RowMapper는 ResultSet 결과를 자바 객체로 변환해주는 것이다. 세 번째 인자 값부터는 쿼리문에서 ? 에 해당되는 값이 들어간다.
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;
}
};
}
[인프런] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술