MySQL의 InnoDB 스토리지 엔진은 주로 B+ Tree를 사용한다. B+ Tree B-Tree에서 파생된 개념이다.
B+Tree는 데이터베이스 인덱스와 파일 시스템 사용에 더 적합할 수 있도록 만들어졌다.
B-Tree와 차별화되는 특징은 아래와 같다.
B+Tree의 리프 노드는 서로 연결 리스트로 서로 연결되어, 형제 노드끼리도 옮겨가며 조회할 수 있다. 연결된 리프 노드의 리스트를 따라가면서 범위 쿼리를 할 수 있어서, 범위 검색 성능이 좋다. internal 노드에는 키만 저장되며, 이 키를 사용해서 자식 노드의 메모리 상 위치를 판단한다..
internal 노드에는 데이터의 포인터도 없으며, 오로지 키만 저장된다. 데이터를 찾기 위한 포인터도 리프 노드에만 있다. internal 노드의 크기를 줄여 메모리 사용이 효율적이다. 실제 데이터는 리프 노드에만 저장되어 있다.
먼저 데이터를 넣어보면서 B-트리의 동작원리에 대해 알아보자
하나의 노드에 최대 2개의 값만 들어갈 수 있고 오름차순으로 정렬돼야하기때문에 위 그림처럼 삽입된다.
50은 42보단 크고 55보단 작아서 그 사이에 위치해야하는데 하나의 노드엔 최대 2개의 값만 들어갈 수 있으므로
트리의 균형을 위해 재배치해준다. 따라서 50이 상위노드로 올라가고 42와 55가 좌우로 연결된 형태로 변한다.
아까 50을 삽입했을 때처럼 55 80 90 에서 재배치를 해야하기 때문에 중간값인 80을 상위로 보낸다. 50이 담겨있던 노드는 하나를 더 담을 수 있기때문에 50 80 노드가 되고 재배치가 종료된다.
60을 넣으면 55 60 70 이렇게 정렬이 되야하는데 초과가 발생하므로 중간값인 60이 상위로 올라간다.
그런데 상위 노드에서도 50 60 80으로 초과가 발생하므로 다시 중간값인 60이 상위로 올라가며 재배치가 끝난다.
B-Tree는 이런 규칙을 따르며 트리의 균형을 유지한다.
그럼 B+Tree는 어떨까?
B-Tree의 예시처럼 42 55 50을 순서대로 삽입한 모습이다.
같은 규칙으로 한 노드에 2개 이상의 값이 들어가자 중간값인 50이 상위로 올라가는 모습이다.
하지만 리프노드가 뭔가 다르단걸 볼 수 있다.
B-Tree의 예시처럼 동일한 값을 전부 삽입한 모습이다.
2개이상의 값이 노드에 있으면 중간값을 올리는 규칙을 따랐지만 뭔가 다른 모습이다.
바로 B+Tree는 실제 값은 전부 리프노드에만 저장되는 특징때문이다.
잘보면 삽입된 값은 전부 리프노드에서 확인할 수 있고 링크드리스트로 연결이 되어있다.
이것이 B+tree와 그냥 B-tree의 차이점이라고 할 수 있다!
B-tree는 리프 노드와 내부 노드가 각각 데이터와 포인터까지 가지고 있기 때문에 B+Tree보다 더 많은 메모리 공간을 차지한다. B+tree는 데이터는 리프 노드에만 저장되고, internal 노드는 키만 갖고 있으면 되므로, 메모리 효율이 좋다.
또한 B-tree는 순차탐색을 위한 알고리즘이 따로 필요한 반면, B+tree는 연결된 리프노드를 통해 순차탐색이 용이하다.
마지막으로 B+Tree가 DB 인덱스에 사용되었을 때 더 좋은 점에 대해서 이야기하면 아래와 같다.
리프 노드를 제외하고 데이터를 담아두지 않기 때문에 internal 노드의 경우, 한정된 메모리 안에 더 많은 key들을 수용할 수 있다.
즉, 하나의 노드에 더 많은 key들을 저장하기 때문에 동일한 데이터 개수라면, 트리의 높이는 더 낮아진다.
cache hit를 높일 수 있을 뿐만 아니라, 범위 검색이나 범위 쿼리에도 큰 강점을 갖고 있으며, 상대적으로 굉장히 느린 secondary storage에 대한 접근 횟수가 현저히 줄어든다.
테이블 풀 스캔 시, B-Tree의 경우에는 데이터가 internal 노드와 리프 노드에 퍼져있기 때문에 모든 노드를 탐색해야 한다.
B+tree는 리프 노드에만 데이터가 존재하고, 존재하는 모든 데이터는 리프 노드에 있기 때문에 한 번의 선형탐색만 하면 되므로 매우 빠르다.
1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다. 물론 이 과정에서 3 way handshake 같은 TCP/IP
연결을 위한 네트워크 동작이 발생한다.
3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달한다.
4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
이게 너무 복잡하고 시간도 많이 소요되니까 커넥션 풀을 사용하는 것!
장점
비용절감
커넥션을 매번 열고 닫는 것은 시간과 자원을 많이 소모한다.
커넥션 풀을 사용하면 커넥션을 재사용해 성능을 높일 수 있다.
성능향상
DB 커넥션 생성/종료 과정을 줄여 응답속도가 빨라짐.
트래픽이 급증해도 커넥션 풀로 안정적인 서비스 제공 가능.
안정성
커넥션을 일정 수만 유지해 DB 과부하 방지함
유휴 커넥션은 자동으로 정리하거나 갱신함
DBCP 동작 원리
1. 애플리케이션 시작 시 미리 커넥션 풀을 생성
2. 요청 발생 시 커넥션 풀에서 사용 가능한 커넥션을 할당
3. 작업 완료 후 커넥션을 닫지 않고 반환
4. 커넥션 풀이 부족하면 새 커넥션 생성 또는 대기
5. 일정 시간이 지난 유휴 커넥션은 자동으로 제거
추천되는 것
HikariCP
경량화와 고성능 커넥션 풀
빠른 응답 속도와 효율적인 자원관리를 해줌
Spring boot 기본으로 채택됨!
DBCP 주요 설정 항목
1. 기본 커넥션 설정
setJdbcUrl(String url)
DB 연결을 위한 JDBC URL 설정
"jdbc:mysql://localhost:3306/mydb"
setUsername(String username)
DB 연결을 위한 사용자 이름 설정
"root"
setPassword(String password)
DB 연결을 위한 비밀번호 설정
"password"
setDriverClassName(String className)
JDBC 드라이버 클래스 설정
"com.mysql.cj.jdbc.Driver"
2. 커넥션 풀 크기 설정
setMaximumPoolSize(int maxPoolSize)
최대 커넥션 수 설정
10
20
setMinimumIdle(int minIdle)
최소 유휴 커넥션 수 설정
동일
5
setIdleTimeout(long milliseconds)
유휴 커넥션 유지 시간(ms)
600,000
300,000
setMaxLifetime(long milliseconds)
커넥션 최대 생존 시간 설정(ms)
1,800,000
1,800,000
setConnectionTimeout(long milliseconds)
커넥션 요청 대기 시간 설정(ms)
30,000
10,000
3. 커넥션 테스트 및 검증
setConnectionTestQuery(String sql)
커넥션 유효성 검사용 쿼리 설정
"SELECT 1"
setValidationTimeout(long milliseconds)
커넥션 유효성 검사 타임아웃(ms) 설정
5,000
setInitializationFailTimeout(long milliseconds)
풀 초기화 실패 시 대기 시간(ms)
1,000
4. 성능 최적화 관련 설정
setAutoCommit(boolean autoCommit)
커넥션 Auto Commit 설정
true
setReadOnly(boolean readOnly)
커넥션 읽기 전용 설정
false
setIsolateInternalQueries(boolean flag)
내부 쿼리를 트랜잭션에서 분리할지 여부
false
setLeakDetectionThreshold(long milliseconds)
커넥션 누수 감지 시간 설정(ms)
0 (비활성화)
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
public class HikariCPExample {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("password");
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 커넥션 풀 설정
config.setMaximumPoolSize(20); // 최대 커넥션 수
config.setMinimumIdle(5); // 최소 유휴 커넥션
config.setConnectionTimeout(10000); // 커넥션 요청 대기 시간 (10초)
config.setIdleTimeout(300000); // 유휴 커넥션 유지 시간 (5분)
config.setMaxLifetime(1800000); // 커넥션 최대 수명 (30분)
// 유효성 검사 쿼리
config.setConnectionTestQuery("SELECT 1");
// 누수 감지 (5초 이상 반환 안 되면 경고)
config.setLeakDetectionThreshold(5000);
// 커넥션 풀 이름
config.setPoolName("MyHikariCP");
// 데이터 소스 초기화
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() throws Exception {
return dataSource.getConnection();
}
public static void close() {
if (dataSource != null) {
dataSource.close();
}
}
}