[본 포스팅은 스프링 DB 1편 - 데이터 접근 핵심 원리 편을 기반으로 작성하였습니다.]
데이터베이스 커넥션을 획득할 때는 복잡한 과정을 거치게 된다. 간단히 말해 TCP 구조다 보니 3 way handshake를 거쳐 DB의 ID/PW 인증과 내부에 DB 세션을 생성하고... 그 외에 많은 과정을 거친다.
이렇게 커넥션 풀을 사용하지 않고 하나의 커넥션만으로 DB에 접근하여 쿼리문을 전송하게 되면 사용자에게 느린 서비스를 제공할 수밖에 없을 것이다.
그래서 개발된 것이 커넥션 풀이다. 그리고 커넥션 풀의 오픈소스 중에 성능이 가장 좋은 HikariCP가 무엇이고 어떻게 사용하는지 포스팅해볼 것이다.
커넥션 풀(Connection Pool)이란?
앞서 말한 것처럼 매작업 시 커넥션을 해주는 것이 아닌 기본적으로 미리 커넥션을 10개를 만들어 수영장(Pool)에 둔 후에 필요할 때마다 꺼내 쓰면 된다. (동시에 여러 번 접근 가능)
따라서 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
따라서 이점이 매우 크기 때문에 실무에서 항상 기본으로 사용한다.
커넥션 풀 사용 방법 두 가지
- 애플리케이션 로직에서 이제는 DB 드라이버를 통해서 새로운 커넥션을 획득하는 것이 아니다.
- 이제는 커넥션 풀을 통해 이미 생성되어 있는 커넥션을 객체 참조로 그냥 가져다 쓰기만 하면 된다.
- 커넥션 풀에 커넥션을 요청하면 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
- 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스에 전달하고 그 결과를 받아서 처리한다.
- 커넥션을 모두 사용하고 나면 이제는 커넥션을 종료하는 것이 아니라, 다음에 다시 사용할 수 있도록 해당 커넥션을 그대로 커넥션 풀에 반환하면 된다.
여기서 주의할 점은 커넥션을 종료하는 것이 아니라 커넥션이 살아있는 상태로 커넥션 풀에 반환해야 한다는 것이다.
커넥션 풀 오픈소스 HikariCP 사용해 보기
먼저 HikariCP의 HikariDataSource는 DataSource 인터페이스를 구현하고 있다.
- HikariDataSource 초기 설정
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPoolName(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
(MemberRepositoryV1 클래스의 코드 내용은 save, findById, update, close 등 데이터를 다루는 로직이다.)
(Connection Pool을 사용할 때 close를 꼭 해줘야 하는데 밑에서 설명하겠다.)
테스트 코드에서 @BeforeEach(실행 시 먼저 구동되는 함수)를 통해 repository를 HikariDataSource를 사용할 것이라고 주입을 해준다.
먼저 HikariDataSource로 객체를 만들어
상수로 설정해둔 URL, USERNAME, PASSWORD를 위와 같이 넣어주었다.
그 외 커넥션 풀 최대 사이즈 설정을 다음과 같이 하거나 (미설정 시 기본 10개)
dataSource.setMaximumPoolSize(10);
풀의 이름을 지정할 수 있다.
dataSource.setPoolName("MyPool");
이렇게 설정된 커넥션 풀을 MemberRepositoryV1 객체에 의존관계 주입을 해준다.
MemberRepositoryV1.class
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
사용 후엔 close를 하자.
커넥션 풀은 커넥션을 사용하고 반환을 해줘야 하기 때문에 CRUD 작업이 완료되었을 때 닫아줘야 한다.
(하나의 세션으로 여러 가지 일을 하는 트랜잭션 사용 시 로직에 따라 close 해줘야 함.)
예를 들어 save 함수 코드는
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, null);
}
}
try catch문에서 finally에 close가 실행되는 것을 볼 수 있다.
close 함수는 아래와 같다.
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
사용한 커넥션을 JdbcUtils를 사용하여 닫아주면 된다. (간단하다.)
참고) save 함수에서 사용된 con = getConnection();은 의존관계 주입된 dataSource를 가져오는 코드로 하단의 코드를 참조해주면 된다.
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
테스트 코드에서 save 해보기
@Test
void crud() throws SQLException {
//save
Member member = new Member("memberV1", 10000);
repository.save(member);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
데이터를 저장해보는 작업은 기존과 똑같이 사용을 해주면 되는데
아래의 Thread.sleep의 경우 대기 시간을 주어야 스레드 풀에 커넥션이 생성되는 로그를 확인할 수 있다.
다음으로는 트랜잭션을 사용하기 위한 포스팅이다.
JdbcTemplate도 사용하기 때문에 코드가 매우 간결해진다.
'JAVA > DB' 카테고리의 다른 글
[Spring] 데이터베이스 동시성 문제 해결 코드 (STEP 1. LOCK) (0) | 2023.07.06 |
---|---|
[DB] 스프링 트랜잭션(@Transactional)이란? 사용해보기 (feat. JdbcTemplate) (0) | 2022.09.28 |