본문 바로가기
Programming/Spring

Spring JdbcTemplate Method

by TinKerBellBass 2017. 8. 22.
728x90
반응형

참고도서

초보 웹 개발자를 위한 스프링4 프로그래밍 입문
국내도서
저자 : 최범균
출판 : 가메출판사 2015.03.02
상세보기



1. DataSource 설정

① maven repository 에서 spring jdbc(스프링에서 제공하는 JDBC), c3p0(커넥션 풀), mysql connector java(데이터베이스)를 검색해서
    설정하고 라이브러리를 다운 받는다.
② 스프링 설정파일 작성

1
2
3
4
5
6
7
8
9
10
11
12
<!-- c3p0 에서 제공하는 ComboPooledDataSource 의 커넥션 풀 기능 사용 -->
 
<!-- destroy-method="close" 스프링 컨테이너가 종료될 때 커넥션 풀에 보관된 Connection 이 종료 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql://localhost/spring4fs?characterEncoding=utf8" />
    <property name="user" value="spring4" />
    <property name="password" value="spring4" />
    <property name="maxPoolSize" value="100" />
    <property name="maxIdleTime" value="600" />
    <property name="idleConnectionTestPeriod" value="300" />
</bean>
cs

ComboPooledDataSource 의 property


 driverClass

 JDBC 드라이버 클래스 지정

 jdbcUrl

 JDBC URL 지정, ?characterEncoding=utf8 로 인코딩 설정

 user

 DB 에 연결할 때의 사용할 사용자 계정

 password

 DB 에 연결할 때의 사용자 암호

 initialPoolSize

 초기의 켜넥션 풀 크기, 기본값은 3

 maxPoolSize

 커넥션 풀의 최대 크기, 기본값은 15

 minPoolSize

 커넥션 풀의 최소 크기, 기본값은 3

 maxIdleTime

 지정한 시간동안 사용되지 않는 커넥션을 제거, 단위는 초, 

 기본 값은 0이면, 값이 0일 경우 제거하지 않는다

 checkoutTimeout

 풀에서 켜넥션을 가져올 때 대기 시간, 1/1000초. 0은 무한히 기다리는 것의 의미하며, 

 지정한 시간 동안 풀에서 커넥션을 가져오지 못할 경우 SQLException이 발생한다. 기본값은 0

 idelConnectionTestPeriod

 풀 속에 있는 커넥션의 테스트 주기. 단위는 초, 기본값은 0이고, 값이 0인 경우 검사하지 않는다.


prorpetry name 은 반드시 제공하고 있는 DataSource 클래스의 setter 이름을 지켜야 한다. 

다른 DataSource 를 사용할 때도 마찬가지인데 DataSource 마다 정의된 setter 이름이 다른다.

통일하면 좋을텐데.. 

DataSource 가 제공하는 property 를 찾으려면 직접 클래스를 뒤져서 setter 이름을 알아낼 수도 있고,

DataSource 를 제공하는 벤더의 웹페이지를 찾아서 API 를 참고해도 된다.

알고는 있는데 기억이 가물가물 할 때는 이클립스에서 제공하는 자동 완성 기능을 사용하면 된다. 

property name="" 에서 "" 안에 커서를 놓고 컨트로 스페이스를 누르면 사용할 수 있는 property 가 주욱 뜬다.

그리고 DB Driver, URL 도 사용할 데이터베이스 웹페이지 API 를 뒤지면 나온다.


2. JdbcTemplate  Method

