필터·인터셉터·예외 처리 편에 이은 마지막 편이다. 파일 업로드·다운로드와 페이징을 다루고, 장 전체를 정리한다.
12. 파일 업로드와 페이징
파일 업로드
텍스트 전송:
application/x-www-form-urlencoded텍스트 + 바이너리 전송:
multipart/form-data
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">업로드</button>
</form>멀티파트 옵션 (application.properties):
# 파일 하나의 최대 크기 (기본 1MB)
spring.servlet.multipart.max-file-size=1MB
# 요청 하나의 전체 합 (기본 10MB)
spring.servlet.multipart.max-request-size=10MB
# 멀티파트 처리 비활성화 (기본 true)
# spring.servlet.multipart.enabled=false업로드 경로 지정:
# 마지막에 / 포함
file.dir=경로/@Value("${file.dir}")
private String fileDir;업로드 처리 — 컨트롤러
전송된 파일은 MultipartFile로 받는다. 핵심은 업로드된 원본 파일명과 서버에 실제로 저장할 파일명을 분리하는 것이다.
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws IOException {
if (file.isEmpty()) { // 파일이 안 왔을 때 방어
return "redirect:/upload?error";
}
String originalName = file.getOriginalFilename(); // 사용자가 올린 원래 이름 (예: 사진.png)
String ext = originalName.substring(originalName.lastIndexOf("."));
String storeName = UUID.randomUUID() + ext; // 서버 저장용 이름 (예: a1b2...e9.png)
file.transferTo(new File(fileDir + storeName)); // 실제 디스크에 저장
// DB에는 보통 두 이름을 함께 저장한다 (originalName 표시용, storeName 조회용)
return "redirect:/files";
}메소드 | 의미 |
|---|---|
| 파일이 비었는지(미선택) 확인 |
| 사용자가 올린 원본 파일명 |
| 파일 크기(byte) |
| MIME 타입 (예: |
| 지정 경로로 파일 저장 |
원본 파일명을 그대로 저장하면 안 되는 이유
이름 충돌: 서로 다른 사용자가 같은 이름(
사진.png)을 올리면 덮어쓰여 버린다. → 그래서UUID등 고유한 저장명을 만든다.보안(경로 조작): 파일명에
../../etc/passwd같은 값이 들어오면 의도치 않은 경로에 쓰일 수 있다. 저장명을 직접 생성하면 이런 위험이 사라진다.여러 파일은
List<MultipartFile> files로 받아 반복 처리한다.
파일 다운로드
다운로드는 저장된 파일을 읽어 응답 바디에 실어 보내고, Content-Disposition 헤더로 "이건 첨부파일이니 저장하라"고 알려 준다. 스프링에서는 Resource와 ResponseEntity를 쓴다.
@GetMapping("/download/{storeName}")
public ResponseEntity<Resource> download(@PathVariable String storeName,
@RequestParam String originalName) throws IOException {
Resource resource = new UrlResource("file:" + fileDir + storeName);
// 한글 파일명 깨짐 방지 — UTF-8로 인코딩
String encoded = UriUtils.encode(originalName, StandardCharsets.UTF_8);
// attachment = 다운로드 / inline = 브라우저에서 바로 열기(이미지·PDF 등)
String contentDisposition = "attachment; filename=\"" + encoded + "\"";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
}헤더/요소 | 역할 |
|---|---|
| 브라우저가 파일로 저장하게 함. |
| 브라우저에서 바로 열기(이미지·PDF 미리보기) |
| 디스크 파일을 응답 바디에 담을 |
표시 이름과 저장 이름
업로드 때 분리해 둔 덕분에, 다운로드 시 서버는 저장명(storeName) 으로 파일을 찾고, 브라우저에는 원본명(originalName) 으로 내려보낼 수 있다. 사용자는 자기가 올린 원래 이름 그대로 받게 된다.
페이징 — MySQL LIMIT
MySQL은 LIMIT(과 OFFSET)으로 페이징한다. LIMIT 개수 또는 LIMIT 시작위치, 개수 형태다. (시작 위치는 0부터)
-- 6~10번째 행 (시작 위치 5에서 5개)
SELECT * FROM employees
ORDER BY first_name
LIMIT 5, 5;
-- 같은 의미 (OFFSET 문법)
SELECT * FROM employees
ORDER BY first_name
LIMIT 5 OFFSET 5;페이지 번호 → OFFSET 계산
화면의 "페이지 번호"를 SQL의 OFFSET으로 바꾸는 공식이 페이징의 핵심이다.
offset = (page - 1) × size // page는 1부터, size는 페이지당 개수
예) 페이지당 10개일 때
1페이지 → OFFSET 0, LIMIT 10 (1~10번째)
2페이지 → OFFSET 10, LIMIT 10 (11~20번째)
3페이지 → OFFSET 20, LIMIT 10 (21~30번째)전체 페이지 수는 전체 개수(COUNT(*))를 함께 조회해 계산한다.
전체 페이지 수 = ⌈ 전체 개수 ÷ size ⌉ // 올림
예) 전체 95개, size 10 → 10페이지즉 페이징 화면 하나에 보통 목록 조회(LIMIT) + 전체 개수 조회(COUNT) 두 개의 쿼리가 필요하다.
DB마다 다른 페이징 문법
페이징 SQL은 DB마다 다르다. MySQL/PostgreSQL은 LIMIT/OFFSET, Oracle은 과거 ROWNUM(현재는 OFFSET ... FETCH), SQL Server는 OFFSET ... FETCH를 쓴다. 그래서 SQL을 직접 짜면 DB에 종속된다.
JPA를 쓴다면
이런 페이징 SQL을 직접 작성하지 않아도, 다음 장 Spring Data JPA의 Pageable을 사용하면 페이징·정렬을 자동으로 처리할 수 있다.
정리
스프링은 IoC/DI로 객체를 관리하며, 의존성은 생성자 주입(+
@RequiredArgsConstructor)이 권장된다.요청은
@RequestParam/@ModelAttribute/@RequestBody로 받고, 응답은 뷰 템플릿 또는@ResponseBody(JSON)로 한다.입력 검증은 Bean Validation(
@Valid+BindingResult)으로 선언적으로 처리한다.공통 처리는 필터·인터셉터로, 예외는
@ControllerAdvice+@ExceptionHandler로 일괄 처리한다.파일 업로드는
multipart/form-data, 페이징은 MySQL의LIMIT/OFFSET또는 JPAPageable로 처리한다.