
사내 서비스가 쑥쑥 성장하면서 주문 건수가 갈수록 늘어났고, 백 오피스 엑셀 생성 기능에 문제들이 터지기 시작했다.
그동안 조회 범위를 제한해 데이터가 커지지 못하도록 막아왔지만 결국 서버를 분리 및 개선하기로 결정 하였다.
처음 MVP 개발에서는 독립 서버 구성과 빠른 실행이라는 키워드로 개발을 진행 하였다.
빠른 실행을 위해 여전히 Buffer 방식을 통해 개발하였지만 여전히 Out of Memory 현상이 나타났다.
독립 서버는 Serverless 환경으로 구축하기 위해 AWS Lambda 를 통해 구성하였는데 Lambda 의 기본 memory 제공량이 부족했기 때문이다.
Lambda 메모리를 증설할 수는 있었지만, 서버 비용 부담이 커지고 추후 데이터가 증가하면 다시 문제가 생길 수 있기 때문에 다른 해결책이 필요 했다.
AWS Lambda 는 128MB에서 10,240MB 사이의 메모리 값을 할당 할 수 있고 단순 비용은 선형적으로 증가한다.
Lambda 메모리 늘리면 비용도 쑥쑥 올라가고, 데이터가 더 늘어나면 OOM 또 터지고… 😭
우선 JSON 파일을 사용하게 된 배경을 설명하자면
그래서 API 서버에서 데이터를 JSON 파일로 생성 후 스트림으로 읽고,
엑셀 행을 스트림으로 작성하는 전략을 사용 하였다.
rows.json, meta.json) 생성 → S3 업로드 → SQS에 알림 발행 rows.json을 스트림으로 읽고 exceljs로 .addRow().commit() WorkbookWriter (stream 모드)으로 엑셀 파일 스트리밍 생성 rows.json >> excel row data
meta.json >> 시트명, 컬럼 정보 등 메타데이터 포함
흐름도
API → S3(json file) → SQS → Lambda(스트림→엑셀) → S3(result.xlsx) → URL
단점:
- exceljs stream 모드는 스타일(font, 배경색 등) 변경을 지원하지 않는다.
- data 증가에 따라 생성 시간은 증가한다
| 구분 | Buffer 방식 | Stream 방식 |
|---|---|---|
| 메모리 | 전체 파일 크기만큼 메모리에 할당 | 청크 단위로 메모리에 잠시만 보관 → 메모리 사용 적음 |
| 처리 방식 | fs.readFileSync / JSON.parse 등 | fs.createReadStream, JSON.parse 스트림 등 |
| 장점 | 코드 간단, 동기적 처리 | 대용량 안정적, 비동기·이벤트 기반 처리 가능 |
| 단점 | OOM 위험, GC 부담 | 구현이 다소 복잡, 흐름(flow) 제어 필요 |
| 모드 | 특징 |
|---|---|
| Paused | .read() 또는 data 리스너 없이는 대기만 함 |
| Flowing | .pipe(), on('data'), .resume() 호출 시 동작 |
parser() → JSON 구조 분해 streamArray() → 배열 요소 하나씩 추출 chain([ Readable, Transform…, handler ]) .addRow().commit() → 한 줄씩 즉시 파일에 기록 import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import * as ExcelJS from 'exceljs';
import { chain } from 'stream-chain';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray';
import * as fs from 'fs';
const s3 = new S3Client({ region: 'ap-northeast-2' });
export async function handler(event) {
const bucket = process.env.BUCKET_NAME!;
const rowsKey = event.rowsKey;
const resultKey = event.resultKey;
// 1. S3에서 JSON 파일 스트림으로 읽기
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: rowsKey }));
// 2. Excel Streaming Writer 세팅
const tmpFile = `/tmp/out_${Date.now()}.xlsx`;
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({ filename: tmpFile });
const sheet = workbook.addWorksheet('Sheet1');
// 3. stream-chain 구성
const pipeline = chain([
Body as NodeJS.ReadableStream,
parser(),
streamArray(),
({ value }) => {
const row = Array.isArray(value) ? value : Object.values(value);
sheet.addRow(row).commit();
},
]);
// 4. 스트림 흐름 시작
pipeline.resume();
await new Promise((res, rej) => {
pipeline.on('end', res);
pipeline.on('error', rej);
});
// 5. 워크북 커밋 & S3 업로드
await workbook.commit();
const buffer = fs.readFileSync(tmpFile);
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: resultKey,
Body: buffer,
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}));
const presigned = await getSignedUrl(
this.s3,
new GetObjectCommand({ Bucket: bucket, Key: resultKey }),
{ expiresIn: 3600 },
);
return { message: 'Excel 파일 생성 완료', url: presigned };
}
※ 이 코드는 추상화된 MVP 버전으로, 실제 운영 환경에서는 상세한 예외 처리, 리소스 관리, 보안 설정 등의 추가 구현이 필요합니다.
Stream 기반으로 대용량 Excel 파일을 생성하는 방법에 대해 살펴보았습니다.
이번 글에서는 Lambda의 운영 및 배포, 현재 구조의 장단점, 대용량 데이터 분석에 대한 다양한 대안 등 추가로 다루고 싶은 주제들이 있었지만, 이는 추후 별도의 포스트에서 보다 깊이 있게 다뤄보겠습니다.
잘봤습니다.
S3 업로드도 스트림으로 업로드할 수 있는 방법이 있을까요?