Skip to main content

Command Palette

Search for a command to run...

AWS Lambda와 Connection Pool 사용 시 발생한 응답 지연 문제 해결기

API 응답이 10초 이상 걸려요!!

Updated
5 min read
AWS Lambda와 Connection Pool 사용 시 발생한 응답 지연 문제 해결기

배경

개발자님! 화면이 너무 늦게 떠요!!

이번 주에 매출 통계 기능을 개발해서 개발 서버에 올렸는데 테스팅을 하다가 갑자기 긴급 호출이 들어왔다. API 속도가 너무 느리다는데 얼마나 느리길래 그런지 확인해봤더니 무려 10~16초가 걸렸다.

신규 레포지토리를 파서 개발한 거라 설정에 문제가 있었나? VPC 간 통신 때문에 지연되는 건가? 별의별 생각이 다 들어서 각 모듈별로 실행되는 시간, 실제 쿼리하는 시간을 전부 측정해봤는데 10~16초나 걸릴 만한 이유를 찾지 못했다.

실제 동작이 query string 받아서 SELECT * From table WHERE a = ? AND b = ? ORDER BY c DESC 정도만 수행하는 아주 간단한 API 였는데, 너무 이상한 수치였다.

CloudWatch 로그를 아무리 뒤져봐도 에러도 보이지 않았고,
코드의 각 부분이 실행되는 시간을 측정해봐도 2초 이내에 모든 실행이 완료되고 있었다.

이상한 점은,

  • 지정한 동작(쿼리 실행, 결과 가공 등)은 모두 끝났는데도

  • 응답을 바로 반환하지 않고 6초 이상 대기한 뒤에야 응답을 반환하고 있다는 것

이었다.

즉, “로직은 다 끝났는데, 어딘가에서 응답이 붙잡혀 있는 상태” 였다.

Connection Pool과 유휴 연결

결론부터 말하면, 문제의 1차적인 원인은

connection pool의 유휴 연결 때문에 Node.js 이벤트 루프가 완전히 비지 않았고,
그로 인해 Lambda가 응답을 바로 반환하지 못하고 지연이 발생한 것이었다.

사용하고 있던 라이브러리는 pg.Pool이었고, idleTimeoutMillis 기본값은 10초다.
node-postgres 문서의 설명은 다음과 같다:

number of milliseconds a client must sit idle in the pool and not be checked out
before it is disconnected from the backend and discarded
default is 10000 (10 seconds) – set to 0 to disable auto-disconnection of idle clients

즉, 한 번 빌려갔다가 돌려준 커넥션이 최소 10초 동안 idle 상태로 pool 안에 남아 있고, 이 동안에는 소켓/타이머 레퍼런스가 살아 있기 때문에 Node.js 이벤트 루프 입장에서는 “아직 완전히 idle이 아니다” 라고 보는 것이다.

Lambda handler 방식과 이벤트 루프의 관계

여기서 자연스럽게 이어지는 질문은 이것이다.

“idle connection이 이벤트 루프를 잡고 있는 건 알겠는데,
이벤트 루프가 안 비면 왜 응답이 늦게 나가는 걸까?”

그 이유는 Lambda handler 방식 때문이다.

Node.js에서 Lambda handler는 크게 두 가지 방식이 있다.

  1. async/await 기반

     exports.handler = async (event, context) => {
       const result = await doSomething(event);
       return {
         statusCode: 200,
         body: JSON.stringify(result),
       };
     };
    
  2. callback 기반 (공식 문서 기준으로 Node.js 22.x까지만 지원된다)

     exports.handler = (event, context, callback) => {
       doSomething(event, (err, result) => {
         if (err) {
           return callback(err);
         }
         callback(null, {
           statusCode: 200,
           body: JSON.stringify(result),
         });
       });
     };
    

callback 방식에서는 기본적으로

  • 콜백을 호출한 뒤에도

  • 이벤트 루프에 남아 있는 작업(타이머, 소켓 등)이 모두 사라질 때까지,

  • 또는 함수 타임아웃에 도달할 때까지

실행을 계속 유지하려는 동작을 한다.

그래서 callback 방식으로 구현한 상태에서 이벤트 루프를 붙잡고 있는 idle connection이 있으면,
로직은 이미 끝났는데도 응답이 바로 나가지 않고 지연되는 상황이 만들어질 수 있다.

fastify-aws-lambda 내부는 callback 기반이었다

