Django의 CONN_MAX_AGE 알아보기

2025년 8월 3일 일요일

Today I Learned

날짜

2025년 8월 3일 일요일

내용

Django Database Connection Management Guide

본 문서는 Django 애플리케이션에서 데이터베이스 연결을 안정적이고 효율적으로 관리하기 위한 두 가지 핵심 도구 close_old_connections()CONN_MAX_AGE 를 하나로 정리한 문서입니다. 웹훅 처리 커맨드에서 close_old_connections를 사용해야 하는 원인을 찾는 과정에서 수행된 실험입니다.


목차


close_old_connections() 가이드

Django close_old_connections 완벽 가이드

Django 애플리케이션을 운영하다 보면, 특히 오래 실행되는 스크립트나 비동기 작업에서 “MySQL server has gone away”와 같은 데이터베이스 연결 오류를 마주칠 때가 있습니다. close_old_connections는 바로 이런 문제를 해결하기 위해 Django가 제공하는 유용한 도구입니다.

핵심 요약

  • 무엇인가?: 오래되거나 비정상적인 데이터베이스 연결을 안전하게 닫아주는 함수입니다.
  • 언제 사용하는가?: 웹 요청-응답 주기를 벗어나는 장기 실행 작업(Long-running tasks)에서 사용합니다.
    • 예: Django 관리 명령어(management commands), Celery 비동기 작업, 데몬 스크립트 등
  • 왜 사용하는가?: 데이터베이스 서버는 유휴 상태(idle)가 오래 지속된 커넥션을 일방적으로 끊을 수 있습니다. 이때 Django 앱은 커넥션이 끊긴지 모르고 쿼리를 시도하다가 오류를 발생시킵니다. close_old_connections는 이런 “죽은” 커넥션을 미리 정리하여 오류를 방지합니다.
  • 어떻게 사용하는가?: 데코레이터(@)나 컨텍스트 관리자(with) 형태로 사용합니다.

1. Django는 언제 데이터베이스 연결을 생성하고 닫는가?

close_old_connections를 이해하려면 먼저 Django의 일반적인 데이터베이스 연결 관리 방식을 알아야 합니다.

일반적인 웹 요청 (Views)

  1. 연결 생성: 사용자의 HTTP 요청이 들어왔을 때, 해당 요청을 처리하는 View 코드 내에서 첫 번째 데이터베이스 쿼리가 실행되는 시점에 데이터베이스 연결이 생성됩니다.
  2. 연결 유지: 생성된 연결은 해당 요청이 끝날 때까지 유지되며, 같은 요청 내의 모든 쿼리는 이 연결을 재사용합니다. 이는 매 쿼리마다 새로운 연결을 만드는 오버헤드를 줄여줍니다.
  3. 연결 종료: Django는 요청에 대한 응답을 반환한 직후, 자동으로 데이터베이스 연결을 닫습니다.

이러한 방식 덕분에 일반적인 웹 환경에서는 개발자가 직접 연결을 관리할 필요가 거의 없습니다.

관리 명령어 및 기타 스크립트

Django 관리 명령어(manage.py <command>)나 별도의 스크립트는 웹 요청-응답 주기를 따르지 않습니다.

  • 연결 생성: View와 마찬가지로, 코드 내에서 첫 번째 쿼리가 실행될 때 연결이 생성됩니다.
  • 연결 유지: 스크립트나 명령어가 실행되는 동안 계속 유지됩니다.
  • 연결 종료: 스크립트/명령어의 프로세스가 완전히 종료될 때 연결이 닫힙니다.

문제는 이 “유지” 단계에서 발생합니다. 만약 스크립트가 매우 오래 실행되거나, 중간에 오랜 시간 대기하는 로직이 있다면 데이터베이스 서버의 타임아웃 설정에 의해 연결이 끊어질 수 있습니다.


2. close_old_connections의 역할과 필요성

close_old_connections는 데이터베이스 작업을 수행하기 전에 호출되어, 현재 스레드에서 사용 중인 데이터베이스 연결이 유효한지 확인하고 오래되었거나(stale) 사용할 수 없는(unusable) 상태라면 안전하게 닫습니다.

