커넥션 풀의 필요성
JDBC로 DB에 접근하려면 먼저 커넥션(JDBC의 커넥션)을 획득해야한다. 커넥션을 새로 생성하는 과정은 아래와 같이 여러 작업이 수행되기에 많은 시간이 소모된다.
DB에 접근할때마다 커넥션을 새로 생성한다면 아래의 두 가지 문제에 직면한다.
- 커넥션 생성에 시간이 소모되기에 요청에 대한 서버 응답 속도가 느려진다.
- 커넥션은 서버의 자원이다. 커넥션을 무한정 생성하면 서버의 자원이 고갈될 수 있다. 따라서 커넥션의 생성 개수를 제한해야 한다.
커넥션 풀을 사용한다면 위 두 가지 문제를 해결할 수 있다.
커넥션 풀
풀은 리소스를 보관하고 관리하는 공간이다. 풀에는 미리 생성된 리소스가 보관되어 있다. 애플리케이션은 풀에서 리소스를 가져와 사용하고 사용이 끝나면 풀에 반납한다. 즉 리소스를 새로 생성하지 않고 재사용하는 것이다. 풀을 사용하면 아래 두 가지 이점을 얻을 수 있다.
- 리소스 생성 시 오버 헤드 방지 : 처음 풀에 리소스를 생성할 때를 제외하고 리소스를 재사용하기 때문에 리소스 생성 시 오버 헤드를 방지할 수 있다.
- 리소스의 관리 책임이 풀에게 있음 : 리소스 관리는 필수다. 리소스의 생성, 반환부터 리소스 생성 개수 제한, Thread-safe한 리소스 사용 등 리소스 관리 작업을 애플리케이션이 맡는다면 응용 프로그래머의 부담이 높아진다. 풀을 사용하면 리소스 관리 책임을 풀에게 위임하기 때문에 애플리케이션은 단순히 리소스를 사용하고 반납만 하면 되어 응용 프로그래머의 부담이 낮아진다.
커넥션 풀이란 커넥션을 보관하고 관리하는 공간이다.
대표적인 커넥션 풀 오픈소스로 commons-dbcp2 , tomcat-jdbc pool , hikariCP등이 있다. 성능과 사용의 편리함 측면에서 최근에는 hikariCP를 주로 사용한다. 스프링 부트 2.0부터는 기본 커넥션 풀로 hikariCP를 제공한다.
DataSource
JDBC로 DB에 접근하려면 커넥션을 얻어야한다. 기존에는 JDBC DriverManger로 커넥션을 얻었지만 커넥션 풀이 등장함에 따라 커넥션을 얻을 수 있는 방법이 많아졌다.
애플리케이션이 각 방법에 의존하기 때문에 다른 방법으로 커넥션을 얻을려면 애플리케이션 로직도 변경해야 한다. 때문에 자바는 JDBC로 DB 접근 과정을 추상화한 것 처럼 커넥션을 얻는 방법도 추상화했다. javax.sql.Datasource 인터페이스는 커넥션을 얻는 방법을 추상화한 인터페이스이다.
public interface DataSource {
Connection getConnection() throws SQLException;
}
대부분의 커넥션 풀은 Datasource 인터페이스를 구현해두었다. java.sql.DriverManage는 Datasource 인터페이스를 사용하지 않는다. 그래서 스프링은 DriverManage도 DataSource를 통해서 커넥션을 반환할 수 있도록 DataSource를 구현한 DriverManagerDataSource 클래스를 제공한다. 정리하면 위 그림의 어떤 방법을 사용하든 애플리케이션은 Datasource를 통해 커넥션을 얻을 수 있다.
DataSouce 적용
Repository가 DataSouce에 의존하고 DataSouce를 통해 커넥션을 얻도록 구현한다.
package hello.jdbc.repository;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.support.JdbcUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.NoSuchElementException;
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection = {}, class = {}", con, con.getClass());
return con;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?,?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, rs);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money = ? where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
private void close(Connection conn, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(conn);
}
}
DriverManager, HikariCp 사용
package hello.jdbc.repository;
import com.zaxxer.hikari.HikariDataSource;
import hello.jdbc.domain.Member;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach() {
//기본 DriverManager - 항상 새로운 커넥션을 획득
// DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
//커넥션 풀링
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException {
//save
Member member = new Member("MemberV11", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
Assertions.assertThat(findMember).isEqualTo(member);
//update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updateMember = repository.findById(member.getMemberId());
Assertions.assertThat(updateMember.getMoney()).isEqualTo(20000);
repository.delete(member.getMemberId());
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId())).
isInstanceOf(NoSuchElementException.class);
}
}
- DrivenManager는 항상 새로운 커넥션을 생성하는 방식이다. DrivenManagerDataSource가 구현체인 DataSource로부터 getConnection 메서드를 호출하면 새로운 커넥션이 생성되어 반환되며 해당 커넥션의 close 메서드를 호출하면 커넥션이 종료된다.
- HikariCp는 어플리케이션 시작 시 커넥션 풀에 커넥션을 생성하고 생성된 커넥션을 재사용하는 방식이다. HikariDataSource가 구현체인 DataSource로부터 getConnection 메서드를 호출하면 스레드 풀에서 커넥션을 가져오고 해당 커넥션의 close 메서드를 호출하면 커넥션이 스레드 풀에 반납된다.
- 커넥션 풀에 커넥션을 생성할 때 시간이 오래 걸리기에 HikariCp는 별도의 스레드에서 커넥션을 생성한다. 이로인해 애플리케이션 시작 시간이 단축 된다.
- 커넥션 풀 사이즈, 최대 대기 시간(connection Timeout) 등의 커넥션 풀 설정은 HikariCp 공식 홈페이지를 참고하자.