데이터베이스 커넥션 생성
- 이전 글에서는, 커넥션(Connection 객체)을 획득하기 위해 DriverManager를 사용했다.
- DriverManager는 JDBC 드라이버를 통해 커넥션을 “획득”하는데, 아래의 과정은 JDBC 드라이버가 커넥션을 “생성”하는 과정에 대한 설명이다.
- 커넥션 생성 과정
- 애플리케이션 로직은 JDBC 드라이버를 통해 커넥션을 조회한다.
- JDBC 드라이버는 데이터베이스 서버와 TCP/IP 커넥션을 연결한다. 이 과정에서 3 way handshake 같은 TCP/IP 연결을 위한 네트워크 동작이 발생한다.
- TCP/IP 커넥션이 연결되면, JDBC 드라이버는 ID, PW, 기타 부가정보를 데이터베이스 서버에 전달한다.
- 데이터베이스 서버는 ID, PW를 통해 내부 인증을 완료하고, 내부에 세션을 생성한다.
- 데이터베이스 서버는 커넥션 생성이 완료되었다는 응답을 보낸다.
- JDBC 드라이버는 커넥션(Connection) 객체를 생성해서 클라이언트(애플리케이션 서버)에 반환한다.
- 매번 커넥션을 새로 생성할 때 발생하는 문제
- 위와 같이, 커넥션을 새로 생성하는 과정은 복잡하기 때문에 애플리케이션 로직에서 매번 커넥션을 새로 생성한다면 다음과 같은 문제들이 발생한다.
- 데이터베이스 서버는 물론이고 애플리케이션 서버에서도 커넥션을 새로 생성하기 위한 리소스를 매번 사용해야 한다.
- 고객이 애플리케이션을 사용할 때, SQL을 실행하는 시간 뿐만 아니라 커넥션을 새로 만드는 시간이 추가되기 때문에 결과적으로 응답 속도에 영향을 준다.
- 이런 문제들을 한번에 해결하는 아이디어가 바로 커넥션을 미리 생성해두고 사용하는 커넥션 풀(커넥션을 관리하는 풀) 방법이다.
- 위와 같이, 커넥션을 새로 생성하는 과정은 복잡하기 때문에 애플리케이션 로직에서 매번 커넥션을 새로 생성한다면 다음과 같은 문제들이 발생한다.
커넥션 풀
- 커넥션 풀은 미리 일정 수의 데이터베이스 커넥션을 생성하여 풀로 관리하고, 필요할 때마다 풀에서 기존의 연결을 재사용하도록 하는 기법이다.
- 이전처럼 DriverManager를 통해 새로운 커넥션을 획득하는 방식이 아니라 이미 생성되어 있는 커넥션을 획득하는 방식이다.
- 따라서, 커넥션 풀 방식을 사용하면 애플리케이션 로직을 수행할 때 커넥션을 새로 생성하는 복잡한 과정을 수행하지 않는다.
- 커넥션 풀은 개념적으로 단순해서 직접 구현할 수도 있지만, 사용도 편리하고 성능도 뛰어난 오픈소스 커넥션 풀이 많기 때문에 오픈소스를 사용하는 것이 좋다.
- 대표적인 커넥션 풀 오픈소스는 commons-dbcp2 , tomcat-jdbc pool , HikariCP 등이 있으며, Spring Boot는 기본적으로 HikariCP를 커넥션 풀로 사용한다.
- 커넥션 풀은 별도의 쓰레드를 사용해서 커넥션을 채운다.
- 커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리는 일이기 때문에, 애플리케이션을 실행할 때 커넥션 풀을 채울 때 까지 마냥 대기하고 있다면 애플리케이션 실행 시간이 늦어진다. 따라서 별도의 쓰레드를 사용해서 커넥션 풀을 채워야 애플리케이션 실행 시간에 영향을 주지 않는다.
- 커넥션 풀에 커넥션을 채우는 것은 상대적으로 오래 걸리는 일이기 때문에, 애플리케이션을 실행할 때 커넥션 풀을 채울 때 까지 마냥 대기하고 있다면 애플리케이션 실행 시간이 늦어진다. 따라서 별도의 쓰레드를 사용해서 커넥션 풀을 채워야 애플리케이션 실행 시간에 영향을 주지 않는다.
- 이전처럼 DriverManager를 통해 새로운 커넥션을 획득하는 방식이 아니라 이미 생성되어 있는 커넥션을 획득하는 방식이다.
- 커넥션 풀 초기화와 연결 상태
- 애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
- 기본값은 보통 10개이며, 적절한 커넥션 풀 숫자는 서비스의 특징과 애플리케이션 서버 스펙, DB 서버 스펙에 따라 다르기 때문에 성능 테스트를 통해서 정해야 한다.
- 커넥션 풀은 서버당 최대 커넥션 수를 제한할 수 있다. 따라서 DB에 무한정 연결이 생성되는 것을 막아주어서 DB를 보호하는 효과도 있다.
- 커넥션 풀에 들어 있는 커넥션은 TCP/IP로 데이터베이스 서버와 커넥션이 연결되어 있는 상태이기 때문에, 언제든지 즉시 SQL을 데이터베이스 서버에 전달할 수 있다.
- 애플리케이션을 시작하는 시점에 커넥션 풀은 필요한 만큼 커넥션을 미리 확보해서 풀에 보관한다.
- 커넥션 풀 사용 과정
- 애플리케이션 로직은 커넥션 풀을 통해 커넥션을 조회한다.
- 커넥션 풀에 커넥션을 요청하면, 커넥션 풀은 자신이 가지고 있는 커넥션 중에 하나를 반환한다.
- 애플리케이션 로직은 커넥션 풀에서 받은 커넥션을 사용해서 SQL을 데이터베이스 서버에 전달하고 그 결과를 받아서 처리한다.
- 커넥션을 모두 사용하고 나면, 커넥션을 종료하는 것이 아니라 다음에 다시 사용할 수 있도록 해당 커넥션이 살아있는 상태로 커넥션 풀에 반환한다.
DataSource
- 커넥션을 획득하는 방법
- 지금까지 설명한 것처럼, 애플리케이션 로직에서 커넥션을 획득하는 방법은 대표적으로 2가지 방법이 있다. (실무에서는 기본으로 커넥션 풀 사용)
- 직접 DriverManager 사용 - 새로운 커넥션
- 커넥션 풀 사용 - 이미 생성되어 있는 커넥션
- 그러나, 애플리케이션 로직에서 DriverManager를 사용해서 커넥션을 획득하다가 HikariCP 같은 커넥션 풀을 사용하도록 변경할 때 문제가 발생한다.
- 의존관계가 DriverManager에서 HikariCP로 변경되기 때문에, 커넥션을 획득하는 애플리케이션 코드도 함께 변경해야 한다.
- 이런 문제를 해결하기 위해 자바에서는 커넥션을 획득하는 방법을 추상화하는 인터페이스인 DataSoucre를 제공한다.
- 지금까지 설명한 것처럼, 애플리케이션 로직에서 커넥션을 획득하는 방법은 대표적으로 2가지 방법이 있다. (실무에서는 기본으로 커넥션 풀 사용)
- DataSouce
public interface DataSource extends CommonDataSource, Wrapper { Connection getConnection() throws SQLException; ... }
- 애플리케이션 로직은 커넥션을 획득할 때 DataSource 인터페이스에만 의존하면 된다.
- 대부분의 커넥션 풀은 DataSource 인터페이스를 이미 구현해두었으며, 커넥션 풀 구현 기술을 변경하고 싶으면 해당 구현체로 갈아끼우기만 하면 된다.
- DriverManager는 DataSource 인터페이스를 사용하지 않기 때문에, DriverManager를 사용하다가 DataSource 기반의 커넥션 풀을 사용하도록 변경하면 관련 코드를 다 고쳐야 한다.
- 이런 문제를 해결하기 위해 스프링은 DriverManager도 DataSource를 통해서 사용할 수 있도록 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다.
- 애플리케이션 로직은 커넥션을 획득할 때 DataSource 인터페이스에만 의존하면 된다.
DataSource 사용 예시 - Spring
이전 글 참고
- 프로퍼티 파일 작성 (src/main/resources/application.properties)
jdbc.url=jdbc:mysql://(DB 서버 주소):3306/(DB명)?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true jdbc.username=(DB 사용자 이름) jdbc.password=(DB 사용자 비밀번호)
- 스프링 설정 클래스 작성 (src/main/java/com/example/myproject/config/)
- DispatcherConfig
@Configuration @ComponentScan(basePackages = "com.example.myproject") @EnableWebMvc public class DispatcherConfig implements WebMvcConfigurer { ... }
- DataSourceConfig
@Configuration @PropertySource("classpath:application.properties") public class DataSourceConfig { // Environment를 사용하여 프로퍼티 파일의 값을 로드 @Autowired private Environment env; @Bean public DataSource dataSource() { // 1. DriverManagerDataSource // DriverManagerDataSource dataSource = // new DriverManagerDataSource(jdbc.url, jdbc.username, jdbc.password); // 2. 커넥션 풀의 DataSource HikariDataSource dataSource = new HikariDataSource(); // 필수 dataSource.setJdbcUrl(env.getProperty("jdbc.url")); dataSource.setUsername(env.getProperty("jdbc.username")); dataSource.setPassword(env.getProperty("jdbc.password")); // 선택 dataSource.setMaximumPoolSize(20); // 설정하지 않으면 기본값은 10 return dataSource; } }
- DispatcherConfig
- MemberRepositoryJdbcDataSource 클래스 작성
@Repository public class MemberRepositoryJdbcDataSource implements MemberRepository { // 외부에서 DataSource를 주입 받아서 사용 private final DataSource dataSource; public MemberRepositoryJdbcDataSource(DataSource dataSource) { this.dataSource = dataSource; } private Connection getConnection() throws SQLException { Connection con = dataSource.getConnection(); return con; } private void close(Connection con, Statement stmt, ResultSet rs) { JdbcUtils.closeResultSet(rs); JdbcUtils.closeStatement(stmt); JdbcUtils.closeConnection(con); } //save(Member member)... //findById(String memberId)... //update(String memberId, int money).... //delete(String memberId).... }
참고 자료
“김영한 스프링 DB 1편” https://docs.oracle.com/javase/8/docs/api/javax/sql/DataSource.html