Fastify 앱을 Lambda로 올릴 때 우리는 아래와 같은 형태로 간단하게 래퍼만 호출하고 있었다.

const awsLambdaFastify = require('@fastify/aws-lambda');
const { createApp } = require('./createApp');

const app = createApp();
const proxy = awsLambdaFastify(app);

module.exports.handler = proxy;

코드만 보면 이 핸들러가 async/await 기반인지, callback 기반인지 전혀 알 수 없다.

그런데 내부 구현을 확인해보니, aws-lambda-fastify는 Fastify 앱을 감싸면서 최종적으로 Lambda에 등록되는 handler를 (event, context, callback) 형태의 callback 기반으로 생성하고 있었다.

그 결과,

  • callback 스타일의 **기본 동작(이벤트 루프가 완전히 빌 때까지 실행을 유지하려는 동작)**이 그대로 적용되고

  • pg.Pool의 idle connection이 이벤트 루프를 비우지 못하는 상황과 맞물리면서

  • 로직은 이미 다 끝났는데도 응답이 수 초 동안 지연되는 현상이 발생했다.

해결방법

1) Lambda context.callbackWaitsForEmptyEventLoop = false

먼저, Node.js Lambda의 Context 객체

함수 이름, 버전, 메모리 등 실행 환경에 대한 정보와 몇 가지 설정 값을 제공하는데,
그중 하나가 callbackWaitsForEmptyEventLoop다. 이 값은 callback 기반 핸들러에서만 의미가 있는 옵션이다.

문서 설명을 보면:

Node.js 이벤트 루프가 빌 때까지 대기하는 대신, 콜백이 실행될 때 즉시 응답을 보내려면 false로 설정합니다.
이것이 false인 경우, 대기 중인 이벤트는 다음 번 호출 중에 계속 실행됩니다.

즉, 이 값을 false로 설정하면

  • 이벤트 루프가 완전히 비기까지 기다리지 않고

  • 이미 준비된 응답을 바로 반환하고

  • 남아 있는 타이머/소켓 같은 것들은 다음 호출에서 재사용될 수 있도록 놔두는

동작을 하게 된다.

Fastify를 Lambda로 올릴 때는 aws-lambda-fastify를 통해 handler를 만들고 있었기 때문에,
이 옵션을 래퍼에 직접 넘겨서 동작을 바꿔줄 수 있었다.

const proxy = awsLambdaFastify(app, {
  callbackWaitsForEmptyEventLoop: false,
});

module.exports.handler = proxy;

이렇게 설정한 이후에는 로직이 끝난 시점과 클라이언트가 응답 받는 시점이 거의 일치하게 바뀌었고,
10초 가까이 붙잡혀 있던 지연 현상도 없어졌다.

2) 유휴 연결시간 줄이기

유휴 연결시간 자체를 줄여서 이벤트 루프에 idle 연결이 더 짧게 남도록 조정하는 방법도 시도했봤다.

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

const pool = new Pool({
  idleTimeoutMillis: 1000, // 1초로 설정
});

기본값(10초)으로 설정했을 때보다 확실히 응답 시간이 줄어들긴 했지만,
여전히 응답이 준비된 이후에 잠깐의 대기 시간이 생기는 문제가 있었다.

그렇다고 idleTimeoutMillis를 너무 짧게 설정하면,
비용이 큰 데이터베이스 연결을 Lambda가 실행될 때마다 계속 새로 시도해야 하는 문제가 생긴다.

결국,

  • context.callbackWaitsForEmptyEventLoop = false로 응답을 먼저 보내도록 만들고

  • idleTimeoutMillis는 사용 패턴에 맞는 적절한 값으로 조정하는 것

이 현실적인 타협점이라고 판단했다.

결론

삽질했던 시간보다 훨씬 간단하게 해결한 문제였다.

이번에 겪은 일은, 결국 내가 사용하고 있는 플랫폼에 대한 이해 부족에서 비롯된 문제였다. 요즘은 AWS 서비스들이 워낙 잘 되어 있다 보니 “설마 인프라 쪽에 원인이 있겠어?”라는 생각으로, 코드만 붙잡고 있었던 것도 한몫했다. 앞으로는 코드에만 집중하기보다, 런타임·플랫폼·네트워크까지 포함해서 문제를 좀 더 다각도로 바라보려 한다.

