2025-04-08T19:56:24
This commit is contained in:
336
docs/22_file upload.md
Normal file
336
docs/22_file upload.md
Normal file
@@ -0,0 +1,336 @@
|
||||
물론이죠! 아래는 **스프링 부트(Spring Boot)**에서의 **파일 업로드**, **멀티 파일 업로드**, **파일 다운로드**를 다루는 실무 중심의 설명 글입니다.
|
||||
|
||||
---
|
||||
|
||||
# ✅ 스프링 부트에서 파일 업로드 및 다운로드 구현하기
|
||||
|
||||
현대 웹 애플리케이션에서는 사용자가 이미지를 업로드하거나 문서를 다운로드하는 기능이 필수입니다. 스프링 부트는 이러한 기능을 손쉽게 구현할 수 있도록 다양한 API를 제공합니다.
|
||||
이 글에서는 다음을 다룹니다:
|
||||
|
||||
- 단일 파일 업로드
|
||||
- 다중(멀티) 파일 업로드
|
||||
- 파일 다운로드
|
||||
- 실무에서 주의할 점
|
||||
|
||||
---
|
||||
|
||||
## 📁 1. 의존성 설정 (`build.gradle` 또는 `pom.xml`)
|
||||
스프링 부트에서는 기본적으로 `spring-boot-starter-web`만으로도 파일 업로드/다운로드가 가능합니다.
|
||||
|
||||
**Gradle 예시:**
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 2. 단일 파일 업로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/files")
|
||||
public class FileUploadController {
|
||||
|
||||
private final Path uploadDir = Paths.get("uploads");
|
||||
|
||||
public FileUploadController() throws IOException {
|
||||
Files.createDirectories(uploadDir);
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) return ResponseEntity.badRequest().body("파일이 비어 있습니다");
|
||||
|
||||
Path targetPath = uploadDir.resolve(file.getOriginalFilename());
|
||||
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return ResponseEntity.ok("업로드 성공: " + file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 설명
|
||||
- `@RequestParam("file")`: HTML `<input type="file">`의 name과 일치해야 합니다.
|
||||
- `MultipartFile`: 업로드된 파일을 표현하는 객체입니다.
|
||||
- `Files.copy(...)`: 서버의 특정 디렉토리에 저장.
|
||||
|
||||
---
|
||||
|
||||
## 📤 3. 멀티 파일 업로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@PostMapping("/upload-multiple")
|
||||
public ResponseEntity<String> uploadMultiple(@RequestParam("files") List<MultipartFile> files) throws IOException {
|
||||
if (files.isEmpty()) return ResponseEntity.badRequest().body("업로드된 파일이 없습니다.");
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (!file.isEmpty()) {
|
||||
Path targetPath = uploadDir.resolve(file.getOriginalFilename());
|
||||
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("멀티 파일 업로드 성공");
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 HTML에서 보내는 방법
|
||||
```html
|
||||
<form method="post" enctype="multipart/form-data" action="/files/upload-multiple">
|
||||
<input type="file" name="files" multiple>
|
||||
<button type="submit">업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 4. 파일 다운로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@GetMapping("/download/{filename}")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) throws IOException {
|
||||
Path filePath = uploadDir.resolve(filename);
|
||||
if (!Files.exists(filePath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.body(resource);
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 주요 포인트
|
||||
- `UrlResource`: 파일을 다운로드 가능한 형태로 변환
|
||||
- `Content-Disposition`: 파일이 브라우저에 표시되지 않고 다운로드되도록 설정
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 실무에서 주의할 점
|
||||
|
||||
### 1. 파일 이름 중복 처리
|
||||
- `UUID`를 붙이거나 시간 기반으로 처리해야 파일 덮어쓰기 방지 가능
|
||||
```java
|
||||
String safeName = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||
```
|
||||
|
||||
### 2. 저장 경로 보안
|
||||
- 사용자 입력을 통한 경로 조작 공격 방지 (`../` 등)
|
||||
- 반드시 경로를 정규화 (`normalize()`)하고 업로드 디렉토리 밖으로 벗어나지 않도록 체크
|
||||
|
||||
```java
|
||||
Path normalized = uploadDir.resolve(file.getOriginalFilename()).normalize();
|
||||
if (!normalized.startsWith(uploadDir)) {
|
||||
throw new SecurityException("잘못된 경로입니다");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 용량 제한 설정
|
||||
**application.properties**
|
||||
```properties
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=20MB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 마무리
|
||||
|
||||
| 기능 | 구현 여부 |
|
||||
|-----------------|-----------|
|
||||
| 단일 파일 업로드 | ✅ |
|
||||
| 멀티 파일 업로드 | ✅ |
|
||||
| 파일 다운로드 | ✅ |
|
||||
|
||||
스프링 부트에서는 `MultipartFile`, `Resource`, `ResponseEntity` 등의 기본 컴포넌트를 통해 파일 업로드/다운로드 기능을 쉽게 구현할 수 있습니다. 다만 실무에서는 보안, 파일 크기 제한, 파일명 중복 등의 다양한 상황을 고려해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
필요하다면 `AWS S3`, `FTP`, 혹은 DB BLOB 저장 방식도 활용 가능하며, 이 글은 **로컬 저장소 기반**의 기본 구현을 중심으로 설명했습니다.
|
||||
더 확장하고 싶은 부분이 있으면 언제든지 말해주세요!
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
좋습니다! 이번엔 **파일을 데이터베이스에 BLOB(Binary Large Object)** 형태로 저장하고, 다시 꺼내서 다운로드하는 방식을 다룬 글을 정리해드릴게요.
|
||||
|
||||
---
|
||||
|
||||
# 🗃️ 스프링 부트에서 파일을 DB(BLOB)로 업로드 및 다운로드하기
|
||||
|
||||
웹 애플리케이션에서 파일을 **데이터베이스(BLOB 컬럼)**에 직접 저장하는 방식은 파일 보안이나 백업, 이식성 측면에서 유리할 수 있습니다. 이 글에서는 다음을 다룹니다:
|
||||
|
||||
- DB에 파일 업로드 (BLOB 저장)
|
||||
- DB에서 파일 다운로드
|
||||
- 실제 구현 예시 (Spring Boot + JPA + H2/MySQL)
|
||||
|
||||
---
|
||||
|
||||
## 📦 1. 테이블 설계 (DDL 예시)
|
||||
|
||||
### ✅ MySQL 예시
|
||||
```sql
|
||||
CREATE TABLE file_data (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_name VARCHAR(255),
|
||||
content_type VARCHAR(100),
|
||||
data LONGBLOB
|
||||
);
|
||||
```
|
||||
|
||||
> `data` 컬럼이 실제 파일의 바이트 데이터를 저장하는 BLOB 필드입니다.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 2. 엔터티 클래스 정의
|
||||
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "file_data")
|
||||
public class FileData {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String fileName;
|
||||
|
||||
private String contentType;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "LONGBLOB")
|
||||
private byte[] data;
|
||||
|
||||
// 생성자, Getter/Setter 생략
|
||||
}
|
||||
```
|
||||
|
||||
- `@Lob` 어노테이션은 `byte[]` 데이터를 BLOB으로 매핑합니다.
|
||||
- `contentType`은 다운로드할 때 MIME 타입을 알려주기 위해 사용합니다.
|
||||
|
||||
---
|
||||
|
||||
## 📂 3. 리포지토리 인터페이스
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface FileDataRepository extends JpaRepository<FileData, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 4. 파일 업로드 (DB 저장)
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/files")
|
||||
public class FileDataController {
|
||||
|
||||
private final FileDataRepository fileDataRepository;
|
||||
|
||||
public FileDataController(FileDataRepository repo) {
|
||||
this.fileDataRepository = repo;
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) return ResponseEntity.badRequest().body("빈 파일입니다");
|
||||
|
||||
FileData fileData = new FileData();
|
||||
fileData.setFileName(file.getOriginalFilename());
|
||||
fileData.setContentType(file.getContentType());
|
||||
fileData.setData(file.getBytes());
|
||||
|
||||
fileDataRepository.save(fileData);
|
||||
|
||||
return ResponseEntity.ok("파일 업로드 성공: ID = " + fileData.getId());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 5. 파일 다운로드 (DB에서 BLOB 조회)
|
||||
|
||||
```java
|
||||
@GetMapping("/download/{id}")
|
||||
public ResponseEntity<byte[]> downloadFile(@PathVariable Long id) {
|
||||
return fileDataRepository.findById(id)
|
||||
.map(file -> ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFileName() + "\"")
|
||||
.body(file.getData()))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 6. Postman 또는 HTML 테스트 예시
|
||||
|
||||
### ✅ HTML 업로드 폼
|
||||
```html
|
||||
<form method="post" enctype="multipart/form-data" action="/files/upload">
|
||||
<input type="file" name="file">
|
||||
<button type="submit">업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### ✅ 다운로드 링크 예시
|
||||
```html
|
||||
<a href="/files/download/1">파일 다운로드</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 실무에서 주의할 점
|
||||
|
||||
### 1. 성능 문제
|
||||
- BLOB 방식은 대용량 파일 처리 시 성능 저하가 발생할 수 있음
|
||||
→ **작은 이미지나 문서 위주에 적합**
|
||||
|
||||
### 2. DB 백업 크기 증가
|
||||
- 파일이 많아지면 DB 백업/복원 시간이 길어질 수 있음
|
||||
|
||||
### 3. 파일 크기 제한 설정
|
||||
```properties
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=20MB
|
||||
```
|
||||
|
||||
### 4. 파일 다운로드 속도
|
||||
- 서버가 파일을 전부 로드해야 하므로, 큰 파일은 **스트리밍 방식 처리**를 고려할 것
|
||||
|
||||
---
|
||||
|
||||
## ✅ 정리
|
||||
|
||||
| 기능 | 구현 방식 |
|
||||
|------------------|-----------|
|
||||
| 파일 업로드 | `MultipartFile` → BLOB 저장 |
|
||||
| 파일 다운로드 | `byte[]` → HTTP 응답 |
|
||||
| 확장성/보안 | 파일 시스템보다 안전하나 대용량엔 부적합 가능 |
|
||||
|
||||
---
|
||||
|
||||
## 🔚 마무리
|
||||
|
||||
파일을 **데이터베이스 BLOB에 저장하는 방식**은 외부 파일 시스템 없이 관리가 간편하고, 트랜잭션 처리와 보안이 뛰어납니다. 단, 대용량 파일이 많을 경우에는 성능과 저장소 이슈를 고려하여 **S3, 파일 시스템 저장 방식**과 함께 혼합하여 쓰는 것이 좋습니다.
|
||||
|
||||
---
|
||||
|
||||
필요하다면 **썸네일 저장, 이미지 미리보기 API, 스트리밍 다운로드**까지 확장도 가능합니다. 원하시면 다음 단계도 이어서 도와드릴게요!
|
||||
Reference in New Issue
Block a user