이렇게 닫힌 후 다음 쿼리가 실행될 때, Django는 자동으로 새로운 데이터베이스 연결을 생성합니다. 결과적으로 “죽은” 연결에 쿼리를 날리는 상황을 원천적으로 방지할 수 있습니다.

주요 사용 사례:

  • 주기적으로 실행되는 Cron 작업: 매 시간 실행되는 스크립트가 있다면, 실행 시작 시점에 호출하여 이전 실행에서 남은 연결을 정리합니다.
  • Celery와 같은 비동기 워커: 여러 작업을 순차적으로 처리하는 워커에서 각 작업이 시작되기 전에 호출하여 작업 간 연결을 격리하고 안정성을 높입니다.
  • 긴 루프를 포함한 관리 명령어: 루프 내에서 time.sleep() 등으로 장시간 대기하는 경우, 대기 후에 쿼리를 실행하기 전에 호출해주는 것이 안전합니다.

3. 사용 방법

close_old_connections는 두 가지 편리한 방법을 제공합니다.

방법 1: 데코레이터(Decorator)로 사용하기

함수 전체를 감싸는 가장 간편한 방법입니다. 함수가 실행되기 직전에 오래된 연결을 닫고, 함수가 종료된 후에도 다시 한번 닫아줍니다.

예시: 관리 명령어에 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# myapp/management/commands/my_long_running_command.py

from django.core.management.base import BaseCommand
from django.db import close_old_connections
from myapp.models import MyModel
import time

class Command(BaseCommand):
    help = '오래 실행되는 작업을 시뮬레이션합니다.'

    @close_old_connections
    def handle(self, *args, **options):
        self.stdout.write("작업 시작. 첫 번째 쿼리를 실행합니다.")
        # 이 시점에 새로운 DB 연결이 생성됨
        count = MyModel.objects.count()
        self.stdout.write(f"현재 MyModel의 객체 수: {count}")

        self.stdout.write("10분 동안 대기합니다... (DB 연결이 끊어질 수 있는 시간)")
        time.sleep(600)

        self.stdout.write("대기 완료. 두 번째 쿼리를 실행합니다.")
        # @close_old_connections 덕분에, 이전 연결이 아닌
        # 새로운 연결을 생성하여 아래 쿼리를 실행하므로 안전합니다.
        # (실제로는 데코레이터가 함수 시작/끝에만 동작하므로,
        # 이 경우는 컨텍스트 관리자가 더 적합합니다. 아래 예시 참고)
        #
        # 데코레이터는 함수 시작 시점에 한 번 정리해주므로,
        # 이 명령어가 여러 번 실행될 때 이전 실행의 연결을 정리하는 데 효과적입니다.
        count = MyModel.objects.count()
        self.stdout.write(f"다시 조회한 MyModel의 객체 수: {count}")
        self.stdout.write(self.style.SUCCESS('작업이 성공적으로 완료되었습니다.'))

방법 2: 컨텍스트 관리자(Context Manager)로 사용하기

with 구문을 사용하여 코드의 특정 블록에만 close_old_connections를 적용하고 싶을 때 유용합니다. 특히 긴 루프 안에서 특정 작업을 수행하기 직전에 호출할 때 효과적입니다.

예시: 루프 안에서 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# myapp/management/commands/process_items_with_delay.py

from django.core.management.base import BaseCommand
from django.db import close_old_connections
from myapp.models import ItemToProcess
import time

class Command(BaseCommand):
    help = '딜레이를 주면서 아이템을 하나씩 처리합니다.'

    def handle(self, *args, **options):
        items = ItemToProcess.objects.filter(processed=False)
        self.stdout.write(f"{len(items)}개의 아이템을 처리합니다.")

        for item in items:
            # with 블록에 진입하기 전에 오래된 연결을 닫습니다.
            with close_old_connections():
                self.stdout.write(f"'{item.name}' 처리 중...")

                # 이 블록 안에서 첫 쿼리가 실행될 때
                # 신선한(fresh) 연결이 보장됩니다.
                item.processed = True
                item.save()

                self.stdout.write(f"'{item.name}' 처리 완료. 다음 아이템까지 1분 대기.")
                time.sleep(60)

        self.stdout.write(self.style.SUCCESS('모든 아이템 처리가 완료되었습니다.'))