(1) JdbcTemplate 객체 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MemberDao {
 
    private JdbcTemplate jdbcTemplate;
 
    public MemberDao(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
 
    // setter 메소드를 통해서 JdbcTemplate 객체를 생성하는 방식도 가능
    // public void setDataSource(DataSource dataSource) {
    // this.jdbcTemplate = new JdbcTemplate(dataSource);
    // }
}
cs


dataSource를 파라미터로 전달해야 JdbcTemplate 객체를 생성할 수 있다.


(2) 스프링 설정 파일에 JdbcTemplate 객체를 사용하는 클래스를 등록


1
2
3
4
5
<bean id="memberDao" class="spring.MemberDao">
    <constructor-arg ref="dataSource" />
    <!-- setter 메소드를 통해 JdbcTemplate 객체를 생성할 때 -->
    <!-- <property name="dataSource" ref="dataSource" /> -->
</bean>
cs


(3) query() 메소드


SELECT 뭐리 실행을 위한 메소드로, JdbcTemplate 클래스를 열어서 query로 검색해 보면 수 많은 메소드가 정의되어 있다.


자주 사용하는 query() 메소드


 List<T> query(String sql, RowMapper<T> rowMapper) 

 List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)

 List<T> query(String sql, RowMapper<T> rowMapper, Object...args)


반환타입이 List<T> 이기 때문에 SELECT 쿼리 실행 결과를 List 로 담을 수 있다.

String sql 파라미터는 실행할 쿼리문을 넣으면 되고,

RowMapper<T> rowMapper 는 쿼리 실행후 결과가 담긴 ResultSet 객체를 자바 객체로 변환하는 역할을 한다.

sql 파라미터가 인덱스 기반 파라미터(PreparedStatement 의 물음표)를 가지는 경우, args 파라미터를 통해 각 인덱스 값을 지정한다.

배열에 담아 넘겨줄 수도 있고, 값을 하나 하나 나열해서 넣을 수도 있다(rowMapper, args 순서에 주의).


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// JdbcTemplate 클래스 안의 query() 메소드
    @Override
    public void query(String sql, RowCallbackHandler rch) throws DataAccessException {
        query(sql, new RowCallbackHandlerResultSetExtractor(rch));
    }
 
    @Override
    public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
        return query(sql, new RowMapperResultSetExtractor<T>(rowMapper));
    }
    @Override
    public <T> T query(PreparedStatementCreator psc, ResultSetExtractor<T> rse) throws DataAccessException {
        return query(psc, null, rse);
    }
 
    @Override
    public <T> T query(String sql, PreparedStatementSetter pss, ResultSetExtractor<T> rse) throws DataAccessException {
        return query(new SimplePreparedStatementCreator(sql), pss, rse);
    }
 
    @Override
    public <T> T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor<T> rse) throws DataAccessException {
        return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse);
    }
 
    @Override
    public <T> T query(String sql, Object[] args, ResultSetExtractor<T> rse) throws DataAccessException {
        return query(sql, newArgPreparedStatementSetter(args), rse);
    }
 
    @Override
    public <T> T query(String sql, ResultSetExtractor<T> rse, Object... args) throws DataAccessException {
        return query(sql, newArgPreparedStatementSetter(args), rse);
    }
 
    @Override
    public void query(PreparedStatementCreator psc, RowCallbackHandler rch) throws DataAccessException {
        query(psc, new RowCallbackHandlerResultSetExtractor(rch));
    }
 
    @Override
    public void query(String sql, PreparedStatementSetter pss, RowCallbackHandler rch) throws DataAccessException {
        query(sql, pss, new RowCallbackHandlerResultSetExtractor(rch));
    }
 
    @Override
    public void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException {
        query(sql, newArgTypePreparedStatementSetter(args, argTypes), rch);
    }
 
    @Override
    public void query(String sql, Object[] args, RowCallbackHandler rch) throws DataAccessException {
        query(sql, newArgPreparedStatementSetter(args), rch);
    }
 
    @Override
    public void query(String sql, RowCallbackHandler rch, Object... args) throws DataAccessException {
        query(sql, newArgPreparedStatementSetter(args), rch);
    }
 
    @Override
    public <T> List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper) throws DataAccessException {
        return query(psc, new RowMapperResultSetExtractor<T>(rowMapper));
    }
 
    @Override
    public <T> List<T> query(String sql, PreparedStatementSetter pss, RowMapper<T> rowMapper) throws DataAccessException {
        return query(sql, pss, new RowMapperResultSetExtractor<T>(rowMapper));
    }
 
    @Override
    public <T> List<T> query(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
        return query(sql, args, argTypes, new RowMapperResultSetExtractor<T>(rowMapper));
    }
 
    @Override
    public <T> List<T> query(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
        return query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper));
    }
 
    @Override
    public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
        return query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper));
    }
cs


다 써볼 일은 없을 것 같은...


(4) RowMapper Interface


query() 메소드의 실행 결과는 ResultSet 객체에 담기고, 이 객체를 자바 객체로 변환해 주는 것이 RowMapper 이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// RowMapper<T> 
    public interface RowMapper<T> {
    /**
     * Implementations must implement this method to map each row of data
     * in the ResultSet. This method should not call {@code next()} on
     * the ResultSet; it is only supposed to map values of the current row.
     * @param rs the ResultSet to map (pre-initialized for the current row)
     * @param rowNum the number of the current row
     * @return the result object for the current row
     * @throws SQLException if a SQLException is encountered getting
     * column values (that is, there's no need to catch SQLException)
     */
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
 
}
cs


RowMapper 의 mapRow() 메소드는 SQL 실행 결과로 구한 ResultSet 으로부터 한 행의 데이터를 읽어와, 

자바 객체로 변환해주는 매퍼 기능을 구현하고 있다.


new RowMapper<T>(){mapRow() 메소드} 처럼익명 클래스를 사용해서 사용할 수도 있고, 

RowMapper 인터페이스를 구현한 클래스를 작성해서 사용할 수도 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public Member selectByEmail(String email) {
 
    // // RowMapper 인터페이스를 구현한 클래스를 사용하지 않고 익명 클래스를 사용하는 경우
    // List<Member> result = jdbcTemplate.query("select * from MEMBER where EMAIL=?", new RowMapper<Member>() {
    // @Override
    // public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
    // Member member = new Member(rs.getString("EMAIL"), rs.getString("PASSWORD"), rs.getString("NAME"), rs.getTimestamp("REGDATE"));
    // member.setId(rs.getLong("ID"));
    // return member;
    // }
    // }, email);
 
    // RowMapper 인터페이스를 구현한 클래스를 사용하는 경우
    final String selectByEmailQuery = "select * from MEMBER where EAMIL=?";
    List<Member> result = jdbcTemplate.query(selectByEmailQuery, new MemberRowMapper(), email);
 
    return result.isEmpty() ? null : result.get(0);
}
 
// RowMapper 인터페이스를 구현한 클래스
class MemberRowMapper implements RowMapper<Member> {
 
    @Override
    public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
        // Member 의 생성자를 이용해서 값을 매핑, 물론 기본 생성자와 해당 파라미터가 정의된 생성자를 만들어 놔야 한다.
        Member member = new Member(rs.getString("EMAIL"), rs.getString("PASSWORD"), rs.getString("NAME"), rs.getTimestamp("REGDATE"));
        member.setId(rs.getLong("ID"));
 
        // Member 의 생성자가 아닌 setter를 이용해서 얻어온 값을 매핑하는 경우
        // member.setEmail(rs.getString("EMAIL"));
        // member.setPassword(rs.getString("PASSWORD"));
        // member.setPassword(rs.getString("NAME"));
        // member.setRegisterDate(rs.getTimestamp("REGDATE"));
 
        return member;
    }
}
cs


(5) queryForObject() 메소드


queryForObject() 메소드는 쿼리 실행 결과가 하나뿐인 SELECT 쿼리에 사용한다.

실행 결과가 없거나 2개 이상이면 IncorrectResultSizeDataAccessException 이 발생한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 결과가 하나 밖에 없는 경우 queryForObject() 메소드 사용
public Member selectOneMember() {
    final String selectOneMemberQuery = "select * from MEMBER where ID=?";
    Member member = jdbcTemplate.queryForObject(selectOneMemberQuery, new MemberRowMapper(), 100);
    return member;
}
 
// queryForObject 를 사용해서 int 결과를 받으려면 Integer.class 를 이용
// queryForInt() 메소드가 있었지만 스프링 3.?.? 버전에서 사라졌다.. ㅠㅠ
public int count() {
    final String countQuery = "select count(*) from MEMBER";
    Integer count = jdbcTemplate.queryForObject(countQuery, Integer.class);
    return count;
}
cs


