5 분 소요

자바에서는 JDBC API를 사용하거나 JAP, MyBatis와 같은 기술을 사용해서 DB연동을 처리한다.
이 페이지에서는 JDBC를 위해 스프링이 제공하는 JdbcTemplate의 사용법을 설명한다.

1. JDBC 프로그래밍의 단점을 보완하는 스프링

JDBC API를 이용하면 DB연동에 필요한 Connection을 구한 다음 쿼리를 실행하기 위한 PreparedStatement를 생성하다. 그리고 커리를 실행한 뒤에는 finally 블록에서 ResultSet, PreparedStatement, Connection을 닫는다.
여기서 문제는 JDBC 프로그래밍을 할 때 구조적으로 반복된다는 것이다.

구조적인 반복을 줄이기 위한 방법은 템플릿 메서드 패턴전략 패턴을 함께 사용하는 것이다. 스프링은 바로 이 두 패턴을 엮은 JdbcTemplate 클래스를 제공한다. 다음은 이 클래스를 사용한 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Member> results = jdbcTemplate.query(
  "select * from MEMBER where EMAIL = ?;",
  new RowMapper<Member>(){
    @Override
    public Member mapRow(ResultSet rs, int rowNum) throws SQLException{
      Member member = new Member(rs.getString("EMAIL"),
          rs.getString("PASSWORD"),
          rs.getString("NAME"),
          rs.getTimestamp("REGDATE"));
      member.setID(rs.getLong("ID"));
      return member;
    }
  },
  email);
return results.isEmpty() ? null : results.get(0);

구조적으로 중복되는 코드가 꽤 줄었다.
스프링이 제공하는 또 다른 장점은 트랜잭션 관리가 쉽다는 것이다. JDBC API로 트랜잭션을 처리하려면 다음과 같이 Connection의 setAutoCommit(false)을 이용해서 자동 커밋을 비활성화하고 commit()과 rollback() 메서드를 이용해서 트랜잭션을 커밋하거나 롤백해야 한다.

스프링을 이용하면 트랜잭션을 적용하고 싶은 메서드에 @Transactional 애노테이션을 붙이기만 하면 된다. 커밋과 롤백 처리는 스프링이 알아서 처리하므로 트랜잭션 처리를 제외한 핵심 코드만 집중해서 작성하면 된다.

2. 프로젝트 준비

pom.xml 파일은 다음과 같다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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>sp5</groupId>
	<artifactId>sp5-chap08</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<dependencies>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>5.0.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId> // JdbcTemplate 등 JDBC 연동에 필요한 기능을 제공
			<version>5.0.2.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.apache.tomcat</groupId>
			<artifactId>tomcat-jdbc</artifactId> // DB 커넥션풀 기능을 제공
			<version>8.5.27</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId> // MySQL 연결에 필요한 JDBC 드라이버를 제공
			<version>8.0.27</version>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.7.0</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
					<encoding>utf-8</encoding>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

스프링이 제공하는 트랜잭션 기능을 사용하려면 spring-tx 모듈이 필요한데, spring-jdbc 모듈에 대한 의존을 추가하면 spring-tx 모듈도 자동으로 포함된다.

커넥션 풀이란?
실제 서비스 운영 환경에서는 서로 다른 장비를 이용해서 자바 프로그램과 DBMS를 실행한다. 자바 프로그램에서 DBMS로 커넥션을 생성하는 시간은 매우 길기 때문에 DB 커넥션을 생성하는 시간은 전체 성능에 영향을 줄 수 있다. 또한 동시 접속하는 사용자 수가 많으면 사용자마다 DB 커넥션을 생성해서 DBMS에 부하를 준다.
최초 연결에 따른 응답 속도 저하와 동시 접속자가 많을 때 발생하는 부하를 줄이기 위해 사용하는 것이 커넥션 풀이다. 커넥션 풀은 일정 개수의 DB 커넥션을 미리 만들어두는 기법이다. DB 커넥션이 필요한 프로그램은 커넥션 풀에서 커넥션을 가져와 사용한 뒤 커넥션을 다시 풀에 반납한다. 커넥션도 일정 개수로 유지해서 DBMS에 대한 부하를 일정 수준으로 유지할 수 있게 해 준다.
이런 이유로 실제 서비스 운영 환경에서는 매번 커넥션을 생성하지 않고 커넥션 풀을 사용해서 DB 연결을 관리한다.
DB 커넥션 풀 기능을 제공하는 모듈로는 Tomcat JDBC, HikariCP, DBCP, c3p0 등이 존재한다.