5. Django 자동 연결 관리의 장단점

Django가 요청-응답 주기마다 자동으로 연결을 열고 닫아주는 방식은 대부분의 경우 매우 편리하지만, 장단점을 가집니다.

장점

  • 개발 편의성: 개발자가 연결 관리에 대해 거의 신경 쓸 필요가 없습니다. 비즈니스 로직에만 집중할 수 있어 생산성이 크게 향상됩니다.
  • 안정성: 각 요청이 격리된 연결을 사용하므로, 한 요청의 문제가 다른 요청에 영향을 미칠 가능성이 적습니다. 응답 후에는 반드시 연결을 닫기 때문에 연결 누수(Connection Leak)의 위험이 거의 없습니다.
  • 자원 효율성: 요청이 있을 때만 연결을 생성하고 끝나면 바로 반환하므로, 유휴 상태의 연결이 서버 자원을 낭비하는 것을 방지합니다.

단점

  • 단기 실행 작업의 오버헤드: 매우 짧고 간단한 쿼리만 실행하는 수많은 요청이 동시에 들어올 경우, 매번 연결을 생성하고 닫는 과정 자체가 약간의 오버헤드가 될 수 있습니다. (대부분의 경우 무시할 수 있는 수준입니다.)
  • 장기 실행 작업의 한계: 이 자동 관리 방식은 웹 요청이라는 짧은 생명주기에 최적화되어 있습니다. 따라서 관리 명령어, 배치 작업, 웹소켓 등 오래 지속되는 작업에서는 Django의 자동 연결 관리가 오히려 문제가 될 수 있으며, 이때 close_old_connections와 같은 수동 개입이 필요해집니다.

7. 영구 연결과 타임아웃: CONN_MAX_AGEwait_timeout

Django는 성능 향상을 위해 영구 연결(Persistent Connections) 기능을 제공합니다. 매 요청마다 데이터베이스 연결을 새로 만드는 대신, 기존 연결을 재사용하는 방식입니다. CONN_MAX_AGE 설정은 이 영구 연결의 수명을 제어합니다.

Django의 CONN_MAX_AGE

  • 설정 위치: settings.pyDATABASES 딕셔너리 안에 설정합니다.
  • 값의 의미:
    • 0 (기본값): 영구 연결을 사용하지 않습니다. 매 요청마다 연결을 열고 닫습니다.
    • 양수 (초 단위): 해당 시간만큼 연결을 재사용합니다. 예를 들어 3600으로 설정하면, 연결은 1시간 동안 유지됩니다.
    • None: 시간 제한 없이 계속 연결을 재사용합니다. (권장하지 않음)
  • 동작: Django는 요청이 시작될 때, 현재 스레드의 연결이 생성된 후 CONN_MAX_AGE 설정 시간을 초과했는지 확인합니다. 만약 초과했다면 해당 연결을 안전하게 닫고, 다음 쿼리가 실행될 때 새로운 연결을 맺습니다.

데이터베이스 서버의 wait_timeout (MySQL/MariaDB 기준)

  • 설정 위치: 데이터베이스 서버의 설정 파일 (my.cnf 등)에 정의됩니다.
  • 의미: 클라이언트(Django 앱)가 연결을 맺은 후, 아무런 쿼리도 실행하지 않고 유휴(idle) 상태로 대기할 수 있는 최대 시간입니다.
  • 동작: 만약 연결이 wait_timeout 시간 동안 아무런 활동이 없으면, 데이터베이스 서버는 일방적으로 해당 연결을 끊어버립니다. 이는 서버 자원을 보호하기 위한 조치입니다.

충돌 문제와 해결책

