서버리스 PaaS 인프라에서 배포 후 겪는 실무 문제 해결책
TuBrief 편집팀
2026년 7월 24일
0
AI/미래기술원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
프론트엔드와 백엔드를 클릭 몇 번으로 배포해 주는 올인원 PaaS는 처음엔 완벽해 보인다. 문제는 실제 유저가 들어오고 난 뒤다. 로컬에선 잘만 도는 코드가 프로덕션에 올라가면 타임아웃을 뿜어내고, DB 스키마 하나 바꾸려다 전체 서비스가 먹통이 되기도 한다. 게다가 벤더에 발이 묶여 이러지도 저러지도 못하는 상황을 맞닥뜨리면 머리가 아파온다.
이런 플랫폼의 편리함을 누리면서 뒤따르는 장애물을 깔끔하게 치워버리는 인프라 패턴 몇 가지를 정리했다.
로컬 Node.js 프로세스는 계속 켜져 있지만, 서버리스 환경은 요청이 들어올 때만 잠깐 떠올랐다 사라진다. 콜드 스타트 순간에 전역 변수나 DB 싱글톤 커넥션을 어설프게 잡으면 커넥션 풀이 금세 말라버린다.
스테이징 서버로 매번 배포해 보면서 확인할 수는 없는 노릇이다. 네트워크 레이어 자체를 로컬에서 모킹하고 핸들러를 띄우는 편이 훨씬 빠르다.
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/v1/user/profile', ({ request }) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader) {
return new HttpResponse(null, { status: 401, statusText: 'Unauthorized' });
}
return HttpResponse.json({
id: 'usr_102938',
email: 'dev@example.com',
role: 'ADMIN',
createdAt: new Date().toISOString()
});
})
];
// src/mocks/node.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
이 패턴을 적용하는 과정은 단순하다.
setupServer로 외부 API 통신을 격리한다..env.local 파일의 DB 접속 주소를 프로덕션 트랜잭션 풀러 엔드포인트와 철저히 분리한다.배포 버튼을 누르지 않고도 백엔드 인터페이스 동작을 로컬에서 100% 동일하게 검증할 수 있다. 오락가락하는 원격 테스트 시간을 버리고 코드 작성 즉시 피드백을 받는 구조다.
Supabase나 Neon 같은 서버리스 PostgreSQL을 쓸 때 단순 ALTER TABLE 문을 그냥 날리면 위험하다. 대상 테이블에 ACCESS EXCLUSIVE 락이 걸리면서 후속 쿼리들이 줄줄이 대기 상태에 빠지고, 결국 타임아웃 장애로 이어진다.
우선 마이그레이션 세션에 타임아웃 한계를 걸어두어야 한다.
SET lock_timeout = '2000ms';
SET statement_timeout = '5000ms';
ALTER TABLE users ADD COLUMN bio VARCHAR(255);
운영 중인 DB 구조를 안전하게 바꾸려면 코드와 DB를 동시에 확장한 뒤 옛것을 깎아내는 방식을 써야 한다.
import { db } from './db';
interface UpdateUserProfileInput {
userId: string;
fullName: string;
}
export async function updateUserProfile({ userId, fullName }: UpdateUserProfileInput) {
await db.transaction(async (tx) => {
await tx.user.update({
where: { id: userId },
data: {
full_name: fullName,
name: fullName
}
});
});
}
안전한 작업을 위한 세 단계다.
lock_timeout을 설정해 락 경합 시 즉시 작업을 중단하도록 안전장치를 만든다.이 절차를 지키면 사용자 요청을 하나도 씹지 않고 스키마를 교체할 수 있다.
서버리스 환경에서 console.log만 남겨두면 여러 인스턴스의 로그가 이리저리 섞여서 난장판이 된다. 요청 하나가 들어와서 나갈 때까지의 동선을 추적하려면 고유한 Request ID와 구조화된 JSON 형식이 필수다.
import { Hono } from 'hono';
import { requestId } from 'hono/request-id';
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label.toUpperCase() })
},
base: { env: process.env.NODE_ENV }
});
const app = new Hono();
app.use('*', requestId());
app.use('*', async (c, next) => {
const reqId = c.var.requestId;
const startTime = performance.now();
c.set('logger', logger.child({ reqId }));
await next();
const durationMs = Math.round(performance.now() - startTime);
logger.info({
reqId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
durationMs
}, 'Request processing finished');
});
외부 모니터링 도구를 쓸 때 한도 초과 요금 폭탄을 피하는 세팅이다.
Server-Timing을 주입해 병목 지점을 모니터링 시스템으로 보낸다.import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
beforeSend(event, hint) {
const error = hint.originalException;
if (error && error instanceof Error && error.message.includes('ECONNRESET')) {
return null;
}
return event;
}
});
무의미한 200 OK 로그와 노이즈 성격의 예외 상황만 솎아내도 모니터링 플랫폼의 무료 티어 한도 내에서 시스템을 충분히 제어할 수 있다.
특정 PaaS 전용 SDK를 비즈니스 로직에 직접 갖다 붙이면 나중에 다른 플랫폼으로 옮길 때 코드를 싹 다 갈아엎어야 한다. Martin Fowler가 정리한 어댑터 패턴을 써서 애플리케이션 핵심 로직과 외부 클라우드 SDK 사이의 접점을 분리하는 이유다.
export interface IStorageService {
uploadFile(path: string, fileBuffer: Buffer, mimeType: string): Promise<{ url: string }>;
deleteFile(path: string): Promise<void>;
}
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
export class S3StorageAdapter implements IStorageService {
private s3: S3Client;
private bucket: string;
constructor(region: string, bucket: string) {
this.s3 = new S3Client({ region });
this.bucket = bucket;
}
async uploadFile(path: string, fileBuffer: Buffer, mimeType: string): Promise<{ url: string }> {
await this.s3.send(new PutObjectCommand({
Bucket: this.bucket,
Key: path,
Body: fileBuffer,
ContentType: mimeType
}));
return { url: `https://${this.bucket}.s3.amazonaws.com/${path}` };
}
async deleteFile(path: string): Promise<void> {
await this.s3.send(new DeleteObjectCommand({ Bucket: this.bucket, Key: path }));
}
}
언제든 탈출할 수 있도록 데이터 백업도 자동화해둔다.
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/tmp/db_backup_${TIMESTAMP}"
S3_BUCKET="s3://my-app-exit-backups/pg_dumps"
export PGPASSWORD="${DB_PASSWORD}"
mkdir -p "${BACKUP_DIR}"
pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" \
-Fc --exclude-table-data='logs_*' > "${BACKUP_DIR}/full_schema_data.dump"
tar -czvf "${BACKUP_DIR}.tar.gz" -C "${BACKUP_DIR}" .
aws s3 cp "${BACKUP_DIR}.tar.gz" "${S3_BUCKET}/${TIMESTAMP}.tar.gz" --endpoint-url "${R2_ENDPOINT_URL}"
rm -rf "${BACKUP_DIR}" "${BACKUP_DIR}.tar.gz"
pg_dump를 활용해 스키마와 데이터를 추출하는 셸 스크립트를 작성한다.crontab에 등록한다.특정 플랫폼에 귀속되는 리스크를 줄여두면 인프라 단가 상승이나 플랫폼 장애 시에도 대처하기 훨씬 수월해진다.