3. DataSource 설정

JDBC API는 DriverManager 외에 DataSource를 이용해서 DB를 연결하는 방법을 정의하고 있다. DataSource를 사용하면 다음 방식으로 Connection을 구할 수 있다.

1
2
3
4
5
6
Connection con = null;
try{
  //dataSource는 생성자나 설정 메서드를 이용해서 주입받음
  conn = dataSource.getConnection();
  ...
}

스프링이 제공하는 DB연동 기능은 DataSource를 사용해서 DB Connection을 구한다. DB연동에 사용할 DataSouce를 스프링 빈으로 등록하고 DB 연동 기능을 구현한 빈 객체는 DataSource를 주입받아 사용한다.

Tomcat JDBC 모듈은 javax.sql.DataSource를 구현한 DataSource 클래스를 제공한다.
이 클래스를 스프링 빈으로 등록해서 DataSource로 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppCtx {
	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver"); 
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
		ds.setUsername("root");
		ds.setPassword("1234");
		ds.setInitialSize(2);
		ds.setMaxActive(10);
		ds.setTestWhileIdle(true);
		ds.setMinEvictableIdleTimeMillis(60000 * 3);
		ds.setTimeBetweenEvictionRunsMillis(10 * 1000);
		return ds;
	}

주요 코드는 다음과 같다.

  • setDriverClassName()
    JDBC 드라이버 클래스를 지정한다. MySQL 드라이버 클래스를 사용한다.
  • setUrl()
    JDBC URL을 지정한다. 데이터베이스와 테이블의 character set을 UTF-8로 설정했으므로 characterEncoding 파라미터를 이용해서 MySQL에 연결할 때 사용할 캐릭터셋을 UTF-8로 지정했다.
  • setUserNAme(), setPassword()
    DB 연결할 때 사용할 사용자 계정과 암호를 지정한다.

@Bean을 보면 destroyMethod 속성값을 close로 설정했다. close 메서드는 커넥션 풀에 보관된 Connection을 닫는다.

3.1 Tomcat JDBC의 주요 프로퍼티

