TIL/[TIL]

[TIL]Spring Boot 파일 업로드, 전역 예외 처리, 인터셉터

namerong 2026. 6. 1. 15:29

1. 학습 주제

Spring Boot에서 파일 업로드를 처리하는 방식과, API 예외를 공통 구조로 응답하는 방법, 그리고 컨트롤러에 도착하기 전 요청을 가로채는 인터셉터 개념까지 이어서 학습했다.

특히 핵심은 다음 세 가지였다.

  • multipart/form-data 요청으로 파일과 일반 값을 함께 받는 방법
  • 업로드/예외/인증 같은 공통 흐름을 API 구조로 정리하는 방법
  • 컨트롤러 실행 전후에 요청을 검사하거나 시간을 재는 인터셉터 흐름

말하자면 이번 범위는 “Spring Boot에서 파일 업로드 API를 만들고, 그 앞뒤 요청 흐름까지 다루는 단계”에 가까웠다.


2. multipart/form-data란 무엇인가

multipart/form-data

파일과 일반 값을 함께 보내는 요청 형식

보통 JSON 요청은 텍스트 데이터만 보내는 데 적합하다.
하지만 파일 업로드는 다르다.

예를 들어 사용자가 이런 요청을 보낼 수 있다.

  • 이미지 파일 1개
  • 또는 파일 여러 개
  • 파일 설명(description)
  • 기타 폼 값

이럴 때는 JSON 하나로 처리하지 않고,
각 값을 여러 “파트(part)”로 나눠 보내는 multipart/form-data 형식을 사용한다.

즉 이 요청 형식은:

  • 파일 데이터도 담을 수 있고
  • 문자열 값도 같이 담을 수 있고
  • 하나의 요청 안에 여러 조각을 함께 보낼 수 있다

는 점이 중요하다.


3. MultipartFile이란 무엇인가

MultipartFile

Spring에서 업로드 파일을 표현하는 객체

클라이언트가 multipart/form-data로 파일을 보내면,
Spring은 그 파일 부분을 MultipartFile 객체로 받아 다룰 수 있게 해준다.

즉 개발자는 raw 바이너리 데이터를 직접 파싱하지 않고,
MultipartFile을 통해 다음 정보를 편하게 꺼낼 수 있다.

  • 원본 파일명
  • 파일 크기
  • Content-Type
  • 파일이 비어 있는지 여부
  • 실제 저장 메서드

즉 MultipartFile은
업로드된 파일을 Spring 안에서 다루기 위한 표준 객체라고 보면 된다.


4. @RequestParam("file") MultipartFile file의 의미

필기 내용 중 이 문장이 아주 중요하다.

@RequestParam("file") MultipartFile file

multipart 요청의 file part를 받는 방법

이 말은, 클라이언트가 보낸 multipart 요청 안에서
이름이 file인 파트를 MultipartFile 객체로 받아오겠다는 뜻이다.

예를 들면 클라이언트 쪽 폼 데이터가 이렇게 생겼다고 보면 된다.

  • file: 실제 업로드 파일
  • description: 파일 설명 문자열

그러면 컨트롤러에서는:

  • file 파트는 MultipartFile file
  • description 파트는 String description

식으로 함께 받을 수 있다.

즉 multipart 요청도 결국 “요청 파트 이름”에 맞춰 컨트롤러 파라미터로 받는 구조다.


5. 단일 파일 업로드 흐름

단일 파일 업로드 메서드는 이렇게 구성되어 있었다.

@PostMapping("/single")
public ResponseEntity<Map<String, Object>> uploadSingleFile(
        @RequestParam("file") MultipartFile file,
        @RequestParam(value="description", required = false) String description
) throws IOException

이 흐름을 순서대로 보면:

  1. 클라이언트가 multipart/form-data 요청 전송
  2. file 파트를 MultipartFile file로 받음
  3. description 문자열도 함께 받음
  4. saveFile() 메서드로 실제 저장 처리
  5. 저장 결과를 FileDTO에 담음
  6. message + file 구조의 JSON 응답 반환

즉 업로드 요청도 결국은:

  • 요청 받기
  • 검증
  • 저장
  • 결과 DTO 만들기
  • JSON 응답

흐름으로 정리된다.


6. 여러 파일 업로드 흐름

다중 파일 업로드는 이 메서드가 담당했다.

