일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- git branch
- Linux oh my zsh
- nginx https 설정
- git 기본 에디터
- ssl 인증서 발급받기
- EC2 Apache2
- EC2 HTTP 호스팅
- linux foreground
- git 계정 설정
- Linux 디렉터리 역할
- Linux 디렉터리 구조
- javascript 정렬
- EC2 zsh
- ec2 ssh unprotected private key file
- HTTP Web Server
- git switch
- Git 브랜치
- Navigation Pattern
- Linux apt-get
- 서버의 서비스 방식
- Logback
- Linux apt
- UNPROTECTED PRIVATE KEY FILE
- AWS EC2 서버 만들기
- 아비트럼 새폴리아 이더 받는법
- arbitrum sepolia eth
- linux background
- javascript scope
- EC2 oh my zsh
- GIT
Archives
- Today
- Total
HyunJun 기술 블로그
스프링 프레임워크 게시판, 파일 업로드 다운로드 구현하기 본문
728x90
1. 게시판 구현하기
기능 구현에 있어서 CRUD(Create, Read, Update, Delete)라는 표현을 많이 사용하는데 CRUD를 연습하기에 게시판같이 좋은 것이 없다고 생각합니다. 이 글에서는 단순 CRUD보다는 파일 입출력에 더 초점을 맞춰서 진행해 볼까 합니다.
- 간단한 게시판 기능
- File Download, Upload를 같이 구현할 예정.
- File들을 조회 시 URL로 반환. (모든 확장자) -> exe, zip 파일은 업로드 불가.
2. 게시판 (파일 업, 다운로드) 로직
기본적으로 아래와 같은 로직으로 구현하려 합니다.
- 게시글 작성 시 json, 멀티 파트로 게시글 제목, 내용, 파일 여러 개(확장자 제한 기능)를 받는다.
- 확장자 제한 확인 후, 파일 자체는 프로젝트 루트 내 ex)) /Users/mycomputer/Spring-Framework/Spring-CRUD-Board-File-Upload-Download/media/파일 이름.확장자로 저장을 하되 db에 fileUri는 http://localhost:8080/api/files/파일 이름.확장자로 저장을 한다. 이때 중복방지를 위해 게시글 고유번호를 파일명 앞에 붙여 저장한다.
- /api/files/{PathVariable}로 api를 만들어 해당 주소로 접근하면 파일을 리턴한다.
3. 구현하기
- Database 환경만 RDS 환경이고 나머지는 Local 환경입니다.
- 실 서버 사용 시 URL 관련 부분만 변경하면 되고, 서버 용량은 최대한 아껴야 하므로, 파일 저장소를 S3로 변경하여 사용해도 됩니다.
build.gradle
/*web*/
implementation 'org.springframework.boot:spring-boot-starter-web'
/*database*/
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
/*lombok*/
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
/*test*/
testImplementation 'org.springframework.boot:spring-boot-starter-test'
/*file io*/
implementation 'commons-io:commons-io:2.6'
application.yml
spring:
profiles:
# datasource 숨기기
include: datasource
jpa:
hibernate:
# update 변경이 이루어지는 부분만 반영
ddl-auto: update
show-sql: true # 쿼리 확인
servlet:
multipart:
max-file-size: 50MB # 하나의 파일당 최대 용량 제한 사이즈
max-request-size: 70MB # 한 요청당 파일들의 최대 용량 제한 사이즈
# 50MB인 2개의 파일 100MB를 업로드 시 실패.
application-datasource.yml
spring:
datasource:
url: jdbc:mysql://myserver.rds.amazonaws.com:3306/spring-board
username: 계정
password: 비밀번호
.gitignore
application-datasource.yml
BoardController
package com.example.springcrudboardfileuploaddownload.controller;
import com.example.springcrudboardfileuploaddownload.dto.BoardReqDto;
import com.example.springcrudboardfileuploaddownload.dto.BoardReadResDto;
import com.example.springcrudboardfileuploaddownload.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class BoardController {
private final BoardService boardService;
/*글 작성(파일 업로드)*/
@PostMapping("/boards")
public void createPost(@RequestPart(value = "requestDto") BoardReqDto reqDto,
@RequestPart(value = "files") List<MultipartFile> files) throws IOException {
boardService.createPost(reqDto, files);
}
/*글 읽기(파일 링크)*/
@GetMapping("/boards")
public BoardReadResDto readPost(@RequestParam Long boardId) {
return boardService.readPost(boardId);
}
/*글 삭제*/
@DeleteMapping("/boards")
public void deletePost(@RequestParam Long boardId) {
boardService.deletePost(boardId);
}
/*글 업데이트*/
@PutMapping("/boards")
public void updatePost(@RequestParam Long boardId, @RequestBody BoardReqDto reqDto) {
boardService.updatePost(boardId, reqDto);
}
/*파일 링크 클릭 시 파일 저장*/
@GetMapping("/files/{fileName}")
public ResponseEntity<?> downloadFile(@PathVariable("fileName") String fileName,
HttpServletRequest request) throws IOException {
/*프로젝트 루트 경로*/
String rootDir = System.getProperty("user.dir");
/*file의 path를 저장 -> 클릭 시 파일로 이동*/
Path filePath = Path.of(rootDir + "/media/" + fileName);
/*파일의 패스를 uri로 변경하고 resource로 저장.*/
Resource resource = new UrlResource(filePath.toUri());
/*컨텐츠 타입을 가지고 온다.*/
String contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
}
createPost()의 @RequestPart를 DTO와 MultiPartFile로 나누면 아래와 같은 구현이 가능합니다.
BoardReadResDto
package com.example.springcrudboardfileuploaddownload.dto;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class BoardReadResDto {
private String title;
private String contents;
private List<FileFormat> fileFormat;
public BoardReadResDto(String title, String contents, List<FileFormat> fileFormat) {
this.title = title;
this.contents = contents;
this.fileFormat = fileFormat;
}
}
BoardReqDto
package com.example.springcrudboardfileuploaddownload.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class BoardReqDto {
private String title;
private String contents;
}
FileFormat
package com.example.springcrudboardfileuploaddownload.dto;
import com.example.springcrudboardfileuploaddownload.entity.File;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class FileFormat {
private String fileName;
private String fileExtension;
private String fileUri;
public FileFormat(File file) {
this.fileName = file.getFileName();
this.fileExtension = file.getFileExtension();
this.fileUri = file.getFileUri();
}
}
Board
package com.example.springcrudboardfileuploaddownload.entity;
import com.example.springcrudboardfileuploaddownload.dto.BoardReqDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@Getter
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String contents;
public Board(BoardReqDto reqDto) {
this.title = reqDto.getTitle();
this.contents = reqDto.getContents();
}
public void updateBoard(BoardReqDto reqDto) {
this.title = reqDto.getTitle();
this.contents = reqDto.getContents();
}
}
File
package com.example.springcrudboardfileuploaddownload.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@Getter
public class File {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String fileName;
@Column
private String fileUri;
@Column
private String fileExtension;
@ManyToOne
@JoinColumn(name = "BOARD_ID", nullable = false)
private Board board;
public File(MultipartFile validatedFile, Board board) {
this.fileName = validatedFile.getOriginalFilename();
this.fileUri = "http://localhost:8080/api/files/" + board.getId() + "_" + this.fileName;
this.fileExtension = this.fileName.substring(this.fileName.lastIndexOf(".") + 1);
this.board = board;
}
}
BoardRepository
package com.example.springcrudboardfileuploaddownload.repository;
import com.example.springcrudboardfileuploaddownload.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
FileRepository
package com.example.springcrudboardfileuploaddownload.repository;
import com.example.springcrudboardfileuploaddownload.entity.File;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface FileRepository extends JpaRepository<File, Long> {
List<File> findAllByBoardId(Long id);
void deleteAllByBoardId(Long id);
}
BoardService
package com.example.springcrudboardfileuploaddownload.service;
import com.example.springcrudboardfileuploaddownload.dto.BoardReqDto;
import com.example.springcrudboardfileuploaddownload.dto.BoardReadResDto;
import com.example.springcrudboardfileuploaddownload.dto.FileFormat;
import com.example.springcrudboardfileuploaddownload.entity.Board;
import com.example.springcrudboardfileuploaddownload.entity.File;
import com.example.springcrudboardfileuploaddownload.repository.BoardRepository;
import com.example.springcrudboardfileuploaddownload.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class BoardService {
private final BoardRepository boardRepository;
private final FileRepository fileRepository;
/*게시글 생성*/
@Transactional
public void createPost(BoardReqDto reqDto, List<MultipartFile> files) throws IOException {
/*게시글 entity 생성*/
Board board = new Board(reqDto);
boardRepository.save(board);
/*지원하지 않는 확장자 파일 제거*/
List<MultipartFile> validatedFiles = filesValidation(files);
/*걸러진 파일들 업로드*/
filesUpload(validatedFiles, board.getId());
/*유효성 검증을 끝낸 파일들을 하나씩 꺼냄.*/
for (MultipartFile validatedFile : validatedFiles) {
/*File Entity 생성 후 저장*/
File file = new File(validatedFile, board);
fileRepository.save(file);
}
}
/*게시글 읽기*/
public BoardReadResDto readPost(Long boardId) {
/*board*/
Optional<Board> optionalBoard = boardRepository.findById(boardId);
if (optionalBoard.isEmpty()) {
throw new IllegalArgumentException("해당 글이 없습니다.");
}
Board board = optionalBoard.get();
/*File*/
List<File> fileList = fileRepository.findAllByBoardId(boardId);
List<FileFormat> fileFormatList = new ArrayList<>();
/*파일이 존재한다면*/
if (fileList != null) {
for (File file : fileList) {
FileFormat fileFormat = new FileFormat(file);
fileFormatList.add(fileFormat);
}
}
BoardReadResDto responseDto = new BoardReadResDto(board.getTitle(), board.getContents(), fileFormatList);
return responseDto;
}
/*게시글 삭제*/
@Transactional
public void deletePost(Long boardId) {
/*해당 boardId를 가지고 있는 file 먼저 삭제*/
fileRepository.deleteAllByBoardId(boardId);
/*board 삭제*/
boardRepository.deleteById(boardId);
}
/*게시글 업데이트*/
@Transactional
public void updatePost(Long boardId, BoardReqDto reqDto) {
Optional<Board> optionalBoard = boardRepository.findById(boardId);
if (optionalBoard.isEmpty()) {
throw new IllegalArgumentException("존재하지 않는 글입니다.");
}
Board board = optionalBoard.get();
board.updateBoard(reqDto);
}
/*파일의 유효성 검증*/
private List<MultipartFile> filesValidation(List<MultipartFile> files) throws IOException {
/*접근 거부 파일 확장자명*/
String[] accessDeniedFileExtension = {"exe", "zip"};
/*접근 거부 파일 컨텐츠 타입*/
String[] accessDeniedFileContentType = {"application/x-msdos-program", "application/zip"};
ArrayList<MultipartFile> validatedFiles = new ArrayList<>();
for (MultipartFile file : files) {
/*원본 파일 이름*/
String originalFileName = file.getOriginalFilename();
/*파일의 확장자명*/
String fileExtension = originalFileName.substring(originalFileName.lastIndexOf(".") + 1);
/*파일의 컨텐츠타입*/
String fileContentType = file.getContentType();
/*accessDeniedFileExtension, accessDeniedFileContentType -> 업로드 불가*/
if (Arrays.asList(accessDeniedFileExtension).contains(fileExtension) ||
Arrays.asList(accessDeniedFileContentType).contains(fileContentType)) {
log.warn(fileExtension + "(" + fileContentType + ") 파일은 지원하지 않는 확장자입니다.");
} else {/*업로드 가능*/
validatedFiles.add(file);
}
}
return validatedFiles;
}
/*파일 업로드 메소드*/
private void filesUpload(List<MultipartFile> files, Long boardId) throws IOException {
/*프로젝트 루트 경로*/
String rootDir = System.getProperty("user.dir");
for (MultipartFile file : files) {
/*업로드 경로*/
java.io.File uploadPath = new java.io.File(rootDir + "/media/" + boardId + "_" + file.getOriginalFilename());
/*업로드*/
file.transferTo(uploadPath);
}
}
}
이때, media 디렉터리를 만들어 놓지 않으면 파일 저장 시 FileNotFOundException이 발생한다.
결과
Github
728x90
Comments