✔ Spring JDBC를 복습하기 위해, 강의를 들으며 JDBC를 활용해 H2 데이터베이스에 CRUD를 구현하는 로직을 작성하고, 테스트 코드까지 작성해 보았다.이때 발생한 의문점에 대해 기록해보고자 한다.
우리는 JDBC와 H2데이터 베이스를 연결하기 위해,
public static Connection getConnection() {
try {
Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("get connection = {}, class = {}", connection, connection.getClass());
return connection;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
H2 DB Driver과 연결 해주는 코드를 작성했고, 서비스 로직으로 CRUD를 모두 만들어 보았다. 아래는 저장 코드만 등록, 구조는 모두 동일하다.
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(?,?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.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(con, pstmt, null);
}
}
이후 Test 코드를 작성하여 CRUD를 한 번에 테스트 할 수 있는 로직을 만들었다. (회원 저장 -> 회원 조회 -> 회원 업데이트 -> 회원 삭제)
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV100", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("find member : {}", findMember);
log.info("member == findMember {}", member == findMember);
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 member
repository.delete(member.getMemberId());
assertThatThrownBy(()->repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
이 테스트 코드를 실행하니 관련 로그가 이런 식으로 나왔다.
18:34:33.656 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
18:34:33.718 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
18:34:33.729 [Test worker] INFO hello.jdbc.repository.MemberRepositoryV0Test -- find member : Member(memberId=memberV100, money=10000)
18:34:33.731 [Test worker] INFO hello.jdbc.repository.MemberRepositoryV0Test -- member == findMember false
18:34:33.951 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn2: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
18:34:33.957 [Test worker] INFO hello.jdbc.repository.MemberRepositoryV0 -- resultSize=1
18:34:33.972 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn3: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
18:34:33.992 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn4: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
18:34:34.010 [Test worker] INFO hello.jdbc.connection.DBConnectionUtil -- get connection = conn5: url=jdbc:h2:tcp://localhost/~/test user=SA, class = class org.h2.jdbc.JdbcConnection
여기에서 든 의문점은, 우리는 모든 서비스 로직 마지막에 Connection을 반환해주는 코드를 작성했다. 그럼에도 불구하고, 로그에 계속 conn0, conn1, conn2, ...처럼 커넥션이 계속 늘어나는 것처럼 보이는 것이다.
찾아보니 그 이유는 간단했다.
우리가 jdbc를 Database와 연결할 때, getConnection()을 호출하는데, 내부에는 DriverManager.getConnection(URL, USERNAME, PASSWORD); 함수가 정의 되어 있는다. DriverManager는 호출 할 때마다 새로운 Connection을 생성하고 이는 재사용 되지 않는다.
이것은 그냥 단순한 Local Practice 이지만, 이것이 실제 서비스라면 매번 새로운 connection을 만들고 연결하는 것은 성능적인 부분에서 상당히 손해를 볼 것이라고 생각했다.
이를 해결 할 수 있는 것이 커넥션 풀 (Connection Pool)이다.
"커넥션 풀은 DB와의 연결을 미리 만들어 재사용하는 저장소"라고 생각하면 되는데,
내가 처음에 이 단어를 들었을 때는 Connection Pull인 줄 알았다.
새로운 connection을 당겨오는.. 뭐 그런거.. 🤷♂️🤷♀️🤦♀️
Connection Pool에 미리 Connection을 저장해 놓고, 필요할 때마다 사용하는 것이다.
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:h2:tcp://localhost/~/test");
config.setUsername("sa");
config.setPassword("");
config.setMaximumPoolSize(10); // 최대 10개 커넥션 유지
HikariDataSource dataSource = new HikariDataSource(config);
// 이제부터는 커넥션을 이렇게 받아옴
Connection con = dataSource.getConnection(); // 커넥션 풀에서 꺼냄
이런식으로 Connection 10개를 미리 만들어놓고, 계속해서 재 사용 할 수 있는것이다.
HikariCP의 경우 기본 대기 시간은 30초 (connectionTimeout).
만약 설정된 시간 안에 커넥션을 얻지 못하면, 예외가 발생하며 처리가 된다.
이를 보니 서비스 규모나 서비스 로직에 따라 Connection pool size를 적절하게 설정하는 것이 중요한 것 같다.
미리 만들어진 커넥션을 돌려쓰니까,
conn0 같은 커넥션이 계속 재사용되고, 성능도 훨씬 좋아짐.
쉬운 강의 였지만, 그 안에서 몰랐던 개념을 그것의 필요성을 직접 깨달아 좋았다. 후에 Connection pool에 대해 구현 할 기회가 오면 포스팅 하겠습니당