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
이 흐름을 순서대로 보면:
- 클라이언트가 multipart/form-data 요청 전송
- file 파트를 MultipartFile file로 받음
- description 문자열도 함께 받음
- saveFile() 메서드로 실제 저장 처리
- 저장 결과를 FileDTO에 담음
- 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() 메서드가 담당했다.
핵심 흐름은 다음과 같다.
- 파일 유효성 검사
- 업로드 폴더 경로 계산
- 디렉토리 생성
- 원본 파일명에서 확장자 추출
- UUID로 저장 파일명 생성
- transferTo()로 디스크에 저장
- 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 호출 전에 헤더를 검사한다.
핵심은 다음과 같다.
- 요청이 들어온다
- X-API-KEY 헤더를 읽는다
- 키가 맞으면 통과
- 키가 없거나 틀리면 401 Unauthorized JSON 응답 반환
- 컨트롤러 실행을 막는다
즉 인터셉터는
컨트롤러에 들어가기 전에 요청을 차단할 수 있다는 점이 중요하다.
이건 관리자 API, 인증, 권한 검사 같은 곳에서 매우 자주 쓰인다.
21. return false의 의미
ApiKeyInterceptor에서 중요한 부분은 이것이다.
return false;
이건:
- 요청 흐름 중단
- 이후 컨트롤러 메서드 실행 안 함
을 의미한다.
즉 인터셉터는 단순히 “구경만 하는 존재”가 아니라,
필요하면 요청을 여기서 끊을 수도 있다.
22. StopwatchInterceptor 흐름
StopwatchInterceptor는 요청 처리 시간을 측정한다.
흐름:
- preHandle()에서 시작 시간 저장
- 요청이 컨트롤러를 거쳐 처리됨
- afterCompletion()에서 종료 시간 계산
- 소요 시간을 응답 헤더 X-Elapsed-Time에 넣음
- 로그로도 출력
즉 이 인터셉터는
요청 성능 측정용 공통 기능을 보여준다.
실무에서도 응답 시간 측정, 로깅, 추적에 자주 쓰이는 방식이다.
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. 핵심 정리
- multipart/form-data는 파일과 일반 값을 함께 보내는 요청 형식이다.
- MultipartFile은 Spring에서 업로드 파일을 표현하는 객체이다.
- @RequestParam("file") MultipartFile file은 multipart 요청의 file 파트를 받는 대표적인 방식이다.
- 여러 파일은 List<MultipartFile>로 한 번에 받을 수 있다.
- 파일 업로드에서는 원본 파일명과 실제 저장 파일명을 분리하는 것이 안전하다.
- UUID를 사용하면 파일명 충돌을 줄일 수 있다.
- 업로드 결과는 실제 파일 데이터가 아니라 메타정보를 담은 DTO로 응답하는 것이 좋다.
- 다중 파일 업로드에서는 중간 실패 시 이미 저장한 파일을 정리하는 로직이 중요하다.
- @RestControllerAdvice와 @ExceptionHandler를 사용하면 예외 응답을 공통 구조로 관리할 수 있다.
- 인터셉터는 컨트롤러 실행 전후에 공통 작업을 수행하거나 요청 자체를 차단할 수 있다.
- ApiKeyInterceptor는 헤더 기반 요청 검사를, StopwatchInterceptor는 처리 시간 측정을 보여주는 예제이다.
- 이번 범위의 핵심은 “파일 업로드 API를 만들고, 그 앞뒤 요청 흐름까지 공통 기능으로 제어하는 구조”를 이해하는 것이다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]MyBatis 영속성 프레임워크, SqlSession 생명주기, XML Mapper CRUD (0) | 2026.06.04 |
|---|---|
| [TIL]Spring Boot 인터셉터 등록, REST API 응답 설계, Validation, Swagger (0) | 2026.06.02 |
| [TIL]Spring Boot JSON 처리와 예외 흐름 기초 (0) | 2026.05.29 |
| [TIL]Spring Boot 요청 매핑, 핸들러 메서드, 세션 처리, 그리고 AOP (0) | 2026.05.28 |
| [TIL]Spring Bean, DI, Life Cycle, Scope, 외부 설정값, AOP 정리 (0) | 2026.05.27 |