가장 흔한 문제 상황은 Django가 연결이 아직 유효하다고 생각하지만, 실제로는 DB 서버가 이미 연결을 끊어버린 경우입니다.

  • 원인: CONN_MAX_AGE > wait_timeout
  • 시나리오:
    1. Django 앱이 DB에 연결합니다.
    2. 앱이 한동안 아무 쿼리도 날리지 않아 wait_timeout이 경과합니다.
    3. MySQL 서버는 이 연결을 끊습니다. 하지만 Django 앱은 이 사실을 모릅니다.
    4. 이후 새로운 요청이 들어와 Django는 CONN_MAX_AGE가 아직 지나지 않았으므로 기존 연결이 유효하다고 판단하고 재사용하려 합니다.
    5. 이미 끊어진 연결에 쿼리를 보내면서 OperationalError: (2006, 'MySQL server has gone away') 오류가 발생합니다.

해결책: CONN_MAX_AGE < wait_timeout

이 문제를 해결하는 가장 확실한 방법은 Django가 DB 서버보다 먼저 연결을 닫도록 설정하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# settings.py

# DB 서버의 wait_timeout 값을 확인한 후 (예: 28800초, 8시간),
# 그보다 훨씬 짧은 값으로 설정하는 것이 안전합니다.
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'mydatabase',
        # ...
        'CONN_MAX_AGE': 3600,  # 1시간. wait_timeout보다 반드시 짧아야 함
    }
}

이렇게 설정하면, DB 서버가 강제로 연결을 끊기 전에 Django가 먼저 수명이 다한 연결을 스스로 정리하고 새로운 연결을 맺기 때문에 오류를 예방할 수 있습니다.

팁: close_old_connections() 함수는 CONN_MAX_AGE 설정과 무관하게 동작합니다. CONN_MAX_AGE는 웹 요청 주기에 맞춰 연결의 수명을 관리하는 설정이고, close_old_connections()는 장기 실행 스크립트 등에서 필요할 때마다 수동으로 연결을 닫기 위해 사용하는 도구입니다. 두 가지를 용도에 맞게 함께 사용하면 연결 관련 문제를 효과적으로 관리할 수 있습니다.

CONN_MAX_AGE 실험 및 분석

Django CONN_MAX_AGE 연결 재사용 메커니즘 분석 실험 보고서

1. 실험 목적

궁금한 점

  • Django의 CONN_MAX_AGE 설정이 실제로 어떻게 동작하는가?
  • 연결의 “절대적인 수명”을 의미하는 것인가, 아니면 다른 메커니즘인가?
  • 요청 간 연결 재사용은 언제, 어떤 조건에서 발생하는가?

실험 목표

  • CONN_MAX_AGE의 실제 동작 메커니즘 규명
  • 요청 간 연결 재사용 조건 파악
  • HTTP 세션과 데이터베이스 연결 재사용의 상관관계 분석

2. 배경 지식 및 가설

Django의 데이터베이스 연결 관리 기본 개념

CONN_MAX_AGE란 무엇인가?

CONN_MAX_AGE는 Django의 데이터베이스 설정에서 개별 데이터베이스 연결의 최대 생존 시간을 초 단위로 정의하는 설정입니다. 이 설정은 Django 1.6에서 도입되어 지속적 연결(Persistent Connections)을 가능하게 합니다.

기본값과 의미:

  • 기본값: 0 (각 요청 후 연결 종료 - Django의 역사적 동작)
  • 양의 정수: 지정된 초 동안 연결 유지
  • None: 무제한 지속적 연결 (프로세스가 종료될 때까지)

Django의 연결 생명주기 관리

전통적인 연결 모델 (CONN_MAX_AGE=0):

  1. HTTP 요청 도착 → 데이터베이스 쿼리 필요 시 → 새 연결 생성
  2. SQL 쿼리 실행
  3. 요청 완료 → 연결 즉시 종료
  4. 매 요청마다 연결 생성/종료 오버헤드 발생 (50-70ms)

지속적 연결 모델 (CONN_MAX_AGE>0):

  1. HTTP 요청 도착 → 기존 연결 확인
  2. 연결이 있고 수명이 남았으면 재사용, 없으면 새로 생성
  3. 요청 완료 → 연결을 연결 풀에 반환
  4. close_at 시간 체크: 현재시간 + CONN_MAX_AGE로 설정
  5. 다음 요청에서 현재시간 >= close_at이면 연결 종료

