[Kotlin Spring] Connection, Connection Pool, DataSource 개념과 작동원리
Topic = Connection, Connection Pool 개념 및 작동원리와 DataSource
Connection 개념 및 생명주기
Connection?
- Connection 이란 특정 DB와의 연결된 세션이다.
Connection은 SQL 문을 연결된 컨텍스트 내에서 실행하고 결과를 반환하느 세션으로 DB와 상호작용을 가능하게 한다.
Connection이 생성되는 과정 ( Connection Pool - X )
- 연결정보 조회
- 애플리케이션 로직이 DB 드라이버를 통해 연결정보를 조회한다.
- ( 각 DB 드라이버 유형마다 자체적인 URL 을 갖는다. 전달된 URL형식과 일치하는 유형의 DB 드라이버가 매핑되고, 해당 DB 시스템의 URL 형식에 맞는지 확인하는 작업 등이 진행된다. ) - 네트워크 연결 시도 및 정보 전달
- URL에 포함된 호스트와 포트 정보를 이용하여 DB 서버에 네트워크 연결을 시도한다.
- 네트워크 연결이 되면, DB에 ID,PW와 기타 부가정보를 DB에 전달한다. - 인증 및 세션 생성 ( DB 내부에서 진행됨 )
- 전달된 연결 정보를 통해 DB내부에서 인증이 진행된다.
- 인증이 완료되면 DB 내부에 DB 세션을 생성한다. - 연결정보 응답 및 Connection 객체 생성
- 인증 작업이 끝나고 세션이 생성되면, DB 응답 정보를 반환한다.
- Connection 객체가 생성되어 반환된다.
Connection 종료
- 클라이언트의 DB 관련 요청이 종료되면, close() 메서드를 통해서 Connection을 종료하면 DB의 해당 Connection 세션이 종료되며 커넥션이 종료되게 된다.
- Connection을 close() 하지 않을 경우, 클라이언트 각각의 Connection이 무한정으로 생성되게 되고 Connection 객체 각각은 네트워크 연결이 된 객체기 때문에 리소스 누수( Connection Leak )가 발생하게 된다.
🚨Connection 관리 문제
- 자원 관리 - Connection 리소스 누수 문제
➡️클라이언트가 DB 관련 요청을 보낼 때 마다 새로운 Connection을 생성하게 되므로, 서비스 로직의 실행되는 시간이 늦춰지는 문제가 발생하게 된다. 이러한 문제로 인해 Connection Pool을 사용하여 리소스 누수를 방지하는 등 성능을 개선하는 방법을 사용하게 된다. - 동시성 처리 - 데이터 무결성 보장
➡️여러 클라이언트가 동시에 데이터베이스에 접근할 때, 각 클라이언트는 독립적인 Connection을 유지해야 한다. 이 문제를 해결하기 위해 DB 트랜잭션 개념이 도입되었다.
Connection Pool
Connection Pool 이란?
Connection Pool 은 데이터베이스와의 연결을 통해 미리 여러 Connection 객체를 생성하여 Connection 저장소인 풀에 저장해두고, 애플리케이션이 필요할 때 생성된 Connection을 가져와 사용하다가 요청 작업이 끝나면 다시 풀에 반환되어 생성된 Connection을 재사용하는 방식으로 작동하는 것을 얘기한다.
Connection Pool의 생성
🟥 Spring Boot 는 Connection 정보를 설정하고 관리할 뿐, Connection Pool의 생성주체는 애플리케이션 로직이 아니라, Connection Pool 라이브러리 ( Hikari CP 등 ) 이다.
♻️작동원리
- 초기화
애플리케이션이 시작될 때, 설정된 수 만큼의 연결정보가 DB와 연결되어 Connection 객체들이 생성되고 Connection Pool에 저장된다. - Connection 요청 및 사용
애플리케이션이 DB에 접근하려고 할 때, DB 드라이버로 접근하여 새로운 커넥션을 획득하는 것이 아닌, Connection Pool에 접근하여 생성되어 있는 커넥션 객체를 참조하여 SQL 쿼리를 실행하고 결과를 수신한다. 이때 각 Connection은 DB와 네트워크 연결이 되어 있는 상태이다. - 연결 반환
작업이 완료된 후, 사용한 연결을 Connection Pool에 반환한다. 해당 Connection은 다른 요청에 재사용 된다. - 유지 관리
주기적으로 업데이트 되지 않은 유휴 상태의 Connection을 업데이트 하는 등의 Connection 최신화 작업 등이 포함된다.
- Connection 만료 및 재생성
- 쿼리 실행 중 오류 발생 시 Connection 재생성
- 백그라운드 스레드를 통한 주기적으로 Connection의 유효성 검사
- DB 스키마 변경 감지
📚Hikari CP의 경우, Connection 요청 시 실제 데이터베이스 Connection을 감싼 hikariProxyDatabase객체가 Connection Pool에서 반환되어 사용된다.
- 추후 proxy 패턴도 학습해보고 Hikari CP 작동 원리 학습해보기.
✅ Spring Boot 2.0 부터 작성일 까지는 Spring Boot에서 Hikari CP를 기본적으로 제공하고 있다. Hikari CP는 tomcat-JDBC Pool, commons-dbcp2 등 다른 Connection Pool에 비해 더 효과적인 성능을 가지고 있다고 한다.
🕹️DB 드라이버가 아닌 Connection Pool에서 커넥션 가져오기
- DataSource ( 추상화 인터페이스 )
- HikariDataSource ( DataSource 구현체 - Connection Pool )
DataSource
DataSource 란?
- Connection을 획득하는 방법을 추상화 하는 인터페이스이다. 해당 인터페이스를 통해 dataSource를 주입 받아 애플리케이션에서 커넥션을 얻어서 사용하는 부분과 Connection 객체 속성이나 설정에 부분을 명확하게 분리하여 사용할 수 있다.
Spring Boot DataSource 자동 Resource ( Bean ) 등록
// applicaton.yml or application.properties
spring:
datasource:
url:
username:
password:
driver-class-name:
- 위와 같이 application.yml 에 정의된 dataSource 속성들은 Spring Boot의 DataSourceAutoConfiguration 에 의해 DataSource가 생성되는데 기본적으로 Hikari CP가 적용된 dataSource가 빈에 등록된다.
[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]
A. JDBC
B. JDBC 개념 정리
B. DB Transaction
[오류,에러 등등]
1. late init var con : Connection 으로 Connection을 사용할 때 발생한 오류
- 원인 - Repository의 각 메서드마다 지역 변수로 connection 객체를 생성하지 않고 close()만 잘해줘도 정상작동될까?? 해서 전역 변수로 선언해서 사용해본 것이 문제.
- 문제 - CRUD 메서드 각각을 테스트 해보면 문제가 없으나 통합 테스트 시 6개 메서드 중 3 - 4개의 메서드만 통과.
- Hikari CP 의 Connection Pool의 1번 Connection만 사용하고 있고 Time Out이 발생됨.
- 해결 - try-with-resource 구문의 AutoCloseable 인터페이스의 .use {} 메서드를 통해 지역변수로 선언하지도 않으면서, finally의 close() 메서드도 지울 수 있게 됨.
- 문제 코드-
class RepositoryImpl( private val dataSource: DataSource ): Repository {
private lateinit var con : Connection
private lateinit var ps : PreparedStatement
private lateinit var rs : ResultSet
override fun create(){
val sql = "SQL"
try {
con = dataSource.connection()
ps = con.prepareStatement(sql).apply {
// sql쿼리문 작성
}
ps.excute()
} catch(){
} finally {
allClose()
}
}
private fun allClose(){
if(this::rs.isInitialized) {
JDBCUtils.closeResultSet(rs)
}
if(this::ps.isInitialized) {
JDBCUtils.closeStatement(ps)
}
if(this::con.isInitialized) {
JDBCUtils.closeConnection(con)
}
}
}
-수정된 코드-
class RepositoryImpl( private val dataSource: DataSource ): Repository {
private lateinit var con : Connection
private lateinit var ps : PreparedStatement
private lateinit var rs : ResultSet
override fun create(){
val sql = "SQL"
try {
dataSource.connection.use { con ->
con.prepareStatement(sql).use { ps ->
ps.apply {
setString...
setInt...
}
ps.executeUpdate()
}
}
} catch(){
//예외 처리 로직
}
}
}
[Comment]
1.
2.
3.
[Reference]