728x90
1. connectionPool의 이해
- connection을 새로 만드는 것은 과정이 복잡하고 시간도 많이 소모된다.
- app과 db의 connection 맺는 과정에서 3 way handshake가 수행되기 때문 - 문제점
- DB , app server에서도 tcp/ip 컨넥션을 새로 생성하기 위한 리소스를 매번 사용
- 고객이 app을 사용할 때, SQL을 실행하는 시간, connection을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다.
- 해결방안
- connection을 미리 생성해두고 사용하는 connection pool 방법 이용
- connection pool
- app이 시작되는 시점에 connection pool은 필요한 만큼 connection을 미리 확보해서 pool에 보관한다.
- default로 10개를 보관
- pool에 있는 connection은 tcp/ip로 db와 connection이 연결되어 있는 상태이기 때문에 언제든지 즉시 sql을 db에 전달 가능하다
- 사용 1
- 새로운 connection을 획득하지 않고 이미 생성되어 있는 connection을 객체 참조로 그냥 가져다 사용하면 된다.
- 사용 2
- connection을 모두 사용하고 나면 connection을 종료하는 것이 아니라, 다음에 다시 사용할 수 있도록 해당 connection을 그대로 connection pool에 반환
- connection을 종료하는 것이 아니라 connection이 살아있는 상태로 pool에 반환
- 정리
- DB 보호 효과 : server당 최대 connection 수 제한
- 이점이 매우 커서 실무에서는 항상 기본으로 사용
- 대표적 connection 오픈 소스
- connons-dbcp2
- tomcat-jdbc pool
- HikariCP : spring boot 에서 주로 사용, jdbc 사용시, jdbc에서 자동으로 HikariCP를 import한다.
2. DataSource 이해
- connection 획득 방법 2가지
- DriverManager : connection pool 사용 안함
- connection pool : connection pool 사용
- connection 방법 변경시 문제점
- 의존관계 때문에 DriverManager -> HIkariCP 로 변경시 app code를 건들여야되는 문제점이 발생
- 의존관계 때문에 DriverManager -> HIkariCP 로 변경시 app code를 건들여야되는 문제점이 발생
- 해결 방법
- 추상화를 이용한 DI 원리 이용
- DataSource 인터페이스 이용
- DataSource
- 자바에서 DataSource 인터페이스 제공
- DataSource는 connection을 획득하는 방법을 추상화 하는 인터페이스
- 핵심 기능은 connection 조회
- DataSource.getConnection()
public interface DataSource {
Connection getConnection() throws SQLException;
}
- 정리
- 대부분의 connection pool은 DataSource interface를 구현
- 이제 app code에서는 DataSource interface에 의존하도록 app logice을 작성
- DriverManger: datasource를 사용하지 않는데 해당 문제점을 해결하기 위해 spring은 DriverManager도 DataSource를 통해서 사용할 수 있도록 DriverManagerDataSource 구현체 class 를 제공한다.
- application logic이 변경하지 않아도 된다.
3. DataSource ConnectionTest
package hello.jdbc.connection;
import static hello.jdbc.connection.ConnectionConst.*;
@Slf4j
public class ConnectionTest {
@Test
@DisplayName("dataSource interface 사용안 한 DriverManger")
void drivermanager() throws SQLException {
// 1.등록 2. 연결이 동시에 이루어짐
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD); // connection 할 때마다 설정 params 넘겨야 함
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass()); // connection=conn0:
log.info("connection={}, class={}", con2, con2.getClass()); // connection=conn1:
}
@Test
@DisplayName("dataSource interface 사용한 DriverManger 구현체인 DriverMangerDataSource")
void dataSourceDriverManger() throws SQLException{
// 1. 설정
// spring이 제공하는 DriverManger
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
// DriverMangerDataSource의 부모인 DataSource 다형성 이용
DataSource dataSource1 = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
/* 결과
* connection=conn0: connection=conn1:
* datasource interface를 이용해서 connection을 했을 뿐, datasource를 사용했다고 connectionPool이 적용되는 것은 아니다.
* 대신, dataSource를 사용하는 시점에서는 params를 받을 필요가 없다. - private void useDataSource() 명시
* 이로써 설정과 사용의 분리를 할 수 있다.
* */
}
@Test
@DisplayName("dataSource interface 사용한 구현체인 HikariDataSource, connectionPool 이용 ")
void dataSourceConnectionPool() throws SQLException, InterruptedException{
// connection pooling : hikariProxyConnection(Proxy) -> JdbcConnection(target)
// springboot의 jdbc 사용시, Hikari가 자동으로 import 된다.
HikariDataSource dataSource = new HikariDataSource();
// DataSource dataSource1 = new HikariDataSource(); // 물론 얘도 DataSource interface 상속 받음
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10); // default 10개
dataSource.setPoolName("MyPool"); // pool 이름
useDataSource(dataSource); // 2. 호출
Thread.sleep(1000); // pool에 connection 담는 것은 다른 쓰레드가 처리하기 때문에 test가 먼저 끝날 경우가 생김. 그럼 로그보기가 힘들다.
}
// 2. 호출, 연결
// DataSource interface의 method를 이용한 connection 사용
private void useDataSource(DataSource dataSource) throws SQLException{
Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
}
3.1. DriverManager
- Drivermanager과 DriverMangerDataSource 차이점
- params 의존 유무
기존 drivermanger는 connection을 때마다 params가 필요하지만 DriverMangerDataSource는 몰라도 된다. - 설정과 사용의 분리
- 설정 : DataSource를 만들고 필요한 속성들을 사용해서 URL, USERNAME, PASSWORD 같은 부분을 입력하는 것
- 사용 : 설정은 신경쓰지 않고 DataSource의 getConnection()만 호출해서 사용
- params 의존 유무
3.2. connection pool
- HikariCP
- connectionPool 사용
- DataSource 인터페이스를 구현
- setMaximumPoolSize(), setPoolName()은 작성 안해도 default로 설정된다.
- connectionPool에 connection 생성과정은 app 실행 속도에 영향을 주지 않기 위해 별도의 Thread에서 작동한다.
- 별도 쓰레드 이름 : pool이름 connection adder
- 현 code에선 close()가 없기 때문에 MyPool - After adding stats (total=10, active=2, idle=8, waiting=0) 의 결과를 가지게 된다.
- 더 자세한 내용 : https://github.com/brettwooldridge/HikariCP
4. DataSource 적용
- 핵심 로직으로 DataSource interface를 이용해서 connection을 연결
- 이제 Connection 방식이 DriverMangerDataSource 이든 HikariCP이든 변경이 되더라도 핵심 logic file을 변경하지 않아도 된다.
- 생성자 주입 방식으로 connection 설정을 받는다.
- repository : app logic
- DataSource DI 주입
- 직접 만든 DBConnectionUtil 사용할 필요가 없다.
- DataSource 표준 interface 덕분에 DriverManagerDataSource 에서 HikariDataSource 로 변경되어도 해당 코드를 변경하지 않아도 된다.
- JdbcUtils 편의 method
- close()를 단순하게 작성 가능해진다.
- DataSource DI 주입
- DataSource 장점 : DI + OCP 지킬 수 있게 된다.
4.1. DataSource Interface를 이용해서 Connection을 수행한 Repository
package hello.jdbc.repository;
/**
* JDBC - DriverManager 사용
*/
@Slf4j
public class MemberRepositoryV1 {
// DataSource Interface 생성자 주입 받음
// DI -> 이말은 설정 파일을 만들어서 그 안에 DataSource를 따로 보관하는 곳이 존재한다는 뜻
private final DataSource dataSource;
// 생성자 생성
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
// 1. 등록
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(? , ?)";
Connection con = null;
PreparedStatement pstmt = null; // sql params를 바인딩 받는 class
try {
con = getConnection(); // 1. connection 연결
pstmt = con.prepareStatement(sql); // 2. db랑 소통
pstmt.setString(1, member.getMemberId()); // sql string values의 첫번째 ?
pstmt.setInt(2, member.getMoney()); // sql string values의 두번째 ?
pstmt.executeUpdate(); // db에서 실행됨
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null); // close하는 과정에서 예외발생시 뒤에것도 close안되니깐 method로 분리
}
}
// 2. 조회
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
ResultSet rs = null; // select query 문의 결과를 담고 있는 통
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery(); // 조회시에는 Query() 사용, 등록시 update 사용
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(con, pstmt, rs);
}
}
// 3. 수정
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
// 4. 삭제
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id = ?";
Connection con = null; // db와 연결
PreparedStatement pstmt = null; // sql문에 추가 값 넣고 실행
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
// getConnection을 Datasource용으로 변경
private Connection getConnection() throws SQLException{
Connection con = dataSource.getConnection(); // 연결이 된다는 것이지 아직 연결된 db는 없다.
/* * 연결된 설정 파일을 안만들었기 때문에 test 할 때 @BeforeEach에 dataSource를 만들어서 넣는다.
* @BeforeEach
* void before(){
* DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
* memberRepositoryV1 = new MemberRepositoryV1(dataSource);
* }
* */
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
// close() 예외 처리 - 더 간략한 방법으로 변경
private void close(Connection con, Statement stmt, ResultSet rs) {
// jdbc에서 제공하는 utils
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con); // hika 머시기는 hika constpool에 감싸져있어서 그 안으로 들어간다.
}
}
4.2.test
- DriverMangerDataSource에서 HikariCP datasource 구현체로 변경을 해도 Repository를 건들이지 않아도 된다.
package hello.jdbc.repository;
import static hello.jdbc.connection.ConnectionConst.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach // 각 test가 실행되기 직전에 호출
void beforeEach() throws Exception {
/* 1. 기본 DriverManager - 항상 새로운 Connection 획득, Connection pool이 없어서 호출마다 생성
*
* DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
* repository = new MemberRepositoryV1(dataSource); // 생성자 주입 수행
* */
/* 2. 변경. HikariDataSource로 dataSource 변경. Connection pool이 존재
*
* DI로 인해서 설정만 변경하면 app code를 건들일 필요가 없게 되었다.
* */
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
// connection pool 같은 경우는 default로 들어감
repository = new MemberRepositoryV1(dataSource); // DriverMangerDataSource, HikariDataSource 모든 자손이기 때문에 다형성 DI 주입이 가능
/* 정리
* DriverManger : 호출마다 connect 생성
* HikariDataSource : pool만 생성되고 같은 자원 계속 사용. close()시 연결이 끊기지 않고 pool로 돌아감
* */
}
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV5", 10000);
repository.save(member);
// findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", findMember);
// 다른 객체 member이지만 equals가 되는 이유는 @Data lombok 기능에서 equals를 오버라이딩 했기 때문
assertThat(findMember).isEqualTo(member);
// update: money: 10000 -> 20000
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
assertThat(updatedMember.getMoney()).isEqualTo(20000);
// delete
repository.delete(member.getMemberId());
assertThatThrownBy(()->repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}