교재
책 : 초보 웹 개발자를 위한 스프링5 프로그래밍 입문 챕터 8
pom.xml
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>in.wonj</groupId>
<artifactId>chap08-practice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.22</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.22</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>10.0.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<release>18</release>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
MySQL Community Downloads 에서 다운로드할 수 있다.
윈도우즈는 인스톨러를 제공한다.
윈도우즈 환경에서 설치할 때 아래 글을 참고했다.
[ Database ] 윈도우 10 MySql 설치
Check Requirements에서 다운로드/설치중 실패가 잦았다.
무시하고 우클릭-재시도 해서 설치를 완료했다.
관리자 유저네임이 root이고 비밀번호가 1234라면 아래와 같이 진입
PS C:\Users\goran> mysql -u root -p
Enter password: **** <-- 비밀번호를 입력했지만 *로 표시되거나 보이지 않을 수 있음
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 136
Server version: 8.0.30 MySQL Community Server - GPL
Copyright (c) 2000, 2022, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
-- 데이터베이스 생성
create database gorani character set=utf8;
-- 유저 생성 및 권한 부여
-- 유저이름 goranimaster / 비밀번호 1q2w3e4r!
create user 'goranimaster'@'localhost' identified by '1q2w3e4r!';
-- goranimaster유저에게 gorani데이터베이스에 대한 모든 권한 허용
grants all privileges on gorani.* to 'goranimaster'@localhost';
-- 테이블 생성
create table gorani.MEMBER (
ID int auto_increment primary key,
NAME varchar(100),
AGE int,
BIRTHDATE datetime,
unique key(NAME)
) engine=InnoDB character set=utf8;
참고
PS C:\Users\goran> mysql -u goranimaster -p
Enter password: *********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 137
Server version: 8.0.30 MySQL Community Server - GPL
Copyright (c) 2000, 2022, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| gorani |
| information_schema |
| performance_schema |
+--------------------+
3 rows in set (0.01 sec)
mysql> use gorani;
Database changed
mysql> show tables;
+------------------+
| Tables_in_gorani |
+------------------+
| member |
+------------------+
1 row in set (0.01 sec)
mysql> desc member;
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| ID | int | NO | PRI | NULL | auto_increment |
| NAME | varchar(100) | YES | UNI | NULL | |
| AGE | int | YES | | NULL | |
| BIRTHDATE | datetime | YES | | NULL | |
+-----------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)
mysql> select * from member;
Empty set (0.00 sec)
mysql>
DB에 쿼리를 날릴 때 마다 DB와 연결을 체결하면 오버헤드가 크다.
어느정도 갯수의 연결(커넥션)을 미리 체결해두고 필요할 때 사용하면 응답성을 높이고 부하를 관리할 수 있다.
Tomcat JDBC는 커넥션 풀 기능을 제공한다.
커넥션 풀 설정을 org.apache.tomcat.jdbc.pool.DataSource에 저장하고 스프링 설정파일에 Bean객체로 등록한다.
이후 DB가 필요한 곳에서 사용한다.
import org.apache.tomcat.jdbc.pool.DataSource
@Configuration
public class GoraniConfig {
@Bean
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/gorani?characterEncoding=utf8");
// mysql 콘솔 진입할 때 사용하는 그 유저네임/패스워드 맞다
ds.setUsername("goranimaster");
ds.setPassword("1q2w3e4r!");
// 커넥션 풀 관련 설정
ds.setInitialSize(2); // 커넥션풀 초기 커넥션 개수
ds.setMaxActive(10); // 최대 활성 커넥션 개수
ds.setTestWhileIdle(true); // 유휴상태인 커넥션 상태체크 여부
ds.setMinEvictableIdleTimeMillis(1000 * 180); // 유휴상태로 유지될 수 있는 최대 시간
ds.setTimeBetweenEvictionRunsMillis(1000 * 10); // 유휴상태 커넥션 상태 체크 주기
return ds;
}
}
놀고있으면 idle, 사용중이면 active
(e.g.)
커넥션 풀이 초기화되면 idle
프로그램이 커넥션을 요청해 점유하면 active
사용을 끝내고 커넥션을 풀에 반환하면 idle
설정에 따라 idle상태의 커넥션이 삭제되기도 한다. (이후 필요해지면 다시 생성)
참고
The Tomcat JDBC Connection Pool
보다 편리하게 DBMS와 통신할 수 있는 객체
org.springframework.jdbc.core.JdbcTemplate
아래 두 메소드로 쿼리를 날릴 수 있다.
JdbcTemplate.query() : 여러 행의 결과 받을 수 있음JdbcTemplate.queryForObject() : 한 행의 결과 받을 수 있음변경을 가할 때는 update()를 사용한다.
//GoraniConfig.java
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import main.GoraniDao;
import org.apache.tomcat.jdbc.pool.DataSource;
@Configuration
public class GoraniConfig {
@Bean
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/gorani?characterEncoding=utf8");
ds.setUsername("goranimaster");
ds.setPassword("1q2w3e4r!");
ds.setInitialSize(2); // 커넥션풀 초기 커넥션 개수
ds.setMaxActive(10); // 최대 활성 커넥션 개수
ds.setTestWhileIdle(true); // 유휴상태인 커넥션 상태체크 여부
ds.setMinEvictableIdleTimeMillis(1000 * 180); // 유휴상태로 유지될 수 있는 최대 시간
ds.setTimeBetweenEvictionRunsMillis(1000 * 10); // 유휴상태 커넥션 상태 체크 주기
return ds;
}
@Bean
public GoraniDao goraniDao() {
return new GoraniDao(dataSource());
}
}
// GoraniDao.java
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.List;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.aop.ThrowsAdvice;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class GoraniDao {
private JdbcTemplate jdbcTemplate;
public GoraniDao(DataSource ds) {
jdbcTemplate = new JdbcTemplate(ds);
}
public List<Gorani> getAll(){
String queryString = "select * from MEMBER";
RowMapper<Gorani> rowMapper = new RowMapper<Gorani>() {
@Override
public Gorani mapRow(ResultSet rs, int rowNum) throws SQLException {
Gorani g = new Gorani();
g.setId(rs.getInt("ID"));
g.setName(rs.getString("NAME"));
g.setAge(rs.getInt("AGE"));
g.setBirthdate(rs.getTimestamp("BIRTHDATE").toLocalDateTime());
return g;
}
};
List<Gorani> list = jdbcTemplate.query(queryString, rowMapper);
return list;
}
public void insert(String name, int age, LocalDateTime birthdate) {
String queryString = "insert into MEMBER (NAME, AGE, BIRTHDATE) values (?, ?, ?)";
jdbcTemplate.update(queryString, name, age, birthdate);
}
}
// Gorani.java
package main;
import java.time.LocalDateTime;
public class Gorani {
private int id;
private String name;
private int age;
private LocalDateTime birthdate;
@Override
public String toString() {
return "id : "+id+" / name : "+name+" / age : "+age+" / birthdate : "+birthdate;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public LocalDateTime getBirthdate() {
return birthdate;
}
public void setBirthdate(LocalDateTime birthdate) {
this.birthdate = birthdate;
}
}
// Main.java
package main;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import config.GoraniConfig;
public class Main {
private static AbstractApplicationContext ctx;
private static List<Gorani> list;
public static void main(String[] args) {
ctx = new AnnotationConfigApplicationContext(GoraniConfig.class);
GoraniDao dao = ctx.getBean("goraniDao", GoraniDao.class);
dao.insert("aa", 123, LocalDateTime.now());
dao.insert("bb", 33, LocalDateTime.now());
dao.insert("cc", 55553, LocalDateTime.now());
list = dao.getAll();
for(Gorani g : list)
System.out.println(g);
}
}
위 코드의 getAll()에서 사용된 RowMapper는 DBMS 질의 결과를 자바 객체로 변환하는 작업을 수행한다.
아래와 같이 람다식으로 작성할 수도 있다.
List<Gorani> list = jdbcTemplate.query(
"select * from MEMBER",
(ResultSet rs, int rowNum) -> {
Gorani g = new Gorani();
g.setId(rs.getInt("ID"));
g.setName(rs.getString("NAME"));
g.setAge(rs.getInt("AGE"));
g.setBirthdate(rs.getTimestamp("BIRTHDATE").toLocalDateTime());
return g;
}
);
위 코드의 insert()에서는 jdbcTemplate.update()로 새로운 레코드를 삽입했고, mysql은 자동적으로 키값 ID를 생성했다.
자동생성된 ID를 가져오기 위해 KeyHolder를 사용한다.
KeyHolder를 사용하기 위해서는 아래 메소드를 사용한다.
int update(PreparedStatementCreator psc, KeyHolder generatedKeyHolder)
PreparedStatementCreator 객체는 내부에서 public PreparedStatement createPreparedStatement(Connection conn)를 오버라이드한다.
위 내용에 따라 insert()를 아래와 같이 변경하였다.
public int insert(String name, int age, LocalDateTime birthdate) {
String queryString = "insert into MEMBER (NAME, AGE, BIRTHDATE) values (?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement(queryString, new String[]{"ID"});
ps.setString(1, name);
ps.setInt(2, age);
ps.setTimestamp(3, Timestamp.valueOf(birthdate));
return ps;
}
},
keyHolder
);
return keyHolder.getKey().intValue();
}
람다식으로 정리하면 아래와 같다.
public int insert(String name, int age, LocalDateTime birthdate) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(
(Connection con) -> {
PreparedStatement ps = con.prepareStatement(
"insert into MEMBER (NAME, AGE, BIRTHDATE) values (?, ?, ?)",
new String[]{"ID"}
);
ps.setString(1, name);
ps.setInt(2, age);
ps.setTimestamp(3, Timestamp.valueOf(birthdate));
return ps;
},
keyHolder
);
return keyHolder.getKey().intValue();
}
특정 메소드를 트랜잭션으로 지정할 수 있다.
내부는 AOP로 동작하므로 트랜잭션으로 지정된 메소드는 프록시로 전환된다.
@Transaction 어노테이션이 붙은 메소드는 트랜잭션으로 동작한다.
기본적으로는 동작 중 RuntimeException이 발생하면 롤백한다.
// GoraniConfig.java
...
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class GoraniConfig {
@Bean
public PlatformTransactionManager platformTransactionManager() {
PlatformTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource());
return manager;
}
...
}
// GoraniDao.java
...
import org.springframework.transaction.annotation.Transactional;
public class GoraniDao {
...
@Transactional
public List<Gorani> getAll(){ ... }
@Transactional
public int insert(...) { ... }
...
}
BadSqlGrammerException가 발생해도 롤백된다.SQLException은 DataAccessException을 상속한 예외로 전환된다.DataAccessException는 RuntimeException을 상속한다.BadSqlGrammerException은 DataAccessException을 상속한다.(A) 트랜잭션인 메소드
(B) jdbcTemplate동작을 포함하는 트랜잭션 아닌 메소드

16. Transaction Management : @Transactional settings