@PostMapping("/multiple")
public ResponseEntity<Map<String, Object>> uploadMultipleFiles(
        @RequestParam("files") List<MultipartFile> files,
        @RequestParam(value = "description", required = false) String description
) throws IOException

여기서 중요한 점은 다음이다.

1. List<MultipartFile>

같은 이름의 파일 파트를 여러 개 보내면
Spring이 이를 List<MultipartFile>로 묶어서 받을 수 있다.

즉 파일 한 개가 아니라 여러 개를 한 번에 처리할 수 있다.

2. 설명 값도 같이 받는다

파일과 설명 문자열을 같은 multipart 요청 안에서 함께 보낸다.

즉 이 메서드는
“파일 여러 개 + 일반 값 하나”를 같이 처리하는 업로드 API다.


7. 다중 업로드에서 롤백 비슷한 처리

다중 업로드 메서드에서 특히 중요한 부분은 예외 처리였다.

try {
    for (MultipartFile file : files) {
        uploadFiles.add(saveFile(file, "multiple", description));
    }
} catch (IOException | RuntimeException e) {
    for(FileDTO uploadedFile : uploadFiles) {
        Files.deleteIfExists(Paths.get(uploadedFile.getFilePath()));
    }
    throw e;
}

이 구조는 매우 의미가 있다.

여러 파일 중:

  • 1번째는 저장 성공
  • 2번째도 저장 성공
  • 3번째에서 실패

하면, 이미 저장된 파일만 남고 요청은 실패하는 “중간 깨짐 상태”가 될 수 있다.

이걸 막기 위해:

  • 중간에 실패하면
  • 이미 저장한 파일들을 다시 삭제하고
  • 예외를 다시 던지는 방식

을 사용했다.

즉 완전한 DB 트랜잭션은 아니지만,
파일 저장에서도 **“중간 실패 시 정리(clean-up)”**가 중요하다는 점을 보여준다.


8. 실제 저장은 어떻게 이루어지는가

실제 파일 저장은 saveFile() 메서드가 담당했다.

핵심 흐름은 다음과 같다.

  1. 파일 유효성 검사
  2. 업로드 폴더 경로 계산
  3. 디렉토리 생성
  4. 원본 파일명에서 확장자 추출
  5. UUID로 저장 파일명 생성
  6. transferTo()로 디스크에 저장
  7. FileDTO 반환

즉 업로드 API에서 중요한 건 단순히 “받기”만이 아니라:

  • 어디에 저장할지
  • 어떤 이름으로 저장할지
  • 메타정보를 어떻게 관리할지

까지 설계하는 것이다.


9. 왜 저장 파일명을 UUID로 바꾸는가

이 부분도 매우 중요하다.

String savedFileName = UUID.randomUUID().toString().replace("-", "") + extension;

원본 파일명을 그대로 저장하면 문제가 생길 수 있다.

예:

  • 같은 이름 파일이 올라오면 덮어쓰기 위험
  • 특수문자나 경로 문제
  • 이름 충돌

그래서 보통은:

  • 원본 파일명은 따로 기록만 하고
  • 실제 저장 파일명은 UUID 같은 고유값으로 바꿔 저장한다

즉:

  • originFileName = 사용자가 올린 원래 이름
  • savedFileName = 서버에 실제 저장한 안전한 이름

으로 분리하는 구조가 일반적이다.


10. 확장자 추출 방식

파일명에서 확장자를 뽑는 메서드도 있었다.

int lastDotIndex = originFileName.lastIndexOf(".");

핵심은:

  • 점(.)이 여러 개 있을 수 있으니 마지막 점을 기준으로 본다
  • 확장자가 없으면 빈 문자열 처리

즉 파일명 처리도 단순해 보이지만,
업로드에서는 꽤 자주 다루는 기본 로직이다.


11. 파일 유효성 검사

업로드 전에 validateFile()로 기본 검사를 했다.

검사 내용:

  • 파일이 비어 있는지
  • 원본 파일명이 있는지

즉 업로드 API도 무조건 저장부터 하면 안 되고,
최소한의 검증이 선행되어야 한다.

이 검증이 필요한 이유는:

  • 빈 파일 요청 방지
  • 잘못된 요청 빠르게 차단
  • 이후 저장 로직에서의 오류 예방

때문이다.


12. MultipartFile에서 자주 쓰는 값들