이번에 크게 느낀 점은 두 가지다.

  • 첫째, 라이브러리의 겉 API만 보지 말고, 중요한 경계(Lambda handler, DB 커넥션, 이벤트 루프 등)를 다루는 부분의 구현은 한 번쯤 직접 뜯어보자는 것이다. 이번에도 aws-lambda-fastify 내부가 callback 기반이라는 사실을 알고 나서야 퍼즐이 맞춰졌다.

  • 둘째, 서버리스를 사용할 때는 기본 동작과 내가 적용한 설정이 어떻게 맞물리는지 반드시 이해해야 한다. Lambda의 실행 모델, handler 방식(async/await vs callback), Connection Pool의 동작 방식, idleTimeoutMillis, callbackWaitsForEmptyEventLoop 같은 설정은 실제 성능과 응답 지연에 직접적인 영향을 준다. “서버리스니까 알아서 처리해주겠지”라는 기대보다는, 내 코드가 서버리스 환경에서 어떻게 실행되는지 명확히 이해하고 사용하는 것이 중요하다는 점을 다시 한 번 느꼈다.

이런 것들을 한 번 겪어두면, 다음 비슷한 장애가 왔을 때 “코드만 들여다보는” 시간은 조금 줄어들 거라고 기대하고 있다.

참고자료

More from this blog

오픈소스 기여모임 10기 후기 - 첫 Pr을 올리기까지

개발자라면 누구나 한 번쯤 오픈소스 기여에 대한 환상을 가져본 적 있을 거다. 하지만 막상 시작하려면 어디서부터 해야 할지 막막하고, 괜히 대단한 걸 해야 할 것 같은 부담감에 선뜻 시작하기는 어려운 것 같다. 나 또한 해보고 싶다는 마음만 가지고 계속 미뤄왔다. 그러다 2025년 말 쯤에 오픈채팅방과 글또 슬랙 채널에서 "오픈소스 기여모임" 10기 모집글을 봤다. 2년 넘게 500명 이상의 참가자와 함께 1000개 이상의 PR을 만들어온 커뮤...

Feb 5, 20265 min read

😢 글또 10기 활동 회고 — “글또야, 가지 마…”

들어가며 드디어 글또 10기 활동 회고를 정리해본다.6개월간의 여정을 뒤돌아보니 정말 많은 일들이 있었다. 글또라는 커뮤니티를 8기가 한창 진행되고 있을 때 알았는데 이름부터 인상이 강렬했다. "글쓰는 또라이가 세상을 바꾼다." 유쾌하고 독특한 문구에 피식 웃으며, '여긴 도대체 어떤 사람들이 모이는 곳이지?' 하고 넘겼었다. 재밌는 건 결국, 나도 그 "또라이들" 중 한 명이 되었다는 것이다. 😌 글또는 개발자들이 2주에 한 번 글을 ...

Jul 31, 20255 min read
😢 글또 10기 활동 회고 — “글또야, 가지 마…”

Serverless 환경에서 배포 전 환경변수 검증 자동화하기: TypeBox와 Bitbucket Pipeline 활용기

들어가며 배포 직후, 환경변수가 제대로 설정되지 않아 여러 API가 제대로 작동하지 않는 일이 있었습니다. 다행히 밤에 사용자가 없을 때 문제가 있었던 거라 영향도는 크지 않았지만 앞으로도 계속해서 발생할 수 있는 문제이기 때문에 해결해야 겠다고 생각했습니다. 개발 단계에서 문제가 발견되면 가장 좋겠지만, 현재 팀 상황에서는 백엔드 개발을 혼자 담당하고 있어 코드 리뷰나 검증 프로세스를 갖추기가 쉽지 않았습니다. 그래서 최소한 배포 전에 자동으...

Mar 16, 20254 min read

Cloudflare Tunnel로 포트포워딩 없이 홈서버 운영하기

이 글에서 다루는 내용 포트포워딩이 안 되는 이유 (CGNAT 환경 이해) CGNAT 우회 방법들의 장단점 비교 Cloudflare Tunnel 설정 방법 (MacOS 기준) 외부에서 내 PC로 접근할 수 있도록 허용하는 방법을 생각하면 포트포워딩이 가장 먼저 떠오릅니다. 공유기에서 특정 포트를 열어 외부에서 서버에 접속할 수 있도록 설정하는 방식으로, 마인크래프트 멀티를 해보셨던 분이라면 분명 해보셨을 방법입니다. 😊 작년에 저는 홈서...

Mar 2, 20256 min read
Cloudflare Tunnel로 포트포워딩 없이 홈서버 운영하기

구름고래 공방

48 posts