2-3 DataSource 예제1 - DriverManager
ConnectionTest - 데이터소스 드라이버 매니저 추가
package hello.jbdc.connection
import hello.jbdc.Log
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.jdbc.datasource.DriverManagerDataSource
import java.sql.DriverManager
import javax.sql.DataSource
import com.zaxxer.hikari.HikariDataSource
internal class ConnectionTest: Log {
@Test
fun driverManager(){
val con1 = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD)
val con2 = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD)
logger.info("connection={}, class={}", con1, con1.javaClass)
logger.info("connection={}, class={}", con2, con2.javaClass)
}
@Test
fun dataSourceDriverManager(){
val dataSource = DriverManagerDataSource(ConnectionConst.URL, ConnectionConst.USERNAME, ConnectionConst.PASSWORD)
useDataSource(dataSource);
}
private fun useDataSource(dataSource: DataSource){
val con1 = dataSource.connection
val con2 = dataSource.connection
logger.info("connection={}, class={}", con1, con1.javaClass)
logger.info("connection={}, class={}", con2, con2.javaClass)
}
}
dataSourceDriverManager() - 실행 결과
DriverManagerDataSource - Creating new JDBC DriverManager Connection to
[jdbc:h2:tcp:..test]
DriverManagerDataSource - Creating new JDBC DriverManager Connection to
[jdbc:h2:tcp:..test]
connection=conn0: url=jdbc:h2:tcp://..test user=SA, class=class
org.h2.jdbc.JdbcConnection
connection=conn1: url=jdbc:h2:tcp://..test user=SA, class=class
org.h2.jdbc.JdbcConnection
- 기존 코드와 비슷하지만 Datasource를 통해서 커넥션을 획득할 수 있다. DriverManagerDataSource은 스프링이 제공
- 기존 DriverManager는 커넥션을 획득할 때 마다 아이디, 패스워드 같은 파라미터를 계속 전달했다. 반면에 DataSource를 사용하는 방식을 처음 객체를 생성할 때만 필요한 파라미터를 넘기고, 이후에는 단순히 getConnection 메서드를 통해 커넥션을 획득한다.
설정과 사용으로 역할 분리
- 설정: DataSource 를 만들고 필요한 속성들을 사용해서 URL , USERNAME , PASSWORD 같은 부분을 입력하는 것을 말한다. 이렇게 설정과 관련된 속성들은 한 곳에 있는 것이 향후 변경에 더 유연하게 대처할 수 있다.
- 사용: 설정은 신경쓰지 않고, DataSource 의 getConnection()만 호출해서 사용하면 된다.
- 이 부분이 작아보이지만 큰 차이를 만들어내는데, 필요한 데이터를 DataSource가 만들어지는 시점에 미리 다 넣어두게 되면, DataSource 를 사용하는 곳에서는 dataSource.getConnection() 만 호출하면 되므로, URL , USERNAME , PASSWORD 같은 속성들에 의존하지 않아도 된다. 그냥 DataSource 만 주입받아서 getConnection() 만 호출하면 된다.
- 쉽게 이야기해서 리포지토리(Repository)는 DataSource만 의존하고, 이런 속성을 몰라도 된다.
- 애플리케이션을 개발해보면 보통 설정은 한 곳에서 하지만, 사용은 수 많은 곳에서 하게 된다.
- 덕분에 객체를 설정하는 부분과, 사용하는 부분을 좀 더 명확하게 분리할 수 있다.
2-4 DataSource 예제2 - 커넥션 풀
@Test
fun dataSourceConnectionPool(){
val dataSource = HikariDataSource().apply {
jdbcUrl = ConnectionConst.URL
username = ConnectionConst.USERNAME
password = ConnectionConst.PASSWORD
maximumPoolSize = 10
poolName = "MyPool"
}
- HikariCP 커넥션 풀을 사용한다. HikariDataSource는 DataSource 인터페이스를 구현하고 있다.
- 커넥션 풀 최대 사이즈를 10으로 지정하고, 풀의 이름을 MyPool 이라고 지정했다.
- 커넥션 풀에서 커넥션을 생성하는 작업은 애플리케이션 실행 속도에 영향을 주지 않기 위해 별도의 쓰레드에서 작동한다. 별도의 쓰레드에서 동작하기 때문에 테스트가 먼저 종료되어 버린다. 예제처럼 Thread.sleep을 통해 대기 시간을 주어야 쓰레드 풀에 커넥션이 생성되는 로그를 확인할 수 있다.
#커넥션 풀 초기화 정보 출력
HikariConfig - MyPool - configuration:
HikariConfig - maximumPoolSize................................10 HikariConfig - poolName................................"MyPool"
#커넥션 풀 전용 쓰레드가 커넥션 풀에 커넥션을 10개 채움
[MyPool connection adder] MyPool - Added connection conn0: url=jdbc:h2:.. user=SA
[MyPool connection adder] MyPool - Added connection conn1: url=jdbc:h2:..
user=SA
[MyPool connection adder] MyPool - Added connection conn2: url=jdbc:h2:..
user=SA
[MyPool connection adder] MyPool - Added connection conn3: url=jdbc:h2:..
user=SA
[MyPool connection adder] MyPool - Added connection conn4: url=jdbc:h2:..
user=SA
...
[MyPool connection adder] MyPool - Added connection conn9: url=jdbc:h2:..
user=SA
#커넥션 풀에서 커넥션 획득1
ConnectionTest - connection=HikariProxyConnection@446445803 wrapping conn0: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
#커넥션 풀에서 커넥션 획득2
ConnectionTest - connection=HikariProxyConnection@832292933 wrapping conn1: url=jdbc:h2:tcp://localhost/~/test user=SA, class=class com.zaxxer.hikari.pool.HikariProxyConnection
MyPool - After adding stats (total=10, active=2, idle=8, waiting=0)
HikariConfig
- HikariCP 관련 설정을 확인할 수 있다. 풀의 이름(MyPool)과 최대 풀 수(10)을 확인할 수 있다.
MyPoll connection adder
- 별도의 스레드를 사용해 커넥션 풀에 커넥션을 채우고 있는 것을 확인할 수 있다.
커넥션 풀에서 커넥션 획득
- 커넥션 풀에서 커넥션을 획득하고 그 결과를 출력했다. 여기서는 커넥션 풀에서 커넥션을 2개 획득하고 반환하지는 않았다. 따라서 풀에 있는 10개의 커넥션 중에 2개를 가지고 있는 상태이다. 그래서 active=2, idle=8의 값을 확인할 수 있다.
2-5. DataSource 적용
MemberRepositoryV1
package hello.jbdc.repository
import hello.jbdc.Log
import hello.jbdc.domain.Member
import org.springframework.jdbc.support.JdbcUtils
import java.sql.*
import javax.sql.DataSource
class MemberRepositoryV1(
dataSource: DataSource,
):Log {
private val dataSource: DataSource
init {
this.dataSource = dataSource
}
fun save(member: Member): Member{
val sql: String = "insert into member(member_id, money) values (?, ?)"
lateinit var con: Connection
lateinit var pstmt: PreparedStatement
try{
con = getConnection()
pstmt = con.prepareStatement(sql)
pstmt.setString(1, member.memberId)
pstmt.setInt(2, member.money)
pstmt.executeUpdate()
return member
} catch(e: SQLException){
logger.error("db error", e)
e.printStackTrace()
throw e
} finally {
close(con, pstmt, null);
}
}
fun findById(memberId: String): Member{
val sql = "select * from member where member_id = ?"
lateinit var con: Connection
lateinit var pstmt: PreparedStatement
lateinit var rs: ResultSet
try{
con = getConnection()
pstmt = con.prepareStatement(sql)
pstmt.setString(1, memberId)
rs = pstmt.executeQuery()
if (rs.next()){
val member = Member(
memberId = rs.getString("member_id"),
money = rs.getInt("money")
)
return member
}else{
throw NoSuchElementException("member not found memberId=" + memberId)
}
} catch (e: SQLException){
logger.error("db error", e)
throw e
} finally {
close(con, pstmt, rs);
}
}
fun update(memberId: String, money: Int){
val sql = "update member set money=? where member_id=?"
lateinit var con: Connection
lateinit var pstmt: PreparedStatement
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
val resultSize = pstmt.executeUpdate()
logger.info("resultSize={}", resultSize)
} catch (e: SQLException){
logger.error("db error", e)
throw e
} finally {
close(con, pstmt, null)
}
}
fun delete(memberId: String){
val sql = "delete from member where member_id=?"
lateinit var con: Connection
lateinit var pstmt: PreparedStatement
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate()
} catch (e: SQLException){
logger.error("db error", e)
throw e
} finally {
close(con, pstmt, null)
}
}
private fun getConnection(): Connection {
val con = dataSource.connection
logger.info("get connection={}, class={}", con, con.javaClass)
return con
}
private fun close(con: Connection?, stmt: Statement?, rs: ResultSet?) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
}
- DataSource 의존관계 주입
- 외부에서 DataSource를 주입 받아서 사용한다. 이제 직접 반든 DBConnectionUtil을 사용하지 않아도 된다.
- DataSource는 표준 인터페이스 이기 때문에 DriverManagerDataSource 에서 HikariDataSource 로 변경되어도 해당 코드를 변경하지 않아도 된다.
MemberRepositoryV1Test
package hello.jbdc.repository
import com.zaxxer.hikari.HikariDataSource
import hello.jbdc.Log
import hello.jbdc.connection.ConnectionConst
import hello.jbdc.domain.Member
import org.junit.jupiter.api.BeforeEach
import org.assertj.core.api.Assertions.*
import org.junit.jupiter.api.Test
internal class MemberRepositoryV1Test: Log {
lateinit var repository: MemberRepositoryV1
@BeforeEach
fun beforeEach(){
val dataSource = HikariDataSource().apply {
jdbcUrl = ConnectionConst.URL
username = ConnectionConst.USERNAME
password = ConnectionConst.PASSWORD
}
repository = MemberRepositoryV1(dataSource)
}
@Test
fun crud(){
logger.info("start")
val member = Member("memberV0", 10000);
repository.save(member)
val memberById = repository.findById(member.memberId)
assertThat(memberById).isNotNull()
repository.update(member.memberId, 20000)
val updateMember = repository.findById(member.memberId)
assertThat(updateMember.money).isEqualTo(20000)
repository.delete(member.memberId)
assertThatThrownBy { repository.findById(member.memberId) }
.isInstanceOf(NoSuchElementException::class.java)
}
}
- MemberRepositoryV1 은 DataSource 의존관계 주입이 필요하다. BeforeEach에서 DataSource 인터페이스 구현체인 HikariDataSource 객체로 DataSource를 정의한 뒤, MemberRepositoryV1에 인자로 전달했다.
DriverManagerDataSource
get connection=conn0: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
get connection=conn1: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
get connection=conn2: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
get connection=conn3: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
get connection=conn4: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
get connection=conn5: url=jdbc:h2:.. user=SA class=class
org.h2.jdbc.JdbcConnection
- DriverManagerDataSource를 사용하면 conn0~5 번호를 통해서 항상 새로운 커넥션이 생성되어서 사용되는 것을 확인할 수 있다.
HikariDataSource
get connection=HikariProxyConnection@xxxxxxxx1 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx2 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx3 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx4 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx5 wrapping conn0: url=jdbc:h2:...
user=SA
get connection=HikariProxyConnection@xxxxxxxx6 wrapping conn0: url=jdbc:h2:...
user=SA
- 커넥션 풀 사용 시 conn0이 재사용 된 것을 확인할 수 있다. 테스트는 순서대로 실행되기 때문에 커넥션을 사용하고 다시 돌려주는 것을 반복하기 때문이다
- 웹 애플리케이션에 동시에 여러 요청이 들어오면 여러 쓰레드에서 커넥션 풀의 커넥션을 다양하게 가져가는 상황을 확인할 수 있다.
DI
- DriverManagerDataSource HikariDataSource 로 변경해도 MemberRepositoryV1 의 코드는 전혀 변경하지 않아도 된다. MemberRepositoryV1 는 DataSource 인터페이스에만 의존하기 때문이다. 이것이 DataSource를 사용하는 장점이다.
- 이는 DI와 OCP 원칙을 지키는 코드가 된다.