[Spring] Spring JDBC
by Roel Downey[링크] : https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#jdbc
Spring JDBC
- JDBC 프로그래밍을 보면 반복되는 개발 요소가 있다.
- 이러한 반복적인 요소는 개발자를 지루하게 만든다.
- 개발하기 지루한 JDBC의 모든 저수준 세부사항을 스프링 프레임워크가 처리해준다.
- 개발자는 필요한 부분만 개발하면 된다.
Spring JDBC - 개발자가 해야 할 일은?
Spring JDBC 패키지
org.springframework.jdbc.core
- JdbcTemplate 및 관련 Helper 객체 제공
org.springframework.jdbc.datasource
- DataSource를 쉽게 접근하기 위한 유틸 클래스, 트랜젝션매니져 및 다양한 DataSource 구현을 제공
org.springframework.jdbc.object
- RDBMS 조회, 갱신, 저장등을 안전하고 재사용 가능한 객제 제공
org.springframework.jdbc.support
- jdbc.core 및 jdbc.object를 사용하는 JDBC 프레임워크를 지원
JDBC Template
- org.springframework.jdbc.core에서 가장 중요한 클래스이다.
- 리소스 생성, 해지를 처리해서 연결을 닫는 것을 잊어 발생하는 문제 등을 피할 수 있도록 한다.
- 스테이먼트(Statement)의 생성과 실행을 처리한다.
- SQL 조회, 업데이트, 저장 프로시저 호출, ResultSet 반복호출 등을 실행한다.
- JDBC 예외가 발생할 경우 org.springframework.dao패키지에 정의되어 있는 일반적인 예외로 변환시킨다.
실습
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);
String.class 여기 자리에는 리턴 받을 타입을 적어주면 원하는 타입으로 리턴을 받을 수 있다.
JdbcTemplate select 예제4
한 건 조회하기
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
여러 건 조회하기
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;
}
});
이때는 query를 사용한거 빼고는 위의 예제와 같다.
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;
}
}
insert , update, delete 모두 update를 사용한다.
JdbcTemplate insert 예제
INSERT 하기
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
- JdbcTemplate에서 JDBC statement 인자를 ?를 사용하는 대신 파라미터명을 사용하여 작성하는 것을 지원
- NamedParameterJdbcTemplate 예제
SimpleJdbcTemplate
- JdbcTemplate과 NamedParameterJdbcTemplate 합쳐 놓은 템플릿 클래스
- 이제 JdbcTemplate과 NamedParameterJdbcTemplate에 모든 기능을 제공하기 때문에 삭제 예정될 예정(deprecated)
- SimpleJdbcTemplate 예제
SimpleJdbcInsert
- 테이블에 쉽게 데이터 insert 기능을 제공
- SimpleJdbcInsert 예제
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란?
- DAO란 Data Access Object의 약자로 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 객체이다.
- 보통 데이터베이스를 조작하는 기능을 전담하는 목적으로 만들어진다.
ConnectionPool 이란?
- DB연결은 비용이 많이 든다.
- 커넥션 풀은 미리 커넥션을 여러 개 맺어 둔다.
- 커넥션이 필요하면 커넥션 풀에게 빌려서 사용한 후 반납한다.
- 커넥션을 반납하지 않으면 프로그램이 늦어지거나 심할 경우에는 장애를 발생시킬 수도 있다.
DataSource란?
- DataSource는 커넥션 풀을 관리하는 목적으로 사용되는 객체이다.
- DataSource를 이용해 커넥션을 얻어오고 반납하는 등의 작업을 수행한다.
- DataSource로 부터 얻은 Connection에 close() 메서드는 반납하도록 구현 되어있다.
Spring JDBC를 이용한 DAO작성 실습
- Spring 컨테이너인 Application Context는 설정파일로 Application Config 라는 클래스를 읽어들인다.
- Application Config 에는 componentScanAnnotaition 이 DAO클래스를 찾도록 설정한다.
찾은 모든 DAO 클래스는 Spring 컨테이너가 관리하게 된다.
- Application Context 는 DBConfig 클래스를 import하게 되고
DBConfig 클래스에서는 데이터 소스와 트랜잭션 매니저 객체를 생성한다.
- DAO 는 필드로 Named ParameterJDBC Template 과 Simple JDBC Insert 를 가지게 될 것이고
두개의 객체는 모두 Data Source를 필요로 할거다. 두 개의 객체 모두 SQL의 실행을 편리하게 하도록
Spring JDBC에서 제공하는 객체이기 때문에 DB 연결을 위해서 내부적으로 DataSource 를 사용하기 때문이다.
이 두 개의 객체는 RoleDao 생성자에서 초기화를 하게 되고 RoleDao 생성자에서 초기화된 두 개의 객체를 이용해서
RoleDao의 메서드를 구현하게 된다.
Spring JDBC를 사용하는 사용자는 파라미터와 SQL을 신경써야한다.
SQL을 RoleDao SQL 의 상수로 정의를 해놓음으로써, 나중에 SQL이 변경될 경우에 좀 더 편하게 수정할 수 있도록 하였다.
한 건의 Role 정보를 저장하고, 전달하기 위한 목적으로 RoleDTO가 사용되고 있는 것을 볼 수 있다.
실습 해보기
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>org.example</groupId>
<artifactId>diexam01</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>diexam01</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version> 4.3.14.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Spring -->
<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>
<!-- basic data source -->
<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>8.0.20</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>
Application Config 는 config라는 패키지를 두어 관리를 한다.
Application Config.class 파일을 하나 만들어준다.
@configuration // spring에서 해당 클래스의 인스턴스를 이용해서 설정파일을 대신한다. 라는 뜻이다.
@Import(DBconfig.class) //DB설정은 DBconfig에다가 해주고 싶어서 이렇게 분리를 했다.
// ApplicationConfig.class : 애플리케이션 설정파일
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({DBConfig.class})
public class ApplicationConfig {
}
- Data source 라는 걸 통해서 DB에 접속하는 부분들을 얻어내기로 했다. 그래서 Data Source를 생성 할 수 있는 클래스가 필요하다.
@Bean 을 등록했다. dataSource는 커넥션을 관리할 것이기 때문에 JDBC 드라이버, url, username, pw 정보를 알아야 한다.
// DBConfig.class
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class DBConfig {
private String driverClassName = "com.mysql.jdbc.Driver";
private String url = "jdbc:mysql://localhost:3306/springTest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Seoul";
private String username = "roel";
private String password = "1234";
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
앞에 만든걸 테스트 하기 위해 DataSourceTest.class 를 만들어보자
Spring 컨테이너가 Bean들을 생성하고 Bean들을 관리해야한다.
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import javax.sql.DataSource;
import java.sql.Connection;
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를 이용한 DAO 작성 실습
select 하기 위해서는 데이터가 왔다갔다 해야한다. DTO(Data Transger Object) 객체를 하나 생성 할 것이다. 목적은 데이터를 전달할 때 사용할 목적으로 만듬 , 그 다음에 query문을 처리할 RoleDaoSqls 클래스를 만들어보자.
데이터 엑세스 할 오브젝트인 selectAll 메서드 추가
Named ParameterJDBCTemplate이라든가 SimpleJDBCInsert 같은 객체들은 SpringJDBC가 만들어준다.
패키지 DTO를 만들어서
DTO를 만들어준다.
Role.class
정수값 id와 문자열 description이 필요하다.
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 + "]";
}
}
dao 패키지에 만들어준다.
RoleDaoSqls.class 라고 만들어준다.
query를 상수로 만들어준다. 상수는 대문자로 써주는게 좋다. 두 단어 이상일 경우는 _ 언더바를 작성한다.
public class RoleDaoSqls {
public static final String SELECT_ALL = "SELECT role_id, description FROM role order by role_id";
}
데이터를 엑세스 할 수있는걸 만든다.
RoleDao.class 라는걸 만들어준다.
Bean을 등록하는 방법으로 컴포넌트, 서비스, 레포지토리, 컨트롤러가 있었는데 DAO 저장소의 역할로 레포지토리로 정한다.
@Repository 를 붙여줬다.
DAO가 NameparameterJdbcTemplate라던가 SimpleJdbcInsert 이런 객체들을 이용한다. 이건 이미 SpringJDBC가 구현 해놓은 객체들이다. select All 할때 select 해올 때에는 NamedParameterJDBCTemplate 여기 안에 메서드를 보면 query(), queryForObject(), update() 이런 메서드를 실행시키기 위해서는 이 객체가 필요하겠다.
여기서 JdbcTemplate은 바인딩 할 때 ? 를 사용했었다. ?를 사용하게 되면 sql 문자열만 봤을때는 어떤 값이 매핑되는지 알아보기가 힘든 문제들이 있었다. NameParameterJdbcTemplate 를 사용하면 이름을 이용해서 바이딩 하거나, 결과 값을 가져올 때 사용할 수 있다.
생성자 부분에 DataSource dataSource에 매개변수로 받는데, Spring 버전 4.3 부터는 ComponentScan으로 객체를 찾았을 때 기본 생성자가 없다면 자동으로 객체를 주입해 주는데 DBConfig에서 Bean으로 등록했던 DataSource가 파라미터로 전달이 되게 된다. 그리고 이 DataSource를 받아들여서 NameParameterJdbcTemplate 객체를 생성하게 된다.
SelectAll : Role여러건을 가져올 것이다. 그래서 List로 담는다.
CollectionsEmptyMapp() 선언하면된다.
return 문에 jdbc.query를 하는데 (진짜 쿼리문(import static 으로 RoleDaoSqls.*을 임폴트 한다, 비어있는 맵객체선언, RowMapper)
select 한건, 한건의 결과 DTO에 저장할 목적으로 BeanPropertyRowMapper 객체를 이용해서 column의 값을 자동으로 DTO에 담아주게 된다. query() 메서드는 결과가 여러건이었을 때 내부적으로 반복하면서 DTO를 생성하고 생성한 DTO를 List에다가 담아주는 일을 하고 해당 List를 반환해준다. DBMS에서는 column명이 단어와 단어를 구분할때 언더바를 사용한다. 자바에서는 카멜케이스를 사용한다.
그런데 BeanPropertyRowMapper 는 DBMS와 JAVA 이름을 맞춰주는게 있다.
Bean을 등록하는 방법으로 어노테이션을 이용했다. 그럼 ApplicationConfig에다가 어떤 설정을 하나 해주어야한다.
나 ComponentScan으로 읽어낼 거다. 라는 설정을 해줘야한다. 그래야 설정 파일을 읽어낼때 약속된 어노테이션을 객체를 찾아 일을 한다.
@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 코드 추가 (dao 패키지 주소)
@ComponentScan(basePackages = { "kr.roel.daoexam.dao" })
// 위에 처럼 해도 되고 아래처럼 해도 된다. 둘중에 하나만 해라.
// 나는 위에 패키지를 몰라서 아래 클래스로 찾아서 했다.
@ComponentScan(basePackageClasses = RoleDao.class)
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);
}
}
}
이번 예제는 insert와 update 구문을 실행 시켜보자.
DAO에서 작성을 해보자.
insert문을 실행하기 위해서는 SimpleJdbcInsert라는 객체가 필요하다. 이 객체를 선언하고 이 객체를 생성하는 일을 수행해보려고 한다.
SimpleJdbcInsert를 추가해주고 생성자에 추가를 해준다. insert문의 경우 자동으로 primary key를 자동으로 생성해야 되는 경우들도 존재한다. 이럴 때는 생성된 primary key 값을 다시 읽어와야하는 부분이 필요하다. 그럴때는 SimpleJdbcInsert 객체가 일을 수행해주는데 그냥 직접 넣어주는걸로 하겠다. 그 다음 단계에서 수정하겠다.
insert 메소드에 Role를 매개변수로 받아서 해당 Role 객체에 있는 값을 웹으로 바꾸어주는데 여기서 알아서 role_id로 알아서 맵 객체를 생성 해줄거다. 이렇게 생성한 맵 객체를 SimpleJdbcInsert 가 가지고 있는 execute() 라는 메서드의 파라미터로 전달을 할 경우에 값이 알아서 저장이 되게 될 거다.
RoleDao.java
@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);
}
}
RoleDaoSqls.java
public static final String UPDATE = "UPDATE role SET description = :description WHERE ROLE_ID = :roleId";
JDBCTest.java
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 + "건 입력하였습니다.");
count = roleDao.update(role);
System.out.println(count + " 건 수정하였습니다.");
}
}
한건만 select 하고 Delete하기
지금 쿼리문을 보면 : (콜론) 하고 roleId라고 적혀있다.
이거는 실행할 때 파라미터로 가져온 값으로 바인딩 할 값이다.
* 로 적는것보다 컬럼명을 정확하게 나열하는것이 더 좋다.
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";
delete 에서 return 을 보면 첫번째는 쿼리문이고 두번째는 맵 객체를 말한다.
RoleDao.java
@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);
'Web > Spring' 카테고리의 다른 글
[Spring, Java] URL @가 %40으로 나오는 오류 해결 (0) | 2023.08.18 |
---|---|
[IntelliJ IDEA Community] 인텔리제이 커뮤니티로 Web 개발하기(1) (0) | 2020.12.18 |
[Spring] java.lang.IllegalStateException: Found multiple @SpringBootConfiguration annotated classes (0) | 2020.11.12 |
[Spring] Spring Boot에서 WAS 변경(tomcat을 undertow 변경하기) (0) | 2020.08.21 |
[Spring] Spring Boot 2.3, Web-starter doesn't bring Validation-starter anymore (0) | 2020.07.28 |
블로그의 정보
What doing?
Roel Downey