운영형 Express 서버: 검증·에러·파일 처리
입력 검증과 살균 분리, 중앙화된 오류 처리, 업로드·스트리밍·PDF 운용 패턴 정리
Posted
By okorion
운영형 Express 서버: 검증·에러·파일 처리
결론 요약
- 운영 관점의 서버는 “기능 구현”이 아니라 “실패를 통제하는 구조”다.
- 입력 검증과 데이터 살균은 목적이 다르며, 섞으면 보안과 UX가 동시에 무너진다.
- 오류 처리는 try/catch 문제가 아니라 에러 전달 경로와 책임 분리 문제다.
- 파일 업로드/다운로드는 I/O 기능이 아니라 메모리·권한·보안 관리 문제다.
- 스트리밍과 PDF 생성은 “잘 되면 편한 기능”, 잘못 만들면 장애 유발 지점이다.
운영 관점의 서버 만들기
검증 · 오류 처리 · 파일 업로드/다운로드 · 스트리밍 · PDF
1️⃣ 입력 검증 vs 데이터 살균: 목적부터 다르다
정의부터 분리
| 구분 | Validation(검증) | Sanitization(살균) |
|---|---|---|
| 목적 | 입력이 유효한가 | 입력이 안전한가 |
| 대상 | 형식, 범위, 필수 여부 | 스크립트, 특수문자 |
| 실패 시 | 사용자에게 피드백 | 내부적으로 처리 |
| UX 영향 | 큼 | 없음 |
왜 분리해야 하는가
- 검증 실패는 사용자가 고쳐야 할 문제
- 살균 실패는 서버가 책임질 문제
이 둘을 섞으면:
- 사용자에게 “왜 안 되는지” 설명 불가
- 보안 로직이 UI 로직에 침투
2️⃣ 검증 실패 UX: 입력값 유지가 핵심이다
문제 상황
- 폼 제출 → 검증 실패 → 입력값 전부 사라짐
- 사용자는 재입력, 이탈률 증가
해결 패턴 (SSR 기준)
1
2
3
4
res.render('form', {
errors,
oldInput: req.body
});
<input name="title" value="<%= oldInput.title %>" />
<% if (errors.title) { %>
<p><%= errors.title %></p>
<% } %>
구조 원칙
- 입력값 유지: 사용자가 입력한 데이터는 그대로 돌려준다
- 에러 메시지 구조화: 필드별 메시지
검증 UX는 “친절함” 문제가 아니라 전환율과 직결된 운영 요소다.
3️⃣ 오류 처리 전략: 에러는 흐름으로 관리한다
3-1. 동기 vs 비동기 에러 전달
| 유형 | 예시 | 전달 방식 |
|---|---|---|
| 동기 | JSON 파싱 실패 | try/catch |
| 비동기 | DB 조회 실패 | next(err) |
| Promise | async/await | catch → next |
❌ 나쁜 예
1
2
3
4
5
try {
await doAsync();
} catch (e) {
console.log(e); // 삼킴
}
✅ 개선 예
1
2
3
4
5
try {
await doAsync();
} catch (e) {
next(e);
}
3-2. Express 에러 핸들러 패턴
1
2
3
4
5
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({
message: err.message
});
});
규칙
- 모든 에러는 중앙으로 모인다
- 라우트에서는 “던지고 위임”만 한다
3-3. 상태 코드 설계 원칙
| 코드 | 의미 | 책임 |
|---|---|---|
| 400 | 검증 실패 | 클라이언트 |
| 401 | 인증 실패 | 클라이언트 |
| 403 | 권한 없음 | 클라이언트 |
| 404 | 리소스 없음 | 클라이언트 |
| 500 | 서버 오류 | 서버 |
상태 코드는 숫자가 아니라 계약이다.
4️⃣ 파일 업로드: 멀티파트는 I/O + 보안 문제다
멀티파트 폼 데이터란
- 텍스트 + 바이너리 혼합 전송
- 서버는 스트림 단위로 처리
Multer의 역할
- 멀티파트 파싱
- 파일 임시 저장
- 메타데이터 제공
1
2
3
4
5
6
multer({
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
cb(null, file.mimetype.startsWith('image/'));
}
});
핵심 방어 포인트
- MIME 타입 필터링
- 용량 제한
- 저장 경로 분리(public vs private)
5️⃣ 다운로드와 스트리밍: preload vs stream
preload (한 번에 로드)
- 작은 파일
- 메모리 여유 있음
stream (조각 전송)
- 큰 파일
- 다수 동시 요청
1
fs.createReadStream(path).pipe(res);
실무 결론
- 다운로드는 기본적으로 stream
- preload는 예외적으로 사용
6️⃣ PDF 생성: 편의 기능이 아니라 위험 구간
PDFKit 사용 시 핵심 포인트
- 메모리에 PDF 전체를 올리지 않는다
- 생성과 동시에 스트리밍
1
2
3
4
const doc = new PDFDocument();
doc.pipe(res);
doc.text('Invoice');
doc.end();
응답 헤더 필수
1
2
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="invoice.pdf"');
PDF 생성은 CPU + 메모리 + I/O를 동시에 사용한다. 잘못 만들면 장애 지점이 된다.
7️⃣ 케이스 스터디 ①: 업로드 실패
문제
- 이미지 업로드 시 서버 메모리 급증
원인
- 용량 제한 없음
- MIME 검증 없음
해결
- Multer limits 설정
- 이미지 타입만 허용
- 실패 시 413 반환
8️⃣ 케이스 스터디 ②: 다운로드 권한 누락
문제
- 누구나 PDF 청구서 접근 가능
원인
- 파일 URL에 권한 검증 없음
해결
- 다운로드 라우트에 인증/권한 미들웨어 추가
- 파일 시스템 직접 노출 금지
운영 체크리스트 (20개)
- 검증과 살균을 분리했다
- 검증 실패 시 입력값을 유지한다
- 필드별 에러 메시지 구조가 있다
- try/catch는 에러를 삼키지 않는다
- 모든 에러는 중앙 핸들러로 모인다
- 상태 코드를 의미에 맞게 사용한다
- 업로드 파일 용량 제한이 있다
- MIME 타입 필터링이 있다
- 업로드 경로가 분리되어 있다
- 임시 파일 정리 전략이 있다
- 다운로드는 기본 stream이다
- 큰 파일 preload를 피한다
- PDF 생성은 스트리밍 기반이다
- PDF 응답 헤더를 명시한다
- 파일 접근에 권한 검증이 있다
- 에러 메시지에 민감 정보가 없다
- 운영 로그에 파일 이벤트가 남는다
- 메모리 사용량을 모니터링한다
- 업로드/다운로드 실패 시나리오가 있다
- “돌아간다”가 아니라 “버텨낸다”를 기준으로 본다
This post is licensed under CC BY 4.0 by the author.