설정 방법과 위치

settings.py에서 설정:

1
2
3
4
5
6
7
8
9
10
11
12
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydatabase',
        'USER': 'myuser',
        'PASSWORD': 'mypassword',
        'HOST': 'localhost',
        'PORT': '5432',
        'CONN_MAX_AGE': 10,  # 10초
    }
}

HTTP Keep-Alive와의 관계

클라이언트별 연결 동작:

  • curl: 매 요청마다 새로운 HTTP 연결
  • 브라우저/OpenAPI: HTTP Keep-Alive 자동 사용
  • requests.Session: HTTP 세션 유지

기존 이해/추정

  • CONN_MAX_AGE는 연결의 절대적인 수명을 의미
  • 설정된 시간이 지나면 연결이 자동으로 새로고침
  • 새로운 고유값(PID)을 가지게 될 것

실험 가설

  • 가설 1: Management Command에서는 CONN_MAX_AGE와 무관하게 연결 유지
  • 가설 2: 첫 번째 요청이 CONN_MAX_AGE(10초)보다 짧은 시간(5초)이면, 두 번째 요청에서 같은 연결 재사용
  • 가설 3: 첫 번째 요청이 CONN_MAX_AGE(10초)보다 긴 시간(15초)이면, 두 번째 요청에서 새로운 연결 생성
  • 가설 4: Django는 요청 완료 시점에 CONN_MAX_AGE를 체크하여 연결을 정리하는 메커니즘을 가짐

3. 실험 환경 및 조건

환경 정보

  • 시스템: Django + PostgreSQL (Docker 환경)
  • 버전: Django 4.x, PostgreSQL
  • 설정: CONN_MAX_AGE = 10 (10초)

제약 조건

  • 단일 데이터베이스 연결
  • PostgreSQL pg_backend_pid() 함수를 이용한 연결 식별

4. 실험 설계 및 시나리오

실험 방법

  1. Management Command에서 15초 동안 1초마다 PID 확인
  2. API View에서 5초/15초 연결 유지 후 PID 변화 확인
  3. 다양한 HTTP 클라이언트로 연속 요청 시 PID 재사용 패턴 분석

테스트 케이스

케이스 조건 예상 결과 측정 지표
TC-1 Management Command 15초 같은 PID 유지 PostgreSQL PID
TC-2 Short(5초) → Long(15초) 같은 PID 재사용 PID 비교
TC-3 Long(15초) → Short(5초) 다른 PID (새 연결) PID 비교
TC-4 HTTP 클라이언트별 차이 세션 유지 여부에 따라 달라짐 PID 일치 여부

5. 실험 결과

5.1 테스트 케이스별 결과

TC-1: Management Command 테스트 (15초)

1
2
3
4
Initial connection backend PID: 1186
Second 1-15: Connection backend PID: 1186 (모두 동일)
Connection state: 0 (IDLE, 정상)

TC-2: Short → Long 테스트 (5초 → 15초)

1
2
3
4
# 첫 번째 요청이 CONN_MAX_AGE(10초)보다 짧은 경우
Short 요청: PID 1201
Long 요청:  PID 1201 (같은 연결 재사용) ✅

TC-3: Long → Short 테스트 (15초 → 5초)

1
2
3
4
# 첫 번째 요청이 CONN_MAX_AGE(10초)보다 긴 경우
Long 요청:  PID 1204
Short 요청: PID 1205 (새로운 연결 생성) ✅

TC-4: HTTP 클라이언트별 차이점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== HTTP 세션 유지 여부에 따른 차이 ===

1. curl (HTTP 세션 없음):
Short → Long: PID 1201 → 1202 (다른 연결)
Long → Short:  PID 1204 → 1205 (다른 연결)

2. requests.Session (HTTP 세션 유지):
Short → Long: PID 1251 → 1251 (같은 연결) ✅
Long → Short:  PID 1254 → 1255 (다른 연결) ✅

