[Spring Boot] 왜 default DBCP는 10개일까?

모지리 개발자·2022년 9월 16일
2
post-thumbnail

Intro

Spring Boot로 프로젝트를 하던 중에 문득 그런 생각이 들었습니다..

application.yml 에 아무런 설정도 하지 않았을 경우 쓰레드 풀의 쓰레드 default 개수는 200개인데 왜 DBCP(hikari CP사용)의 default 개수는 10개일까?

막연하게 "요즘 이렇게 좋은 하드웨어, 소프트웨어 세상에 기본개수를 10개?" 전지전능한 개발자분들이 DBCP의 기본개수를 10개로 지정한 것에는 이유가 있을 것이라는 생각이 들어서 관련된 자료를 찾으면서 글을 작성해보게되었습니다.

우선 글의 이해를 위해 간단히 쓰레드풀과 DB 커넥션풀에 대해 작성해보겠습니다.

쓰레드풀이란?


스프링부트에서는 요청하나마다 쓰레드를 생성합니다. 쓰레드풀을 사용하지 않고 요청이 한번에 무지막지하게 들어오게 되면 쓰레드를 무한정 만들어내게 되고 이는 결국 에플리케이션에 큰 문제를 초래합니다.

이러한 문제를 막기 위해 쓰레드풀을 생성해놓고 요청이 들어오면 쓰레드풀에서 쓰레드를 가져다가 사용하고 사용이 완료되면 다시 반납됩니다.

아래는 쓰레드 풀 설정 예시 입니다.

application.yml

(적어놓은 값은 default)
server:
  tomcat:
    threads:
      max: 200 # 생성할 수 있는 thread의 총 개수
      min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
    max-connections: 8192 # 수립가능한 connection의 총 개수
    accept-count: 100 # 작업큐의 사이즈
    connection-timeout: 20000 # timeout 판단 기준 시간, 20초
  port: 8080 # 서버를 띄울 포트번호

DB 커넥션풀이란?


DB와 커넥션을 맺고있는 객체를 관리하는 역할을 합니다.

쓰레드풀과 마찬가지로 DB에 요청이 무지막지하게 들어와서 들어올 때마다 connection을 생성한다면 DB에 큰 부하를 줄 수 있고 에플리케이션에 큰 문제를 초래할 수 있습니다. 또 클라이언트가 요청을 할 때마다 드라이버를 로드하고 커넥션 객체를 생성하여 연결하고 종료하기 때문에 매우 비효율적입니다.

DB 커넥션풀을 사용한다면 이미 연결된 커넥션을 가져다 사용하기 때문에 DB 부하를 줄이고 무한정 커넥션이 생성되는 것을 예방할 수 있습니다.

아래는 DBCP 설정 예시입니다.

application.yml

(적어놓은 값은 default)
spring:
  ...
  datasource:
    hikari:
      jdbc-url: url
      connectionTimeout: 30000 # 30초를 뜻함. 클라이언트가 pool에 connection을 요청하는데 기다리는 최대시간
      maximumPoolSize: 10 # 유휴 및 사용중인 connection을 포함하여 풀에 보관가능한 최대 커넥션 개수
      maxLifetime: 1800000 # 30분을 뜻함. connection의 최대 유지 시간을 설정
      poolName: HikariCP
      readOnly: false # pool에서 얻은 connection이 기본적으로 readOnly인지 지정하는 설정, 데이터베이스가 readOnly 속성을 지원할 경우에만 사용가능

사실 검증

우선 알고있던 사실이 맞는지 확인하기 위해 예제코드를 구성해봤습니다.Github 예제코드

AsyncConfig.java

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(100); ## 기본 쓰레드 수, 변화시키면서 확인
        executor.setMaxPoolSize(100); ## 최대 쓰레드 수, 기본 쓰레드 수와 똑같이 변화시키면서 확인
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("seung-pool-");
        executor.initialize();
        return executor;
    }
}

AsyncTestEntity.java

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Builder
@AllArgsConstructor
public class AsyncTestEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", length = 200)
    private String name;
}

AsyncController.java

@RestController
@RequiredArgsConstructor
public class AsyncController {

    private final AsyncService asyncService;

    @GetMapping("/async")
    public String goAsync() {
        for(int i=1;i<100;i++) {
            asyncService.onAsync(i);
        }

        String str = "Hello Spring Boot Async!!";
        return str;
    }
}

AsyncService.java

@Service
@RequiredArgsConstructor
public class AsyncService {
    
    private final AsyncRepository asyncRepository;
    //비동기로 동작하는 메소드
    @Async
    public void onAsync(int i) {
        Optional<AsyncTestEntity> byId = asyncRepository.findById((long) i);
        AsyncTestEntity asyncTestEntity = byId.get();
        System.out.println(asyncTestEntity.getName());
    }
}

