Connection Pool & Case Study

Taesoo Kim·2024년 12월 22일

Connection & Pooling

🔹 DB 커넥션(Connection)이란?

DB 커넥션은 애플리케이션과 데이터베이스(DB) 간의 통신 채널입니다.

  • 애플리케이션이 DB에 쿼리를 전송하고 결과를 받기 위해 생성하는 연결입니다.
  • DB에 데이터를 조회, 삽입, 수정, 삭제하려면 반드시 커넥션을 통해야 합니다.

🔸 커넥션의 동작 흐름

  1. 커넥션 생성
    • 애플리케이션이 DB 서버와 연결을 맺고, TCP/IP 연결을 통해 세션을 설정합니다.
  2. 쿼리 실행
    • 애플리케이션에서 SQL 쿼리를 커넥션을 통해 DB에 전송합니다.
  3. 결과 반환
    • DB는 쿼리 결과를 커넥션을 통해 애플리케이션으로 반환합니다.
  4. 커넥션 종료
    • 작업이 끝나면 커넥션을 닫아(DB 세션 종료) 자원을 반환합니다.

한 커넥션에 이렇게 많은 리소스가 모소 됩니다. 그래서 미리 연결해 놓은 커넥션을 모아둔 Pool을 주로 사용합니다.

🔹 DB Connection Pool이란?

DB Connection Pool(커넥션 풀)은 데이터베이스와의 연결(Connection)을 미리 생성해두고 재사용하는 메커니즘입니다.

  • DB 커넥션은 생성 비용이 크기 때문에, 매번 새로 연결을 생성하는 것은 성능 저하를 유발합니다.
  • 커넥션 풀은 미리 일정 개수의 커넥션을 생성해두고 요청이 있을 때 바로 제공함으로써 성능을 향상시킵니다.

🔸 동작 방식 (Flow)

  1. 애플리케이션 시작 시, 일정 개수의 DB 커넥션을 미리 생성
    • 예: 10개의 커넥션을 미리 생성해 풀(Pool)에 보관
  2. 요청 발생 시 커넥션 풀에서 유휴(Idle) 커넥션을 할당
    • 사용 가능한 커넥션이 있으면 즉시 반환
    • 사용 가능한 커넥션이 없으면 대기하거나 새로운 커넥션을 추가 생성
  3. 작업 종료 후 커넥션은 반환되어 다시 풀에서 재사용 가능 상태가 됨
  4. 최대 커넥션 수를 초과하면 대기하거나 예외 발생
    • 커넥션 풀 크기를 초과하는 경우, 일정 시간 동안 대기(Timeout)하거나 실패

Pooling vs No Pooling 벤치마크
https://amaran-th.github.io/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4/[DB]%20DB%20Connection%20Pool/

아마란스 생각님의 DB Pooling

대충 커넥션이 많아질수록 성능차이가 많이 난다는 뜻

🔸 커넥션 풀 동작 예시

spring:
  datasource:
    hikari:
      minimum-idle: 5             # 최소 유휴 커넥션 수
      maximum-pool-size: 20       # 최대 커넥션 수
      idle-timeout: 30000         # 30초 동안 유휴 상태면 커넥션 종료
      max-lifetime: 1800000       # 커넥션 최대 수명 (30분)
      connection-timeout: 2000    # 커넥션 요청 대기 시간 (2초)
  • 최소 5개 커넥션을 유지하고, 최대 20개까지 커넥션을 생성합니다.
  • 30초 동안 사용되지 않은 커넥션은 제거됩니다.
  • 커넥션 요청이 몰릴 경우, 최대 2초 동안 대기하다가 커넥션을 얻지 못하면 예외가 발생합니다.

풀이 없다면 예외를 발생합니다

const ctxUserId = ctx.userData?.id || 0;

//여기서 getConnection은 단일 커넥션이 아닌 Pool에서 가져오는 커넥션
//connection.release시 까지 커넥션을 독점합니다.
const connection = getConnection();
const queryRunner = connection.createQueryRunner();

await queryRunner.connect();
await queryRunner.startTransaction();

try {
  let result = false;

  const { 
    customerUid, 
    payType, 
    pg, 
    cardNumber, 
    cardName 
  } = inputIamportUsingSubscription;

  // Request Billing Key
  const requestFromSomewhere = await axios({
    url: `https://somewhere.co.kr/subscribe/customers/${customerUid}`,
    method: 'get',
    headers: { 
      Authorization: accessToken, 
      'Content-Type': 'application/json' 
    }
  });

  // Validate Response
  if (requestFromSomewhere.status !== 200 || requestFromSomewhere.data.code !== 0) {
    throw new Error(requestFromSomewhere.message);
  }

  // Create New Iamport Payment Record
  const newPayment = repoPayment.create();

  if (customerUid) {
    newPayment.customerUid = customerUid;
    newPayment.paymentType = payType;  // @ts-ignore
    newPayment.pg = pg;                // @ts-ignore
    newPayment.cardNumber = cardNumber;
    newPayment.cardName = cardName;
  }

  newPayment.user = await repoUser.findOneOrFail(ctxUserId);

  const findDefaultPaymentInfo = await repoPayment
    .createQueryBuilder('imp')
    .where('imp.userId = :userId', { userId })
    .andWhere('imp.isDefault = true')
    .getOne();

  newPayment.isDefault = findDefaultPaymentInfo ? false : isDefault;

  // Save to DB
  await queryRunner.manager.save(newPayment);
  
  ///////////////////내 첫번째 변경사항///////////////////
  await Promise.all([
        queryRunner.manager.save(newPayment),
        queryRunner.manager.save(subscriberResolved)
      ]);
      
	////////////////////////////////////////////////////////
	
  await queryRunner.commitTransaction();

  return result;

} catch (e) {
  await queryRunner.rollbackTransaction();
  throw new TooningServerRegisterBillingKeyError(e);
} finally {
  await queryRunner.release();
}
profile
뭔 생각을 해. 그냥 하는 거지 뭐

0개의 댓글