Spring DB : DataSource 적용
- Spring/Spring DB
- 2022. 4. 28.
이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.
DriverManager와 DataSource
DriverManager 살펴보기
Connection conn = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, PASSWORD);
- DriverManager는 커넥션을 요청할 때 마다 생성해서 전달해준다.
- DriverManager를 통해 커넥션을 얻을 때마다 설정값을 전달해줘야한다. (URL / Username / Password)
DataSource 살펴보기
DataSource는 각 DB마다 불러오는 커넥션을 얻는 방법이 다르다는 것에 착안해서, 커넥션을 얻는 방법을 추상화한 인터페이스다. 이 인터페이스는 Connection을 얻어오는 방법에 따라 나눠지며, DriverManagerDataSource, HikariDataSource등의 구현체로 나눠진다.
DriverManagerDataSource
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
- DriverManagerDataSource는 DriverManager를 이용해 DB에서 Connection을 얻어오는 클래스다.
HikariDataSource
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10);
dataSource.setPoolName("MyPool");
- HikariDataSource는 Hikari Connection Pool의 DataSource 인터페이스 구현체다.
- 풀 사이즈 등을 설정할 수 있다.
DriverManager / DriverManagerDataSource / HikariDataSource 실행 비교
DriverManager 테스트 코드
@Test
void driverManager() throws SQLException {
Connection conn1 = DriverManager.getConnection(URL,USERNAME, PASSWORD);
Connection conn2 = DriverManager.getConnection(URL,USERNAME, PASSWORD);
Connection conn3 = DriverManager.getConnection(URL,USERNAME, PASSWORD);
Connection conn4 = DriverManager.getConnection(URL,USERNAME, PASSWORD);
log.info("Connection = {}, class = {}", conn1, conn1.getClass());
log.info("Connection = {}, class = {}", conn2, conn2.getClass());
log.info("Connection = {}, class = {}", conn3, conn3.getClass());
log.info("Connection = {}, class = {}", conn4, conn4.getClass());
}
- DriverManager 테스트 코드를 통해 Connection을 얻어오는 방법이다.
- 매번 DriverManager에 설정 정보를 전달해서 DB 커넥션을 얻는다. → 매번 얻기 때문에 쓸모없는 시간 낭비가 존재한다.
- 실행 결과에서도 DriverManager가 요청마다 커넥션을 새로 만들어주는 것을 알 수 있다.
- 각 Connection은 요청 때 마다 새로 만들어지기 때문에 conn 번호가 0~3까지 변경되는 것을 확인할 수 있다.
DriverManagerDataSource 테스트 코드
@Test
void driverManagerDataSource() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
Connection conn3 = dataSource.getConnection();
Connection conn4 = dataSource.getConnection();
log.info("Connection = {}, class = {}", conn1, conn1.getClass());
log.info("Connection = {}, class = {}", conn2, conn2.getClass());
log.info("Connection = {}, class = {}", conn3, conn3.getClass());
log.info("Connection = {}, class = {}", conn4, conn4.getClass());
}
- DriverManager에게 Connection을 얻는 방법을 DataSource를 통해 추상화했다.
- DriverManager에게 요청할 때 마다, Connection을 얻는 방식으로 동작한다.
코드 실행 결과 Connection이 요청될 때 마다 만들어지는 것을 볼 수 있다.
HikariDataSource 테스트 코드
@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);
sleep(2000);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection conn1 = dataSource.getConnection();
Connection conn2 = dataSource.getConnection();
Connection conn3 = dataSource.getConnection();
Connection conn4 = dataSource.getConnection();
log.info("Connection = {}, class = {}", conn1, conn1.getClass());
log.info("Connection = {}, class = {}", conn2, conn2.getClass());
log.info("Connection = {}, class = {}", conn3, conn3.getClass());
log.info("Connection = {}, class = {}", conn4, conn4.getClass());
}
- HikariDataSource는 Hikari ConnectionPool이 Connection을 얻는 방법을 DataSource 인터페이스로 추상화 해둔 것이다.
- Connection Pool은 I/O Bound Job이다. 따라서 오래 걸리는 일이 많다. 따라서, Connection Pool을 만들기 위한 Thread가 하나 따로 만들어져서 처리가 된다. 즉, 병렬 프로그래밍으로 처리됨.
- 이런 이유 때문에 sleep()을 이용한다. 테스트 환경만 아니면 Sleep을 사용하지 않아도 된다. 왜냐하면 Connection Pool이 다 만들어지자마자 끝이 나기 때문에 커넥션 풀에서 정상적으로 Connection을 얻어온 것이 잘 보이지 않음.
실행 코드를 봤을 때, 현재 0~3번 Connection이 배정된 것으로 확인된다. 이것은 당연하다. 왜냐하면 4개의 요청이 왔고, 4개의 요청이 Connection을 반납하지 않았기 때문이다. 따라서 Connection Pool의 Connection이 4개가 대여된 것이고, 아직도 사용하고 있는 중이다.
DataSource와 DriverManager를 사용했을 때 차이 (설정과 사용의 분리)
// DriverManager
DriverManager.getConnection(URL, USERNAME, PASSWORD);
// DataSource
DataSource.getConnection();
DriverManager와 DataSource를 사용하는 것은 실제 코드에서 큰 차이를 보여준다. 바로 '설정 부분'과 '사용 부분'의 분리가 이루어진다는 것이다. 이렇게 설정과 사용이 분리되게 되면 코드의 유지 보수 관점에서 큰 장점이 존재한다.
설정과 사용 분리
- 어플리케이션을 개발할 때 주로 설정은 한 부분에서만 처리가 된다.
- 설정 : DataSource를 만들고 필요한 속성들을 설정하는 부분이다.
- 어플리케이션을 개발할 때 사용은 어플리케이션 전반에서 사용된다.
- 사용 : 설정은 신경쓰지 않고 getConnection()을 호출해서 사용하면 된다.
위 관점에서 바라본다면 설정하는 부분이 한 지점으로 모이게 되는 것이다. 변경 지점이 줄어들기 때문에 만약 코드를 변경해야하는 일이 발생한다면, 딱 그 부분만 수정을 하면 된다. 따라서 유지 보수 관점에서 코드의 개선이 가능해진다.
Connection Pool 로그 살펴보기
MyPool Connection Adder
- 커넥션 풀을 채우기 위해서 별도로 동작하는 쓰레드다. 이 쓰레드는 커넥션 풀에 커넥션을 최대 풀수까지 채워준다.
- Connection Pool은 메인 쓰레드가 아닌, 자식 쓰레드가 채워준다.
Pool stats
- 현재 Connection Pool의 상태를 알려준다. total은 Connection Pool의 전체 Connection 갯수 / Active는 현재 사용중인 Connection 갯수, Idle은 현재 대기중인 Connection의 갯수다.
HikariProxyConnection
- Hikari Connection Pool에 Connection을 요청하면 HikariProxyConnection을 반환해준다.
- Hikari Connection Pool은 Connection 요청을 받으면 Connection을 새로 생성한 HikariProxyConnection으로 한번 감싸서 전달해준다.
- 따라서 HikariProxyConnection의 주소는 매번 다르지만, 실제 Connection은 같은 번호를 가짐
JDBCUtils
- Connection Pool은 사용이 끝난 Connection 및 자원을 반환해줘야한다.
- JDBCUtils.Close()를 통해 각각의 자원을 Release 해주면 Connection은 파괴되는 것이 아니라 Connection Pool로 반환된다.
DataSource 적용한 코드 (DrvierManager → DataSource로 리팩토링, 기존 V0 코드에 참조 추가)
코드 변경
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
다음과 같이 내부적으로 DataSource를 참조하도록 하고, DI 해준다.
// DataSource 사용시
conn = dataSource.getConnection();
// DataSource 미사용시
conn = DriverManager.getConnection(ConnectionConst.URL, ConnectionConst.USERNAME, PASSWORD);
- DataSource를 사용할 때 / 사용하지 않을 때, Connection을 받아오는 것이 다른 것을 확인했다. 위의 코드에서 볼 수 있듯이 DriverManager를 사용(DataSource 사용하지 않을 때)할 때를 살펴보면, Connection을 불러오기 위해 설정값을 넣는 것을 볼 수 있다.
- DataSource를 이용하면 Connection을 불러오기 위해 초기 셋팅만 하고, 추후에는 설정과 관계없이 getConnection()만 해주는 방식으로 코드 리팩토링이 가능하다.
private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
JdbcUtils.closeConnection(connection);
}
- DrvierManager를 사용할 때는 Null인지를 따지고, Exception이 발생하는지를 따져서 Try / Catch를 처리해줘야 했다. 따라서 코드 복잡도가 올라갔다.
- DataSource를 활용할 때, JdbcUtils를 이용해서 자원을 손쉽게 Release할 수 있다. 이 때, 반납된 Connection은 다시 Connection Pool로 들어간다.
DataSource 적용한 코드 (DriverManagerDataSource → HikariDS)
// memberRepository
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
dataSource라는 인터페이스는 커넥션을 얻어오는 방법을 추상화했다. 그리고 현재 MemberRepositoryV1은 dataSource 인터페이스에 의존한다.
dataSource DI 수정 코드
@BeforeEach
void beforeEach() {
// DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
DriverManagerDataSource / HikariDataSource는 모두 DataSource 인터페이스를 구현한 구현체다. 따라서 같은 DataSource 인터페이스를 구현한 구현체기 때문에 DriverManagerDataSource에서 HikiarDataSource를 이용하기 위해서는 의존성 주입 코드만 변경해주면 된다. 즉, 변경점이 매우매우 최소화 된다. 스프링 컨테이너를 사용하는 경우는 코드 변경이 아예 없을 수 있다.
HIKARI Connection Pool 적용한 테스트 코드 + 테스트 코드 결과
@BeforeEach
void beforeEach() {
// DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException {
String memberId = "member5";
Member member = new Member(memberId, 10000);
repository.save(member);
//findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember = {}", findMember);
// 주소비교 + 해쉬비교
log.info("member == findMember : {}", findMember == member);
log.info("member equals findMember : {}", member.equals(findMember));
assertThat(findMember).isEqualTo(member);
// update : money 10000 -> 20000
repository.update(memberId, 20000);
Member updateMember = repository.findById(memberId);
assertThat(updateMember.getMoney()).isEqualTo(20000);
// delete
repository.delete(member.getMemberId());
assertThatThrownBy(() -> repository.findById(memberId)).isInstanceOf(NoSuchElementException.class);
}
- 다음과 같이 DriverManager를 이용하던 테스트 코드를 DI만 수정해서 사용할 수 있다.
- Connection Pool에서 얻은 ProxyConnection 객체의 주소는 매번 바뀐다. 그렇지만 Connection은 항상 Conn0이다.
- Connection Pool에서 얻은 커넥션의 이름이 항상 conn0이다. 왜 이런 일이 발생할까? 테스트 코드에서는 커넥션을 사용하고 Close를 한다. 커넥션 풀에서 얻은 Connection을 JdbcUtils로 Close()를 하게 되면, 이 커넥션은 커넥션 풀에 반환된다.
- Conn은 항상 "conn0"으로 들어오는데 HikariProxyConnection의 주소는 항상 바뀌는 것을 볼 수 있다. 이것은 Hikari Connection Pool에 값을 달라고 했을 때, Hikari CP는 DB Connection을 HikariProxy 객체를 하나 생성해서 감싸서 전달해준다. DB Connection(세션 관점)에서는 동일한 객체다.
정리
- DI 관점에서 DriverManagerDataSource를 HikariCP로 변경해도 MemberRepositoryV1의 코드를 바꿀 필요가 전혀 없었다. 왜냐하면 DataSource를 사용했기 때문이다.
- DataSource는 커넥션을 얻어오는 방법에 대한 추상화 인터페이스이고 MemberRepositoryV1이 추상화 인터페이스에 의존하기 때문이다.
- 커넥션 풀에서 얻어진 커넥션은 Close하면 커넥션 풀로 반환된다.
- 커넥션 풀에서 제공되는 커넥션은 ProxyConnection 객체로 한번 감싼 다음에 요청하는 사람들에게 제공된다.
참고
Close 기본코드
private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (Exception e) {
log.info("error",e);
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (Exception e) {
log.info("error",e);
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception e) {
log.info("error",e);
}
}
}
Close 수정 코드(DataSource)
private void close(Connection connection, PreparedStatement pstmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
JdbcUtils.closeConnection(connection);
}
'Spring > Spring DB' 카테고리의 다른 글
Spring DB : 데이터베이스 연결 구조와 DB 세션 (0) | 2022.04.28 |
---|---|
Spring DB : 트랜잭션 개념 (0) | 2022.04.28 |
Spring DB : DataSource 이해 (0) | 2022.04.28 |
Spring DB : 커넥션 풀 (0) | 2022.04.28 |
Spring DB : JDBC 개발 + 등록 + 조회 + 수정 + 삭제 (0) | 2022.04.28 |