본문 바로가기
Spring/스프링 기본 원리

[Spring DB] Hikari DataSource로 이해하는 DataSource - 1. Connection Pool 생성하기

by Taler 2022. 9. 7.

이전 포스팅:

2022.09.07 - [Spring/스프링 기본 원리] - [Spring DataSource] Connection Pool, DataSource 란?

 

DataSource는 메타데이터를 통해서 Connection Pool을 생성하고, 해당 ConnectionPool과 상호작용해서 사용자에게 여러 인터페이스를 제공한다는 것은 알겠다. 하지만 어떻게 하는 지는 알 수 없다. 그렇게 크게 알 필요가 있을 것 같지는 않지만 그냥 궁금해서 한 번 파해쳐봤다.

 

지금부터는 그래서 DataSource가 어떻게, 어떤 과정을 거쳐서 제 역할들을 하고 있는지 HikariDataSource 코드를 사용해서 한 번 알아보자.

 

Recall: DataSource 생성


DB Migrator 프로젝트에서 사용중인 HikariDataSource Configuration

 

위 코드는 DataSource Bean을 직접 생성해서 등록하는 Configuration 코드이다.

 

HikariConfig라는 설정값을 보관하는 오브젝트에 외부 파일에서 각종 설정값을 읽어와 지정하고, 마지막으로 해당 Config오브젝트를 사용해서 HikariDataSource를 생성한다. 꼭 Hikari가 아니더라도 대부분 위와 유사한 과정을 거쳐서 DataSource를 초기화한다.

 

그럼 이제 맨 마지막 return문에 포함된 HikariDataSource의 생성자를 타고 들어가면서 정확히 DataSource의 역할을 분석해보자. 분석하고자 하는 것들은 1. 어떤 방식으로 Connection Pool을 생성하는지 2. 해당 Connection Pool에서 어떻게 Connection을 가져오는지였다.

 

이번 글에서는 어떤 방식으로 Connection Pool을 생성하는 과정에 대해서만 다룬다. (그래도 너무 길었다..)

 

Hikari Pool 생성


HikariDataSource에서 사용할 HikariPool 변수들
Config Object를 사용하는 HikariDataSource 생성자 코드. (HikariDataSource.java)

 

위 코드를 보면, Config Object인 configuration 변수를 먼저 검증하고, 해당 configuration의 세팅 값들을 이 HikariDataSource에 복사해 가져온다. 그 다음, 음영 처리가 된 부분을 확인해보자. 위에서 복사해 Class 안에 저장해둔 설정 값들을 사용해서 새로운 HikariPool Object를 생성하고, pool과 fastPathPool에 저장한다. 여기서 HikariPool이란, 앞서 다뤘던 Connection Pool의 구현체 중 하나이다.

 

참고로 위에서 HikariPool을 담는 pool 변수는 volatile로 선언된 모습을 볼 수 있다. volatile 키워드는 main memory에 해당 변수를 저장하겠다는 뜻이다. (volatile memory에서 따온 것 같다) 이렇게 선언된 이유는 Multi-threading 환경에서 pool을 참조하고자 할 때, 값이 변하면 안되기 때문이다.

 

다시 해당 Connection Pool 생성자를 사용한 부분을 자세히 들여다보자.

위 코드에서 사용한 HikariPool의 생성자. 여기서부터 HikariPool.java

위 코드를 위에서부터 하나씩 보자.

 

가장 수상한 녀석은 처음에 바로 나온다. connectionBag. 이녀석이 뭔가 HikariPool에서 Connection Pool을 담당할 것 같은 변수명을 갖는다.

 

ConnectionBag의 클래스인 ConcurrentBag의 생성자. HikariPool 자체를 변수로 받는다.
ConcurrentBag는 다음과 같은 내부 변수들을 갖는다.

간단히 살펴보면, ConcurrentBag (Hikari 안에서는 ConnectionBag)은 말 그대로 멀티스레딩 환경에서 Thread-safe한 하나의 ArrayList로 보인다. HikariPool에서는 PoolEntry에 대한 ConcurrentBag으로 생성한다. 당연히 PoolEntry는 Connection 객체의 Wrapper Class이다.

 

ConnectionBag의 Type인 PoolEntry 클래스. 여러 값들 사이에 Connection이라는 우리가 찾던 것이 보인다.

즉, ConnectionBag은 Connection들을 모아둔 저장소가 맞았다. 하지만 우리가 본 ConnectionBag 생성자 코드에서는 DB Connection 즉, DB에 연결을 시도하는 코드는 따로 보이지 않았다. 즉, connection을 위한 공간만 확보했을 뿐 실제로 DB와 연결되어있는 것은 아니다.

 

다시 원래의 HikariPool 생성자 코드를 봐보자. 여러 내부 변수나 객체들의 initialize가 끝난 직후, checkFailFast라는 내부 메소드가 보인다.

내부 메소드인 checkFailFast. DB 연결이 가능한지 먼저 빠르게 확인하고 Connection을 생성한다.

드디어 Connection을 생성하고 ConnectionPool인 Connection Bag에 넣어주는 코드를 찾았다. checkFailFast 함수에서는 createPoolEntry 함수를 통해서 먼저 PoolEntry를 생성한다. createPoolEntry 함수 내부로 들어가면 newPoolEntry 함수와 PoolBase.java파일 내부에 있는 newConnection 함수를 통해서 DB와의 connection을 만드는 것이다. 아래 코드를 보면 실제로 입력했던 username, password 등을 사용해 connection을 가져오는 모습을 볼 수 있다.

 

