인프런 김영한 - 스프링 DB 1 강의를 듣고 정리한 글 입니다.
목차
- Connection Pool 이란
- DataSource 란
- DataSourc 예제
1. 커넥션 풀 이해
이전 게시물의 방법대로 JDBC 를 사용한다면 데이터베이스에 접근할 때 마다 매번 커넥션을 획득해야하고 아래와 같은 불필요한 커넥션 과정을 거쳐야합니다.
커넥션 획득 과정
- 어플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
- DB 드라이버는 DB 와 TCP/IP 커넥션을 연결한다. 물론 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생한다.
- DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW 와 기타 부가정보를 DB 에 전달한다.
- DB는 ID, PW 를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
- DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
- DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
커넥션의 문제점
- 데이터베이스에 접근할 떄마다, 커녁션 생성/삭제 과정이 추가된다면, 고객사용 서비스에서 "SQL 처리 시간 + 커녁션 생성 시간"이 추가되기 때문에 응답 속에데 영향을 줍니다.
커넥션 풀
- 이러한 문제를 해결하기 위해, 커넥션을 미리 생성해두고 사용하는 커넥션 풀 방법을 이용합니다.
- 커넥션 풀이란, 이름 그대로 커넥션을 관리하는 풀(수영장) 입니다.
커넥셕 풀 초기화 및 연결 상태
- 어플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼의 커넥션을 미리 확보해서 커넥션 풀에 보관합니다.
- 보통 커넥션의 개수는, 서비스의 특징과 스펙에 따라 다르지만 보통 Default 개수는 10개 입니다.
- 커넥션 풀에 들어있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있습니다.
커넥션 풀 사용 과정
- 🚀 애플리케이션 로직에서 이제는 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니라 커넥션 풀을 통해 생성되어 있는 커넥션을 객체 참조로 가져다 사용하기만 하면 됩니다.
- 사용자가 커넥션 풀에 커넥션을 요청합니다.
- 커넥션 풀은 자신이 가지고 있는 커넥션이 있다면 커넥션 중에 하나를 반환합니다.
- 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리합니다.
- 로직에서 커넥션을 다 사용했다면, 종료하지 않고(재사용할 수 있도록) 커넥션 풀에 반납합니다.
커넥션 풀 정점 및 정리
- 적절한 커넥션 풀 숫자는 각 상황에 따라 다르기 떄문에 성능 테스트를 통해서 정해야한다.
- 사용자는 최초에 커넥션 풀에 담긴 커넥션만 사용할 수 있기 때문에, 무한정으로 DB 에 연결이 생성되는 것을 막아 DB 를 보호해주는 장점도 있습니다.
- 실무에서는 통상적으로 커넥셔풀을 기본으로 사용한다고 합니다.
- 커넥션풀을 직접 구현할수도 있지만 이미 구현되어있는 오픈소스가 많아서 가져다 사용하면됩니다.
- commons-dbcp2, tomcat-jdbc pool, HikariCP 등등..
- 대표적으로 아래와 같은 오픈소스들이 있지만, 성능과 사용의 편리함 측면에서 hikariCP를 주로 사용합니다.
- 스프링 부터 2.0 부터는 Default 커넥션 풀로 hikariCP 를 제공해줍니다. (그만큼 안정적이고 거의 시장을 먹었다..?)
2. DataSource 이해
- JDBC 를 이용해 DB 구현체를 쉽게 바꿀수 있었던 것 처럼 Driver Manager를 사용하다가 hikariCP 커넥셕 풀로 변경한다거나
- 다른 커넥션풀을 사용하다가 커넥션 풀을 변경할 떄, 똑같이 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 합니다.
- 의존관계가 DriverManager 에서 HikariCP 로 병경되기 때문입니다.
DataSource 란 - 커넥션을 획득하는 방법을 추상화
- 자바에서는 이런 문제를 해결하기 위해서 jvax.sql.DataSource라는 인터페이스를 제공합니다.
- ✅ DataSource 는 커넥션을 획득하는 방법을 추상화하는 인터페이스입니다.
- DataSource 인터페이스의 핵심 기능은 커넥션 조회 하나입니다. (다른건 중요하지 않다고 하네요!)
DataSoruce 정리
- 대부분의 커넥션 풀은 DataSource인터페이스 구현체가 존재합니다. 따라서 개발자는 DBCP2 커넥션 풀, HikariCP 커넥션 풀의 코드를 직접 의존하는 것이 아니라 DataSource 인터페이스에만 의존하도록 어플리케이션 로직을 작성하면 됩니다.
- 따라서 커넥셕 풀 구현 기술을 변경하고 싶다면, 구현체만 갈아 끼우면 됩니다.
- 단, DriverManager는 DataSource 인터페이스를 사용하지 않습니다. 따라서 DriverManager는 직접 사용해야 합니다.
- 이렇게 되면, 커넥션 풀 ↔️ DriverManager 로 변경한다면 관련 코드를 수정해야합니다.
- 이러한 문제점을 해결하기위해 DriverManagerDataSource 라는 DataSource를 구현한 클래스를 제공합니다.
3. 예제 코드
DataSource 예제1 - DriverManager
이해가 가지 않다면 이전 게시물을 보자..!
1) DriverManager 를 이용해서 커넥션을 획득하는 방법
@Test
void driverManager() throws SQLException {
Connection connection1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection connection2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection={}, class={}", connection1, connection1.getClass());
log.info("connection={}, class={}", connection2, connection2.getClass());
}
2) DriverManagerDataSource 를 사용해서 커넥션을 얻는 방법
- DriverManagerDataSource는 스프링이 제공하는 DataSource 가 적용된 DriverManager 입니다.
- DriverManagerDataSource는 DataSource 를 통해서 커넥션을 획득할 수 있습니다.
- 이렇게 함으로써 Repository는 DataSource 에만 의존하고, URL, UserName, Passeword 같은 설정값들은 전혀 몰라도 되는 장점이 있습니다.
- 설정과 사용이 분리됨을 의미합니다.
@Test
void dataSourceDriverManager() throws SQLException {
//DriverManagerSource - 항상 새로운 커넥션을 획득
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
log.info("connection={}, class={}", connection1, connection1.getClass());
log.info("connection={}, class={}", connection2, connection2.getClass());
}
DataSource 예제2 - 커넥션 풀
- HikariCP 커넥션 풀을 사용합니다. HikariDataSource 는 DataSource 인터페이스를 구현하고 있습니다.
- 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동합니다.
- 별도의 쓰레드에서 동작하기 때문에 테스타가 먼저 종료되어서, Sleap을 이용해 시간을 주어 쓰레드 풀에 커넥션이 생성되는걸 로그로 확인할 수 있습니다.
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
//hikari pool 을 사용해서 커넥션 pooling
//커넥션 풀링: HikariProxyConnection(Proxy) -> JdbcConnection(Target)
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
//커넥션에서 커넥션 풀이 생성되는 걸 보기위해
Thread.sleep(1000);
}
📌 커넥션 풀이 별도의 쓰레드를 사용하는이유
- 커넥션 풀에 커넥션을 채우는 일은 상대적으로 오래걸리는 일입니다.
- 따라서, 어플리케이션이 실행할 때 커넥션 풀을 채울때까지 마냥 대기하고 있다면 실행시간이 늦어지기에
- 별도의 쓰레드를 사용하여 커넥션풀을 채우는 것입니다.
📌 커넥션 풀 로그 분석
DataSourcr 적용
어플리케이션 코드에 DataSource 를 적용해보자!
📌 Repository 에 DataSource 적용
- 외부에서 DataSource를 주입받아서 사용하기 때문에, 이전 게시물에서 직접만들었던 DBConnectionUtil을 사용하지 않아도 되고
- DataSource 는 표준 인터페이스이기 때문에, DriverManagerDataSource나 HikariDataSource 등으로 변경해도 비지니스 로직은 코드가 수정되지 않습니다.
- JdbcUtils 를 이용해서 조금 더 편리하게 커넥션을 닫을 수 있습니다.
/**
* JDBC - DataSource 사용, JdbcUtils 사용
*/
@Slf4j
public class MemberRepositoryV1 {
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
//CRUD 로직 생략
private void close(Connection con, Statement stmt, ResultSet resultSet) {
//JdbcUtils 에서 제공하는 close 메소드 사용
JdbcUtils.closeResultSet(resultSet);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection connection = dataSource.getConnection();
log.info("get connection={}, clas={}", connection, connection.getClass());
return connection;
}
📌 TEST
- beforeEach 에서 사용할 DataSource 구현체를 주입
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repositoryV1;
@BeforeEach
void beforeEach(){
//기본 DriverManager - 항상 새로운 커넥션 회득
//DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
//이러면 속도가 너무 느리니, 커넥션 풀을 이용
//커넥션 풀링
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repositoryV1 = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException, InterruptedException {
//save
Member memberV0 = new Member("memberV0", 100000);
repositoryV1.save(memberV0);
//findById
Member findMember = repositoryV1.findById(memberV0.getMemberId());
log.info("findMember = {}", findMember);
//2개는 다른 인스턴스지만, isEqualTo 가 equals() 를 호출해 값만 비교 하기 때문에 true
//@Data 롬복은, 모든 상태 값을 비교할수 있는 equals 와 고유한 hashCode 를 자동으로 만들어준다.
assertThat(findMember).isEqualTo(memberV0);
//update
int updateMoney = 200000;
repositoryV1.update(memberV0.getMemberId(), updateMoney);
Member updateMember = repositoryV1.findById(memberV0.getMemberId());
assertThat(updateMember.getMoney()).isEqualTo(updateMoney);
//delete
repositoryV1.delete(memberV0.getMemberId());
assertThatThrownBy(() -> repositoryV1.findById(memberV0.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
//커넥션 풀 채우는 거 볼려고
Thread.sleep(1000);
}
}
커넥션 풀, DataSource 적용하기 끝!
'Spring > Spring 김영한' 카테고리의 다른 글
[Spring DB 1편] (3) 트랜잭션의 이해 (0) | 2022.10.28 |
---|---|
[Spring DB 1편] (1) Jdbc의 이해 (0) | 2022.09.08 |
[Spring 기본편] (2) Spring 컨테이너의 필요성 - 스프링 컨테이너란, Bean의 관리 (0) | 2022.05.14 |
[Spring 기본편] (1) 스프링이란 / 스프링과 객체 지향에 대하여 (0) | 2022.05.13 |