Spring 06 : mysql세팅, DataSource, JdbcTemplate, Transaction

LeeWonjin·2022년 8월 30일

2022 백엔드스터디

목록 보기
16/20

교재
책 : 초보 웹 개발자를 위한 스프링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 세팅

다운로드 및 설치

MySQL Community Downloads 에서 다운로드할 수 있다.
윈도우즈는 인스톨러를 제공한다.

윈도우즈 환경에서 설치할 때 아래 글을 참고했다.
[ Database ] 윈도우 10 MySql 설치

Check Requirements에서 다운로드/설치중 실패가 잦았다.
무시하고 우클릭-재시도 해서 설치를 완료했다.

sql콘솔 진입

관리자 유저네임이 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>

mysql-spring연동

커넥션 풀(Connection Pool)

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

JdbcTemplate

보다 편리하게 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);
	}
}

RowMapper

위 코드의 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;
	}
);

KeyHolder

위 코드의 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();
}

Transaction

특정 메소드를 트랜잭션으로 지정할 수 있다.
내부는 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(...) { ... }
    ...
}

롤백이 발생하는 특이한 경우

  1. BadSqlGrammerException가 발생해도 롤백된다.
  • SQLExceptionDataAccessException을 상속한 예외로 전환된다.
  • DataAccessExceptionRuntimeException을 상속한다.
  • BadSqlGrammerExceptionDataAccessException을 상속한다.
  1. 아래 두 메소드가 있고 (A)가 (B)를 호출한다고 하자.
(A) 트랜잭션인 메소드
(B) jdbcTemplate동작을 포함하는 트랜잭션 아닌 메소드
  • 여기서 (B)는 (A)의 트랜잭션 범위에 포함된다.
  • 이유 : 그것이 JdbcTemplate이니까

@EnableTransactionManagement 주요속성

  • proxyTargetClass
  • order

@Transaction 주요속성

16. Transaction Management : @Transactional settings

  • value : PlatformTransactionManager Bean객체 이름
  • timeout : 초단위 트랜젝션 제한시간 (기본값 -1 : DBMS설정 상속)
  • propagation : 트랜잭션 전파 유형 (기본값 REQUIRED)
    ** T : Transaction
    • REQUIRED : T필요. 없으면 새로운 T 생성
    • MANDATORY : T필요. 없으면 Exception
    • REQUIRES_NEW : 새로운 T 생성
    • SUPPORTS : T필요없음. T가 있으면 거기에 포함
    • NOT_SUPPORTED : T필요없음. T가 있으면 잠시 중지
    • NEVER : T필요없음. 있으면 Exception
    • NESTED : 기존 T 있으면 그 안에 새로운 T 생성 / 기존 T 없으면 새로운 T 생성
  • isolation : 트랜잭션 격리 수준 (기본값 DEFAULT)
    • DEFAULT
    • READ_UNCOMMITTED : 다른 트랜잭션이 커밋하지 않은 데이터도 읽기가능
    • READ_COMMITTED : 다른 트랜잭션이 커밋한 데이터 읽기가능
    • REPEATABLE_READ : 첫 번째, 두 번째 읽은 데이터 동일
    • SERIALIZABLE : 어떤 한 개 트랜잭션씩만 접근 가능
profile
노는게 제일 좋습니다.

0개의 댓글