Tomcat JDBC 모듈의 org.apache.tomcat.jdbc.pool.DataSource 클래스는 커넥션 풀 기능을 제공하는 DataSource 구현 클래스이다. DataSource 클래스는 커넥션을 몇 개 만들지 지정할 수 있는 메서드를 제공한다.
주요 메서드는 아래와 같다.

  • setInitialSize(int)
    커넥션 풀을 초기화할 떼 생성할 초기 커넥션 개수를 지정한다. 기본값은 10이다.
  • setMaxActive(int)
    커넥션 풀에서 가져올 수 있는 최대 커넥션 개수를 지정한다. 기본값은 100이다.
  • setMaxIdle(int)
    커넥션 풀에 유지할 수 있는 최대 커넥션 개수를 지정한다. 기본값은 100이다.
  • setMinIdle(int)
    커넥션 풀에 유지할 최소 커넥션 개수를 지정한다 기본값은 InitialSize에서 가져온다.
  • setMaxWait(int)
    커넥션 풀에서 커넥션을 가져올 때 대기할 최대 시간을 밀리초 단위로 지정한다. 기본값은 30000ms이다.
  • setMaxAge(long)
    최초 커넥션 연결 후 커넥션의 최대 유효 시간을 밀리초 단위로 지정한다. 기본값은 0이다. 0은 유효 시간이 없음을 의미하낟.
  • setValidationQuery(String)
    커넥션이 유효한지 검사할 때 사용할 쿼리를 지정한다. 언제 검사할지는 별도 설정으로 지정한다. 기본값은 null이다. null이면 검사를 하지 않는다. “select 1”이나 “select 1 from dual”과 같은 쿼리를 주로 사용한다.
  • setValidationQueryTimeout(int)
    검사 쿼리의 최대 실행 시간을 초 단위로 지정한다. 이 시간을 초과하면 검사에 실패한 것으로 간주한다. 0 이하로 지정하면 비활성화한다. 기본값은 -1이다.
  • setTestOnBorrow(boolean)
    풀에서 커넥션을 가져올 때 검사 여부를 지정한다. 기본값은 false이다.
  • setTestOnReturn(boolean)
    풀에 커넥션을 반환할 때 검사 여부를 지정한다. 기본값은 false이다.
  • setTestWhileIdel(boolean)
    커넥션이 풀에 유휴 상태로 있는 동안에 검사할지 여부를 지정한다. 기본값은 false이다.
  • setMinEvictableIdleTimeMillis(int)
    커넥션 풀에 유휴 상태로 유지할 최소 시간을 밀리초 단위로 지정한다. testWhileIdle이 true이면 유휴 시간이 이 값을 초과한 커넥션을 풀에서 제거한다. 기본 값은 60000ms이다.
  • setTimeBetweenEvictionRunsMillis(int)
    커넥션 풀의 유휴 커넥션을 검사할 주기를 밀리초 단위로 지정한다. 기본값은 5000ms이다. 이 값은 1초 이하로 설정하면 안 된다.

커넥션 풀은 커넥션을 생성하고 유지한다. 커넥션 풀에 커넥션을 요청하면 해당 커넥션은 활성(active)상태가 되고, 커넥션을 다시 커넥션 풀에 반환하면 유휴(Idle)상태가 된다. DataSouce#getConnection()을 실행하면 커넥션 풀에서 커넥션을 가져와 커넥션이 활성 상태가 된다. 반대로 커넥션을 종료(close)하면 커넥션은 풀로 돌아가 유휴 상태가 된다.

커넥션 풀에서 생성된 커넥션은 지속적으로 재사용된다. 그런데 한 커넥션이 영원히 유지되는 것은 아니다. DBMS 설정에 따라 일정 시간 내에 쿼리를 실행하지 않으며 연결을 끊기도 한다. 예를 들어 커넥션 풀에 특정 커넥션이 5분 넘게 유휴 상태로 존재했다고 하자. 이 경우 DBMS는 해당 커넥션의 연결을 끊지만 커넥션은 여전히 풀 속에 남아 있다. 이 상태에서 해당 커넥션을 풀에서 가져와 사용하면 연결이 끊어진 커넥션이므로 익셉션이 발생하게 된다.

이런 문제를 방지하려면 커넥션 풀의 커넥션이 유효한지 주기적으로 검사해야 한다. 이와 관련된 속성이 minEvictableIdleTimeMillis, timeBetweenEvictionRunsMillis, testWhileIdle이다.

예를 들어 10초 주기로 유휴 커넥션이 유효한지 여부를 검사하고 최소 유휴 시간을 3분으로 지정하고 싶다면 다음 설정을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Bean(destroyMethod = "close")
public DataSource dataSource() {
	DataSource ds = new DataSource();
	ds.setDriverClassName("com.mysql.jdbc.Driver");
	ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
	ds.setUsername("root");
	ds.setPassword("1234");
	ds.setInitialSize(2);
	ds.setMaxActive(10);
	ds.setTestWhileIdle(true); // 유휴 커넥션 검사
	ds.setMinEvictableIdleTimeMillis(60000 * 3); // 최소 유휴 시간 3분
	ds.setTimeBetweenEvictionRunsMillis(10 * 1000); // 10초 주기
	return ds;
}

Ref.

  • 최범균, 스프링프로그래밍입문5, 가메출판사.

카테고리:

업데이트: