소개
자바 스프링 환경에서 파일 업로드, 다운로드, 삭제는 웹 개발에서 가장 자주 사용되는 기능 중 하나다.
요즘 회사에서 문서를 그렇게..만든다. 저장하고~ 다운 업로드 어쩌구
사실 파일은 파일만으로 끝나는게 아니라 보안 암호화 성능 어쩌고 고려사항이 진짜많긴함
하지만 기본 file 클래스에 대해서만 오늘 포스팅하려고한다.
사실 이런 유틸 클래스같은 경우는 이런게 있다 정도만 알아두고 필요할때 찾아쓰는게 제일이긴하다.
굳이 안찾아봐도 Files. 하고 쩜찍으면 필요한거 다나오긴함..ㅋ
1. 파일 업로드 - 가장 기본
@RestController
public class FileController {
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
// 파일이 없으면 NullPointerException 발생 방지
if (file.isEmpty()) {
return "파일이 없습니다";
}
try {
// 파일을 서버에 저장 (Files 클래스 사용)
String fileName = file.getOriginalFilename();
Path targetPath = Paths.get("uploads", fileName);
// 실제 파일 저장
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
return "업로드 성공: " + fileName;
} catch (IOException e) {
// 파일 저장 중 디스크 공간 부족, 권한 문제 등으로 오류 발생 가능
e.printStackTrace();
return "업로드 실패";
}
}
}
핵심 포인트:
- file.isEmpty(): 빈 파일 체크 필수 (NullPointerException 방지)
- Files.copy(): NIO2의 파일 복사 방법
(FILE은 호환을 위에 아직있긴하나 굳이? FILES가 편함 static으로 되어있어서 그냥 가따가 쓰면된다.) - `StandardCopyOption.REPLACE_EXISTING`: 기존 파일 덮어쓰기 옵션
2. 파일 다운로드 - 필수 기능
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
try {
// 파일 경로 생성
Path filePath = Paths.get("uploads/" + fileName);
Resource resource = new UrlResource(filePath.toUri());
// 파일 존재 여부 확인 (404 에러 방지)
if (!resource.exists()) {
return ResponseEntity.notFound().build();
}
// 다운로드용 헤더 설정
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + fileName + "\"")
.body(resource);
} catch (Exception e) {
return ResponseEntity.internalServerError().build();
}
}
핵심 포인트:
- Files.exists(): NIO2 방식의 파일 존재 확인
- Files.delete(): 더 명확한 예외 처리 (IOException 발생)
- Path 사용: 경로 처리가 더 안전하고 직관적
3. 파일 삭제 - 기본 CRUD
@DeleteMapping("/delete/{fileName}")
public String deleteFile(@PathVariable String fileName) {
try {
Path filePath = Paths.get("uploads", fileName);
// 파일 존재 여부 확인 (Files 사용)
if (!Files.exists(filePath)) {
return "파일이 존재하지 않습니다";
}
// 실제 삭제 수행
Files.delete(filePath);
return "삭제 완료";
} catch (IOException e) {
return "삭제 중 오류 발생: " + e.getMessage();
}
}
4. 파일 크기 제한 - 서버 보호
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
// 메모리 부족으로 인한 서버 다운 방지
long maxSize = 10 * 1024 * 1024; // 10MB
if (file.getSize() > maxSize) {
return "파일 크기가 너무 큽니다 (최대 10MB)";
}
// 나머지 업로드 로직...
}
application.yml 설정:
spring:
servlet:
multipart:
max-file-size: 10MB # 개별 파일 최대 크기
max-request-size: 50MB # 전체 요청 최대 크기
5. 보안 검증 - 파일명 검증
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
String fileName = file.getOriginalFilename();
// 경로 순회 공격 방지 (Path Traversal Attack)
if (fileName.contains("../") || fileName.contains("..\\")) {
return "잘못된 파일명입니다";
}
// 위험한 확장자 차단
if (fileName.endsWith(".jsp") || fileName.endsWith(".exe") ||
fileName.endsWith(".sh") || fileName.endsWith(".bat")) {
return "허용되지 않는 파일 형식입니다";
}
// 안전한 파일명으로 변경
String safeFileName = UUID.randomUUID().toString() + "_" + fileName;
// 저장 로직...
}
6. 폴더 생성 - 사전 준비
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
String uploadDir = "uploads";
Path uploadPath = Paths.get(uploadDir);
// 업로드 폴더가 없으면 생성 (Files 사용)
try {
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
// 파일 저장
Path targetPath = uploadPath.resolve(file.getOriginalFilename());
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
return "업로드 완료";
} catch (IOException e) {
return "업로드 실패: " + e.getMessage();
}
}
7. 파일 정보 조회 - 디버깅과 로깅
예전 파일 방식의 속성조회
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
// 파일 정보 로깅 (운영 환경에서 중요)
log.info("업로드된 파일 - 이름: {}, 크기: {} bytes, 타입: {}",
file.getOriginalFilename(), file.getSize(), file.getContentType());
// 개발 환경에서 디버깅용
System.out.println("파일명: " + file.getOriginalFilename());
System.out.println("파일 크기: " + file.getSize() + " bytes");
System.out.println("Content-Type: " + file.getContentType());
return "업로드 완료";
}
FILES 전체속성조회
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
Path filePath = Paths.get("uploads", file.getOriginalFilename());
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
// Files로 파일 속성 전체 조회
BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
log.info("=== 파일 상세 정보 ===");
log.info("파일명: {}", filePath.getFileName());
log.info("파일 크기: {} bytes", attrs.size());
log.info("생성 시간: {}", attrs.creationTime());
log.info("수정 시간: {}", attrs.lastModifiedTime());
log.info("액세스 시간: {}", attrs.lastAccessTime());
log.info("디렉토리 여부: {}", attrs.isDirectory());
log.info("일반 파일 여부: {}", attrs.isRegularFile());
log.info("심볼릭 링크 여부: {}", attrs.isSymbolicLink());
// 권한 정보 (POSIX 시스템에서)
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
PosixFileAttributes posixAttrs = Files.readAttributes(filePath, PosixFileAttributes.class);
log.info("파일 권한: {}", PosixFilePermissions.toString(posixAttrs.permissions()));
log.info("소유자: {}", posixAttrs.owner().getName());
log.info("그룹: {}", posixAttrs.group().getName());
}
// 개별 메서드들도 있어요
log.info("파일 존재: {}", Files.exists(filePath));
log.info("읽기 가능: {}", Files.isReadable(filePath));
log.info("쓰기 가능: {}", Files.isWritable(filePath));
log.info("실행 가능: {}", Files.isExecutable(filePath));
log.info("숨김 파일: {}", Files.isHidden(filePath));
// MIME 타입 감지
String mimeType = Files.probeContentType(filePath);
log.info("감지된 MIME 타입: {}", mimeType);
return "업로드 완료";
}
주의사항
// 잘못된 예시 - 레거시 방식 사용
@PostMapping("/upload-old")
public String oldUpload(@RequestParam("file") MultipartFile file) {
// 실수 1: File 클래스 사용 (레거시)
File targetFile = new File("uploads/" + file.getOriginalFilename());
file.transferTo(targetFile);
// 실수 2: 예외처리 누락 - IOException 발생 시 500 에러
// 실수 3: 파일 크기 체크 누락 - 대용량 파일로 서버 다운 가능
return "완료";
}
정리
꼭 확인
- 파일 존재 여부 체크 - file.isEmpty() 또는 resource.exists()
- 예외 처리 - 모든 파일 작업에 try-catch 적용
- 파일 크기 제한 - 메모리 부족으로 인한 서버 다운 방지
- 파일명 검증 - 경로 순회 공격 및 위험한 확장자 차단
- 폴더 존재 확인 - 저장 전 디렉토리 생성
자주 하는 실수
- 레거시 File 클래스 사용 (현재는 Files와 Path 권장)
- 유효성 검증 없이 파일 저장
- 예외 처리 누락으로 인한 500 에러
- 파일 크기 제한 없이 무제한 업로드 허용
- 보안 검증 없는 파일명 처리
마무리
다음 포스팅....할것..!! (공부할꺼)
- 스트리밍 처리: 대용량 파일을 메모리 효율적으로 처리하는 방법
- 파일 압축/해제: ZIP, GZIP 등을 활용한 파일 압축 처리
- 이미지 리사이징: Thumbnailator를 활용한 이미지 처리
- CSV/Excel 파일 처리: Apache POI를 활용한 엑셀 파일 읽기/쓰기
고급 단계:
- 비동기 파일 처리: @Async를 활용한 비동기 업로드/처리
- 클라우드 스토리지 연동: AWS S3, Google Cloud Storage 연동
- CDN 연동: CloudFront 등을 활용한 파일 배포 최적화
- 파일 암호화: AES를 활용한 파일 암호화/복호화
- 청킹 업로드: 대용량 파일을 분할하여 업로드하는 기법
참고 자료
공식 문서
'공부일기.. > Java' 카테고리의 다른 글
| [java] Enum 추상 메서드 활용과 이해 (0) | 2025.10.03 |
|---|---|
| [코테] JAVA 백준 알고리즘 시작하기 - 입출력 가이드 !!성능최적화!! (0) | 2025.09.29 |
| [java] static 제대로 활용하기 - 패턴과 함정 (0) | 2025.09.21 |
| [java] static 키워드 - 제약사항~ (0) | 2025.09.20 |
| [java] 자바 메모리 구조 - 김영한~자바 (0) | 2025.09.19 |