Post

운영형 Express 서버: 검증·에러·파일 처리

입력 검증과 살균 분리, 중앙화된 오류 처리, 업로드·스트리밍·PDF 운용 패턴 정리

운영형 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)
Promiseasync/awaitcatch → 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.