Tuesday, November 6, 2018

어느 날 갑자기 AWS 서버가 응답을 멈췄다: 프리티어의 숨겨진 비용, DB 커넥션 풀

클라우드의 시대, 개인 개발자나 소규모 팀에게 AWS 프리티어(Free Tier)는 축복과도 같습니다. 최소한의 비용, 혹은 전혀 비용 없이 아이디어를 프로토타입으로 만들고, 사이드 프로젝트를 세상에 선보일 수 있는 강력한 발판을 제공하기 때문입니다. 하지만 이 달콤한 '무료'라는 단어 뒤에는 종종 간과하기 쉬운 기술적 함정이 숨어 있습니다. 이 글은 어느 날 갑자기 응답이 현저히 느려지거나 멈춰버리는 당신의 소중한 개발 서버를 구하기 위한 실전 가이드입니다. 많은 개발자들이 원인을 찾지 못해 EC2 인스턴스를 재부팅하며 연명하다가 결국 프리티어의 성능 한계 탓으로 돌리곤 하지만, 진범은 예상치 못한 곳에 있을 수 있습니다. 바로 '데이터베이스 커넥션' 관리의 부재입니다.

우리는 이 글을 통해 프리티어 환경에서 왜 서버 성능 문제가 자주 발생하는지, 그 중심에 있는 데이터베이스 커넥션이 무엇이며 어떻게 시스템 전체를 마비시키는지 심도 있게 파헤칠 것입니다. 그리고 가장 효과적인 해결책인 '커넥션 풀(Connection Pool)'의 개념과 실제 언어별 적용 방법, 나아가 근본적인 아키텍처 개선 방안까지 단계별로 상세히 다룰 것입니다. 단순히 EC2 인스턴스를 업그레이드하거나 RDS로 이전하는 임시방편을 넘어, 안정적이고 확장 가능한 애플리케이션을 구축하는 개발자로서 반드시 알아야 할 핵심 지식을 얻어 가시길 바랍니다.


1. AWS 프리티어, 달콤하지만 위태로운 양날의 검

이야기를 시작하기에 앞서, 우리가 발 딛고 있는 AWS 프리티어 환경의 특성을 명확히 이해해야 합니다. 왜 유독 이 환경에서 문제가 두드러지는지 알면, 해결의 실마리를 더 쉽게 찾을 수 있습니다.

프리티어의 매력과 그 이면

AWS는 신규 고객 유치를 위해 12개월 동안 특정 서비스를 무료로 사용할 수 있는 프리티어 프로그램을 운영합니다. 대표적인 혜택은 다음과 같습니다.

  • Amazon EC2: 월 750시간의 t2.micro 또는 t3.micro 인스턴스 사용 시간
  • Amazon S3: 5GB의 표준 스토리지
  • Amazon RDS: 월 750시간의 db.t2.micro 또는 유사 스펙 인스턴스 사용 시간
  • AWS Lambda: 월 100만 건의 무료 요청

특히 EC2 t2.micro 인스턴스는 1개의 가상 CPU(vCPU)와 1GiB의 메모리를 제공하는데, 이는 개인 블로그, 간단한 API 서버, 소규모 커뮤니티 등을 운영하기에 초기에는 부족함이 없어 보입니다. 그러나 이 '무료' 서버의 심장부에는 '버스터블 성능 인스턴스(Burstable Performance Instance)'라는 중요한 작동 방식이 숨어 있습니다.

CPU 크레딧: 성능의 롤러코스터

t2, t3와 같은 버스터블 인스턴스는 평소에는 낮은 기준(Baseline) 성능으로 동작하다가, 순간적으로 높은 트래픽이나 무거운 작업이 필요할 때 최대 성능으로 '버스트(Burst)'할 수 있는 능력을 가집니다. 이 버스트 능력은 'CPU 크레딧'이라는 개념으로 관리됩니다.

  • 크레딧 적립: 인스턴스의 CPU 사용량이 기준 성능보다 낮을 때, 사용하지 않은 만큼의 CPU 시간이 크레딧으로 적립됩니다.
  • 크레딧 소모: CPU 사용량이 기준 성능을 초과할 때, 적립해 둔 크레딧을 소모하여 최대 성능을 발휘합니다.
  • 크레딧 고갈: 만약 장시간 동안 기준 성능을 초과하는 작업을 계속하여 적립된 크레딧을 모두 소모하면, 인스턴스의 CPU 성능은 기준선까지 뚝 떨어지게 됩니다. 예를 들어 t2.micro의 기준 성능은 전체 CPU 성능의 10%에 불과합니다.

