[Java] JDBC & JDBCTemplate & NamedParameterJDBCTemplate
목차
개요
JDBC와 JDBCTemplate 더불어 NamedParameterJDBCTemplate에 대한 간단한 사용법 모음
1. Test Schema
이 글은 JDBC를 사용하여 DataBase에 SQL을 날리는 예제를 적을 것이므로 SQL을 날리게 되는 테이블을 다음과 같이 정의한다.
CREATE TABLE `Member` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(20) NOT NULL,
`nickname` varchar(20) NOT NULL,
`birthday` date NOT NULL,
`createdAt` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2. JDBC
Java에서는 DataBase와 데이터를 입출력하기 위해 JDBC를 제공한다. JDBC는 DBMS의 종류와 상관없이 동일하게 사용할 수 있는 클래스와 인터페이스로 구성된다.
2.1 JDBC Interface and Class
- DriverManager 클래스는 JDBC Driver를 관리하며 DB와 연결해서 Connection 구현 객체를 생성한다.
- Connection 인터페이스는 Statement, CallableStatement, PreparedStatement 구현 객체를 생성하며, 트랜잭션 처리 및 DB 연결을 끊을 때 사용한다.
- Statement 인터페이스는 SQL의 DDL과 DML을 실행할 떄 사용한다. 주로 변경되지 않는 정적 SQL문을 실행할 때 사용한다.
- PreparedStatement는 Statement와 동일하게 SQL의 DDL, DML문을 실행할 때 사용한다. 차이점은 매개변수화된 SQL문을 사용할 수 있기 때문에 편리성과 보안성이 좋다. 그래서 Statement보다는 PreparedStatement를 주로 사용한다.
- CallableStatement는 DB에 저장되어있는 프로시저와 함수를 호출할 때 사용한다.
- ResultSet은 DB에서 가져온 데이터를 읽을 때 사용한다.
2.2 JDBC 사용법
2.2.1 Class.forName으로 Driver 로딩
JDBC를 통해 DB와 연결하기 위해 가장 먼저해야할 작업은 JDBC Driver를 메모리로 로딩하는 것이다. Class.forName()를 통해 해당 작업을 수행할 수 있으며 Class.forName()은 문자열로 주어진 JDBC Driver 클래스를 Build Path에서 찾고 메모리로 로딩을 수행한다.
Class.forName("com.mysql.cj.jdbc.Driver");
이 과정에서 JDBC Driver 클래스의 static 블록이 실행되면서 DriverManager에 JDBC Driver 객체를 등록하게 된다. 만약 해당 Driver를 찾을 수 없다면 ClassNotFoundException이 발생한다.
2.2.2 Connection interface를 통해 DB 연결 설정
이후 DB와 연결하기 위한 Connection interface를 사용한다.
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:9901/sample", "root", "1234");
Connection 정보는 사용하는 DataBase 벤더에 따라 다르다는 것에 주의가 필요
2.2.3 PreparedStatement를 이용해 DML 질의
PreparedStatement를 통해 SQL 질의를 실행할 수 있다. PreparedStatement는 Column의 순서로도 접근할 수 있고 Column명으로도 접근할 수 있다. 이 예제에서는 Column 명으로 접근하는 방식을 사용했다. 앞서 언급했듯 SELECT 문과 같이 사용하는 경우 ResultSet을 통해 DB에서 가져온 데이터를 읽을 수 있다.
PreparedStatement pstmt = connection.prepareStatement("SELECT * FROM Member");
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("Id") + "," + rs.getString("email") + "," + rs.getString("nickname"));
}
예를 들어 잘못 접근하는 데이터 타입은 “SQLDataException”이 일어난다. 예를 들어 nickname은 String이어야 하지만 Int로 접근하는 경우가 그 예이다.
Exception in thread "main" java.lang.RuntimeException: java.sql.SQLDataException: Cannot determine value type from string 'string'
SQL 작업은 여러 매개변수를 추가로 사용하는 경우도 빈번하다. INSERT, UPDATE, DELETE가 예시다. 이 경우에는 PreparedStatement에 다음과 같이 매개변수를 추가로 지정할 수 있다.
String insertSql = "INSERT INTO sample.`Member` (email, nickname, birthday, createdAt) VALUES(?, ?, ?, ?);";
PreparedStatement pstmt = connection.prepareStatement(insertSql);
pstmt.setString(1, "test@test.com");
pstmt.setString(2, "test");
pstmt.setString(3, "2001-01-01");
pstmt.setString(4, "1999-01-01");
int rows = pstmt.executeUpdate();
3. JDBC Template
JDBC의 불편함을 해결하기위해 JDBC Template을 사용할 수 있다. Springframework package를 통해 JDBC Template을 사용할 수 있다.
3.1 JDBC의 불편함 ?
앞서 정리해 놓은 JDBC에서 발생하는 불편함은 “구조적인 반복”이다. 다음은 JDBC를 사용해 DataBase로부터 데이터를 얻기 위한 과정이다.
- Class.forName으로 Driver 로딩 명시
- Connection 인터페이스로 DB 연결명시
- PreparedStatement를 통해 SQL 질의, 이후 ResultSet을 통해 Column 접근
- 2번 3번 과정에서 try ~ catch를 통해 Exception 처리, 이 과정이 끝나면 DataBase 연결에 사용된 Resource를 Close처리
SQL 하나를 날리기 위해 여러 작업들을 반복해야 한다.
JDBC Template은 이러한 작업들 간편하게 다룬다. 그러나 이 글에서 정리하려는 jdbc template은 사용법에 초점을 맞춘다.
3.2 intelli-j에서 JDBC Template 설정
JDBC Template은 springframework 패키지에서 제공한다. Spring Initializer를 통해 Spring Boot 프로젝트를 생성할 때 “Spring Data JDBC”를 Dependencies에 추가하면 JDBC Template 사용이 가능하다. 하지만 라이브러리 하나 설치하려고 Spring Boot Project를 통째로 생성하는 건 과하다 싶은 생각이 든다.
Intelli-j에서 위와 같이 “springframwork.spring.jdbc”를 설치하면 SpringBoot Project를 생성하지 않고도 jdbc template이 사용 가능하다. intelli-j에서 라이브러리를 추가하는 방법은 이 링크를 참조하자.
라이브러리 설치가 끝났으면 다음 package를 import 한 뒤 jdbc template이 사용가능하다.
import org.springframework.jdbc.core.JdbcTemplate;
3.3 DriverManagerDataSource를 이용해 연결 설정
이 글에서는 Spring Boot를 통해 JDBC Template을 사용하는 것이 아니기 때문에 JDBC Template 클래스를 초기화할 때 DataSource interface의 구현 클래스를 지정했다.
Spring/SpringBoot에서 DataSource interface의 구현 클래스는 대표적으로 JDBC DrvierManager를 기반으로 한 DriverManagerDataSource와 HikariCP 커넥션 풀을 기반으로 한 HikariDataSource가 있다고 한다.
이 중에서 따로 설치가 필요 없는 DriverManagerDataSource를 이용해 다음과 같이 JDBC Template을 사용할 수 있었다.
DriverManagerDataSource driverManager = new DriverManagerDataSource();
driverManager.setDriverClassName("com.mysql.cj.jdbc.Driver");
driverManager.setUrl("jdbc:mysql://127.0.0.1:9901/sample");
driverManager.setUsername("root");
driverManager.setPassword("1234");
JdbcTemplate jdbcTemplate = new JdbcTemplate(driverManager);
3.4 Row Mapper를 이용한 결과 매핑
JDBC를 이용할 때는 ResultSet을 이용해 Column에 하나씩 접근하는 방법을 사용했지만 JDBC Template을 이용할 떄는 RowMapper 클래스를 통해 SQL 실행 후 해당 결과를 Java 클래스로 변환 후 사용할 수 있다.
import org.springframework.jdbc.core.RowMapper;
예시로 정의한 DDL을 Java 클래스로 정의하면 다음과 같다.
class Member {
final private Long id;
final private String email;
final private String nickname;
final private LocalDateTime birthday;
final private LocalDateTime createdAt;
...
}
다음은 Member클래스에 대해 RowMapper를 이용한 예시다.
RowMapper<Member> memberRowMapper = new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet resultSet, int i) throws SQLException {
return new Member(
resultSet.getLong("id"),
resultSet.getString("email"),
resultSet.getString("nickname"),
resultSet.getObject("birthday", LocalDateTime.class),
resultSet.getObject("createdAt", LocalDateTime.class)
);
}
};
RowMapper<Member> memberRowMapper = (ResultSet resultSet, int row) -> new Member(
resultSet.getLong("id"),
resultSet.getString("email"),
resultSet.getString("nickname"),
resultSet.getObject("birthday", LocalDateTime.class),
resultSet.getObject("createdAt", LocalDateTime.class)
);
3.5 SQL 관련 함수들
아래는 jdbcTemplate을 이용해 DataBase에 query를 날릴 때 사용하는 예제들이다.
3.5.1 jdbcTemplate.query(sql, rowMapper) : 결과 집합 읽기
결과 집합, 즉 List로 읽어 들일 때는 query()를 사용한다.
String sql = "SELECT * FROM Member;";
List<Member> members = jdbcTemplate.query(sql, memberRowMapper);
for (Member m : members) {
System.out.println(m.toString());
}
// OutPut
**Member{id=1, email='string', nickname='string'}
Member{id=2, email='string', nickname='string'}
Member{id=3, email='string', nickname='test'}
...**
조회 결과가 없는 경우 빈 결과 집합을 가지고 있다.
3.5.2 jdbcTemplate.queryForObject(sql, rowMapper) : 단일 결과 읽기
단일 결과를 읽어올 때는 queryForObject()를 사용한다.
String sql = "SELECT * FROM Member WHERE id =1;";
Member member = jdbcTemplate.queryForObject(sql, memberRowMapper);
if (member != null) {
System.out.println(member.toString());
}
결과가 없는 경우 “EmptyResultDataAccessException”이 발생한다.
Exception in thread "main" org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
결과가 둘 이상인 경우 “IncorrectResultSizeDataAccessException”이 발생한다.
Exception in thread "main" org.springframework.dao.IncorrectResultSizeDataAccessException: Incorrect result size: expected 1, actual 7
3.5.3 jdbc.update(sql, …args) : INSERT, UPDATE, DELETE 시
INSERT, UPDATE, DELETE 시에는 SQL에 Parameter를 넣어야 한다. 이는 update()를 사용해서 수행할 수 있다. 아래 예시는 INSERT 문을 통한 예시이다.
String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(?, ?, ?, ?)";
int rows = jdbcTemplate.update(
insertSQL,
"test2",
"test2@email.com",
"2002-01-01",
"2002-02-02"
);
4. NamedParameterJDBCTemplate
앞서 jdbcTemplate의 update() 메서드에서 SQL 질의 시 특정 파라미터에 대응하는 값은 “?”라는 place holder를 이용하고 해당 Mark에 대응하는 Value를 지정했다.
순서에 따라서 값을 넣어줘야 하므로 가독성이 다소 떨어진다고 볼 수 있는데 NamedParameterJDBCTemplate을 사용하면 Key, Value로 형태로 제어가 가능하다.
4.1 NamedParameterJDBCTemplate 초기화
Spring/SpringBoot를 사용하지 않는 방식에서 NamedParamaterJDBCTemplate를 초기화시키는 방법은 JdbcTemplate과 방식과 같다.
NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(driverManager);
4.2 BeanPropertySqlParameterSource : Java Class를 parameter로 넘기기
BeanPropertrySqlParameterSource는 Java Class의 instance를 parameter로 사용할 수 있게 만드는 방법이다.
Member member = new Member(
22L,
"test3@email.com",
"test3",
LocalDateTime.now(),
LocalDateTime.now()
);
SqlParameterSource params = new BeanPropertySqlParameterSource(member);
단, Parameter로 넘기고 싶은 클래스의 인스턴스에 Getter가 구현되어있어야 한다.
4.3 MapSqlParameterSource : Key, Value를 명시하기
MapSqlParamaterSource를 사용하면 이를 Key, Value를 이용해 입력값을 명시적으로 다룰 수 있다. 아래는 INSERT 문을 질의하는 예시다.
NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(driverManager);
String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(:email, :nickname, :birthday, :createdAt)";
MapSqlParameterSource mapSqlParameterSource = new MapSqlParameterSource()
.addValue("email", "test2@test2.com")
.addValue("nickname", "test2")
.addValue("birthday", "1999-01-01")
.addValue("createdAt", "1999-01-01");
namedParameterJdbcTemplate.update(insertSQL, mapSqlParameterSource);
4.4Keyholder : INSERT 후 PK 얻기
데이터를 DB에 삽입하고 난 후 생성된 데이터에 대해 PK를 얻는 방법이 필요하다면 KeyHolder를 사용할 수 있다. 그러나 이 방법은 PK 가 AUTO_INCREMENT로 정의된 Table에 대해 가능한 듯하다.
String insertSQL = "INSERT INTO Member (email, nickname, birthday, createdAt) VALUES(:email, :nickname, :birthday, :createdAt)";
SqlParameterSource params = new BeanPropertySqlParameterSource(
new Member(
"test4@email.com",
"test4",
LocalDateTime.now(),
LocalDateTime.now()
)
);
KeyHolder keyHolder = new GeneratedKeyHolder();
namedParameterJdbcTemplate.update(insertSQL, params, keyHolder);
long key = keyHolder.getKey().longValue();
System.out.println(key);