<3. Spring JDBC>
http://www.edwith.org/boostcourse-web
* Spring JDBC
JDBC 프로그래밍을 보면 반복되는 개발 요소가 있다. (드라이버를 로딩하고, statement 객체를 얻어내고, ResultSet객체로 결과를 받아낸 후 객체들을 일일이 닫아주는 등...) 이러한 반복적인 요소는 생산성을 떨어뜨리는 주요인이다.
Spring JDBC를 사용하면 개발하기 지루한 JDBC의 모든 저수준 세부사항을 스프링 프레임워크가 처리해준다.
=> 개발자는 필요한 부분만 개발하면 되는 것!
- Spring JDBC에서 개발자가 해야 할 일은?
연결 파라미터 정의에서는 어떤 데이터베이스에 접속할 것인지, SQL문, 파라미터가 어떤 값을 가지고 쿼리문을 수행할 것인지는 프레임워크가 알 수 없기 떄문에 이 부분은 개발자가 지정해준다.
- Spring JDBC 패키지
- org.springframework.jdbc.core
- org.springframework.jdbc.datasource
- org.springframework.jdbc.object
- org.springframework.jdbc.support
- JDBC Template
- org.springframework.jdbc.core에서 가장 중요한 클래스이다.
- 리소스 생성, 해지를 처리해서 연결을 닫는 것을 잊어 발생하는 문제 등을 피할 수 있도록 한다.
- 스테이먼트(Statement)의 생성과 실행을 처리한다.
- SQL 조회, 업데이트, 저장 프로시저 호출, ResultSet 반복호출 등을 실행한다.
- JDBC 예외가 발생할 경우 org.springframework.dao 패키지에 정의되어 있는 일반적인 예외로 변환시킨다.
Jdbcemplate 실습
JdbcTemplate select 예제1
열의 수 구하기
실제 테이블에 몇 건의 데이터가 들어있는지를 출력한다.
int rowCount = this.jdbcTemplate.queryForInt("select count(*) from t_actor");
JdbcTemplate select 예제2
변수 바인딩 사용하기
두 번째 파라미터를 이용해서 ?에 값을 채워서 쿼리를 실행한다.
int countOfActorsNamedJoe = this.jdbcTemplate.queryForInt("select count(*) from t_actor where first_name = ?", "Joe");
JdbcTemplate select 예제3
String값으로 결과 받기
세 번째 파라미터르 받아들인 값에 리턴받을 타입을 적어주면 원하는 타입으로 리턴을 받을 수 있다.
String lastName = this.jdbcTemplate.queryForObject("select last_name from t_actor where id = ?", new Object[]{1212L}, String.class);
JdbcTemplate select 예제4
한 건 조회하기
해당하는 컬럼을 원하는 객체에 매핑을 해줄 때 사용하는 RowMapper 객체를 이용한다.
Actor actor = this.jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
new Object[]{1212L},
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
JdbcTemplate select 예제5
여러 건 조회하기
한 건을 조회할 때와는 다르게 query 메서드를 이용한다.
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
new RowMapper<Actor>() {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
});
JdbcTemplate select 예제6
중복 코드 제거 (1건 구하기와 여러 건 구하기가 같은 코드에 있을 경우)
public List<Actor> findAllActors() {
return this.jdbcTemplate.query( "select first_name, last_name from t_actor", new ActorMapper());
}
private static final class ActorMapper implements RowMapper<Actor> {
public Actor mapRow(ResultSet rs, int rowNum) throws SQLException {
Actor actor = new Actor();
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
JdbcTemplate insert 예제
INSERT 하기
모두 update 메서드를 이용해서 실행한다.
this.jdbcTemplate.update("insert into t_actor (first_name, last_name) values (?, ?)", "Leonor", "Watling");
JdbcTemplate update 예제
UPDATE 하기
this.jdbcTemplate.update("update t_actor set = ? where id = ?", "Banjo", 5276L);
JdbcTemplate delete 예제
DELETE 하기
this.jdbcTemplate.update("delete from actor where id = ?", Long.valueOf(actorId));
- JdbcTemplate 외의 접근방법
NamedParameterJdbcTemplate: ? 대신에 이름을 사용해서 바인딩 할 수 있도록 해준다.
SimpleJdbcTemplate: JdbcTemplate, NamedParameterJdbcTemplate에서 가장 빈번하게 사용되는 작업을 합쳐놓은 객체
SimpleJdbcInsert: 조금 더 간단하게 insert 작업을 수행할 수 있다.
* DTO란?
DTO란 Data Transfer Object의 약자
계층 간 데이터 교환을 위한 자바 빈즈이다.
여기서의 계층이란 컨트롤러, 뷰, 비지니스 계층, 퍼시스턴스 계층을 의미한다.
일반적으로 DTO는 로직을 가지고 있지 않으며 순수한 데이터 객체이다.
필드와 getter, setter를 가진다.
추가적으로 toString(), equals(), hashCode()등의 Object 메소드를 오버라이딩 할 수 있다.
데이터를 한꺼번에 가지고 다닐 수 있는 용도로 사용된다고 할 수 있다.
- DTO의 예
public class ActorDTO {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
}
* DAO란?
* ConnectionPool 이란?
커넥션 풀은 미리 커넥션을 여러 개 맺어 두고 커넥션이 필요하면 커넥션 풀에게 빌려서 사용한 후 반납한다.
사용 후 반납을 꼭 해야한다. 사용 가능한 커넥션이 없어서 장애를 발생시킬 수 있기 때문이다.
- * DataSource란?
- DataSource는 커넥션 풀을 관리하는 목적으로 사용되는 객체
- DataSource를 이용해 커넥션을 얻어오고 반납하는 등의 작업을 수행한다.
- Spring JDBC를 이용한 DAO작성 실습
* 실습 프로그램의 구조
applicationContext는 설정파일로 ApplicationConfig라는 파일을 읽어들인다. (Spring이 제공해주는 클래스, IoC/DI 컨테이너라고 할 수 있다.)
ApplicationConfig는 componentScan 어노테이션이 DAO를 찾을 수 있도록 설정한다.
찾은 모든 DAO 클래스는 스프링 컨테이너가 관리한다.
applicationContext는 DBConfig를 import 한다.
DBConfig 클래스에서는 Data Source와 TransactionManager 객체를 생성한다.
DAO는 필드로 NamedParameterJdbcTemplate, SimpleJdbcInsert(Spring 제공해주는 객체)를 가지며 두 개의 이 객체는 모두 DataSource를 필요로 한다.
SQL은 RoldDao에 상수로 정의되어 있으며, RoleDaosqls에서 SQL을 작성하여 수정이 쉽도록 한다.
한 건의 Role 정보를 저장하고 전달하기 위한 목적으로 DTO를 사용한다.
데이터베이스 접속 실습
설정과 라이브러리 추가를 위해 아래와 같이 pom.xml 에 추가하기 -> 추가한 후 Maven 업데이트 꼭 해주기.
DataSource는 아파치에서 제공하는 commons-dbcp2를 사용한다.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>kr.or.connect</groupId>
<artifactId>daoexam</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>daoexam</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
ApplicationConfig.java
설정에 관련된 부분으로 @Configuration 어노테이션을 붙여 설정에 대한 정보들을 읽어들일 수 있도록 한다.
DBConfig를 Import하여 데이터베이스에 관련된 설정을 따로 빼서 관리한다. (하나에 클래스가 모든 정보를 가지고 있으면 유지보수가 힘들어지기 때문이다.)
package kr.or.connect.daoexam.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({DBConfig.class})
public class ApplicationConfig {
}
DBConfig.java
Database 설정만 따로 가지고 있다. 드라이버 설정, 사용하는 데이터베이스, 유저 정보를 추가한다.
@Bean 어노테이션을 추가하여 DataSource를 생성하는 클래스를 추가한다. 여기서 만들어진 DataSource 객체는 커넥션을 관리할 때 쓰인다.
DBConfig도 Configuration이므로 @Configuration 어노테이션이 필요하다. 또한 트랜잭션을 위한 어노테이션도 추가해준다.
package kr.or.connect.daoexam.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class DBConfig {
private String driverClassName = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8";
private String username = "connectuser";
private String password = "connect123!@#";
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
DataSourceTest.java
데이터베이스에 접속이 잘 되는지 확인하기 위한 클래스
ApplicationContext로어떤 클래스에서 정보를 읽어올 것인지를 설정한다. (ApplicationConfig.class)
그리고 getBean을 이용하여 DataSource 객체를 얻어온다. (DataSource.class)
그 후 커넥션 객체를 사용하여 연결이 되었는지 확인한다.
package kr.or.connect.daoexam.main;
import java.sql.Connection;
import javax.sql.DataSource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import kr.or.connect.daoexam.config.ApplicationConfig;
public class DataSourceTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
DataSource ds = ac.getBean(DataSource.class);
Connection conn = null;
try {
conn = ds.getConnection();
if(conn != null)
System.out.println("접속 성공^^");
}catch (Exception e) {
e.printStackTrace();
}finally {
if(conn != null) {
try {
conn.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
Spring JDBC 작성 실습 (Select)
Role.java
select 해오기 위해서는 데이터의 이동이 필요하므로 DTO를 생성한다.
package kr.or.connect.daoexam.dto;
public class Role {
private int roleId;
private String description;
public int getRoleId() {
return roleId;
}
public void setRoleId(int roleId) {
this.roleId = roleId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Role [roleId=" + roleId + ", description=" + description + "]";
}
}
RoleDaoSqls.java
쿼리문을 작성하기 위한 클래스이다. 쿼리문의 수정을 쉽게하기 위해 여기에 쿼리문을 모아둔다.
상수(모두 대문자, 언더바로 구분하는 것이 관례)로 지정한다.
package kr.or.connect.daoexam.dao;
public class RoleDaoSqls {
public static final String SELECT_ALL = "SELECT role_id, description FROM role order by role_id";
}
RoleDao.java
컴포넌트, 서비스, 레파지토리, 컨트롤러 어노테이션 중에 @Repository를 이용하여 저장소의 역할을 한다고 표시한다.
Named~(바인딩 할 때 ? 말고 문자열을 사용하기 위해), Simple~ 객체를 사용한다.
생성자 부분에서는 DataSource를 파라미터로 받아들이고 있다. @Bean으로 등록했던 datasource가 파라미터로 전달된다.
RoleDaoSqls의 쿼리문을 사용하기 위해서는 import static을 해주어야 한다.
BeanPropertyRowMapper 객체를 이용해서 컬럼의 값을 자동으로 DTO에 담아주게 된다.
* 자바와 데이터베이스는 이름 규칙이 다른데, BeanPropertyRowMapper는 자동으로 다른 이름 규칙을 맞추어준다.
package kr.or.connect.daoexam.dao;
import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import kr.or.connect.daoexam.dto.Role;
@Repository
public class RoleDao {
private NamedParameterJdbcTemplate jdbc;
private SimpleJdbcInsert insertAction;
private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);
public RoleDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource)
.withTableName("role");
}
public List<Role> selectAll(){
return jdbc.query(SELECT_ALL, Collections.emptyMap(), rowMapper);
}
}
ApplicationConfig.java 에 추가하기
ComponentScan으로 읽어낼 것이라는 설정을 해주어야 한다. 그래야 설정파일을 읽어낼 때 약속 된 어노테이션이 붙어있는 객체들을 찾아서 작업을 진행하기 때문이다.
때문에 자동으로 RoleDao에 @Repository 어노테이션이 붙어있는 클래스를 빈으로 등록해준다.
@ComponentScan(basePackages = { "kr.or.connect.daoexam.dao" })
SelectAllTest.java
Select를 테스트 할 수 있는 클래스
package kr.or.connect.daoexam.main;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import kr.or.connect.daoexam.config.ApplicationConfig;
import kr.or.connect.daoexam.dao.RoleDao;
import kr.or.connect.daoexam.dto.Role;
public class SelectAllTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
RoleDao roleDao =ac.getBean(RoleDao.class);
List<Role> list = roleDao.selectAll();
for(Role role: list) {
System.out.println(role);
}
}
}
Spring JDBC 작성 실습 (Insert, Update)
RoleDaoSqls.java에 쿼리문 추가하기
public static final String UPDATE = "UPDATE role SET description = :description WHERE ROLE_ID = :roleId";
RoleDao.java
insert문에서는 SimpleJdbcInsert가 필요하므로 이 객체를 생성하는 코드를 추가한다.
withTableName은 어떤 테이블에 넣을 것인지를 설정하는 것이다.
jdbc.update 할 때 첫번째 파라미터는 sql, 두번째 파라미터는 맵 객체이다.
package kr.or.connect.daoexam.dao;
import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import kr.or.connect.daoexam.dto.Role;
@Repository
public class RoleDao {
private NamedParameterJdbcTemplate jdbc;
private SimpleJdbcInsert insertAction;
private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);
public RoleDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource)
.withTableName("role");
}
public int insert(Role role) {
SqlParameterSource params = new BeanPropertySqlParameterSource(role);
return insertAction.execute(params);
}
public int update(Role role) {
SqlParameterSource params = new BeanPropertySqlParameterSource(role);
return jdbc.update(UPDATE, params);
}
}
JDBCTest.java
insert 테스트
insert 하기 전에 DTO 객체인 Role에 값이 들어있어야 하므로 set 해준다.
package kr.or.connect.daoexam.main;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import kr.or.connect.daoexam.config.ApplicationConfig;
import kr.or.connect.daoexam.dao.RoleDao;
import kr.or.connect.daoexam.dto.Role;
public class JDBCTest {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
RoleDao roleDao = ac.getBean(RoleDao.class);
Role role = new Role();
role.setRoleId(201);
role.setDescription("PROGRAMMER");
int count = roleDao.insert(role);
System.out.println(count + "건 입력하였습니다.");
int count = roleDao.update(role);
System.out.println(count + " 건 수정하였습니다.");
}
}
Spring JDBC 작성 실습 (1건 Select, Delete)
RoleDaoSqls.java에 추가하기
public static final String SELECT_BY_ROLE_ID = "SELECT role_id, description FROM role where role_id = :roleId";
public static final String DELETE_BY_ROLE_ID = "DELETE FROM role WHERE role_id = :roleId";
RoleDao.java에 추가하기
SqlParameterSource를 이용해서 객체를 이용해 map으로 바꿔도 되지만, delete는 값이 하나만 들어올 것이므로 Collections.singletonMap을 이용해보자.
해당 조건에 맞는 조건이 없을 때를 고려하여 예외처리를 해주어야 한다.
package kr.or.connect.daoexam.dao;
import static kr.or.connect.daoexam.dao.RoleDaoSqls.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import kr.or.connect.daoexam.dto.Role;
@Repository
public class RoleDao {
private NamedParameterJdbcTemplate jdbc;
private SimpleJdbcInsert insertAction;
private RowMapper<Role> rowMapper = BeanPropertyRowMapper.newInstance(Role.class);
public RoleDao(DataSource dataSource) {
this.jdbc = new NamedParameterJdbcTemplate(dataSource);
this.insertAction = new SimpleJdbcInsert(dataSource)
.withTableName("role");
}
public int deleteById(Integer id) {
Map<String, ?> params = Collections.singletonMap("roleId", id);
return jdbc.update(DELETE_BY_ROLE_ID, params);
}
public Role selectById(Integer id) {
try {
Map<String, ?> params = Collections.singletonMap("roleId", id);
return jdbc.queryForObject(SELECT_BY_ROLE_ID, params, rowMapper);
}catch(EmptyResultDataAccessException e) {
return null;
}
}
}
JDBCTest.java에 추가하기
Role resultRole = roleDao.selectById(201);
System.out.println(resultRole);
int deleteCount = roleDao.deleteById(500);
System.out.println(deleteCount + "건 삭제하였습니다.");
Role resultRole2 = roleDao.selectById(500);
System.out.println(resultRole2);