PoolBase.java의 newConnection 메서드. 막상 connection은 또 다시 dataSource.getConnection을 사용한다.

Connection을 가져오는 모습을 확인할 수 있다고 말했지만, 사실 dataSource.getConnection에 username과 password를 넣어서 가져온다. 그러나 지금 진행되던 것이 그 DataSource의 생성자 부분이 아니었던가! 지금까지 추적하고 있었던 것이 DataSource 생성자에 포함된 Connection Pool을 생성하는 과정이었다. 그런데 그 Connection Pool에서 Connection을 만들기 위해 다시 DataSource를 참조한다는 것은 어불상설이었다.

 

그래서 어떻게 된 일인지 궁금했는데, 결론부터 말하자면 PoolBase의 생성자에 포함된 initailizeDataSource 메소드가 새로운 DriverDataSource를 만들며(!), 해당 과정에서 DB와의 실질적인 Connection이 생긴다.

 

PoolBase의 생성자에 포함된 initializeDataSource 메소드. 드디어 실제로 DB에 연결되는 부분을 찾았다.

 

즉, HikariDataSource는 DB Connection을 생성하기 위해서 DriverDataSource를 추가로 생성해서 해당 DataSource가 만들어준 DB와의 Connection을 가져다가 자신의 Connection Pool을 초기화한다.

 

클린 코드를 읽을 때, 외부 API를 wrapper class로 감싸서 최대한 side effect를 줄이고 api 교체를 용이하게 하라는 강령을 본 기억이 있다. HikariDataSource도 DriverDataSource라는 다른 API를 직접 사용하는 것이 아니라 PoolBase라는 wrapper class로 감싸고 wrapper class와 상호작용해서 connection을 가져다 쓰는 것이라고 이해했다.

 

하지만, checkFastFail 메소드의 이름에서도 알 수 있듯, 해당 과정은 간단히 DB가 연결 가능한지 여부만 판단하기 때문에, 1개의 DB Connection만 만들어 connectionBag에 담는다. 해당 과정은 config의 initializationTimeout이 설정되어 있는 경우에만 수행되며, 이는 기본값이 1로 설정되어있기 때문에 특별한 튜닝을 거치지 않는 이상 항상 수행된다.

 

이제 단순히 1개의 Connection을 채우는 과정만 파악했다. 나머지는 어떻게 채우는지 확인하기 위해서 HikariPool 생성자를 마저 살펴보자.

 

Hikari에서는 멀티스레딩으로 MinimumIdle까지 CP를 채운다. blockUntilFilled flag로 인해 그때까지 애플리케이션은 pool을 사용할 수 없다.
CP에 connection을 채우는 것은 'HouseKeeper'라는 Scheduling Executor가 담당한다.

 

HikariPool에서는 위 코드에서 볼 수 있듯, HouseKeeper라는 Scheduled Executor에 포함된 fillPool() 메소드를 통해서 채워진다. fillPool 메소드는 미리 준비해둔 addConnectionExecutor를 호출해 DB Connection 생성을 명령한다.

 

참고로 HouseKeeper의 역할은 idleConnection 수가 minimum Idle 값보다 더 많다면 사용하지 않는 Connection에서 minimum만큼을 뺀 만큼의 idleConnection을 지우고, 더 작다면 fillPool() 메소드를 통해서 idleConnection을 생성한다. 간단히 idleConnection 수를 조절한다고 보면 된다. 대충 코드만 보고도 해당 부분이 어떤 역할을 하는지 알 수 있으니 참 추상화가 잘 된 코드라는 생각이 들었다. (HouseKeeper라니.. 이름 정말 잘 지었다..!)

 

connection을 지우는 부분은 closeConnection 메서드를 이용해 실행되고, 채우는 것은 fillPool 메서드를 활용해 실행된다.

 

fillPool() 메소드, Connection Pool에 필요한 양 만큼 poolEntryCreator를 실행시킨다.

 

여기서 한 번 더 보자면, fillPool 메서드는 필요한 Connection 개수만큼 addConnectionExecutor를 통해 PoolEntryCreator 혹은 postFillPoolEntryCreator를 호출한다. 이 둘은 모두 PoolEntryCreator 클래스에 속한다.

 

Callable 클래스인 PoolEntryCreator의 call 메서드

 

addConnectionExecutor가 PoolEntryCreator를 호출하면, 위 함수가 실행된다. (Callable) 위에서 봤던 것처럼, 똑같이 createPoolEntry 메서드를 활용해서 DB Connection을 생성하고, 이를  wrapper class인 PoolEntry를 생성해서 connectionBag에 담는 것을 볼 수 있다. Connection이 필요한 양 만큼 이 함수가 호출되기 때문에, 여기서 실질적으로 DB Connection이 생성된다...!

 

드디어 HikariPool을 생성하는 과정까지 마무리됐다.

 

 

마무리


여기까지 Connection을 위한 공간과 Connection Pool을 생성하고, 실제로 DB와 연결해서 Connection wrapper였던 PoolEntry에 넣는 과정까지 확인했다. getConnection 메소드는 이렇게 생성된 Connection들 중 하나를 사용하겠다는 뜻으로 쓰인다. 다음 글에서는 getConnection에 대해서 알아보자.

 

댓글