AsyncRepository.java

@Repository
public interface AsyncRepository extends JpaRepository<AsyncTestEntity, Long> {
}

application.yml

spring:
  h2:
    console:
      enabled: true
      path: /h2-console

  datasource:
    hikari:
      jdbc-url: jdbc:h2:tcp://localhost/~/test
      connectionTimeout: 30000
      maximumPoolSize: 50 ## 커넥션 개수 (이쪽을 변화 시키면서 확인)
      maxLifetime: 1800000
      poolName: HikariCP
      readOnly: false
      connectionTestQuery: SELECT 1
    driver-class-name: org.h2.Driver
    username: sa
    password: 1234

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate:
        format_sql: true
        show_sql: true
        dialect: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: update

logging:
  level:
    com.zaxxer.hikari: TRACE
    com.zaxxer.hikari.HikariConfig: DEBUG

데이터를 우선 100개 넣어두고 테스트 해야합니다. Github예제코드 들어가셔서 확인하실 수 있습니다.

실험 1. 기본 쓰레드 수 20개, 커넥션 수 20개로 같게 지정했을 경우(쓰레드 수 = 커넥션 수)

  1. 커넥션 20개 생성 확인

  2. 최대 20개씩 출력 확인

실험 2. 기본 쓰레드 수 20개, 커넥션 수 10개로 지정했을 경우(쓰레드 수 > 커넥션 수)

최대 10개씩 출력 확인

실험 3. 기본 쓰레드 수 10개, 커넥션 수 20개로 지정했을 경우(쓰레드 수 < 커넥션 수)

최대 10개씩 출력 확인

사실 검증완료

우선 기본적으로 알고 있던 사실이 틀리지 않았음을 확인은 했습니다.
1. 쓰레드 개수를 한정해놓고 동시에 한정한 개수 이상의 요청이 들어오게 되면 요청은 쓰레드를 할당받을 때까지 대기한다.
2. 커넥션 풀도 마찬가지로 한정한 개수 이상의 요청이 들어오게 되면 커넥션을 할당받을 때까지 대기한다.

쓰레드풀의 개수와 커넥션풀의 개수는 성능에 맞게 적절한 크기로 지정되어야한다.

Hikari CP에서는 Dead Lock을 피하기 위한 최적의 Maximum Pool Size 방법을 제공한다.

pool size = Tn x (Cm - 1) + 1
Tn : Thread의 최대 수
Cm: 하나의 Task에서 필요한 Connection 갯수

라는 것은 알겠는데... 왜 커넥션풀의 default 개수가 10개 일까가 궁금했습니다.
어플리케이션을 개발하다보면 대부분의 장애는 DB에서 발생한다고 생각해서(아닐수도있슴다...개인적으로 그렇게 생각해요) DB의 부하를 줄이기 위해서 커넥션의 개수를 적게 하는 것이 아무래도 부담을 덜 줄 수 있겠다는 생각은 드는데.. 아무리 그래도 10개는 컴퓨터라는 위대한 발명품에게 너무나도 적은 개수 같은..? 느낌이 들었습니다.

결론 : 그래서 왜 10개일까?

자료를 찾고싶었지만... 왜 10개인지에 대한 자료를 찾지 못했습니다.
스스로 내린 결론은 다음과 같습니다.

어짜피 커스텀해서 개수를 지정할 수 있기 떄문에 어떠한 성능을 가진 어플리케이션이든 문제없이 어플리케이션이 실행될 수 있도록 커넥션풀의 개수를 보수적으로(아주 적은 10개라는 숫자) 라고 지정했다.

찾아보고 추가할 내용이 있다면 더 추가해서 작성해보겠습니다.

제가 잘못이해하고 있거나 잘못 작성한 부분이 있다면 지적, 비판, 피드백 뭐든 해주시면 감사하겠습니다!

참고 블로그
https://velog.io/@sihyung92/how-does-springboot-handle-multiple-requests
https://linked2ev.github.io/spring/2019/08/14/Spring-3-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80%EC%9D%B4%EB%9E%80/
https://bamdule.tistory.com/166
https://jaehun2841.github.io/2020/01/27/2020-01-27-hikaricp-maximum-pool-size-tuning/#Dead-lock%EC%9D%84-%ED%94%BC%ED%95%98%EB%8A%94-Maximum-pool-size%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%84%A4%EC%A0%95%ED%95%98%EB%82%98%EC%9A%94

profile
항상 부족하다 생각하며 발전하겠습니다.

0개의 댓글