실행 결과를 받기 위해 RowMapper 인터페이스를 구현한 클래스를 사용하고 있다.

물론 익명 클래스를 사용해도 되지만.. 개인적으로 복잡하게 보여서 익명 클래스를 안 좋아하는.. 

그리고  RowMapper 클래스를 만들어 두면 필요한 곳마다 익명 클래스를 사용할 필요가 없어 중복 코드가 줄어든다.


자주 사용하는 queryForObject() 메소드


 T queryForObject(String sql, class<T> requiredType)

 T queryForObject(String sql, class<T> requiredType, Object... args)

 T queryForObject(String sql, RowMapper<T> rowMapper)

 T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)


대부분의 파라미터의 사용법은 query() 메소드와 같고, 

class<T> requiredType 파라미터가 있기 때문에 Integer.class 를 사용해서 정수 결과를 리턴할 수 있다.

더 많은 queryForObject() 메소드를 보고 싶으면 JdbcTemplate 클래스를 까보면 된다.

클래스를 까볼 때마다 느끼는 거지만.. 언어나 라이브러리 프레임워크 만드는 개발자들은 괴물인가...


(6) update() 메소드


INSERT, UPDATE, DELETE 쿼리를 위한 메소드이다.


자주 사용하는 update() 메소드


 int update(String sql)

 int update(String sql, Object... args)

 int update(String sql, Object[] args)


파라미터로 sql 과 쿼리의 인덱스를 사용할 수 있다.

잠시 착각한 적이 있는데 INSERT 쿼리에 int insert() 메소드를 쓰면 안 된다.. int update(INSERT 쿼리)이다.

실행 결과 값으로 int 를 반환하고 있는데, 쿼리 실행 후 변경된 건수를 반환한다.

예를 들어 UPDATE 로 수정한 DB 행이 두 행이면 2, 한 행이면 1, 실패하면 0.


(7) PreparedStatementCreator Interface


query(), update() 메소드에 쿼리의 인덱스 파라미터 값을 Object 타입의 배열을 넣거나, 직접 값을 나열할 수 있으나,

PreparedStatementCreator Interface 를 사용해서 PreparedStatement 객체를 생성하고

PreparedStatement 클래스의 set 메소드를 사용해서 인덱스 파라미터 값을 설정할 수도 있다.

개인적으로 진짜 필요하지 않으면 안 쓸 것 같다..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface PreparedStatementCreator {
 
    /**
     * Create a statement in this connection. Allows implementations to use
     * PreparedStatements. The JdbcTemplate will close the created statement.
     * @param con Connection to use to create statement
     * @return a prepared statement
     * @throws SQLException there is no need to catch SQLExceptions
     * that may be thrown in the implementation of this method.
     * The JdbcTemplate class will handle them.
     */
    PreparedStatement createPreparedStatement(Connection con) throws SQLException;
 
}
cs


Connection 객체가 파라미터에 들어가 있다. 즉 Connection 객체를 이용해서 PreparedStatement 객체를 생성한다.


PreparedStatementCreator 인터페이스를 파라미터로 갖는 메소드


 List<T> query(PreparedStatementCreator psc, RowMapper<T> rowMapper) 

 int update(PreparedStatementCreator psc)

 int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// update() 메소드에 인덱스 파라미터 값을 직접 전달하지 않고,
// PreparedStatementCreator 인터페이스를 사용해서 전달
// 익명 클래스 사용, PreparedStatementCreator 인터페이스를 구현한 클래스를 사용해도 된다.
public void insert(final Member member) {
    jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
            final String insertQuery = "insert into MEMBER(EMAIL,PASSWORD,NAME,REGDATE) values (?,?,?,?)";
            // 파리미터로 전달받은 Connection을 이용해서 PreparedStatement 객체 생성            
            PreparedStatement pstmt = con.prepareStatement(insertQuery);
            // 인덱스 파리미터 값 설정
            pstmt.setString(1, member.getEmail());
            pstmt.setString(2, member.getPassword());
            pstmt.setString(3, member.getName());
            pstmt.setTimestamp(4new Timestamp(member.getRegisterDate().getTime()));
            // 생성한 PreparedStatement 객체 반환
            return pstmt;
        }
    });    
}
// // PreparedStatementCreator 인터페이스를 사용하지 않고 직접 파라미터 전달
// public void insert(Member member) {
// final String insertQuery = "insert into MEMBER(EMAIL,PASSWORD,NAME,REGDATE) values (?,?,?,?)";
// // 배열을 사용해서 파라미터 값 넘겨 주기
// Object[] args = {member.getEmail(), member.getPassword(), member.getName(), new Timestamp(member.getRegisterDate().getTime())};
// jdbcTemplate.update(insertQuery, args);
// }
cs