이번 코드에서 실제로 사용한 MultipartFile 관련 정보는 다음과 같다.

  • getOriginalFilename() : 원본 파일명
  • getSize() : 파일 크기
  • getContentType() : MIME 타입
  • isEmpty() : 빈 파일 여부
  • transferTo(...) : 실제 저장

즉 MultipartFile 하나로
업로드 파일의 메타정보와 저장 기능까지 다룰 수 있다.


13. FileDTO는 왜 필요한가

업로드 결과를 그대로 문자열로 보내는 대신 FileDTO를 사용했다.

FileDTO에는 이런 값이 들어 있다.

  • 원본 파일명
  • 저장 파일명
  • 저장 경로
  • 설명
  • 파일 크기
  • Content-Type

즉 FileDTO는
실제 파일 데이터 자체가 아니라, 저장 결과 메타정보를 응답하기 위한 객체다.

이 구조가 좋은 이유는:

  • 프론트엔드가 업로드 결과를 구조적으로 다룰 수 있고
  • 어떤 파일이 어떻게 저장됐는지 응답 형식이 분명해지기 때문이다.

14. 업로드 설정값을 application.properties에 두는 이유

설정 파일에는 이런 값들이 있었다.

  • 최대 파일 크기
  • 최대 요청 크기
  • 업로드 루트 경로

예:

  • spring.servlet.multipart.max-file-size=10MB
  • spring.servlet.multipart.max-request-size=50MB
  • file.upload-dir=uploads

이렇게 외부 설정으로 분리하는 이유는:

  • 코드 수정 없이 용량 제한을 바꿀 수 있고
  • 환경에 따라 저장 경로를 바꾸기 쉽고
  • 운영/개발 환경 설정을 분리할 수 있기 때문이다

즉 파일 업로드 기능은 코드뿐 아니라
설정값 관리도 매우 중요하다.


15. @Value로 설정값 주입받기

컨트롤러 생성자에서는 업로드 경로를 이렇게 받았다.

public FileUploadController(@Value("${file.upload-dir}") String uploadDir)

즉 application.properties에 적어둔 값을
Spring이 주입해주는 방식이다.

이 구조의 장점은:

  • 하드코딩을 줄일 수 있고
  • 파일 경로 변경이 쉬우며
  • 코드와 설정을 분리할 수 있다는 점이다.

16. 전역 예외 처리

핵심 클래스는 GlobalExceptionHandler였다.

@RestControllerAdvice

여러 컨트롤러에서 발생한 예외를 한 곳에서 처리하고,
JSON 응답으로 통일하는 역할을 한다.

즉 컨트롤러마다 try-catch를 반복하기보다
공통 예외 처리 지점을 따로 둔 것이다.


17. @ExceptionHandler가 하는 일

GlobalExceptionHandler 안에서는 예외 타입별로 메서드를 나눴다.

예:

  • MemberNotFoundException
  • InvalidMemberRequestException
  • 그 외 일반 Exception

이 방식의 의미는:

  • 어떤 예외가 발생했는지에 따라
  • 다른 HTTP 상태 코드와 다른 응답 메시지를 줄 수 있다는 점이다.

즉 예외도 “종류별로 의미 있게 응답”할 수 있다.


18. ErrorResponse 구조

실패 응답도 객체로 만들었다.

포함된 정보:

  • timestamp
  • status
  • error
  • message
  • path

이 구조가 중요한 이유는,
프론트엔드가 항상 같은 형태로 에러를 받을 수 있기 때문이다.

즉 실패 응답도 성공 응답 못지않게
일관된 JSON 구조를 갖는 것이 중요하다.


19. 인터셉터란 무엇인가

인터셉터

컨트롤러에 도착하기 전이나, 컨트롤러 실행이 끝난 뒤 요청 흐름을 가로채서 공통 작업을 수행하는 기능

즉 파일 업로드든 일반 조회 API든
컨트롤러가 실행되기 전에 공통 검사를 넣고 싶을 때 인터셉터를 사용할 수 있다.

AOP와 비슷해 보일 수 있지만,
인터셉터는 특히 웹 요청 흐름에 더 밀접하다.


20. ApiKeyInterceptor 흐름

ApiKeyInterceptor는 관리자 API 호출 전에 헤더를 검사한다.

