728x90
기존에 게시판의 기본적인 생성, 조회 등의 기능은 구현되어 있다는 가정하에 진행하겠다.
※ 개발환경
- 프레임워크: Spring Boot 2.7.12
- DB: MyBatis, MyBatis
- Java: JDK 1.8
- 템플릿 엔진: Thymeleaf
1. DB 설계
- 기존에 게시판기능이 이미 구현이 되어 있으니 게시판 테이블은 다음과 같이 존재한다.

Column | Description |
seq | 게시판 테이블의 기본키, serial 타입으로 인해 Auto Increament 속성 적용 |
title | 게시글의 제목 |
writer | 게시글의 작성자 |
reg_date | 게시글의 작성일자 |
count | 게시글의 조회수 |
content | 게시글의 내용 |
- 해당 테이블이 존재한다는 가정하에 파일 테이블을 다음과 같이 생성한다.

Column | Description |
seq | 파일 테이블의 기본키, serial 타입으로 인해 Auto Increament 속성 적용 |
board_seq | 게시판 테이블의 기본키를 참조하는 외래키, 게시글마다 첨부파일을 연결하기 위한 칼럼 |
original_name | 업로드하는 첨부파일의 원본 파일명 |
save_name | 컴퓨터 내 물리적으로 보관될 파일명 |
size | 파일의 크기 |
delete_yn | 파일의 삭제 여부 |
created_date | 파일의 생성일자 |
deleted_date | 파일의 삭제일자 |
- 여기서 핵심적인 칼럼은 original_name과 save_name이다. 두 칼럼 모두 파일의 이름을 의미하지만, DB 테이블에만 저장되는 original_name과 컴퓨터 내 물리적으로 저장되는 save_name은 서로 달라야 한다.
- 예를 들어, 서로 다른 사용자가 "춘식이.jpg" 라는 이름의 파일을 저장했을 때 이 첨부파일의 이름을 그대로 서버 컴퓨터 내 저장하게 되면 각각 어느 사용자가 업로드한 춘식이인지 구별할 수 없다. 그래서 실제로 저장되는 파일명은 랜덤값을 통해 고유한 식별값으로 지정되어야 한다.
2. FileRequest 클래스 생성
@Getter
public class FileRequest {
private int boardSeq; // 게시글 번호(FK)
private String originalName; // 원본 파일명
private String saveName; // 저장 파일명
private long size; // 파일 크기
@Builder
public FileRequest(String originalName, String saveName, long size) {
this.originalName = originalName;
this.saveName = saveName;
this.size = size;
}
public void setBoardSeq(int boardSeq) {
this.boardSeq = boardSeq;
}
}
- 앞서 생성한 파일 테이블에 저장하기 위한 파일 정보를 담을 용도인 FileRequest 클래스이다.
- 파일 정보를 손쉽게 설정하기 위해 빌더패턴을 사용한다.
- boardSeq(게시글 번호)는 게시글이 생성된 후에 설정을 해줘야 하기 때문에 파일 정보들과 설정 시기가 맞지 않아 따로 Setter 메소드로 제외한다.
3. FileMapper 인터페이스 생성
@Mapper
public interface FileMapper {
void saveAll(List<FileRequest> files); // 파일 정보 저장
}
- saveAll(List<FileRequest files): 들어온 FileRequest 클래스를 통해 파일의 정보를 DB에 저장한다. 게시판에 다중 파일 첨부 기능을 도입할 것이기 때문에 여러 파일 정보를 저장하기 위해 List<FileRequest> 타입으로 받는다.
4. FileMapper XML 생성
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.file.service.FileMapper">
<sql id="fileColumns">
BOARD_SEQ
, ORIGINAL_NAME
, SAVE_NAME
, SIZE
, DELETE_YN
, CREATED_DATE
, DELETED_DATE
</sql>
<insert id="saveAll" parameterType="List">
INSERT INTO BOARD_FILE (
<include refid="fileColumns"></include>
) VALUES
<foreach item="file" collection="files" separator=",">
(
#{file.boardSeq}
, #{file.originalName}
, #{file.saveName}
, #{file.size}
, 'N'
, NOW()
, NULL
)
</foreach>
</insert>
</mapper>
- 여러개의 파일을 저장할 것이기 때문에 List 형태로 넘어온 FileRequest를 <foreach>태그로 한꺼번에 INSERT 한다.
5. FileService 클래스 생성
@Service
@RequiredArgsConstructor
public class FileService {
private final FileMapper fileMapper;
@Transactional
public void saveFiles(final int boardSeq, final List<FileRequest> files) {
if(CollectionUtils.isEmpty(files)) {
return;
}
for(FileRequest file : files) {
file.setBoardSeq(boardSeq);
}
fileMapper.saveAll(files);
}
}
- 파일을 업로드할 게시글의 번호와 파일 정보를 받아 fileMapper의 saveAll 메소드를 호출하여 DB까지 연결되도록 한다.
- saveAll(files)를 통해 여러개의 파일이 삽입되던 중 한가지의 파일을 삽입하다가 문제가 발생되면 해당 메소드 호출 이전으로 롤백될 수 있도록 @Transactional 어노테이션을 사용한다.
6. FileUtils 클래스 생성
@Component
public class FileUtils {
// 물리적으로 파일을 저장할 위치(C:\\boardPrc\\upload-files)
private final String uploadPath = Paths.get("D:", "boardPrc", "upload-files").toString();
// 다중 파일 업로드
public List<FileRequest> uploadFiles(final List<MultipartFile> multipartFiles) {
List<FileRequest> files = new ArrayList<>();
for (MultipartFile multipartFile : multipartFiles) {
if(multipartFile.isEmpty()) {
continue;
}
files.add(uploadFile(multipartFile));
}
return files;
}
// 단일 파일 업로드
public FileRequest uploadFile(final MultipartFile multipartFile) {
if(multipartFile.isEmpty() ) {
return null;
}
// 디스크(폴더)에 저장할 파일명
String saveName = generateSaveFilename(multipartFile.getOriginalFilename());
// 오늘날짜 (240706)
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyMMdd")).toString();
// 파일의 업로드 경로 (D:\\boardPrc\\upload-files\\240706\\saveName)
String uploadPath = getUploadPath(today) + File.separator + saveName;
// 업로드할 경로의 파일 객체
File uploadFile = new File(uploadPath);
try {
multipartFile.transferTo(uploadFile); // 파일 업로드
} catch (IOException e) {
throw new RuntimeException(e);
}
return FileRequest.builder() // 업로드 한 파일의 정보를 빌더패턴으로 DTO에 담는다.
.originalName(multipartFile.getOriginalFilename())
.saveName(saveName)
.size(multipartFile.getSize())
.build();
}
// saveName 생성 (실제로 폴더에 저장될 파일명)
private String generateSaveFilename(final String filename) {
String uuid = UUID.randomUUID().toString().replaceAll("-", ""); // 파일의 식별을 위해 랜덤값 지정
String extension = StringUtils.getFilenameExtension(filename);
return uuid + "." + extension;
}
private String getUploadPath() {
return makeDirectories(uploadPath);
}
private String getUploadPath(final String addPath) {
return makeDirectories(uploadPath + File.separator + addPath);
}
private String makeDirectories(final String path) {
File dir = new File(path);
if(dir.exists() == false) {
dir.mkdirs();
}
return dir.getPath();
}
- 파일을 직접 MultipartFile 객체를 이용해 서버 컴퓨터 내 물리적으로 업로드를 하고, 업로드 할 파일의 정보를 가지고 FileRequest 객체를 만드는 클래스이다.
- 이곳에서 만들어진 FileRequest 객체를 가지고 DB의 파일 정보 테이블에 삽입하게 된다.
- String uploadPath = Paths.get("D:", "boardPrc", "upload-files").toString(): 물리적으로 파일을 저장할 경로를 담는 변수이다.
- FileRequest uploadFile(MultipartFile multipartFile): 물리적으로 저장되는 파일명인 saveName 및 추가적인 폴더명을 생성하여 만든 파일 저장 경로를 가지고 서버 컴퓨터 내 실제로 파일을 업로드하고, 업로드 된 정보를 빌더 패턴을 통해 FileRequest 객체로 변환하여 리턴한다.
- List<FileRequest> uploadFiles(List<MultipartFile> multipartFiles): MultipartFile 객체가 여러개일 경우 List 형태로 받아서 반복문으로 각 파일을 uploadFile() 메소드를 사용하여 다중 파일 처리를 하는 메소드이다.
- String generateSaveFilename(String filename): 서버 컴퓨터 내 폴더에 물리적으로 저장할 파일명을 생성하는 메소드이다. 앞서 설명했듯이 디스크에 저장되는 파일명들은 각 고유한 식별값을 가져야 하므로 UUID라는 랜덤값을 이용해 생성한다.
- String getUploadPath(): 맨 끝 상단에서 정의한 uploadPath 변수에 설정된 경로를 리턴하는데, 추가적인 경로 문자열을 파라미터로 넘기면 해당 경로를 추가하여 채로 리턴한다.
- String makeDirectories(String path): getUploadPath() 메소드에서 사용하는 메소드로, 경로를 파라미터로 받고 해당 경로가 존재하지 않으면 폴더를 새로 생성하고, 존재하면 경로 그대로를 다시 리턴한다.
7. BoardController 게시글 등록 매핑 메소드 수정
@PostMapping("/registBoard")
public String registBoard(
@RequestParam("title") String title,
@RequestParam("writer") String writer,
@RequestParam("content") String content,
@RequestParam("files") List<MultipartFile> multipartFiles) {
Map<String, Object> board = new HashMap<>();
board.put("title", title);
board.put("writer", writer);
board.put("content", content);
try {
boardService.registBoard(board, multipartFiles);
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/";
}
- 파일을 처리하는 역할의 모든 코드들을 완성했다. 이제 게시글 등록 Controller와 Service를 수정해주면 된다.
- Controller는 클라이언트를 통해 넘어온 파일을 MultipartFile 객체 형태로 받을 수 있게 수정해주면 된다.
- @RequestParam("files") List<MultipartFile> multipartFiles: 클라이언트를 통해 넘어온 "files" 라는 이름의 데이터를 MultipartFile 형태로 받는다.
- boardService.registBoard(board, multipartFiles): 게시글을 등록하는 Service 메소드 내에서 파일 처리 메소드를 호출할 것이기 때문에 multipartFiles 변수를 파라미터로 추가한다.
8. BoardService 게시글 등록 서비스 메소드 수정
public void registBoard(Map<String, Object> board, List<MultipartFile> multipartFiles) {
boardMapper.registBoard(board);
List<FileRequest> files = fileUtils.uploadFiles(multipartFiles);
fileService.saveFiles((int) board.get("SEQ"), files);
}
- BoardService의 registBoard() 메소드는 기존에 boardMapper를 통해 게시글만 DB에 삽입하는 역할 뿐만 아니라 MultipartFile 객체를 처리하여 파일 업로드, 파일정보 DB 삽입 기능 등을 FileUtils와 FileService 클래스에게 직접 위임하는 역할이 추가되었다.
- List<FileRequest> files = fileUtils.uploadFiles(multipartFiles): MultipartFile 객체를 앞서 작성한 FileUtils의 uploadFiles() 메소드에게 넘기며 호출한다.
- fileService.saveFiles((int) board.get("SEQ"), files): 생성된 게시글의 기본키 값과 생성된 FileRequest를 가지고 FileService에게 DB에 삽입되도록 saveFiles() 메소드를 호출한다.
9. application.properties 수정 - 파일 사이즈 설정
spring.servlet.multipart.maxFileSize=10MB
spring.servlet.multipart.maxRequestSize=50MB
- 스프링 부트에서는 요청되는 MultipartFile 객체 파일의 사이즈에 제한이 되어있는데, 요청에 포함되는 파일 한 개당 최대 1MB, 전체 파일의 합은 최대 10MB로 설정되어 있다.
- 이를 한 개당 10MB, 총 50MB로 바꿔보자.
10. 게시글 등록 폼에 파일 영역 추가
<form action="/registBoard" method="post" enctype="multipart/form-data">
<label for="title">제목:</label>
<input type="text" id="title" name="title" required>
<br><br>
<label for="author">작성자:</label>
<input type="text" id="author" name="author" required>
<br><br>
<label for="content">내용:</label>
<textarea id="content" name="content" rows="10" cols="50" required></textarea>
<br><br>
<label for="files">파일 선택:</label> <!-- 파일 영역 추가 -->
<input type="file" id="files" name="files" multiple>
<br><br>
<input type="submit" value="작성">
</form>
※ 결과



728x90
'웹개발 > Java, Spring' 카테고리의 다른 글
[Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (2/2) (0) | 2024.07.07 |
---|---|
[Spring Boot] 스프링부트 & MyBatis 게시판 파일 다운로드 예제 (1/2) (0) | 2024.07.07 |
[MyBatis] Mapper XML 내 ResultMap 사용법 (0) | 2024.06.29 |
[Spring Boot] MyBatis SQL 쿼리 로그 설정 (0) | 2024.06.29 |
[Spring Boot] Thymeleaf HTML 템플릿 수정 시 브라우저 반영 설정 (0) | 2024.06.26 |