바로 이 지점이 많은 개발자들이 처음으로 '서버가 갑자기 느려졌어요'라고 느끼게 되는 순간입니다. 빌드, 배포, 혹은 순간적인 트래픽 증가로 CPU 크레딧이 모두 소진되면 서버는 마치 거북이처럼 느려집니다. 이것은 프리티어의 명백한 제약 사항이지만, 우리가 다룰 '진짜' 문제는 이 CPU 크레딧 고갈을 가속화하고, 크레딧이 충분한 상황에서도 서버를 멈추게 만드는 다른 원인에 있습니다.

1GiB라는 제한된 메모리 역시 치명적인 약점입니다. 운영체제가 기본적인 구동에 사용하는 메모리를 제외하면 애플리케이션이 사용할 수 있는 공간은 매우 협소합니다. 이런 환경에서 메모리 누수(Memory Leak)가 발생하면 시스템은 스왑(Swap) 메모리를 사용하기 시작하고, 이는 디스크 I/O를 발생시켜 전반적인 시스템 성능을 급격히 저하시킵니다. 최악의 경우, OOM(Out of Memory) Killer에 의해 애플리케이션 프로세스가 강제 종료될 수도 있습니다.

이처럼 프리티어 EC2 인스턴스는 CPU와 메모리 자원이 극도로 제한된 환경입니다. 작은 자원 누수 하나가 시스템 전체에 미치는 영향이 대형 인스턴스에 비해 훨씬 더 크고 즉각적일 수밖에 없습니다. 이제, 이 연약한 서버를 벼랑 끝으로 모는 주범, 데이터베이스 커넥션을 만나보겠습니다.


2. 조용한 암살자, 관리되지 않는 데이터베이스 커넥션

애플리케이션이 갑자기 느려지는 현상이 발생하면, 개발자는 보통 최근에 배포한 코드에 버그가 있는지, 혹은 특정 API에 비효율적인 로직이 있는지부터 의심합니다. 하지만 서버를 재부팅하면 잠시 괜찮아졌다가 시간이 지나면서 다시 악화되는 패턴이 반복된다면, 이는 자원 누수를 강력하게 시사하는 신호입니다. 그리고 그 누수의 가장 흔한 원천 중 하나가 바로 데이터베이스 연결입니다.

데이터베이스 커넥션의 생명주기와 비용

우리가 작성한 코드가 데이터베이스의 데이터를 읽거나 쓰기 위해서는 다음과 같은 과정을 거칩니다.

  1. 연결 생성(Establish Connection): 애플리케이션이 데이터베이스 서버에 연결을 요청합니다. 이 과정에는 TCP/IP 핸드셰이크, 데이터베이스 인증(사용자 이름/비밀번호 확인), 세션 설정 등 보이지 않는 여러 단계가 포함됩니다.
  2. 쿼리 실행(Execute Query): 생성된 연결을 통해 SQL 쿼리를 데이터베이스에 전송하고 결과를 수신합니다.
  3. 연결 종료(Close Connection): 작업이 끝나면 연결을 명시적으로 닫아 자원을 해제합니다.

여기서 핵심은 1번 '연결 생성' 과정이 생각보다 비용이 비싼 작업이라는 점입니다. 애플리케이션 서버와 데이터베이스 서버 양쪽 모두에서 CPU와 메모리 자원을 소모하며, 네트워크 지연 시간도 발생합니다. 따라서 사용자의 모든 요청에 대해 매번 새로운 연결을 생성하고 파괴하는 것은 매우 비효율적입니다.

연결을 닫지 않았을 때 벌어지는 일들