핵심은 다음과 같다.

  1. 요청이 들어온다
  2. X-API-KEY 헤더를 읽는다
  3. 키가 맞으면 통과
  4. 키가 없거나 틀리면 401 Unauthorized JSON 응답 반환
  5. 컨트롤러 실행을 막는다

즉 인터셉터는
컨트롤러에 들어가기 전에 요청을 차단할 수 있다는 점이 중요하다.

이건 관리자 API, 인증, 권한 검사 같은 곳에서 매우 자주 쓰인다.


21. return false의 의미

ApiKeyInterceptor에서 중요한 부분은 이것이다.

return false;

이건:

  • 요청 흐름 중단
  • 이후 컨트롤러 메서드 실행 안 함

을 의미한다.

즉 인터셉터는 단순히 “구경만 하는 존재”가 아니라,
필요하면 요청을 여기서 끊을 수도 있다.


22. StopwatchInterceptor 흐름

StopwatchInterceptor는 요청 처리 시간을 측정한다.

흐름:

  1. preHandle()에서 시작 시간 저장
  2. 요청이 컨트롤러를 거쳐 처리됨
  3. afterCompletion()에서 종료 시간 계산
  4. 소요 시간을 응답 헤더 X-Elapsed-Time에 넣음
  5. 로그로도 출력

즉 이 인터셉터는
요청 성능 측정용 공통 기능을 보여준다.

실무에서도 응답 시간 측정, 로깅, 추적에 자주 쓰이는 방식이다.


23. 인터셉터와 업로드/예외 처리의 연결

파일 업로드

  • 컨트롤러가 multipart 요청을 받아 파일 저장

전역 예외 처리

  • 저장 중 오류나 잘못된 요청이 발생하면 공통 JSON 에러 응답으로 변환

인터셉터

  • 컨트롤러 진입 전 인증 헤더 검사
  • 요청 처리 시간 측정

즉 단순히 “업로드 컨트롤러 하나 만들기”가 아니라
실제 API가 동작할 때 앞에서 검사하고, 안에서 처리하고, 실패를 응답으로 바꾸는 전체 흐름을 다루기 시작한 것이다.


24. 학습 흐름 정리

24.1 파일 업로드

  • multipart/form-data 요청 형식 이해
  • MultipartFile로 파일 받기
  • @RequestParam("file") MultipartFile file 구조 이해
  • 단일 파일, 다중 파일 업로드 처리
  • UUID 기반 저장 파일명 생성
  • 업로드 메타정보를 FileDTO로 응답
  • 업로드 설정값을 properties로 분리

24.2 예외 처리

  • @RestControllerAdvice로 전역 예외 처리
  • @ExceptionHandler로 예외 타입별 응답 분리
  • ErrorResponse 구조로 실패 응답 통일

24.3 인터셉터

  • 요청 전 API Key 검사
  • 잘못된 요청을 컨트롤러 전에 차단
  • 요청 처리 시간 측정
  • 응답 헤더에 소요 시간 추가

25. 핵심 정리

  1. multipart/form-data는 파일과 일반 값을 함께 보내는 요청 형식이다.
  2. MultipartFile은 Spring에서 업로드 파일을 표현하는 객체이다.
  3. @RequestParam("file") MultipartFile file은 multipart 요청의 file 파트를 받는 대표적인 방식이다.
  4. 여러 파일은 List<MultipartFile>로 한 번에 받을 수 있다.
  5. 파일 업로드에서는 원본 파일명과 실제 저장 파일명을 분리하는 것이 안전하다.
  6. UUID를 사용하면 파일명 충돌을 줄일 수 있다.
  7. 업로드 결과는 실제 파일 데이터가 아니라 메타정보를 담은 DTO로 응답하는 것이 좋다.
  8. 다중 파일 업로드에서는 중간 실패 시 이미 저장한 파일을 정리하는 로직이 중요하다.
  9. @RestControllerAdvice와 @ExceptionHandler를 사용하면 예외 응답을 공통 구조로 관리할 수 있다.
  10. 인터셉터는 컨트롤러 실행 전후에 공통 작업을 수행하거나 요청 자체를 차단할 수 있다.
  11. ApiKeyInterceptor는 헤더 기반 요청 검사를, StopwatchInterceptor는 처리 시간 측정을 보여주는 예제이다.
  12. 이번 범위의 핵심은 “파일 업로드 API를 만들고, 그 앞뒤 요청 흐름까지 공통 기능으로 제어하는 구조”를 이해하는 것이다.