[준비]
IDE : 인텔리제이
java : java version "21.0.2" 2024-01-16 LTS
DB : MySQL 8
View : thymeleaf (거의 사용 안 함)
기타 : PostMan , 스프링 이니셜라이저 사용 (Dependecy는 아래 build.gradle 코드 확인)
build : gradle
[project 구조]

[ application.yml ]
server:
port: 8080
spring:
datasource:
url: "jdbc:mysql://localhost/files"
username: "root"
password: "각자 root 사용자 비밀번호"
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: create # 처음에는 create 나중에는 update로 변경
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
# view location 뷰리졸버 설정
view:
spring:
thymeleaf:
suffix: .html
# encoding korean
encoding:
spring:
http:
encoding:
charset: UTF-8
servlet:
filter:
character:
encoding:
enabled: true
force-request: true
force-response: true
encoding: UTF-8
force: true
[build.gradle]
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.fileupload'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '21'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 파일 다운로드를 위해 추가한 의존성
implementation 'commons-io:commons-io:2.7'
}
tasks.named('test') {
useJUnitPlatform()
}
<다중 파일 업로드 + 다운로드>
- Files.java ( 엔티티)
package com.fileupload.part.domain;
import com.fileupload.part.dto.FilesDto;
import jakarta.persistence.*;
import jakarta.servlet.http.Part;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.List;
@ToString
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Files {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "path")
private String path;
@Column(name = "domain_id")
private Long domainId; //todo 여러 테이블에서 이 첨부파일 테이블을 함께 사용할 경우 비참조로 임의로 넣어줘야함.
@Column(name = "domain")
private String domain; //todo 어떤 게시물 종류에 첨부파일들인지
@Column(name = "deleteTF")
@ColumnDefault("0") // default
private boolean deleteTF; // todo 1, 0 --> 0이면 미삭제 // 1이면 삭제 default 0임.
@CreationTimestamp
@Column(name = "upload_date")
private LocalDateTime uploadDate; //todo 저장된 날짜.
@Column(name = "size")
private Long size;
@Builder
public Files(String name, String path, Long domainId, String domain, Long size) {
this.name = name;
this.path = path;
this.domainId = domainId;
this.domain = domain;
this.size = size;
}
//todo dto를 entity로 바꿔주는 메소드
public static Files toEntity(FilesDto filesDto){
return Files.builder()
.name(filesDto.getName())
.path(filesDto.getPath())
.domainId(filesDto.getDomainId())
.domain(filesDto.getDomain())
.size(filesDto.getSize())
.build();
}
}
- FilesDto.java
package com.fileupload.part.dto;
import jakarta.servlet.http.Part;
import lombok.Getter;
import lombok.NoArgsConstructor;
//todo requestDto 안 쓰는 파일
@Getter
@NoArgsConstructor
public class FilesDto {
String name;
String path;
Long domainId;
String domain;
Long size;
}
- FilesResponseDto.java
package com.fileupload.part.dto;
import com.fileupload.part.domain.Files;
import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@NoArgsConstructor
public class FilesResponseDto {
private Long id;
private String name;
private String path;
private Long domainId;
private String domain;
private boolean deleteTF;
private LocalDateTime uploadDate;
private Long size;
@Builder
public FilesResponseDto(Long id, String name, String path, Long domainId, String domain, boolean deleteTF, LocalDateTime uploadDate, Long size) {
this.id = id;
this.name = name;
this.path = path;
this.domainId = domainId;
this.domain = domain;
this.deleteTF = deleteTF;
this.uploadDate = uploadDate;
this.size = size;
}
public FilesResponseDto toRespDto(Files files) {
return FilesResponseDto.builder()
.id(files.getId())
.name(files.getName())
.path(files.getPath())
.domainId(files.getDomainId())
.domain(files.getDomain())
.deleteTF(files.isDeleteTF())
.uploadDate(files.getUploadDate())
.size(files.getSize())
.build();
}
}
- FileController.java
package com.fileupload.part.controller;
import com.fileupload.part.dto.FilesDto;
import com.fileupload.part.dto.FilesResponseDto;
import com.fileupload.part.service.FileService;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.Blob;
import java.util.List;
//todo 파일업로드할 때 경로, 처리 사이즈 단위, ,저장 가능 파일 사이즈 , 요청할 수 파일 사이즈 최대
@MultipartConfig(
//location = "C:/User/fakeuser/file" //todo 다른 os에서는 디폴트 경로가 c가 아닐 수도 있으므로 비권장
fileSizeThreshold = 1024*1024, //todo 아마 1mb 일 거임 검색 ㄱㄱ
maxFileSize = 1024*1024*50, //todo 50mb
maxRequestSize = 1024*1024*50 //todo 50mb
)
@Slf4j
@RestController
@RequestMapping("/api/file")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping("upload")
public List<FilesResponseDto> fileUpload(HttpServletRequest request) throws ServletException, IOException {
//todo
// 파일업로드하는 서비스 메소드 실행
return fileService.fileUpload(request);
}
@GetMapping("download")
public void fileDownload(@RequestParam("domain") String domain,
@RequestParam("domainId") String domainId,
@RequestParam("fileId") String fileId,
HttpServletResponse response) throws IOException {
//todo
//파일다운로드 하는 서비스 메소드 실행
FilesResponseDto filesInfo = fileService.getFile(domain.toUpperCase(), Long.parseLong(domainId), Long.parseLong(fileId));
try{
byte[] files = FileUtils.readFileToByteArray(new File(filesInfo.getPath()));
Blob blob = new javax.sql.rowset.serial.SerialBlob(files);
response.setContentType("application/octet-stream");
response.setContentLength(files.length);
response.setHeader("Content-Disposition","attachment; fileName=\""+ URLEncoder.encode(filesInfo.getName(), StandardCharsets.UTF_8)+"\";");
response.setHeader("Content-Transfer-Encoding","binary");
log.info(response.toString());
response.getOutputStream().write(files);
response.getOutputStream().flush();
} catch (Exception e) {
log.error(e.getMessage());
e.getStackTrace();
}
response.getOutputStream().close();
}
}
- FileService.java(인터페이스)
package com.fileupload.part.service;
import com.fileupload.part.dto.FilesDto;
import com.fileupload.part.dto.FilesResponseDto;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;
public interface FileService {
public List<FilesResponseDto> fileUpload(HttpServletRequest request) throws ServletException, IOException;
// 전체 파일 리스트 출력
public List<FilesResponseDto> getAllFile();
// 파일 다운로드를 위해 파일 정보 가지고오는 메소드
public FilesResponseDto getFile(String domain, Long domainId, Long fileId);
}
- FileServiceImpl.java (구현체)
package com.fileupload.part.service;
import com.fileupload.part.domain.Files;
import com.fileupload.part.domain.FilesRepository;
import com.fileupload.part.dto.FilesDto;
import com.fileupload.part.dto.FilesResponseDto;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
import jakarta.transaction.Transactional;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Service
public class FileServiceImpl implements FileService{
private final FilesRepository filesRepository;
public FileServiceImpl(FilesRepository filesRepository) {
this.filesRepository = filesRepository;
}
//todo 다중, 단일 둘다 사용가능한 파일 업로드
@Transactional
@Override
public List<FilesResponseDto> fileUpload(HttpServletRequest request) throws ServletException, IOException {
List<FilesResponseDto> filesResponseDtoList = new ArrayList<>(); //todo 반환시켜줄 respDto 리스트 빈 객체
Collection<Part> fileCollection = request.getParts(); //todo 폼의 모든 데이터들을 가지고 오겠다.
for(Part filePart : fileCollection){
if(!filePart.getName().equals("file")) continue;
String fileName = filePart.getSubmittedFileName(); //todo 확장자명을 포함한 파일이름
long fileSize = filePart.getSize();
InputStream fis = filePart.getInputStream();
// String realPath = request.getServletContext().getRealPath() //todo -> 자바가 지정한 파일이 저장될 경로 : 권장
// String realPath = request.getServletContext().getRealPath("\\FileDownloads") //todo -> 자바가 지정한 파일이 저장될 경로 + "\\마지막 임의 경로 "
String myPath = "C:\\Users\\wnsqh\\Desktop\\FileDownloads"; //todo 내가 지정한 임의 경로
File folder = new File(myPath);
if (!folder.exists()){
try {
folder.mkdir(); //todo 파일을 저장할 경로에 디렉터리가 존재하지 않으면 생성하겠다.
}catch (Exception e) {
e.getStackTrace();
}
}
//todo 실제 이 파일의 파일명을 포함한 전체 경로가 db에 저장되어야 한다.
String filePath = myPath + "\\" + UUID.randomUUID() + "-" +fileName; //todo 파일 중복으로 인한 덮어쓰기를 예방하기 위한 uuid 붙여줌.
FileOutputStream fos = new FileOutputStream(filePath);
byte[] buf = new byte[1024]; //todo 파일 처리할 byte 단위
int size;
//todo 단위로 연산한 파일 크기 읽어서 size 변수에 저장
while ( (size = fis.read(buf)) != -1) {
fos.write(buf, 0, size);
}
Files files = Files.builder()
.name(fileName)
.path(filePath)
.domainId(1L)
.domain("NOTICE")
.size(fileSize)
.build();
files = filesRepository.save(files); //todo INSERT 쿼리문 DB에 전달 실행.
FilesResponseDto filesResponseDto = new FilesResponseDto().toRespDto(files);
filesResponseDtoList.add(filesResponseDto); //todo 마지막에 반환시켜줄 리스트에 하나씩 쌓는다 어떤 파일을 업로드 했는지 정보를
fos.close();
fis.close();
} // end of for
return filesResponseDtoList;
}
// 파일 리스트 전체 출력
@Override
public List<FilesResponseDto> getAllFile() {
if ( filesRepository.findAll().isEmpty() ) {
throw new IllegalArgumentException("not existed any file");
}
FilesResponseDto filesResponseDto = new FilesResponseDto();
return filesRepository.findAll().stream().map(files -> new FilesResponseDto().toRespDto(files))
.collect(Collectors.toList());
}
@Override
public FilesResponseDto getFile(String domain, Long domainId, Long fileId) {
Files findFiles = filesRepository.findByDomainAndDomainIdAndId(domain, domainId, fileId).orElseThrow(() -> new IllegalArgumentException("not existed file"));
log.info("findFiles -> {}", findFiles);
return new FilesResponseDto().toRespDto(findFiles);
}
}
- FileRepository.java
package com.fileupload.part.domain;
import org.springframework.data.jpa.repository.JpaRepository;
import java.io.File;
import java.util.List;
import java.util.Optional;
public interface FilesRepository extends JpaRepository<Files, Long> {
// domain과 domainId로 파일 정보 가지고 오기
Optional<Files> findByDomainAndDomainIdAndId(String domain, Long domainId, Long fileId);
}
- IndexController.java (그냥 파일리스트 출력하고 다운로드 버튼 클릭 시 다운로드 되는지 확인하기 위해 생성)
package com.fileupload.part;
import com.fileupload.part.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequiredArgsConstructor
@RequestMapping("api/filelist")
public class IndexController {
private final FileService fileService;
@GetMapping
public ModelAndView fileListPage(ModelAndView mav){
mav.setViewName("index");
mav.addObject("fileList", fileService.getAllFile());
return mav;
}
}
- index.html (파일리스트 출력 + 다운로드 진행할 view)
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.w3.org/1999/html">
<head>
<meta charset="UTF-8">
<title>FileList & FileDownLoad</title>
</head>
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<body>
<div>
<h1>
파일 다운로드 리스트 페이지
</h1>
</div>
<div>
<ul>
<li th:each = "file: ${fileList}">
<span th:text="${file.name}"></span>
<button
type="button"
th:value="${file.id}"
onclick="fileDownload(this.value)"
> 파일 다운로드
</button>
</li>
</ul>
</div>
<!--
미리 등록되어 있는 파일들은 service 에서 domain: "NOTICE", domainId: "1"로 임의 지정되어 저장되어 있는 형태이며,
따라서 다운로드 url도 domain: "NOTICE", domainId: "1" 로 고정되어 있다.
실제로 사용할 때에는 게시글 종류에 맞는 식별자가 domain에 그 게시물 종류에서 원하는 게시물의 pk가 domainId에 들어가면 된다.
-->
<script type="text/javascript">
function fileDownload(fileId) {
const url = "/api/file/download?domain=notice&domainId=1&fileId="+fileId;
window.location.href=url;
}
</script>
</body>
</html>
- 실행
-- #1 postman 으로 파일 업로드


