김영한님의 스프링 DB 1 편 - 데이터접근 핵심원리 를 듣고 정리한 내용입니다.
커넥션 풀 이해
데이터베이스가 커넥션을 매번 획득
순서
1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다.
물론 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생한다.
3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달한다.
4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
커넥션을 새로 만드는 것은 과정도 복잡하고, 시간도 많이 소모되는 일이다.
DB는 물론 어플리케이션 서버에도 TCP/IP 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야한다.
진짜 문제는 고객이 어플리케이션 사용할때 응답속도에 영향을 미치게 된다.
해당 문제를 해결하기 위해 커넥션을 미리 생성해두고 사용하는 커넥션 풀이라는 방법이 있다.
어플리케이션 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
커넥션 풀에 들어있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있어 언제든 사용 가능
어플리케이션 로직에서는 DB 드라이버를 통해 커넥션을 획득하는것이 아님
커넥션 풀을 통해 이미 생성되어있는 커넥션을 객체 참조로 그냥 가져다 쓴다
커넥션풀에 커넥션 요청 시 커넥션 풀은 가지고 있던 커넥션 반환
어플리케잇녀 로직은 커넥션 풀에서 받은 커넥션을 사용해 SQL DB 전달하고 결과 받아 처리
커넥션 모두 사용 시 커넥션 종료가 아닌 재활용 위한 커넥션을 커넥션 풀에 반납, 살아있는 상태로 반환
정리
적절한 커넥션 풀 숫자는 성능 테스트 통해 정한다.
커넥션 풀은 서바당 최대 커넥션 수 제한할 수 있다. DB무한연결 생성 방지
커넥션풀의 이점이 크기 때문에 필수 사용
오픈소스 커넥션풀이 많아 오픈소스 사용
DataSource 이해
어플리케이션 로직에서 DriverManager을 사용하면서 커넥션 획득하다가 HikariCP같은 커넥션 풀 사용하도록 변경한다면
커넥션을 획득하는 어플리케이션 코드도 함께 변경되어야 한다.
의존관계가 DriverManager에서 HikariCP로 변경되므로.
따라서 커넥션을 획득하는 방법을 추상화 시킨다.
자바에서는 이런 문제 해결을 위해 DataSource 라는 인터페이스를 제공한다.
커넥션을 획득하는 방법을 추상화하는 인터페이스이다.
public interface DataSource extends CommonDataSource, Wrapper {
/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
*
* @return a connection to the data source
* @exception SQLException if a database access error occurs
* @throws java.sql.SQLTimeoutException when the driver has determined that the
* timeout value specified by the {@code setLoginTimeout} method
* has been exceeded and has at least tried to cancel the
* current database connection attempt
*/
Connection getConnection() throws SQLException;
/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
*
* @param username the database user on whose behalf the connection is
* being made
* @param password the user's password
* @return a connection to the data source
* @exception SQLException if a database access error occurs
* @throws java.sql.SQLTimeoutException when the driver has determined that the
* timeout value specified by the {@code setLoginTimeout} method
* has been exceeded and has at least tried to cancel the
* current database connection attempt
* @since 1.4
*/
Connection getConnection(String username, String password)
throws SQLException;
/**
* {@inheritDoc}
* @since 1.4
*/
@Override
java.io.PrintWriter getLogWriter() throws SQLException;
/**
* {@inheritDoc}
* @since 1.4
*/
@Override
void setLogWriter(java.io.PrintWriter out) throws SQLException;
/**
* {@inheritDoc}
* @since 1.4
*/
@Override
void setLoginTimeout(int seconds) throws SQLException;
/**
* {@inheritDoc}
* @since 1.4
*/
@Override
int getLoginTimeout() throws SQLException;
// JDBC 4.3
/**
* Create a new {@code ConnectionBuilder} instance
* @implSpec
* The default implementation will throw a {@code SQLFeatureNotSupportedException}
* @return The ConnectionBuilder instance that was created
* @throws SQLException if an error occurs creating the builder
* @throws SQLFeatureNotSupportedException if the driver does not support sharding
* @since 9
* @see ConnectionBuilder
*/
default ConnectionBuilder createConnectionBuilder() throws SQLException {
throw new SQLFeatureNotSupportedException("createConnectionBuilder not implemented");
};
}
정리
대부분의 커넥션풀은 DataSource 인터페이스를 구현해두었다.
따라서 개발자는 DataSource 인터페이스에만 의존하도록 로직을 작성하면 된다.
변경 시 해당 구현체로 갈아끼우기만 하면 된다.
DriverManager도 DataSource를 통해서 사용할 수 있도록 DriverManagerDataSource라는 클래스를 제공한다.
DataSoruce 예제 1 - Drivermanager
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", con1, con1.getClass());
log.info("connection={}, class={}", con2, con2.getClass());
}
@Test
void dataSourceDriverManager() throws SQLException {
//DriverManagerDataSource - 항상 새로운 커넥션을 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
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());
}
DriverManager
DriverManager.getConnection(URL, USERNAME, PASSWORD)
DriverManager.getConnection(URL, USERNAME, PASSWORD)
DataSource
void dataSourceDriverManager() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
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());
}
DriverManager 는 커넥션을 획득할 때 마다 URL , USERNAME , PASSWORD 같은 파라미터를 계속 전달해야 한다.
DataSource 를 사용하는 방식은 처음 객체를 생성할 때만 필요한 파리미터를 넘겨두고,
커넥션을 획득할 때는 단순히 dataSource.getConnection() 만 호출하면 된다.
설정과 사용의 분리
설정 : URL , USERNAME , PASSWORD 같은 설정과 관련된 속성들은
한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있다.
사용: 설정은 신경쓰지 않고, DataSource 의 getConnection() 만 호출해서 사용하면 된다.
DataSource 예제2 - 커넥션 풀
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
//커넥션 풀링
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000);
}
[main] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn0: url=jdbc:h2:tcp://localhost/~/test user=SA
[main] INFO com.zaxxer.hikari.HikariDataSource - MyPool - Start completed.
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn1: url=jdbc:h2:tcp://localhost/~/test user=SA
[main] INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@1552326679 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
[main] INFO hello.jdbc.connection.ConnectionTest - connection=HikariProxyConnection@1551629761 wrapping conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
[MyPool housekeeper] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Pool stats (total=2, active=2, idle=0, waiting=0)
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn2: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn3: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn4: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn5: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn6: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn7: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn8: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - Added connection conn9: url=jdbc:h2:tcp://localhost/~/test user=SA
[MyPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)
hed with exit code 0
HikariConfig
HikariCP 관련 설정을 확인할 수 있다. 풀의 이름 (My Pool)과 최대 풀 수 ( 10 ) 확인 가능
MyPool connection adder
별도의 쓰레드를 사용해서 커넥션 풀에 커넥션을 채우는 것을 확인 가능
왜 별도 쓰레드를 사용하냐면 커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리는 일이다. 어플리케이션의 실행시간에 영향을 주지 않기 위해 별도 쓰레드 사용
커넥션 풀에서 커넥션 획득
풀에서 커넥션을 2개 획득하고 반환하지는 않았다.
마지막 로그를 보면 사용중인 커넥션 active=2 , 풀에서 대기 상태인 커넥션 idle=8 을 확인할 수 있다.
기존 코드 변경
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
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);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
외부에서 DataSource 를 주입 받아서 사용한다. 이제 직접 만든 DBConnectionUtil 을 사용하지 않는다.
JdbcUtils 을 사용하면 커넥션을 좀 더 편리하게 닫을 수 있다.
테스트코드
@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.setPoolName(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV100", 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember={}", 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
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
DI
DriverManagerDataSource HikariDataSource 로 변경해도
MemberRepositoryV1 의 코드는 전혀 변경하지 않아도 된다.
MemberRepositoryV1 는 DataSource 인터페이스에만 의존하기 때문이다.
이것이 DataSource 를 사용하는 장점이다.(DI + OCP)
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard
스프링 DB 1편 - 데이터 접근 핵심 원리 - 인프런 | 강의
백엔드 개발에 필요한 DB 데이터 접근 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔
www.inflearn.com
'스터디 > 2023_스프링부트' 카테고리의 다른 글
[study] 스프링 DB 1편 - 4. 스프링과 문제 해결 - 트랜잭션 (0) | 2023.08.21 |
---|---|
[study] 스프링 DB 1편 - 3. 트랜잭션 이해 (0) | 2023.08.21 |
[study] 스프링 DB 1편 - 1. JDBC 이해 (0) | 2023.08.21 |
[study] 스프링MVC2 - 10. 파일 업로드 (0) | 2023.08.14 |
[study] 스프링MVC2 - 9. 스프링 타입 컨버터 (0) | 2023.08.14 |