진짜 문제는 '연결을 사용한 뒤 닫지 않았을 때' 발생합니다. 개발자가 코드에서 연결을 닫는 로직을 누락했다고 가정해 봅시다. 어떤 재앙이 펼쳐질까요?

  • 애플리케이션 서버 측면:
    • 메모리 누수: 닫히지 않은 커넥션 객체는 가비지 컬렉터(Garbage Collector)에 의해 수거되지 않고 메모리에 계속 남아있게 됩니다. 요청이 들어올 때마다 이런 '유령 커넥션'이 하나씩 쌓이면, 가용 메모리는 순식간에 고갈됩니다. 앞서 언급했듯, 1GiB 메모리의 프리티어 서버에게 이는 치명타입니다.
    • 파일 디스크립터 고갈: 네트워크 연결(소켓)은 운영체제 수준에서 파일 디스크립터(File Descriptor)라는 자원으로 관리됩니다. 프로세스당 열 수 있는 파일 디스크립터의 개수는 제한되어 있습니다. 닫히지 않은 커넥션이 이 제한에 도달하면, 애플리케이션은 더 이상 새로운 네트워크 연결(DB 연결 포함)을 맺지 못하고 모든 요청이 실패하기 시작합니다.
  • 데이터베이스 서버 측면:
    • 자원 점유: 데이터베이스는 클라이언트로부터 들어온 각 연결을 관리하기 위해 별도의 프로세스나 스레드를 할당하고 메모리 공간을 사용합니다. 애플리케이션이 연결을 닫지 않으면, 데이터베이스 입장에서는 해당 클라이언트가 여전히 무언가 작업을 할 수 있는 '활성' 상태로 인식하고 관련 자원을 계속 점유합니다. 이런 유휴(idle) 커넥션이 쌓이면 데이터베이스 서버의 메모리와 CPU에 큰 부담을 줍니다.
    • `max_connections` 초과: 데이터베이스에는 동시에 맺을 수 있는 최대 연결 개수(`max_connections`) 설정이 있습니다. 이는 서버 자원의 과다 사용을 막기 위한 안전장치입니다. 관리되지 않는 연결들이 계속 쌓여 이 한계에 도달하면, 데이터베이스는 더 이상 새로운 연결 요청을 받아주지 않습니다. 이 시점부터 애플리케이션의 모든 데이터베이스 관련 기능은 완전히 마비됩니다. 사용자에게는 '서비스 접속 오류'나 무한 로딩 화면만이 보일 뿐입니다.

이것이 바로 '서버를 재부팅하면 괜찮아지는' 현상의 실체입니다. 재부팅을 하면 애플리케이션 프로세스가 종료되면서 운영체제가 강제로 모든 네트워크 연결을 끊어버리고, 메모리도 초기화되기 때문에 일시적으로 문제가 해결된 것처럼 보이는 것입니다. 하지만 근본 원인인 코드 속 '연결 누수'는 그대로 남아있기에, 시간이 지나면 어김없이 동일한 비극이 반복됩니다.

프리티어 EC2 인스턴스에 애플리케이션과 데이터베이스(예: MySQL, PostgreSQL)를 함께 설치해 운영하는 경우, 문제는 더욱 심각해집니다. 가뜩이나 부족한 1GiB의 메모리와 1개의 vCPU를 두 개의 무거운 프로세스가 나눠 써야 합니다. 애플리케이션에서 발생한 커넥션 누수는 애플리케이션 자신의 메모리를 잠식하는 동시에, 데이터베이스 프로세스의 자원까지 좀먹으며 시스템을 공멸의 길로 이끄는 것입니다.

이 지점에서 많은 개발자들이 RDS로 데이터베이스를 이전하는 선택을 합니다. RDS를 사용하면 데이터베이스 프로세스가 EC2 인스턴스로부터 분리되므로, EC2 인스턴스의 메모리와 CPU 부담이 줄어드는 것은 사실입니다. 하지만 이는 증상을 완화시켰을 뿐, 병의 근원을 치료한 것이 아닙니다. 애플리케이션의 연결 누수 버그는 그대로 남아있으며, 이제는 RDS 인스턴스의 `max_connections`를 향해 차곡차곡 시한폭탄을 쌓아 올리고 있는 셈입니다.


