Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- table status
- 스터디2기
- 오블완
- oh-my-zsh
- 오일러 경로
- zsh
- Pager
- 데이크스트라
- 분할정복 방법
- cs
- 인프런
- 순차탐색
- 이진탐색
- spring boot
- 터미널
- 맥
- mycli
- 티스토리챌린지
- 인프런워밍업클럽
- MySQL
- Less
- mysql 표 출력
- 알고리즘
- mysql 표
- zsh theme
- 욕심쟁이 방법
- VI
- 네트워킹데이
- CS스터디
- 동적 프로그래밍 방법
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 08. 파일 업로드 구현하기 (feat. 프로필 이미지 업로드) 본문
안녕하세요 .ᐟ
오늘은 Spring Boot에서 파일 업로드를 어떻게 안전하게 구현하는지 알아보겠습니다.
파일 업로드는 프로필 이미지, 게시글 첨부파일 등 다양한 곳에서 사용되는 필수기능입니다.
구현할 기능:
- 프로필 이미지 업로드 API
- 파일 검증 (크기, 확장자, 보안)
- UUID 기반 파일명 생성
1. 파일 업로드 설정
application.properties
# 파일 업로드 설정
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB # 개별 파일 최대 크기
spring.servlet.multipart.max-request-size=50MB # 전체 요청 최대 크기
spring.servlet.multipart.file-size-threshold=2KB # 메모리 임계값
# 파일 저장 경로
file.upload-dir=./uploads
file.profile-dir=${file.upload-dir}/profiles
file.post-dir=${file.upload-dir}/posts
설정 의미
- max-file-size: 너무 큰 파일로 서버 과부하 방지
- max-request-size: 여러 파일을 동시에 업로드할 때의 전체 크기 제한
- file-size-threshold: 작은 파일은 메모리에서 처리하여 성능 향상
- 상대 경로(./uploads): 프로젝트 루트 기준, 개발/배포 환경 모두 대응
2. 파일 저장 유틸리티 구현
FileStorageUtil.java
- 파일 저장의 핵심 로직을 담당하는 유틸리티 클래스입니다.
- UUID 파일명 생성
- 파일명 충돌 방지
- OS별 인코딩 이슈 해소 (한글 파일명)
- 파일명으로 개인정보 노출 방지
String storedFilename = UUID.randomUUID().toString() + "." + fileExtension;
// 예: 550e8400-e29b-41d4-a716-446655440000.jpg
- 화이트리스트 방식으로 확장자 검증 (허용된 이미지 형식만 업로드)
- 실행 파일이나 스크립트 업로드 차단
private static final List<String> ALLOWED_EXTENSIONS =
Arrays.asList("jpg", "jpeg", "png", "gif", "webp", "heic");
- 경로 traversal 공격 방어
- ../../etc/passwd 같은 상위 디렉토리 접근 시도 차단
if (originalFilename.contains("..")) {
throw new FileStorageException("파일명에 부적절한 경로가 포함되어 있습니다");
}
- 파일 크기 제한
- 서버 디스크 용량 보호
- DoS(서비스 거부) 공격 방지
if (file.getSize() > MAX_FILE_SIZE) {
throw new FileStorageException("파일 크기가 너무 큽니다");
}
3. 통일된 응답 구조
- 모든 API가 일관된 응답 형식을 가지도록 래퍼 클래스를 만들었습니다.
ApiResponse.java
- 일관성
- 프론트엔드에서 항상 같은 구조로 응답 받음
- API 명세서의 가독성이 높아짐
- 타입 안정성
- 컴파일 타임에 타입체크
- 제네릭으로 타입 보장
- 에러 처리 통일
- 성공/실패를 success 필드의 boolean 으로 구분
- 성공
{
"success": true,
"message": "프로필 이미지가 업로드되었습니다.",
"data": {
"id": 1,
"nickname": "길동이",
"profileImage": "550e8400-e29b-41d4-a716-446655440000.jpg"
}
}
- 실패
{
"success": false,
"message": "파일 크기가 너무 큽니다. 최대 10MB까지 업로드 가능합니다.",
"data": null
}
4. 서비스 계층 구현
FileService.java
- 단일 책임 원칙
- FileService: 파일 관련 비즈니스 로직
- FileStorageUtil: 실제 파일 I/O 처리
- 추후 스토리지 변경 시 FileStorageUtil 만 교체
- 테스트 시 Mock 으로 대체 가능
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FileService {
private final FileStorageUtil fileStorageUtil;
public String uploadProfileImage(MultipartFile file) {
return fileStorageUtil.storeProfileImage(file);
}
public String uploadPostImage(MultipartFile file) {
return fileStorageUtil.storePostImage(file);
}
public void deleteFile(String filename, boolean isProfile) {
fileStorageUtil.deleteFile(filename, isProfile);
}
}
FileController.java (임시 파일 업로드)
- 게시글 작성 시 에디터에서 이미지를 먼저 업로드 하게 함
- 사용자가 에디터에 이미지 드래그앤드롭
- POST /files/temp로 즉시 서버에 업로드 → 파일명 반환
- 나중에 게시글 작성 시 파일명을 포함해서 전송
@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@PostMapping("/temp")
public ResponseEntity<ApiResponse<Map<String, String>>> uploadTempFile(
@RequestParam("file") MultipartFile file) {
String filename = fileService.uploadPostImage(file);
Map<String, String> data = new HashMap<>();
data.put("filename", filename);
data.put("url", "/uploads/posts/" + filename);
return ResponseEntity.ok(ApiResponse.success(
data,
"파일이 업로드되었습니다."
));
}
}
File upload 확인
uploads/
├── profiles/ # 프로필 이미지
└── posts/ # 게시글 이미지
5. 테스트
HTTP Client Test
### 1. 프로필 이미지 업로드
POST http://localhost:8080/users/1/profile-image
Content-Type: multipart/form-data; boundary=boundary
--boundary
Content-Disposition: form-data; name="file"; filename="profile.png"
Content-Type: image/png
< /Users/cho/Pictures/profile.png
--boundary--
### 2. 사용자 조회 (이미지 확인)
GET http://localhost:8080/users/1
느낀 점
- MultipartFile 처리 흐름: Spring이 multipart/form-data를 어떻게 파싱하는지 이해
- 파일 보안의 중요성: 확장자 검증, 경로 traversal 방어 필수
- UUID 활용: 파일명 충돌 방지 및 보안성 향상
- static 메서드 패턴: Factory Pattern과 제네릭을 조합한 편의 메서드
- ApiResponse 적용: 일관된 응답 구조로 프론트엔드 개발 편의성 증가

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| 초보 백엔드가 무중단 배포와 CI/CD를 직접 구축하면서 만난 에러들 (0) | 2025.12.04 |
|---|---|
| [Spring Boot] 게시판 - 09. 게시글 이미지 업로드 구현하기 (feat. 이미지 여러개 업로드 & 순서 관리) (0) | 2025.11.30 |
| [Spring Boot] 게시판 - 07. 통합 테스트 작성하기 (MockMvc & Fixture 패턴) (1) | 2025.11.27 |
| [Spring Boot] 게시판 - 06. 단위 테스트 작성하기 (Mockito & AssertJ) (0) | 2025.11.24 |
| [Spring Boot] 게시판 - 05. Validation 검증 (입력값 유효성 검사) (1) | 2025.11.23 |