3. 브라우저/OpenAPI (자동 HTTP 세션):
Short → Long: PID 1260 → 1260 (같은 연결) ✅
Long → Short:  PID 1263 → 1264 (다른 연결) ✅

TC-4: OpenAPI/브라우저 테스트

  • 사용자 보고: 브라우저에서 Short → Long 시 같은 PID 사용
  • 사용자 보고: 브라우저에서 Long → Short 시 다른 PID 사용

5.2 관찰된 패턴

  • Management Command: 지속적인 프로세스에서는 CONN_MAX_AGE 무시하고 연결 유지
  • CONN_MAX_AGE 기반 연결 정리: 첫 번째 요청의 지속 시간이 CONN_MAX_AGE를 초과하면, 요청 완료 시 연결이 정리되어 두 번째 요청에서 새로운 연결 생성
  • HTTP 세션 의존성: HTTP Keep-Alive가 유지되는 경우에만 DB 연결 재사용 가능
  • 클라이언트별 차이: curl(세션 없음) vs requests.Session/브라우저(세션 유지)의 동작 차이

6. 실험 결과 분석

가설 검증

  • 가설 1: ✅ Management Command에서는 CONN_MAX_AGE와 무관하게 연결 유지
  • 가설 2: ✅ 첫 번째 요청이 CONN_MAX_AGE보다 짧으면 두 번째 요청에서 같은 연결 재사용
  • 가설 3: ✅ 첫 번째 요청이 CONN_MAX_AGE보다 길면 두 번째 요청에서 새로운 연결 생성
  • 가설 4: ✅ Django는 요청 완료 시점에 CONN_MAX_AGE를 체크하여 연결을 정리하는 메커니즘을 가짐

의외의 발견

  • CONN_MAX_AGE는 요청 완료 시점에 체크됨: 요청 처리 중에는 시간과 무관하게 연결 유지, 요청 완료 시에만 CONN_MAX_AGE 검증
  • 연결 정리 타이밍: Django는 request_finished 신호에서 연결의 수명을 체크하고 필요시 정리
  • HTTP 세션과의 조합: CONN_MAX_AGE 기반 연결 정리는 HTTP Keep-Alive가 유지되는 경우에만 효과적

원인 분석

  • Django의 연결 정리 메커니즘: Django는 request_finished 신호에서 현재 연결의 수명을 체크
  • CONN_MAX_AGE 검증 시점: 요청 완료 시점에 현재시간 - 연결생성시간 >= CONN_MAX_AGE 조건 검사
  • 연결 정리 조건: 조건을 만족하면 연결을 즉시 닫고, 다음 요청에서 새로운 연결 생성
  • HTTP 세션과의 연동: HTTP Keep-Alive가 유지되는 경우에만 같은 스레드에서 연결 재사용 가능

7. 결론 및 정리

핵심 발견사항

  1. CONN_MAX_AGE의 실제 동작: 요청 완료 시점에 연결 수명을 체크하여 정리하는 메커니즘
  2. 연결 정리 타이밍: 첫 번째 요청의 지속 시간이 CONN_MAX_AGE를 초과하면, 요청 완료 시 연결이 정리됨
  3. HTTP 세션과의 조합: CONN_MAX_AGE 기반 연결 정리는 HTTP Keep-Alive가 유지되는 경우에만 효과적
  4. Management Command 차이: 웹 요청-응답 주기를 따르지 않는 명령어에서는 CONN_MAX_AGE가 적용되지 않음

실무 적용 시사점

  • CONN_MAX_AGE 설정 최적화: 평균 API 요청 처리 시간을 고려하여 적절한 값 설정 (너무 짧으면 연결 재사용 효과 감소, 너무 길면 리소스 낭비)
  • API 설계 고려사항: 연관된 여러 API 호출은 HTTP 세션 내에서 연속 수행하여 연결 재사용 효과 극대화
  • 성능 모니터링: 요청 처리 시간과 DB 연결 재사용률의 상관관계 추적
  • Management Command 주의: 장기 실행 작업에서는 close_old_connections() 사용 권장