3. 커넥션 풀(Connection Pool): 현명한 자원 관리의 시작

매번 연결을 생성하고 닫는 것은 비싸고, 열어놓고 닫지 않는 것은 재앙을 부릅니다. 이 딜레마를 해결하기 위해 등장한 표준적인 해법이 바로 '커넥션 풀(Connection Pool)'입니다. 이름 그대로, 데이터베이스 커넥션을 미리 정해진 개수만큼 만들어 '풀(Pool)'에 담아두고, 필요할 때마다 빌려 쓰고 반납하는 방식입니다.

커넥션 풀의 작동 원리

커넥션 풀 라이브러리를 도입하면 애플리케이션의 데이터베이스 접근 방식이 다음과 같이 바뀝니다.

  1. 초기화: 애플리케이션이 시작될 때, 커넥션 풀은 설정값에 따라 일정 개수의 데이터베이스 커넥션을 미리 생성하여 풀에 준비해 둡니다.
  2. 대여(Borrow): 애플리케이션 로직이 데이터베이스 접근을 필요로 할 때, 커넥션 풀에 연결을 '요청'합니다. 이때 새로운 물리적 연결을 만드는 것이 아니라, 풀에 대기 중인 기존 연결 중 하나를 빌려옵니다.
  3. 사용(Use): 빌려온 커넥션을 사용하여 쿼리를 실행합니다.
  4. 반납(Return/Release): 작업이 끝나면 물리적 연결을 닫는(`close()`) 것이 아니라, 커넥션을 풀에 '반납'(`release()`)합니다. 반납된 커넥션은 다른 요청이 사용할 수 있도록 다시 대기 상태가 됩니다.

만약 모든 커넥션이 사용 중일 때 새로운 요청이 들어오면, 커넥션 풀은 설정에 따라 잠시 대기하거나, 혹은 최대 풀 크기(max pool size)에 도달하지 않았다면 새로운 커넥션을 추가로 생성할 수도 있습니다. 반대로 오랫동안 사용되지 않는 커넥션은 풀에서 제거하여 불필요한 자원 점유를 막습니다.

커넥션 풀 도입의 핵심 이점

  • 성능 향상: 비싼 연결 생성 과정을 대부분 생략하고 기존 연결을 재사용하므로, 데이터베이스 작업의 응답 시간이 극적으로 단축됩니다. 이는 애플리케이션의 전반적인 처리량과 성능 향상으로 이어집니다.
  • 자원 통제 및 안정성: 커넥션 풀은 애플리케이션이 생성할 수 있는 총 DB 커넥션의 수를 제한하는 강력한 통제 장치 역할을 합니다. 갑작스러운 트래픽 증가에도 `max_connections`를 초과하여 데이터베이스가 다운되는 사태를 예방할 수 있습니다. 즉, 애플리케이션의 부하가 데이터베이스 시스템 전체를 마비시키는 것을 막아주는 방파제와 같습니다.
  • 연결 관리 자동화: 좋은 커넥션 풀 라이브러리는 연결의 유효성을 주기적으로 검사(Connection Health Check)하여, 네트워크 문제 등으로 끊어진 '죽은' 커넥션을 자동으로 풀에서 제거하고 새로운 커넥션으로 교체해 줍니다. 이는 애플리케이션의 안정성을 크게 높여줍니다.

결론적으로, 커넥션 풀은 '선택'이 아닌 '필수'입니다. 특히 프리티어와 같이 자원이 제한적인 환경에서는 더욱 그렇습니다. 이제 실제 코드에서 커넥션 풀을 어떻게 적용하는지 주요 언어와 프레임워크별로 살펴보겠습니다.


4. 실전! 언어/프레임워크별 커넥션 풀 설정 가이드

현대의 대부분의 웹 프레임워크나 데이터베이스 라이브러리는 커넥션 풀링 기능을 내장하고 있거나, 손쉽게 통합할 수 있는 표준 라이브러리를 제공합니다. 중요한 것은 '그냥 쓰는 것'이 아니라, 주요 설정을 이해하고 내 환경에 맞게 튜닝하는 것입니다.

