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>

 

 

 

※ 결과


게시판 테이블
파일 테이블, board_seq 칼럼 값이 게시글 테이블의 seq와 외래키로써 같은 것을 볼 수 있다.
앞서 설장한 파일 경로에 파일 테이블의 save_name 값으로 파일명이 생성되고, 춘식이 사진이 잘 업로드 되었다.

728x90