-- #2 DB INSERT 확인

-- #3 실제 파일이 약속된 경로에 업로드 되었는지 확인

-- #4 리스트 출력 및 다운로드 동작 확인을 위한 VIEW 페이지 접근

-- #5 다운로드 확인 완료

- 이렇게 해서 Part 데이터 타입을 이용한 다중 파일 업로드 다운로드 api를 완성하였다.
- Part 데이터 타입 대신 MultiPartFile 데이터 타입을 이용하고 싶은 경우
- Part 대신 MultiPartFIle 로 선언한 후 getPath 이런 메소드만 MultiPartFile 에 맞춰 수정해주면 된다.
- 추후 MultiPartFile 버전도 작업해서 올릴 예정이다.
'API' 카테고리의 다른 글
#1 TEST CODE 작성 : Todo API 만들기 (1) | 2024.12.14 |
---|---|
[spring webSocket - 스프링 웹소켓] : 간이 채팅방 구현 (4) | 2024.09.16 |
WebSocketHandler - 오버라이드 메소드, override method 정리 (0) | 2024.09.16 |
메일 API : 이메일 보내기 API - (구글, 스프링부트) (0) | 2024.05.29 |
Security/OAuth2(구글로그인)/JWT(access, refresh) 발행 및 저장 -개인 복습(API) 1부 (0) | 2024.05.01 |