---
term: '관리형 데이터베이스의 서버리스 커넥션 풀링'
seoTitle: '서버리스 커넥션 풀링이란? 관리형 데이터베이스의 연결 관리'
headline: '서버리스 커넥션 풀링이란?'
slug: serverless-connection-pooling
category: database
shortDefinition: '서버리스 커넥션 풀링은 소수의 실제 데이터베이스 연결을 수명이 짧은 다수의 함수 인스턴스가 공유하게 하는 기법입니다.'
relatedTerms:
  - multi-tenant-database-architecture
  - serverless-architecture
  - auto-generated-database-apis
  - database-index
contrastsWith:
  - serverless-architecture
faq:
  - question: '서버리스는 왜 데이터베이스 연결을 고갈시킵니까?'
    answer: '확장 모델이 충돌하기 때문입니다. 서버리스 컴퓨팅은 요청에 따라 인스턴스를 늘리지만, 데이터베이스의 연결 예산은 고정되어 있습니다(기본값은 보통 100 안팎). 함수 인스턴스마다 자체 연결을 열고 유휴 시간에도 유지하므로, 인스턴스 500개를 띄우는 트래픽 급증은 한꺼번에 연결 500개를 요구합니다. 데이터베이스는 초과분을 거부하고, 관련 없는 쿼리까지 실패하기 시작합니다.'
  - question: 'PgBouncer 같은 커넥션 풀러는 실제로 무엇을 합니까?'
    answer: '코드와 데이터베이스 사이에 위치해 비용이 싼 클라이언트 연결을 수천 개 받아들이고, 이를 소수의 실제 서버 커넥션 풀 위에서 다중화합니다. 클라이언트는 연결을 가지고 있다고 생각하지만 실제로는 트랜잭션이나 세션 동안 하나를 빌렸다가 반납합니다. 함수가 몇 개나 실행되든 데이터베이스는 안정적인 고정 연결 집합만 보게 됩니다.'
  - question: '트랜잭션 풀링과 세션 풀링의 차이는 무엇입니까?'
    answer: '임대 기간입니다. 세션 풀링은 클라이언트의 세션 전체 동안 실제 연결을 할당하므로 모든 기능에 안전하지만, 유휴 클라이언트 하나도 연결 하나를 붙잡아 둡니다. 트랜잭션 풀링은 트랜잭션이 실행되는 동안에만 연결을 빌려주고 회수하므로 공유 효율이 크게 높아지지만 세션 상태가 깨집니다. prepared statement, 세션 변수, LISTEN/NOTIFY는 주의가 필요하며 그렇지 않으면 실패합니다.'
  - question: '함수가 확장될 때 연결 수는 왜 급증합니까?'
    answer: '서버리스 플랫폼은 인스턴스를 복제해 동시성을 처리하고, 각 복제본은 자체 데이터베이스 클라이언트를 초기화합니다. 연결 재사용은 하나의 웜 인스턴스 안에서만 일어나며 인스턴스 사이에서는 결코 일어나지 않습니다. 따라서 동시성 300은 크기 1짜리 풀 300개를 뜻합니다. 공유는 전혀 없이 풀링의 오버헤드만 떠안는 최악의 형태입니다. 이 팬아웃은 구조적인 것이지 코드의 버그가 아닙니다.'
  - question: '데이터베이스 커넥션 풀은 얼마나 커야 합니까?'
    answer: '직관보다 훨씬 작아야 합니다. 처리량은 보통 풀 크기가 수백이 아니라 데이터베이스의 실제 병렬성 근처일 때 정점에 이릅니다. 흔한 경험칙은 코어 수의 두 배이며, 디스크 대기에 따라 조정합니다. 그 이상의 연결은 처리량은 늘리지 않고 경합과 메모리 비용만 더합니다. 풀러의 역할은 바로 작은 풀을 호출자에게 무한한 것처럼 느끼게 하는 것입니다.'
  - question: 'BaaS를 사용해도 풀러가 필요합니까?'
    answer: '여러분의 앱이 보내는 트래픽에는 필요 없으며, 바로 그 점이 핵심입니다. 관리형 백엔드는 클라이언트 요청을 API 계층에서 무상태 HTTPS 호출로 종료하고, 자체 서버 계층이 데이터베이스에 대해 수명이 길고 적절한 크기의 풀을 유지합니다. 함수와 앱은 데이터베이스 연결을 전혀 보유하지 않으므로 고갈 시나리오는 완화되는 것이 아니라 아예 사라집니다. 풀러는 외부 도구를 데이터베이스에 직접 연결할 때만 다시 필요해집니다.'
  - question: '커넥션 풀러의 단점은 무엇입니까?'
    answer: '네트워크 홉 하나만큼의 지연 시간이 더해지고, 직접 운영하고 모니터링해야 하는 구성 요소가 되며, 트랜잭션 모드는 의미를 조용히 바꿉니다. 세션 범위 기능이 문서대로 동작하지 않게 됩니다. 단일 풀러 프로세스는 급증하는 부하에서 그 자체가 병목이 될 수 있고, 크기를 잘못 잡으면 대기열의 위치만 옮겨집니다. 필수 인프라이지만, 결국 직접 운영해야 할 인프라가 하나 더 늘어나는 셈입니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'PgBouncer features — pooling modes'
    url: 'https://www.pgbouncer.org/features.html'
  - name: 'PostgreSQL connection settings (max_connections)'
    url: 'https://www.postgresql.org/docs/current/runtime-config-connection.html'
  - name: 'Connection pool (Wikipedia)'
    url: 'https://en.wikipedia.org/wiki/Connection_pool'
  - name: 'Serverless Architectures — Mike Roberts (martinfowler.com)'
    url: 'https://martinfowler.com/articles/serverless.html'