Java: Spring Boot & HikariCP

Java 진영의 사실상 표준인 Spring Boot는 `spring-boot-starter-jdbc` 의존성을 추가하면 자동으로 HikariCP라는 고성능 커넥션 풀 라이브러리를 설정해 줍니다. 설정은 `application.yml` (또는 `application.properties`) 파일에서 손쉽게 변경할 수 있습니다.


spring:
  datasource:
    url: jdbc:mysql://:3306/
    username: 
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      # 풀이 가질 수 있는 최대 커넥션 수. 이 숫자가 DB의 max_connections보다 작아야 함.
      # 프리티어 환경에서는 5~10 사이의 작은 값으로 시작하는 것이 안전.
      maximum-pool-size: 10
      # 풀이 유지하는 최소한의 유휴(idle) 커넥션 수.
      minimum-idle: 5
      # 커넥션이 풀에서 유휴 상태로 머물 수 있는 최대 시간(ms). 이 시간을 초과하면 풀에서 제거될 수 있음. (기본값 10분)
      idle-timeout: 600000
      # 풀의 커넥션을 얻기 위해 대기하는 최대 시간(ms). 이 시간을 초과하면 예외 발생. (기본값 30초)
      connection-timeout: 30000
      # 커넥션의 최대 생존 시간(ms). 이 시간이 지나면 커넥션은 풀에서 제거되고 새로운 커넥션으로 교체됨.
      # DB나 네트워크 방화벽의 타임아웃보다 짧게 설정하여 '죽은' 커넥션을 방지하는 효과. (기본값 30분)
      max-lifetime: 1800000

Spring의 `JdbcTemplate`이나 JPA(Hibernate)를 사용하면 개발자가 직접 커넥션을 얻고 반납하는 코드를 작성할 필요가 없습니다. 프레임워크가 트랜잭션 범위에 맞춰 자동으로 커넥션의 대여와 반납을 처리해주기 때문에, 개발자는 비즈니스 로직에만 집중할 수 있습니다.

Node.js: `pg` (PostgreSQL) 또는 `mysql2`

Node.js 환경에서는 데이터베이스 드라이버 자체가 풀링 기능을 제공하는 경우가 많습니다. `node-postgres` (보통 `pg`로 알려짐)의 예시입니다.


const { Pool } = require('pg');

// 풀은 애플리케이션 전체에서 단 한 번만 생성하여 재사용합니다.
const pool = new Pool({
  user: 'your_user',
  host: 'your_rds_endpoint',
  database: 'your_database',
  password: 'your_password',
  port: 5432,
  // 풀이 가질 수 있는 최대 클라이언트(커넥션) 수
  max: 10,
  // 유휴 클라이언트가 풀에 남아있는 시간(ms). 0이면 비활성화.
  idleTimeoutMillis: 30000,
  // 클라이언트를 얻기 위해 대기하는 시간(ms). 0이면 비활성화.
  connectionTimeoutMillis: 2000,
});

// 사용 예시 (async/await)
async function getUser(id) {
  // try...finally 블록은 필수입니다. 에러 발생 여부와 관계없이 반드시 클라이언트를 반납해야 합니다.
  const client = await pool.connect(); // 풀에서 커넥션을 빌려옵니다.
  try {
    const res = await client.query('SELECT * FROM users WHERE id = $1', [id]);
    return res.rows[0];
  } catch (err) {
    console.error('Error executing query', err.stack);
    throw err;
  } finally {
    client.release(); // 물리적 연결을 닫는 것이 아니라, 풀에 반납합니다.
  }
}

가장 중요한 부분은 `try...finally` 구문입니다. `client.release()`는 쿼리 성공/실패 여부와 상관없이 항상 호출되어야 커넥션이 풀에 정상적으로 반납됩니다. 이를 누락하는 것이 바로 커넥션 누수의 주된 원인입니다.

Python: SQLAlchemy

Python의 대표적인 ORM이자 DB 툴킷인 SQLAlchemy는 `Engine` 객체를 통해 투명하게 커넥션 풀링을 관리합니다.