(8) KeyHolder Interface


MySQL 처럼 키 값을 자동으로 증가시켜 주는 auto increment 기능을 사용할 수 있는 데이터베이스에서

자동 증가 칼럼을 가지는 테이블을 만들었을 경우, INSERT 쿼리 실행문에 자동 증가 칼럼은 보통 지정하지 않는다.

이 때, 쿼리 실행 후 자동으로 생성된 키 값을 알고 싶을 때 사용할 수 있는 것이 KeyHolder 인터페이스이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void insert(final Member member) {
    // KeyHolder 인터페이스, 자동 생성 키 값 구하는데 사용, update() 메소드의 두 번째 파라미터로 지정
    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbcTemplate.update(new PreparedStatementCreator() {
        @Override
        public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
            final String insertQuery = "insert into MEMBER(EMAIL,PASSWORD,NAME,REGDATE) values (?,?,?,?)";
            // new String[] {"ID"} 자동생성되는 키 칼럼 목록 지정, mySQL의 auto increment key
            // PreparedStatement가 실행된 후, 자동 생성된 키 값은 KeyHolder에 보관된다.
            PreparedStatement pstmt = con.prepareStatement(insertQuery, new String[] {"ID"});
            pstmt.setString(1, member.getEmail());
            pstmt.setString(2, member.getPassword());
            pstmt.setString(3, member.getName());
            pstmt.setTimestamp(4new Timestamp(member.getRegisterDate().getTime()));
            return pstmt;
        }
    }, keyHolder);
    // KeyHolder 인터페이스의 getKey() 메소드로 키 값 얻어옴, 반환 타입은 Number
    Number keyValue = keyHolder.getKey();
    // Number 타입의 키 값을 Long 타입으로 형변환
    member.setId(keyValue.longValue());
}
 
// PreparedStatement 생성자의 두 번째 파라미터는 자동 생성되는 키 칼럼 목록을 지정할 때 사용된다.
// Connetcion 인터페이스에 정의되어 있음
// PreparedStatement prepareStatement(String sql, String columnNames[]) throws SQLException;
cs



3. JdbcTemplate 의 예외처리

JdbdTemplate 의 DB 처리 메소드를 사용하면서 try-catch 를 사용하지 않았다.
DB 처리나 파일 처리 같이 예외가 발생할 수 있는 코드를 작성할 때는 반드시 try-catch 처리를 해 줘야 한다.
스프링은 JDBC API 를 사용하는 과정에서 발생하는 SQLException 을 적절하게 DataAccessException 으로 변환해서 발생 시킨다.
그렇기 때문에, JDBC, hibernate, JPA 모두 예외처리를 동일하게 할 수 있다.

그리고, DataAccessException 은 RuntimeException 이기 때문에 필요한 경우에만 예외처리를 해주면 된다.

1
2
3
4
5
6
7
public abstract class DataAccessException extends NestedRuntimeException {
}
 
public abstract class NestedRuntimeException extends RuntimeException {
}
 
// 자바의 RuntimeException 을 상속하고 있다. 
cs


728x90
반응형

'Programming > Spring' 카테고리의 다른 글

Spring MVC 기본 설정  (0) 2017.08.23
Spring JdbcTemplate Transaction  (0) 2017.08.22
데이터 변환  (0) 2017.08.20
다국어 처리  (0) 2017.08.20
파일 업로드 / 예외처리  (0) 2017.08.20

댓글