Chapter10. 스프링과 JDBC를 사용하여 데이터베이스 사용하기
데이터 액세스
- 데이터 액세스 프레임워크 초기화
- 커넥션 오픈
- 다양한 예외처리
- 연결 종료
스프링 데이터 액세스
- 다양한 기술들로 결합된 데이터 액세스 프레임워크 제공
ex) JDBC, 하이버네이트, JPA(Java Persistence API), 퍼시스턴스 프레임워크, NoSQL 데이터베이스 등- 퍼시스턴스 코드에서 데이터 액세스를 제거
- 하위 레벨(low-level) 데이터 액세스 작업시 스프링을 사용하여 데이터 관리를 수행
이번 장에서는 JDBC를 위한 스프링 지원 사항에 대해서 중점적으로 살펴본다.
10-1 스프링의 데이터 액세스 철학
저장소 (DAO, Data-Acess Object)
- 퍼시스턴스 로직이 산란(scattering)되는 것을 피하기 위해서 데이터 액세스는 해당 태스크에 집중된 하나 이상의 컴포넌트로 이루어진다.
- 애플리케이션이 저장소 내 특정 데이터 액세스 전략에 커플링되는 것을 막기 위해 인터페이스를 사용하여 기능을 외부로 제공한다.
- [그림10.1] 서비스 객체는 자신의 데이터 액세스를 처리하지 않는다. 대신 데이터 액세스를 저장소에 위임한다. 저장소의 인터페이스는 서비스 객체에 느슨하게 커플링한다. (p341)
인터페이스와 스프링
- 서비스 객체는 인터페이스를 통해서 저장소에 액세스 한다. (테스트가 용이해짐)
- 데이터 액세스 계층은 퍼시스턴스 기술에 상관없이 액세스 한다.
- 데이트 액세스 메소드만 인터페이스를 통해서 노출시킨다. (애플리케이션 나머지 부분에 최소한의 영향)
- 스프링은 데이터 액세스 계층을 모든 퍼시스턴스 옵션을 가지는 일관적인 예외 계층 구조를 사용하여 애플리케이션의 나머지와 격리한다.
10-1-1 스프링의 데이터 액세스 예외 계층 구조
SQLExcetion은 무쓸모
- 데이터베이스 액세스 동안 무엇인가 문제가 있다는 사실을 의미한다.
- 하지만 무엇이 잘못되었고 어떻게 처리해야 하는지에 대한 설명은 거의 없다.
- 대부분 catch 블록에서 복구가 불가능한 치명적인 상황이다.
- 예외 발생 근본 원인에 대한 상세 정보를 획득하기 위해 예외가 가진 모든 프로퍼티를 낱낱이 살펴봐야 한다.
- 하이버네이트는 다양한 예외를 제공하지만 하이버네이트에 특화되어 있다. (플랫폼 독립적인 예외로 다시 던져야 함)
- JDBC의 예외 계층 구조는 너무 포괄적이다.
스프링의 독립적인 예외
- 예외가 발생한 문제상황을 잘 설명하는 다양한 데이터 액세스 예외들 제공
ex) [표10.1] JDBC 예외 vs 스프링 데이터 액세스 예외 (p344)- 퍼시스턴스 솔루션에 독립적인 일관성 있는 예외 (퍼시스턴스 기술을 데이터 액세스 계층 내에 캡슐화)
스프링의 비검사형(unchecked) 예외
- 스프링 데이터 액세스 예외는 DataAccessException을 확장한 것이다.
- DataAcessException은 비검사형 예외 이다.
- 스프링이 던지는 모든 데이터 액세스 예외는 반드시 잡아서 처리할 필요가 없다. (예외를 잡을지 말지는 개발자가 결정)
- 이러한 이점을 취하기 위해서는 반드시 스프링이 제공하는 데이터 액세스 템플릿을 이용해야 한다.
참고: 역자 한마디 "스프링의 핵심 장점 중 하나는 검사형 예외를 비검사형 예외로 변환해 주는 것" (p345)
- 검사형(checked) 예외
- throws 된다고 선언된 메소드는 호출시 반드시 try-catch 블록으로 감싸야만 컴파일 가능
- ex) java.sql.SQLException, java.io.IOException 등
- 비검사형(unchecked) 예외
- try-catch 블록을 사용하지 않아도 컴파일이 되는 예외 (런타임 예외, 실행시간 예외)
- ex) java.lang.RuntimeException, java.lang.Error를 확장한 NullPointException, IllegalArgumentException 등
- 스프링은 비검사형 예외를 강력히 지지 한다.
- 역자 또한 자바 API를 포함한 많은 라이브러리가 검사형 예외를 남용한다고 생각한다.
- 스프링은 많은 예외가 catch 블록 내에서 해결할 수 없는 문제 때문에 발생한다고 보고 있다.
- catch 블록의 작성을 강제하는 대신 비검사형 예외를 사용하도록 촉진한다.
- 여타 프레임워크가 가진 검사형 예외를 비검사형 예외로 변환해주는 프레임워크는 스프링이 거의 유일하다.
10-1-2 데이터 액세스 템플릿화
ex) 비행기 수하물 이동
템플릿 메소드(Template Method) 패턴
- 어떤 절차의 골격을 정의
- 어떤 절차의 구현 종속적인 부분을 특정 인터페이스에 위임
- 인터페이스를 다르게 구현함으로써 위임된 부분에 특화된 부분을 정의
- 스프링이 데이터 액세스에 적용한 패턴
스프링 데이터 액세스 절차
- 고정된 단계와 가변적인 단계를 템플릿(tempalte)과 콜백(callback)이라는 두 가지의 별도의 클래스로 분리
- 템플릿 클래스: 트랜잭션 제어, 자원 관리 및 예외 처리와 같은 데이터 액세스의 고정된 부분 담당
- 콜백(callback) 클래스: 질의객체(statement) 생성, 파라미터 바인딩, 질의 결과 추출과 변환 등
- 스프링의 데이터 액세스 탬플릿 클래스는 공통적인 데이터 액세스 의무에 대한 책임을 진다.
- 애플리케이션에 특화된 작업을 처리하기 위해서 템플릿 클래스는 맞춤형 콜백 객체를 호출한다.
- 데이터 액세스 로직에만 집중할 수 있다.
[표10.2] 스프링 데이터 액세스 템플릿 종류 (p348)
스프링은 퍼시스턴스 플랫폼에 따라 선택할 수 있는 다양한 템플릿을 제공한다.
템플릿 클래스 (org.springframework.* ) |
용도 |
---|---|
jca.cci.core.CciTemplate | JCA CCI 연결 |
jdbc.core.JdbcTemplate | JDBC 연결 |
jdbc.core.namedparam.NamedParameterJdbcTemplate | 명명된 파라미터(named parameter)가 지원되는 JDBC 연결 |
jdbc.core.simple.SimpleJdbcTemplate | 자바5를 활용해서 더 쉬워진 JDBC 연결 (스프링3.1에선 없어짐) |
orm.hibernate3.HibernateTemplate | Hibernate 3.x 세션 |
orm.ibatis.SqlMapClientTemplate | iBATIS SqlMap 클라이언트 |
orm.jdo.JdoTemplate | JDO(Java Data Object) 구현체 |
orm.jpa.JpaTemplate | JPA(Java Persistence API) 엔티티 관리자 |
- 10장 JDBC: 가장 기본적인 방법
- 11장 하이버네이트, JPA: POJO 기반 ORM(Object-Relational Mapping) 솔루션
- 12장 NoSQL: 비스키마(schemaless) 데이터
10.2 데이터 소스 설정
사용자가 사용하는 스프링 지원 데이터 액세스가 어떤 형태라도 데이터 소스에 대한 레퍼런스 설정이 필요함
- JDBC 드라이버를 통해 선언된 데이터 소스
- JNDI에 등록된 데이터 소스
- 커넥션 풀링(pooling)하는 데이터 소스
- 상용 application에서는 커넥션 풀에서 커넥션을 획득하는 데이터 소스를 권장
10.2.1 JNDI 데이터 소스 이용
- 스프링 애플리케이션은 대부분 JEE 애플리케이션 서버에 배포 되어서 사용됨.
- 이런 서버들은 WebSphere나 JBoss 또는 Tomcat 과 같은 웹컨테이너 역할을 함
JNDI 설명 (Java Naming and Directory Interface)
- server.xml 등 서버에 DataSource 에 대한 몇몇 설정이 필요함
- 장점
- 데이터 소스가 애플리케이션 외부에서 관리
- 애플리케이션에서는 데이터소스에 접근할 시점에 단순히 데이터소스를 요청하기만 하면 됨
- 서버에 의해 관리되는 데이터소스는 향상된 성능을 위해 풀링 기능을 갖기도 하고 시스템 관리자가 운영시 동적으로 교체 가능함
애플리케이션 설정 방식
<jee:jndi-lookup id="datasource" jndi-name="/jdbc/SplitterDS" resource-ref="true" />
@Bean
public JndiObjectFactoryBean dataSource() {
JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
jndiObjectFB.setJndiName("jdbc/SplitterDS");
jndiObjectFB.setResourceRef(true);
jndiObjectFB.setProxyInterface(javax.sql.DataSource.class.class);
return jndiObjectFB;
}
- 자바 설정을 사용한다면 JNDI에서 DataSource를 검색하기 위해서 JndiObjectFactoryBean을 사용함
10.2.2 풀링 기능이 있는 데이터 소스 사용하기
JNDI 를 사용하지 않을 경우, 풀링 기능이 있는 데이터 소스를 스프링에 직접 설정 가능함
- 스프링 자체가 풀링 기능을 제공하지는 않음
- 오픈 소스를 사용해서 풀링 가능한 데이터 소스 활용을 할 수 있음
- Apache 공통 DBCP (http://jakarta.apache.org/commons/dbcp)
- c3p0 (http://sourceforge.net/c3p0)
- BoneCP (http://jolbox.com)
<bean id="datasource" class="org.apache.commons.dbcp.BasicDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~spitter"
p:username="sa"
p:password=""
p:initailSize="5"
p:maxActive="10" />
@Bean
public BasicDataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~spitter");
ds.setUsername("sa");
ds.setPassword("");
ds.setInitailSize(5);
ds.setMaxActive(10);
return ds;
}
BasicDataSource 의 풀 프로퍼티
풀 설정 프로퍼티 | 설명 |
---|---|
intialSize | 해당 풀이 시작될 때 생성할 커넥션 수 |
maxActive | 해당 풀에서 동시에 제공할 수 있는 최대 커넥션 수. 0은 무제한을 나타낸다 |
maxIdle | 해당 풀에서 동시에 휴면 상태로 유지될 수 있는 최대 커넥션 수. 0은 무제한을 나타낸다 |
maxOpenPreparedStatements | 질의 객체(statement) 풀에서 동시에 제공할 수 있는 최대 PreparedStatement 의 수. 0은 무제한을 나타낸다ㅏ |
maxWait | 해당 풀에 커넥션을 요청했을 때(제공 받을 수 있는 커넥션이 없어서) 대기 가능한 최대 시간. 이 시간이 지나면 예외가 발송핸다. 1은 무한히 대기함을 의미한다. |
minEvictableIdleTimeMillis | 해당 풀에서 커넥션을 제거하기 전에 휴면 상태로 남아 있을 수 있는 시간 |
minIdle | 해당 풀에서 커넥션이 휴면 상태로 유지될 수 있는 최소 커넥션 수 |
poolPreparedStatements | PreparedStatement 의 풀링(pooling) 여부를 나타내는 boolean 값 |
[참고] Statement vs PreparedStatement
Statement
executeQuery() 나 executeUpdate() 를 실행하는 시점에 파라미터로 SQL문을 전달하는데, 이 때 전달되는 SQL 문은 완성된 형태로 한눈에 무슨 SQL 문인지 파악하기 쉽다. 하지만, 이 녀석은 SQL문을 수행하는 과정에서 매번 컴파일을 하기 때문에 성능상 이슈가 있다. ( 이 컴파일을 Parsing 한다고도 표현한다. )
String sql = "select * from users where _id=1"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery( sql );
PreparedStatement
미리 컴파일 되기 때문에 Statement 에 비해 성능상 이점이 있다.
String sql = "select * from users where _id=?"; PreparedStatement pstmt = conn.prepareStatement( sql ); pstmt.setInt( 1, 1 ); ResultSet rs = pstmt.executeQuery();
10.2.3 JDBC 드라이버 기반 데이터 소스
스프링에 설정할 수 있는 가장 단순한 데이터 소스는 JDBC 드라이버를 통해 정의 (스프링 내 org.springframework.jdbc.datasource 패키지 안에 존재)
JDBC Driver | 설명 | Pooling 기능 제공 |
---|---|---|
DriverManagerDataSource | 애플리케이션이 커넥션 요청 시마다 새로운 커넥션을 반환 | 풀링 지원 안함 |
SimpleDriverDataSource | OSGi 컨테이너와 같이 특정 환경에서 발생할 수 있는 클래스 로딩 문제를 극복하기 위해 사용하는 것을 제외하면 DriverManagerDataSource 와 동일 | 풀링 지원 안함 |
SingleConnectionDataSource | 항상 동일 커넥션을 반환 | 오직 한 커넥션만을 풀링 |
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="org.h2.Driver"
p:url="jdbc:h2:tcp://localhost/~spitter"
p:username="sa"
p:password="" />
- 풀링 설정 프로퍼티가 존재 안함
@Bean
public BasicDataSource dataSource() {
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("org.h2.Driver");
ds.setUrl("jdbc:h2:tcp://localhost/~spitter");
ds.setUsername("sa");
ds.setPassword("");
return ds;
}
Guide
- DriverManagerDataSource, SingleConnectionDataSource : 작은 규모의 애플리케이션과 개발 단꼐에서는 데이터 소스가 좋은 선택이 될 수 있지만,
- 상용 애플리케이션에서는 심각한 성능 저하를 고려하여 커넥션 풀링이 있는 데이터 소스를 사용할 것을 강력하게 권장함
10.2.4 임베디드 데이터 소스 사용하기
- 사용자가 사용할 추가 데이터 소스로 임베디드 데이터베이스가 있음.
- 애플리케이션이 연결하는 독립 데이터베이스 서버 대신에 임베디드 데이터 베이스가 애플리케이션 중 하나로 동작
<jdbc:embedded-database> type 프로퍼티를 사용함
10.2.5 데이터 소스 선택을 위한 프로파일링하기
- 개발, 사용 단계에서의 데이터 소스 선택이 필요한 상황이 있음
- 런타임 시 스프링의 데이터 소스 선택을 위한 프로파일링이 필요
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
@Configuration
public class DataSourceConfiguration {
@Profile("dev")
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Profile("production")
@Bean
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
10.3 스프링과 JDBC
10.3.1 지저분한 JDBC 코드 해결
- cf. jdbc example
//STEP 1. Import required packages
import java.sql.*;
public class JDBCExample {
// JDBC driver name and database URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost/STUDENTS";
// Database credentials
static final String USER = "username";
static final String PASS = "password";
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try{
//STEP 2: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
//STEP 3: Open a connection
System.out.println("Connecting to a selected database...");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
System.out.println("Connected database successfully...");
//STEP 4: Execute a query
System.out.println("Inserting records into the table...");
stmt = conn.createStatement();
String sql = "INSERT INTO Registration " +
"VALUES (100, 'Zara', 'Ali', 18)";
stmt.executeUpdate(sql);
sql = "INSERT INTO Registration " +
"VALUES (101, 'Mahnaz', 'Fatma', 25)";
stmt.executeUpdate(sql);
sql = "INSERT INTO Registration " +
"VALUES (102, 'Zaid', 'Khan', 30)";
stmt.executeUpdate(sql);
sql = "INSERT INTO Registration " +
"VALUES(103, 'Sumit', 'Mittal', 28)";
stmt.executeUpdate(sql);
System.out.println("Inserted records into the table...");
}catch(SQLException se){
//Handle errors for JDBC
se.printStackTrace();
}catch(Exception e){
//Handle errors for Class.forName
e.printStackTrace();
}finally{
//finally block used to close resources
try{
if(stmt!=null)
conn.close();
}catch(SQLException se){
}// do nothing
try{
if(conn!=null)
conn.close();
}catch(SQLException se){
se.printStackTrace();
}//end finally try
}//end try
System.out.println("Goodbye!");
}//end main
}//end JDBCExample
package com.vaannila.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.vaannila.domain.Forum;
public class JDBCForumDAOImpl implements ForumDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void insertForum(Forum forum) {
/**
* Specify the statement
*/
String query = "INSERT INTO FORUMS (FORUM_ID, FORUM_NAME, FORUM_DESC) VALUES (?,?,?)";
/**
* Define the connection and preparedStatement parameters
*/
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
/**
* Open the connection
*/
connection = dataSource.getConnection();
/**
* Prepare the statement
*/
preparedStatement = connection.prepareStatement(query);
/**
* Bind the parameters to the PreparedStatement
*/
preparedStatement.setInt(1, forum.getForumId());
preparedStatement.setString(2, forum.getForumName());
preparedStatement.setString(3, forum.getForumDesc());
/**
* Execute the statement
*/
preparedStatement.execute();
} catch (SQLException e) {
/**
* Handle any exception
*/
e.printStackTrace();
} finally {
try {
/**
* Close the preparedStatement
*/
if (preparedStatement != null) {
preparedStatement.close();
}
/**
* Close the connection
*/
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
/**
* Handle any exception
*/
e.printStackTrace();
}
}
}
}
- 간단한 테이블 입력 하나에도 너무나 많은 Exception 코드가 들어 있다.
- 실질적인 작업 수행은 단순한 몇개의 줄로만 이루어진다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="forumDAO" class="com.vaannila.dao.ForumDAOImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
10.3.2 JDBC 템플릿과 놀아보자
스프링의 JDBC 프레임워크는 자원 관리와 예외 처리를 처리함으로서 개발자는 오직 데이터 처리 부분 구현에 집중 가능
세개의 선택 가능한 JDBC 템플릿 클래스를 제공
- JdbcTemplate : 스프링의 가장 기본적인 JdbcTemplate 으로, 색인된 파라미터(indexed parameter) 기반 의 쿼리를 통해서 데이터베이스에 쉽게 액세스하는 기능을 제공
- NamedParameterJdbcTemplate : 명명된 파라미터(named parameter)로 바인딩하여 쿼리 실행
- SimpleJdbcTemplate : 스프링 버전 3.1에서 삭제
// JDBC를 사용하는 DAO 클래스...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// 이름있는 파라미터들이 어떻게 위의 'Actor' 클래스의 프로퍼티에서 일치되는 것을 찾는지 주의깊게 봐라.
String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForInt(sql, namedParameters);
}
package com.vaannila.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import com.vaannila.domain.Forum;
public class ForumDAOImpl implements ForumDAO {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Forum selectForum(int forumId) {
/**
* Specify the statement
*/
String query = "SELECT * FROM FORUMS WHERE FORUM_ID=?";
/**
* Implement the RowMapper callback interface
*/
return (Forum) jdbcTemplate.queryForObject(query, new Object[] { Integer.valueOf(forumId) },
new RowMapper() {
public Object mapRow(ResultSet resultSet, int rowNum) throws SQLException {
return new Forum(resultSet.getInt("FORUM_ID"), resultSet.getString("FORUM_NAME"),
resultSet.getString("FORUM_DESC"));
}
});
}
}
- RowMapper를 통해 재사용성을 높일 수 있음
- org.springframework.jdbc.core.RowMapper
(Forum) jdbcTemplate.queryForObject(query, new Object[] { Integer.valueOf(forumId) },
new ForumRowMapper());
package com.javarticles.spring.integration.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
public class ForumRowMapper implements RowMapper<Forum> {
public Article mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Forum(resultSet.getInt("FORUM_ID"), resultSet.getString("FORUM_NAME"),
resultSet.getString("FORUM_DESC"));
}
}
- main method
package com.vaannila.dao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.vaannila.domain.Forum;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
ForumDAO forumDAO = (ForumDAO) context.getBean("forumDAO");
Forum springForum = new Forum(1,"Spring Forum", "Discuss everything related to Spring");
forumDAO.insertForum(springForum);
System.out.println(forumDAO.selectForum(1));
}
}