from sqlalchemy import create_engine

# Engine을 생성하는 시점에 커넥션 풀도 함께 설정됩니다.
# 이 Engine 객체는 전역적으로 한 번만 만들어 사용합니다.
engine = create_engine(
    "postgresql+psycopg2://user:password@host:port/dbname",
    pool_size=5,        # 풀에 유지할 최소한의 커넥션 수
    max_overflow=10,    # 풀 크기를 초과하여 생성할 수 있는 임시 커넥션 수 (pool_size + max_overflow 가 총 커넥션 한도)
    pool_timeout=30,    # 커넥션을 얻기 위해 대기할 시간(초)
    pool_recycle=1800   # 커넥션 재사용 시간(초). 이 시간이 지나면 커넥션을 재연결. (MySQL의 wait_timeout보다 짧게 설정)
)

# 컨텍스트 매니저(with 구문)를 사용하면 커넥션 반납이 자동으로 처리됩니다.
def get_user_by_id(user_id):
    with engine.connect() as conn:
        result = conn.execute(f"SELECT * FROM users WHERE id = {user_id}")
        return result.fetchone()

# SQLAlchemy ORM의 세션을 사용하는 경우에도 내부적으로 같은 원리로 동작합니다.
# Session 스코프가 끝나면 커넥션은 자동으로 풀에 반납됩니다.

SQLAlchemy의 `with engine.connect() as conn:` 구문은 Node.js의 `try...finally`와 동일한 역할을 수행하여 코드 실수를 방지하고 안정성을 높여줍니다.


5. 내 서버는 건강한가? 모니터링 및 진단 기법

커넥션 풀을 적용했더라도, 설정이 적절한지, 혹은 다른 곳에서 누수가 발생하고 있지는 않은지 확인하는 과정이 필요합니다. 문제 해결의 첫걸음은 정확한 진단입니다.

데이터베이스 직접 확인

가장 직접적이고 확실한 방법입니다. 데이터베이스에 직접 접속하여 현재 연결 상태를 확인합니다.

  • MySQL / MariaDB:
    SHOW FULL PROCESSLIST;
    이 명령어를 실행하면 현재 모든 연결 목록이 나옵니다. 여기서 `Command` 컬럼이 `Sleep`이고 `Time` 컬럼의 숫자가 매우 높은 연결들이 많다면, 애플리케이션이 사용이 끝난 커넥션을 제대로 닫거나 반납하지 않고 방치하고 있다는 강력한 증거입니다.
  • PostgreSQL:
    SELECT pid, datname, usename, application_name, client_addr, state, query_start, state_change FROM pg_stat_activity WHERE datname = 'your_database';
    여기서는 `state` 컬럼을 주목해야 합니다. `idle` 또는 `idle in transaction` 상태로 `state_change` 이후 오랜 시간이 지난 커넥션이 다수 발견된다면 커넥션 누수를 의심할 수 있습니다.

AWS CloudWatch를 이용한 모니터링

데이터베이스를 Amazon RDS로 운영하고 있다면 AWS가 제공하는 CloudWatch 지표를 적극 활용해야 합니다.

  • `DatabaseConnections`: 현재 데이터베이스에 연결된 클라이언트의 수입니다. 이 지표가 시간이 지남에 따라 계단식으로 계속해서 우상향하는 그래프를 그린다면 100% 커넥션 누수 문제입니다. 정상적인 애플리케이션이라면 트래픽에 따라 오르내리되 일정한 범위 내에서 안정적인 패턴을 보입니다.
  • `CPUUtilization`: 데이터베이스 인스턴스의 CPU 사용률입니다. 관리되지 않는 유휴 커넥션이 많아지면 이 수치 또한 서서히 증가할 수 있습니다.
  • `FreeableMemory`: 가용 메모리입니다. 역시 유휴 커넥션 증가는 메모리 사용량을 높여 이 지표를 감소시킵니다.