cta:
  title: '연결 계산은 아예 건너뛰기'
  text: 'Back4app에서는 클라이언트와 Cloud Code 함수가 자동 생성된 API와 무상태 HTTPS로 통신하고, 플랫폼이 관리형 데이터베이스에 대한 적정 크기의 풀을 유지합니다. 배포할 풀러도, 조정할 max_connections도, 새벽 3시의 고갈 장애도 없습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-11-03'
translationKey: serverless-connection-pooling
---

**서버리스 커넥션 풀링은 소수의 실제 데이터베이스 연결을 수명이 짧은 다수의 함수 인스턴스가 공유하게 하는 기법입니다.** 이 기법이 존재하는 이유는 두 확장 모델이 서로 맞지 않기 때문입니다. [서버리스 컴퓨팅](/glossary/ko/serverless-architecture/)은 부하에 인스턴스를 늘리는 방식으로 대응하는 반면, 데이터베이스는 모든 연결을 메모리를 점유하는 비싼 자원으로 취급하며 엄격한 상한을 둡니다. 둘을 단순하게 결합하면 적당한 트래픽 급증만으로도 `too many connections` 장애가 됩니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 충돌 | 함수는 복제로 확장되고, 연결은 상한이 있으며 비쌈 |
| 증상 | 급증 시 `FATAL: sorry, too many clients already` |
| 해결책 | 풀러가 많은 클라이언트를 소수의 실제 연결 위에서 다중화 |
| 조절 다이얼 | 트랜잭션 vs. 세션 풀링: 공유 효율 vs. 호환성 |
| BaaS의 답 | 클라이언트는 무상태 HTTPS로 통신하고, 풀은 플랫폼이 소유 |

## 데이터베이스에서 관찰한 장애

```sql
-- 서버리스 트래픽 급증 중 데이터베이스가 보는 모습:
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
--  active               |   14
--  idle                 |  483     ← 웜 함수 인스턴스가 붙잡아 둔 연결
SHOW max_connections;    --  100    ← 이미 뚫려 버린 상한

-- 각 연결은 실제 메모리를 점유하는 실제 프로세스입니다.
-- 그래서 상한이 존재하며, 상한을 올리는 것은 해결책이 아닙니다.
```

문제의 형태는 이렇습니다. 함수 인스턴스마다 자체 연결을 열고, 호출 사이의 유휴 시간에도 그 연결을 유지하며, 인스턴스 수는 개발자가 아니라 트래픽이 결정합니다. 재사용은 웜 인스턴스 *안에서만* 일어나고 인스턴스 사이에서는 결코 일어나지 않으므로, 동시성 300은 연결 하나짜리 풀 300개를 뜻합니다. [`max_connections`](https://www.postgresql.org/docs/current/runtime-config-connection.html)를 올리면 실제 메모리 비용을 치르고 여유를 확보할 뿐, 더 큰 급증이 오면 다시 한계에 부딪힙니다.

관리형 백엔드에서는 애플리케이션 코드가 이 문제에서 완전히 벗어납니다. SDK 호출은 무상태 HTTPS 요청이며, 어떤 클라이언트도 데이터베이스 연결을 보유하지 않습니다.

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// Inside a serverless function: no driver, no pool, no connection to leak.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize('APP_ID', 'JS_KEY');
Parse.serverURL = 'https://parseapi.back4app.com';

export async function handler(event) {
  const query = new Parse.Query('Order');
  query.equalTo('status', 'pending');
  query.limit(20);
  const orders = await query.find();  // request returns; nothing stays open
  return orders.map((o) => o.id);
}
// 1,000 concurrent invocations = 1,000 HTTP requests,
// not 1,000 database connections.
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
await Parse().initialize(
  'APP_ID',
  'https://parseapi.back4app.com',
  clientKey: 'CLIENT_KEY',
);

Future<List<String>> pendingOrderIds() async {
  final query = QueryBuilder<ParseObject>(ParseObject('Order'))
    ..whereEqualTo('status', 'pending')
    ..setLimit(20);
  final response = await query.query(); // request returns; nothing stays open
  return response.results!
      .map((o) => (o as ParseObject).objectId!)
      .toList();
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections.
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
ParseSwift.initialize(
  applicationId: "APP_ID",
  clientKey: "CLIENT_KEY",
  serverURL: URL(string: "https://parseapi.back4app.com")!
)

let query = Order.query("status" == "pending")
  .limit(20)
query.find { result in
  if case .success(let orders) = result {
    render(orders)                 // request returned; nothing stays open
  }
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections.
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize(
    Parse.Configuration.Builder(context)
        .applicationId("APP_ID")
        .clientKey("CLIENT_KEY")
        .server("https://parseapi.back4app.com")
        .build()
)

val query = ParseQuery.getQuery<ParseObject>("Order")
query.whereEqualTo("status", "pending")
query.limit = 20
query.findInBackground { orders, e ->
    if (e == null) render(orders)  // request returned; nothing stays open
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections.
```

## 풀러가 팬아웃을 흡수하는 방식

```mermaid
flowchart LR
  accTitle: 서버리스 함수와 데이터베이스 사이의 커넥션 풀러
  accDescr: 수명이 짧은 함수 인스턴스 수백 개가 풀러에 비용이 싼 클라이언트 연결을 열고, 풀러는 이를 소수의 고정된 실제 데이터베이스 연결 위에서 다중화해 데이터베이스를 연결 상한 아래로 유지합니다.
  F1["함수 ×500<br/>(급증)"] --> P["풀러<br/>(예: PgBouncer)"]
  F2["함수 ×500<br/>(다음 급증)"] --> P
  P -->|"실제 연결 20개"| DB[("데이터베이스<br/>max_connections: 100")]
  P -. "실패하는 대신<br/>잠시 대기열에서 기다림" .-> F1
```

[PgBouncer](https://www.pgbouncer.org/features.html) 같은 풀러는 클라이언트 연결을 수천 개 단위로 받아들이고(풀러 쪽에서는 각각의 비용이 쌉니다), 작업이 도착할 때만 소수의 풀에서 실제 서버 연결을 빌려줍니다. 데이터베이스는 안정적인 연결 20개를 보고, 함수 500개는 각자 연결을 하나씩 가졌다고 믿습니다. 수요가 풀을 넘으면 클라이언트는 오류를 받는 대신 대기열에서 몇 밀리초를 기다립니다. 치명적인 실패가 약간의 지연 시간으로 바뀌는 것입니다.

풀 크기 산정은 직관이 가장 크게 틀리는 지점입니다. 데이터베이스의 유효 병렬성은 코어와 디스크에 묶여 있습니다. 처리량은 보통 풀 크기가 수십 개 이내의 낮은 수준, 즉 코어 수의 두 배 근처일 때 정점에 이르고, 연결이 더 늘면 경합이 커져 오히려 *떨어집니다*. 풀러의 계산이 성립하는 이유는 서버리스 요청이 짧기 때문입니다. 8 ms짜리 트랜잭션을 처리하는 연결 하나는 초당 100건을 훌쩍 넘게 처리할 수 있으므로, 실제 연결 20개로 수천 건의 함수 호출을 넉넉히 흡수합니다. [풀](https://en.wikipedia.org/wiki/Connection_pool)은 편의를 위한 캐시가 아니라, 대기 비용이 싼 곳에 의도적으로 둔 병목입니다.

결정적인 조절 다이얼은 *임대가 얼마나 오래 지속되는가*입니다.

## 트랜잭션 풀링 vs. 세션 풀링

| 항목 | 세션 풀링 | 트랜잭션 풀링 |
| --- | --- | --- |
| 임대 기간 | 클라이언트 세션 전체 | 트랜잭션 하나, 이후 회수 |
| 공유 배수 | 낮음: 유휴 클라이언트가 연결을 붙잡아 둠 | 높음: 짧은 서버리스 작업에 이상적 |
| Prepared statement | 완전히 지원 | 프로토콜 수준 지원을 명시적으로 설정한 경우에만 |
| 세션 상태(`SET`, 임시 테이블, LISTEN/NOTIFY) | 동작함 | 깨짐: 트랜잭션마다 다른 연결에 닿을 수 있음 |
| 적합한 용도 | 수명이 긴 앱, 관리 도구, 마이그레이션 | 웹과 서버리스의 요청/응답 트래픽 |

트랜잭션 모드는 서버리스 워크로드를 가능하게 만드는 요소입니다. 8 ms 트랜잭션 하나를 실행하는 함수가 몇 분에 걸친 웜 수명 내내 연결을 소유해서는 안 됩니다. 하지만 공짜는 아닙니다. SQL 문 사이에 "내 연결"이 유지된다고 가정하는 모든 것이 미묘한 버그가 됩니다. 일반적인 권장 사항은 요청 트래픽에는 트랜잭션 풀링을, 마이그레이션과 상태가 있는 모든 작업에는 직접 연결이나 세션 풀링 연결을 쓰는 것입니다.

## 일반적인 사용 사례

- **SQL에 접근하는 서버리스 함수와 엣지 함수.** 대표적인 사례로, 급변하는 인스턴스 수가 고정된 연결 예산과 맞부딪힙니다.
- **작은 서비스 여럿, 데이터베이스 하나.** 서비스 20개 × 기본 풀 크기 10 = 아무도 계획하지 않은 연결 200개. 공유 풀러를 두면 연결 수를 다시 통제할 수 있습니다.
- **[멀티테넌트 플랫폼](/glossary/ko/multi-tenant-database-architecture/).** 테넌트별 워크로드가 연결 수요를 몇 배로 늘리며, 풀링을 쓰면 공유 데이터베이스의 연결 예산을 계속 지킬 수 있습니다.
- **작은 데이터베이스에 몰리는 트래픽 버스트.** 출시일의 급증이 데이터베이스에서 오류를 내는 대신 풀러에서 몇 밀리초 동안 대기합니다.
- **장애 중 프라이머리 보호.** 고정 크기 풀은 격벽(bulkhead)입니다. 폭주하는 클라이언트는 데이터베이스의 메모리가 아니라 풀러의 대기열을 소진합니다.

## 자체 풀러를 운영해야 할까? 결정 매트릭스

| 풀러를 운영해야 하는 경우 | 건너뛰어도 되는 경우 |
| --- | --- |
| 함수나 여러 서비스가 SQL에 직접 연결함 | 관리형 BaaS가 API 계층에서 클라이언트 트래픽을 종료함 |
| 부하 급증 시 연결 오류가 나타남 | 클라이언트 측 풀을 갖춘 소수의 장기 실행 서버에서 오는 꾸준한 트래픽 |
| 데이터베이스와 그 예산을 직접 운영함 | 플랫폼이 이미 API 계층과 데이터베이스 사이에서 풀링함 |
| 여러 테넌트나 팀이 데이터베이스 하나를 공유함 | 앱 하나, 풀 하나로 한도 안에 충분히 들어오는 워크로드 |
| 핵심 구성 요소를 하나 더 운영할 여력이 있음 | 풀러를 담당하는 온콜이 없음 |

솔직히 말해 둘은 대등하지 않습니다. 풀러는 연결 고갈을 *해결*하지만, 관리형 백엔드는 연결 고갈이 생길 여지를 *아예 없앱니다*. 클라이언트가 [자동 생성된 API](/glossary/ko/auto-generated-database-apis/)와 HTTPS로 통신하면 팬아웃은 데이터베이스 계층에 아예 닿지 않습니다. 풀링은 여전히 일어나지만, 직접 운영하는 구성 요소가 아니라 다른 누군가가 충분히 검증한 인프라로서 일어납니다.

## 한계와 트레이드오프

- **풀러는 하나의 구성 요소입니다.** 배포, 모니터링, 장애 조치, 업그레이드가 필요합니다. 자원 문제를 운영 문제와 맞바꾼 것이며, 유리한 거래이긴 하지만 비용이 없는 것은 아닙니다.
- **트랜잭션 모드는 의미를 바꿉니다.** prepared statement, 세션 변수, advisory lock이 문서대로 동작하지 않게 됩니다. 실패는 간헐적이고 부하에 따라 달라지는, 가장 나쁜 종류입니다.
- **홉이 하나 더 늘면 지연 시간이 듭니다.** 같은 네트워크 안에서는 보통 1밀리초 미만이지만, 모든 쿼리의 경로 위에 있습니다.
- **풀러가 병목이 될 수 있습니다.** 단일 스레드 풀러는 격렬한 급증 아래에서 대기열이 깊어집니다. 상한은 이동했을 뿐 사라지지 않았습니다.
- **풀링은 느린 쿼리를 고치지 않습니다.** 풀링은 연결을 배급할 뿐이며, 몇 초 동안 연결을 붙잡는 쿼리는 풀을 고갈시킵니다. 빠른 쿼리와 그 뒤의 [인덱스](/glossary/ko/database-index/)가 여전히 진짜 용량 조절 수단입니다.

## Back4app에서의 커넥션 풀링

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. 이 아키텍처는 풀링 문제에 구조적으로 답합니다. 클라이언트 앱과 Cloud Code 함수는 무상태 HTTPS 요청을 보내고, 개발자의 코드가 아니라 플랫폼의 서버 계층이 관리형 데이터베이스에 대해 수명이 길고 적정 크기인 커넥션 풀을 유지합니다. 컴퓨팅 쪽의 인스턴스 팬아웃이 데이터베이스 쪽의 연결 팬아웃으로 이어지는 일은 없으며, 이 글에서 다룬 기법들이 얻으려는 것이 바로 이 속성입니다.
