데이터베이스에 이미지와 같은 파일을 저장하면 성능 상 불리한 점이 많습니다.
따라서 실제 바이너리 데이터는 별도의 공간에 저장하고, 데이터베이스에는 바이너리 데이터에 대한 메타 정보(파일명, 크기, 유형 등)만 저장하는 것이 좋습니다.
전체 동작 흐름
서비스에서 파일 업로드시 발생흐름은Client -> Contoller -> BinaryContentService.create()BinaryContentService.create()내 DB에 bytes를 제외한 이름,size,contentType등의 메타 데이터 저장binaryContentStorage.put()으로 로컬 디스크에 저장. {root}/{UUID} 경로에 ...
다운로드 흐름
Client -> GET /api/binaryContents{binaryContentId}/download URI로 요청BinaryContentController -> binaryContentStorage.download() 반환
BinaryContent
id : 550e8400-e29b-41d4-a716-446655440000
fileName : profile.png
contentType : image/png
size : 24500
이런 메타데이터가 DB에 저장이 된다.
로컬 디스크 저장 위치는 storage/550e8400-e29b-41d4-a716-446655440000
이 파일은 실제 PNG 바이너리 데이터가 들어있는 파일이다.
@Override
public ResponseEntity<Resource> download(BinaryContentDto dto) {
InputStream stream = get(dto.id());
Resource resource = new InputStreamResource(stream);
return ResponseEntity.ok()
.header(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + dto.fileName() + "\""
)
.contentType(MediaType.parseMediaType(dto.contentType()))
.body(resource);
}
다운로드 요청을 하게 되면
GET /api/binaryContents/550e8400-e29b-41d4-a716-446655440000/download
이렇게 들어오고, Controller 내부에서
BinaryContentDto dto = new BinaryContentDto(
550e8400-e29b-41d4-a716-446655440000,
"profile.png",
24500L,
"image/png",
null
);
이렇게 생성되고, 이를
binaryContentStorage.download(binaryContentDto);
를 호출한 결과를 반환한다.
BinaryContentStorage의 구현체 내 download() 실행
@Override
public InputStream get(UUID id) {
Path path = resolvePath(id);
try {
return Files.newInputStream(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ResponseEntity<Resource> download(BinaryContentDto dto) {
InputStream stream = get(dto.id());
Resource resource = new InputStreamResource(stream);
return ResponseEntity.ok()
.header(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + dto.fileName() + "\""
)
.contentType(MediaType.parseMediaType(dto.contentType()))
.body(resource);
}
- get() 함수내 resolvePath(id)에는
private Path resolvePath(UUID id){
return root.resolve(id.toString());
}
"550e8400-e29b-41d4-a716-446655440000"라는 문자열
root는 사용자가 설정한 "storage" 라는 폴더 경로.
storage.resolve("550e8400-e29b-41d4-a716-446655440000") -> storage/550e8400-e29b-41d4-a716-446655440000
가 resolvePath의 반환값이 된다.
따라서 get()내 path = storage/550e8400-e29b-41d4-a716-446655440000가 될것이고,
Files.newInputStream(path)를 통해 "storage/550e8400-e29b-41d4-a716-446655440000" 파일을 열고 읽을 수 있는 스트림을 만들어줘 -> 해당 파일이 실제로 존재하면 파일을 읽을 수 있는 InputStream 객체가 생성된다.
따라서 download()내 stream 객체에는 파일을 읽을 수 있는 InputStream 객체가 담긴다.
-> storage/550e8400-e29b-41d4-a716-446655440000 파일을 읽는 스트림
그 다음 stream을 Spring이 HTTP 응답으로 다룰 수 있게 포장한다.
다운 흐름
Local Disk
storage/{UUID}
│
▼
Files.newInputStream()
│
▼
InputStream
│
▼
InputStreamResource
│
▼
ResponseEntity<Resource>
│
▼
HTTP Response
│
▼
Browser Download
'코드잇 스프린트 > 실습' 카테고리의 다른 글
| 디스코드 프로젝트 실습: JPA Public 채널생성시 ReadStatus에 채널id가 null값으로 들어간다. (0) | 2026.03.10 |
|---|---|
| MapStruct는 <T> 제네릭 타입 메서드 자체는 구현코드 생성 못한다. (0) | 2026.03.10 |
| MapStruct 라이브러리를 통해 Entity를 DTO로 자동 변환 (0) | 2026.03.09 |
| Entity를 Controller까지 그대로 노출 할때 발생할 수 있는 문제점 (0) | 2026.03.09 |
| 디스코드 프로젝트 실습: Entity 정의하기(JPA 어노테이션,cascade,고아객체) (0) | 2026.03.05 |