애플리케이션 및 OS 레벨 확인

  • 애플리케이션 로그: 커넥션 풀 라이브러리는 대부분 타임아웃이 발생했을 때 로그를 남깁니다. 'Connection is not available, request timed out' 같은 메시지가 로그에 자주 보인다면, 풀 크기가 부족하거나 어딘가에서 커넥션이 누수되어 반납되지 않고 있음을 의미합니다.
  • OS 명령어: EC2 인스턴스에 SSH로 접속하여 네트워크 상태를 확인할 수 있습니다.
    
    # DB 포트(MySQL:3306, PostgreSQL:5432)로 연결된 개수 확인
    netstat -an | grep ":3306" | grep ESTABLISHED | wc -l
            
    이 명령어로 확인한 연결 수가 비정상적으로 많고 계속 증가한다면 문제입니다.

6. 근본적인 아키텍처 개선을 향하여

커넥션 풀링은 훌륭한 해결책이지만, 더 많은 트래픽과 더 복잡한 환경에 대비하기 위한 아키텍처 수준의 개선도 고려해 볼 수 있습니다.

AWS RDS Proxy: 똑똑한 중간 관리자

AWS Lambda와 같은 서버리스 환경에서는 커넥션 풀링이 더욱 복잡해집니다. 각 Lambda 함수 실행이 독립적인 컨테이너에서 이루어지기 때문에, 기존의 애플리케이션 레벨 커넥션 풀이 효과적으로 동작하기 어렵습니다. 수많은 Lambda 함수가 동시에 실행되면 순식간에 DB의 `max_connections`를 고갈시키는 '커넥션 폭풍'이 발생할 수 있습니다.

이런 문제를 해결하기 위해 AWS는 **RDS Proxy**라는 관리형 서비스를 제공합니다. RDS Proxy는 애플리케이션과 RDS 데이터베이스 사이에 위치하는 중간 계층입니다.

  • 애플리케이션은 RDS가 아닌 RDS Proxy에 연결합니다.
  • RDS Proxy는 내부적으로 데이터베이스에 대한 커넥션 풀을 효율적으로 관리하고 유지합니다.
  • 수천 개의 애플리케이션 연결이 들어와도, RDS Proxy는 이를 소수의 실제 DB 커넥션으로 다중화(Multiplexing)하여 전달합니다.

RDS Proxy를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 확장성 및 복원력 향상: 데이터베이스의 연결 부하를 크게 줄여주며, DB 장애 조치(failover) 시에도 애플리케이션의 재연결을 매끄럽게 처리하여 가용성을 높입니다.
  • 서버리스 환경에 최적화: Lambda와 같은 환경에서 발생하는 커넥션 관리 문제를 근본적으로 해결해 줍니다.
  • 보안 강화: AWS Secrets Manager와 통합하여 DB 자격 증명을 코드에서 제거하고 IAM 인증을 통해 안전하게 연결을 관리할 수 있습니다.

맺음말: 성장의 밑거름이 되는 경험

처음에는 그저 '프리티어라서 느린가 보다'라고 생각했던 작은 문제에서 시작해, 우리는 데이터베이스 커넥션 관리의 중요성, 커넥션 풀의 작동 원리와 실제 적용법, 그리고 시스템의 건강 상태를 진단하는 모니터링 기법까지 깊이 있게 탐험했습니다. AWS 서버가 응답을 멈추는 현상은 단순한 해프닝이 아니라, 개발자로서 한 단계 성장할 수 있는 값진 기회입니다.

자원 관리는 화려한 기능 구현만큼이나, 어쩌면 그보다 더 중요합니다. 특히 모든 것이 제한적인 프리티어 환경에서의 경험은 대규모 서비스를 운영할 때 마주할 수많은 문제를 미리 예방하는 훌륭한 예방 주사가 될 것입니다. 이 글을 통해 얻은 지식을 바탕으로, 오늘 당신의 `application.yml`을 열어 `maximum-pool-size`를 확인해보고, 코드 속 DB 연결 로직에 `finally`나 `with` 구문이 잘 적용되어 있는지 점검해 보십시오. 그 작은 점검이 미래의 서버 장애를 막고, 당신의 밤잠을 지켜줄 것입니다.


0 개의 댓글:

Post a Comment