추가 연구 필요 사항

  • 멀티 스레드 환경에서의 연결 풀 동작 분석
  • 다양한 데이터베이스(MySQL, Redis)에서의 동작 비교
  • 프로덕션 환경에서의 성능 메트릭 수집

8. 참고 자료 및 코드

실험 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Management Command 테스트
class Command(BaseCommand):
    def handle(self, *args, **kwargs):
        connection = connections["default"]
        cursor = connection.cursor()

        cursor.execute("SELECT pg_backend_pid()")
        initial_pid = cursor.fetchone()[0]

        for i in range(15):
            time.sleep(1)
            pid = cursor.execute("SELECT pg_backend_pid()").fetchone()[0]
            print(f"Second {i+1}: PID {pid}, Same: {pid == initial_pid}")

# API View 테스트
class ConnectionReuse__ShortView(APIView):
    def get(self, request):
        connection = connections["default"]
        with connection.cursor() as cursor:
            cursor.execute("SELECT pg_backend_pid()")
            initial_pid = cursor.fetchone()[0]

            time.sleep(5)  # CONN_MAX_AGE 미만

            cursor.execute("SELECT pg_backend_pid()")
            final_pid = cursor.fetchone()[0]

        return Response({
            "initial_pid": initial_pid,
            "final_pid": final_pid,
            "connection_maintained": initial_pid == final_pid
        })

# requests 세션 테스트
import requests
session = requests.Session()
resp1 = session.get('<http://localhost/data-insight/test-short>')
resp2 = session.get('<http://localhost/data-insight/test-long>')
print(f'연결 재사용: {resp1.json()["initial_pid"] == resp2.json()["initial_pid"]}')

관련 문서

  • Django 공식 문서: Database connections
  • PostgreSQL 문서: pg_stat_activity

9. 재현 가이드

환경 설정

1
2
3
4
5
6
7
8
# Django 설정
DATABASES = {
    'default': {
        'CONN_MAX_AGE': 10,
        # 기타 DB 설정
    }
}

실행 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Management Command 테스트
docker exec core-backend python manage.py database_session_test

# API 테스트
curl "<http://localhost:8000/data-insight/test-short>"
curl "<http://localhost:8000/data-insight/test-long>"

# requests 세션 테스트
python -c "
import requests
session = requests.Session()
resp1 = session.get('<http://localhost/data-insight/test-short>')
resp2 = session.get('<http://localhost/data-insight/test-long>')
print(f'연결 재사용: {resp1.json()[\\"initial_pid\\"] == resp2.json()[\\"initial_pid\\"]}')
"

예상 결과

  • Management Command: 15초 동안 동일한 PID 유지 (CONN_MAX_AGE 무시)
  • Short → Long (5초 → 15초): 같은 PID 재사용 (첫 번째 요청이 CONN_MAX_AGE 미만)
  • Long → Short (15초 → 5초): 다른 PID (첫 번째 요청이 CONN_MAX_AGE 초과로 연결 정리됨)
  • HTTP 클라이언트별: HTTP Keep-Alive 유지 여부에 따라 연결 재사용 패턴 달라짐

실험 완료일: 2025-07-30

문서 작성일: 2025-07-30


요약 및 베스트 프랙티스

상황 권장 접근 방식
웹 요청/응답 사이클 CONN_MAX_AGE 값(예: 60초)을 설정해 커넥션 풀링 효과를 얻는다.
장기 실행 작업 (Celery, 관리 커맨드 등) 주기적으로 close_old_connections() 호출해 유휴 커넥션을 정리한다.
여러 DB 사용 / 멀티스레드 각 스레드/프로세스마다 필요 시 close_old_connections() 호출.
DB 서버가 idle timeout 짧을 때 CONN_MAX_AGE를 timeout 이하로, 추가로 작업 루프에서 close_old_connections() 사용.

두 설정을 함께 사용하면 Django의 ORM을 통한 데이터베이스 연결을 더욱 안정적이고 효율적으로 관리할 수 있습니다.

회고

이 글은 AI가 정리했습니다.