커넥션 풀 이해
데이터베이스 커넥션을 획득할 때
- 애플리케이션 로직은 DB 드라이버를 통해 커넥션 조회
- DB 드라이버는 DB와 TCP/IP 커넥션을 연결. 이 과정에서 TCP/IP 연결을 위한 네트워크 동작이 발생
- DB 드라이버는 TCP/IP 커넥션이 연결되면 ID와 PW와 기타 부가 정보를 DB에 전달
- DB는 ID와 PW를 통해 내부 인증하고, 내부에 DB 세션을 생성
- DB는 커넥션 생성이 완료되었다는 응답을 전송
- DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환
이렇게 하면 DB와 애플리케이션 서버는 커넥션을 생성하기 위한 리소스를 매번 사용해야하고, 복잡하고, 시간이 많이 든다. 그래서 커넥션을 미리 생성해두고, 사용하는 커넥션 풀이라는 방법을 사용한다. 애플리케이션을 시작하는 시점에 필요한 만큼 커넥션을 미리 만들어두고 보관한다. 기본값은 보통 10개다.
커넥션 풀에 들어가 있는 커넥션은 TCP/IP로 DB와 커넥션이 연결되어 있는 상태이기 때문에 언제든지 즉시 SQL을 DB에 전달할 수 있다.
그래서 애플리케이션 로직은 커넥션 풀에서 이미 생성되어 있는 커넥션을 객체 참조로 꺼내쓰고 SQL을 DB에 전달하고 그 결과를 받고나서 커넥션을 다 쓰면 커넥션 풀에 반환하면 된다. 커넥션 풀을 종료시키고 반환하는 것이 아니라 그냥 살아있는 상태로 반환하는 것이다.
이런 커넥션 풀은 최대 커넥션 수 제한까지도 할 수 있어서 DB를 보호하는 기능도 한다.
DataSource 이해
커넥션을 얻는 방법은 DriverManager를 사용하거나, 커넥션 풀을 사용하는 등의 방법이 있다.
만약 원래 DriverManager를 사용하다가 HikariCP 커넥션 풀을 사용하려고 하면 DriverManager에서 HikariCP로 의존관계가 변경되기 때문에 애플리케이션 코드를 변경해야하는 문제가 생긴다.
이를 해결하기 위해 커넥션을 획득하는 방법을 추상화하는 DataSource 인터페이스를 사용한다. getConnection()이 핵심 기능이다.
public interface DataSource {
Connection getConnection() throws SQLException;
}
- 커넥션풀을 사용할 때 DataSource를 구현하는 코드를 직접 짜야하는 것이 아니라 hikariCP 커넥션 풀의 코드에 구현되어 있어서 DataSource 인터페이스에만 의존하도록 애플리케이션 코드를 짜면 된다. 그래서 구현체만 변경하면 된다.
- DriverManager는 DataSource를 사용하는 것이 아니다. 그래서 기술을 바꾸려면 애플리케이션 코드를 바꿔야 하는데 이 문제를 스프링에서 DriverManagerDataSource라는 클래스를 제공하여 해결할 수 있다.
@Slf4j
public class ConnectionTest {
@Test
void driverManager() throws SQLException {
Connection con1 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
Connection con2 = DriverManager.getConnection(URL, USERNAME, PASSWORD);
log.info("connection = {}, class = {}", con1, con1.getClass());
log.info("connection = {}, class = {}", con2, con2.getClass());
}
@Test
void dataSourceDriverManager() throws SQLException {
DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
useDataSource(dataSource);
}
private void useDataSource(DataSource dataSource) throws SQLException {
Connection con1 = dataSource.getConnection();
Connection con2 = dataSource.getConnection();
log.info("connection = {}, class = {}", con1, con1.getClass());
log.info("connection = {}, class = {}", con2, con2.getClass());
}
}
위는 DriverManager를 사용한 것이고, 아래는 DataSourceDriverManager를 사용한 것이다.
위에서는 URL, USERNAME, PASSWORD를 사용할 때마다 전달해야하지만, 아래에 DataSourceDriverManager에서는 처음에 생성할 때만 URL, USERNAME, PASSWORD를 전달하면 된다.
useDataSource()와 같이 사용할 때는 단순히 DataSource만 의존성을 주입받아서 getConnection()만 하면 된다.
HikariCP에서 사용
@Test
void dataSourceConnectionPool() throws SQLException, InterruptedException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
dataSource.setMaximumPoolSize(10); //디폴트가 10
dataSource.setPoolName("MyPool");
useDataSource(dataSource);
Thread.sleep(1000); //커넥션 풀에서 커넥션 생성 시간 대기. 안하면 생성보다 테스트가 먼저 종료됨.
}
토이 프로젝트에 DataSource 적용
@Slf4j
@Repository
public class MemberRepositoryWithDBV1 {
private final DataSource dataSource;
private static long sequence = 0L;
public MemberRepositoryWithDBV1(DataSource dataSource) {
this.dataSource = dataSource;
}
...
//save()
//findById(Long id)
//findAllMember()
//findByLoginId(String loginId)
//delete(Long id)
...
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
private Connection getConnection() throws SQLException {
Connection con = dataSource.getConnection();
log.info("get connection = {}, class = {}", con, con.getClass());
return con;
}
}
이렇게 Repository에는 DataSource에만 의존하도록하였고, getConnection() 메서드를 따로 만들었다.
@Slf4j
class MemberRepositoryWithDBV1Test {
MemberRepositoryWithDBV1 repository;
@BeforeEach
void beforeEach() throws Exception {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryWithDBV1(dataSource);
}
@Test
void crud() throws SQLException {
//save
Member member1 = new Member("cd", "gogi", "massida");
repository.save(member1);
Member member2 = new Member("yc", "fish", "good");
repository.save(member2);
//findById
Member findMember = repository.findById(1L);
log.info("findMember = {}", findMember);
//findAll
List<Member> memberList = repository.findAllMember();
log.info("memberList = {}", memberList);
//findByLoginId
String findLoginMember = repository.findByLoginId("gogi");
log.info("findLoginMember = {}", findLoginMember);
//delete
repository.delete(1L);
assertThatThrownBy(() -> repository.findById(1L))
.isInstanceOf(NoSuchElementException.class);
repository.delete(2L);
assertThatThrownBy(() -> repository.findById(1L))
.isInstanceOf(NoSuchElementException.class);
}
}
그리고 테스트에 @BeforeEach로 hikariCP를 사용하도록 했다. 이렇게 사용하는 곳에서 구현체를 정하면 된다. 테스트는 성공적으로 수행되어 애플리케이션 로직도 잘 돌아가는 것을 확인할 수 있었다.
'Spring' 카테고리의 다른 글
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 4. 스프링과 문제 해결 - 트랜잭션 - 1 (0) | 2024.02.29 |
---|---|
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 3. 트랜잭션 이해 (3) | 2024.02.27 |
[Spring DB 1편 듣고 복습, 토이 프로젝트 수정] 1.JDBC 이해 (0) | 2024.02.20 |
스프링으로 예외처리와 반복 문제 해결 (0) | 2024.02.19 |
스프링으로 트랜잭션 문제 해결 (0) | 2024.02.13 |