커넥션 풀을 사용하는 이유
DB에 쿼리를 하나 날리기 위해서는 아래와 같은 과정을 거쳐야 한다.
- TCP/IP 연결 수립 (3-way handshake): DB 서버와 네트워크 연결을 맺는다. 물리적으로 시간이 가장 많이 걸린다.
- ID/PW 인증: 전달받은 계정 정보를 DB 내부적으로 확인한다.
- DB 세션 생성: DB가 해당 클라이언트를 위한 메모리와 리소스를 할당한다.
- SQL 실행: 실제 원하는 작업을 수행한다.
- 연결 종료: 리소스 해제 및 TCP 연결 종료
실제 중요한 4번보다, 1~3번 과정의 준비 과정(연결 수립) 시간이 더 오래 걸리는 배보다 배꼽이 더 큰 상황이 발생하므로 커넥션 풀을 사용한다.
커넥션 풀
- TCP/IP 연결을 수립하는 과정에서 시간이 소요되므로, 미리 생성해두고 사용하는 것. 보통 10개이다.
- hikariCP가 기본적으로 커넥션 풀로 사용된다.
@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());
}
/**
* DB 접속 정보를 한 번만 넘겨서 DataSource를 생성하면, 그 이후부터는 커넥션을 가져올 때 DB 접속 정보가 필요 없다.
* getConnection()만 호출하면 된다. -> 설정과 사용을 분리한 것
*/
@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());
}
}
즉, DataSource는 커넥션을 얻는 방법을 추상화하는 인터페이스이다.
@Slf4j
class MemberRepositoryV1Test {
MemberRepositoryV1 repository;
@BeforeEach
void beforeEach() {
// 기본 DriverManger - 항상 새로운 커넥션 획득하기 -> 성능 낮음
// DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
// repository = new MemberRepositoryV1(dataSource);
// 커넥션 풀링 - 커넥션 풀에 만들어진 커넥션 획득하기 -> TCP/IP 연결 수립 과정 생략 및 반환된 커넥션을 재사용
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(URL);
dataSource.setUsername(USERNAME);
dataSource.setPassword(PASSWORD);
repository = new MemberRepositoryV1(dataSource);
}
@Test
void crud() throws SQLException {
// save
Member member = new Member("memberV100", 10000);
repository.save(member);
// findById
Member findMember = repository.findById(member.getMemberId());
log.info("findMember = {}", findMember);
Assertions.assertThat(member).isEqualTo(findMember);
// update
repository.update(member.getMemberId(), 20000);
Member updatedMember = repository.findById(member.getMemberId());
Assertions.assertThat(updatedMember.getMoney()).isEqualTo(20000);
// delete
repository.delete(updatedMember.getMemberId());
Assertions.assertThatThrownBy(() -> repository.findById(member.getMemberId()))
.isInstanceOf(NoSuchElementException.class);
}
}
DB 연결 구조

- 커넥션에 연결된 세션을 통해 요청이 실행된다.
- 사용자가 커넥션을 닫거나 관리자가 세션을 강제 종료하면 세션이 종료된다.
트랜잭션
- 수동 커밋 모드를 시작하는 것을 관례적으로 트랜잭션을 시작한다 라고 한다. (기본이 자동 커밋이기 때문)
- 해당 모드는 세션 내에서 한 번 설정되고 나면, 해당 세션이 종료될 때까지 변경될 수 없다.
- 커밋이 되지 않으면, 변경된 데이터는 해당 세션에서만 볼 수 있고 다른 세션에서는 볼 수 없다. -> 다른 세션에서 트랜잭션 시작하기 전 변경되기 전 데이터로 조회. (READ UNCOMMITED 제외)
- 또한, 트랜잭션을 시작하려면 커넥션이 필요하며, 트랜잭션 내에서는 같은 커넥션을 유지해야 한다.
DB 락
- 세션이 트랜잭션을 시작하고 데이터를 변경하는 동안, 다른 세션에서 해당 데이터를 변경하지 못하도록 막는 것.
- 변경하고자 하는 데이터 (row)에 대해 락을 획득해야 update sql을 수행할 수 있다. 즉, 락은 update 시점에만 걸린다.
- select for update 구문을 사용하면 조회 시점에 락을 획득할 수 있다. 그러면 조회만 했지만, 다른 세션에서 해당 row에 대해 update sql을 락을 획득할 때 까지 수행하지 못한다.
- 락을 획득하지 못한 상태라면, 락을 획득할 때가지 기다려야 한다. (대기 시간이 설정을 넘어가면 타임아웃 오류)
'공부 > Spring' 카테고리의 다른 글
| [인프런 김영한] 자바 예외 (0) | 2026.02.08 |
|---|---|
| [인프런 김영한] 스프링과 트랜잭션 (0) | 2026.02.05 |
| [인프런 김영한] OSIV와 성능 최적화 (0) | 2025.11.25 |
| [인프런 김영한] 지연 로딩과 조회 성능 최적화 (0) | 2025.11.25 |
| [인프런 김영한] DTO를 이용한 데이터 전달 (0) | 2025.11.19 |
