먼저 이미지를 저장하는 방식이 여러 방법이 있다고 한다... 그 사실을 몰라서 구글링을 통해 여러 가지 방법을 찾았는 데
대표적인 방법들만 설명하자면
첫 번째는 이미지 자체를 DB에 저장하는 방식 (BLOB 형식 그대로 사용)
Binary Large Object의 약자로서 2진으로 저장을 하며, 주로 소리, 사진 등 멀티미디어들을 가르킵니다.
두 번째는 경로 저장 방식으로 DB에 간접적으로 저장하는 방식입니다.
장점은 위 방식과 다르게 더 효율적인 저장소에 따로 저장을 하고, 이에 맞는 주소나 접근에 필요한 정보를
DB에 저장하는 방식을 사용함으로써 보다 유동적이라는 점입니다.
단점은 저장소가 요구됩니다.
두 번째 방법을 채택하여 테스트를 해보았습니다.
[프로젝트를 진행하는 도중에 구현하였기에 다른 여러 코드가 섞여있습니다.]
@Getter
@SuperBuilder
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = PROTECTED)
@ToString
@EqualsAndHashCode
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
@CreatedDate
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime modifyDate;
}
import com.std.sbb.domain.wine.entity.Wine;
import com.std.sbb.global.jpa.BaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToOne;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@ToString
@Data
public class Board extends BaseEntity {
@NotNull
private long boardIdx;
@NotNull
private String originalFileName;
@NotNull
private String storedFileName;
private long fileSize;
@OneToOne
private Wine wine;
}
기존 파일명을 기록하기 위한 originalFileName, 그리고 저장한 파일명인 storedFileName
굳이 파일명을 나눈 이유는 기존 파일명이 이미 DB에 저장되어 있을 가능성은 높지만 따로 이름을 바꿔서 저장한 파일명이 같을 경우는 낮기 때문에 굳이 나눠서 저장하였습니다.
(14자리 수가 전부 같을 경우는... 굉장히 낮습니다.)
이름을 따로 뭐라고 지어야 될지 몰라 FileHandler라고 지었습니다.
import com.std.sbb.global.imagesfile.entity.Board;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Component
public class FileHandler {
public List<Board> parseFileInfo(List<MultipartFile> multipartFiles) throws Exception {
// 반환을 할 파일 리스트
List<Board> fileList = new ArrayList<>();
// 파일이 빈 것이 들어오면 빈 것을 반환
if (multipartFiles.isEmpty()) {
return fileList;
}
// 파일 이름을 업로드 한 날짜로 바꾸어서 저장할 것이다
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
// String current_date = simpleDateFormat.format(new Date());
// 프로젝트 폴더에 저장하기 위해 절대경로를 설정 (Window 의 Tomcat 은 Temp 파일을 이용한다)
String absolutePath = new File("").getAbsolutePath() + "\\";
// 경로를 지정하고 그곳에다가 저장
String path = "src/main/resources/static/images/";
// current_date 일단 생략
File file = new File(path);
// 저장할 위치의 디렉토리가 존지하지 않을 경우
if (!file.exists()) {
// mkdir() 함수와 다른 점은 상위 디렉토리가 존재하지 않을 때 그것까지 생성
file.mkdirs();
}
// 파일들을 이제 만져볼 것이다
for (MultipartFile multipartFile : multipartFiles) {
// 파일이 비어 있지 않을 때 작업을 시작해야 오류가 나지 않는다
if (!multipartFile.isEmpty()) {
// jpeg, png, gif 파일들만 받아서 처리할 예정
String contentType = multipartFile.getContentType();
String originalFileExtension;
// 확장자 명이 없으면 이 파일은 잘 못 된 것이다
if (ObjectUtils.isEmpty(contentType)) {
break;
} else {
if (contentType.contains("image/jpeg")) {
originalFileExtension = ".jpg";
} else if (contentType.contains("image/png")) {
originalFileExtension = ".png";
} else if (contentType.contains("image/gif")) {
originalFileExtension = ".gif";
}
// 다른 파일 명이면 아무 일 하지 않는다
else {
break;
}
}
// 각 이름은 겹치면 안되므로 나노 초까지 동원하여 지정
String new_file_name = System.nanoTime() + originalFileExtension;
// 생성 후 리스트에 추가
Board board = createBoardObject(multipartFile, path, new_file_name);
// Board board = Board.builder()
// .originalFileName(multipartFile.getOriginalFilename())
// .storedFileName(path + "/" + new_file_name)
// .fileSize(multipartFile.getSize())
// .createDate(LocalDateTime.now())
// .build();
fileList.add(board);
// 저장된 파일로 변경하여 이를 보여주기 위함
file = new File(absolutePath + path + "/" + new_file_name);
multipartFile.transferTo(file);
}
}
return fileList;
}
private Board createBoardObject(MultipartFile multipartFile, String path, String new_file_name) {
return Board.builder()
.originalFileName(multipartFile.getOriginalFilename())
.storedFileName(new_file_name)
.fileSize(multipartFile.getSize())
.createDate(LocalDateTime.now())
.build();
}
}
위 핸들러는 service에서 사용됩니다.
import com.std.sbb.global.imagesfile.controller.FileHandler;
import com.std.sbb.global.imagesfile.entity.Board;
import com.std.sbb.global.imagesfile.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final FileHandler fileHandler;
@Autowired
public BoardService(BoardRepository boardRepository) {
this.boardRepository = boardRepository;
this.fileHandler = new FileHandler();
}
public List<Board> addBoard(List<MultipartFile> files) throws Exception {
// 파일을 저장하고 그 Board 에 대한 list 를 가지고 있는다
List<Board> list = fileHandler.parseFileInfo(files);
if (list.isEmpty()){
// TODO : 파일이 없을 땐 어떻게 해야할까.. 고민을 해보아야 할 것
}
// 파일에 대해 DB에 저장하고 가지고 있을 것
else{
List<Board> pictureBeans = new ArrayList<>();
for (Board boards : list) {
pictureBeans.add(boardRepository.save(boards));
}
}
return list;
}
public List<Board> findBoards() {
return boardRepository.findAll();
}
public Optional<Board> findBoard(Long id) {
return boardRepository.findById(id);
}
}
그 후 RestController가 이를 Post를 할 수 있도록 합니다.
import com.std.sbb.global.imagesfile.entity.Board;
import com.std.sbb.global.imagesfile.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
public class BoardController {
private final BoardService boardService;
@GetMapping("/board")
public String getBoard(@RequestParam long id) {
Board board = boardService.findBoard(id).orElseThrow(RuntimeException::new);
String imgPath = board.getStoredFileName();
return "<img src=" + imgPath + ">";
}
}
[프로젝트 진행 중이였기 때문에 Post구문을 따로 작성하지 않고 원래 생성되던 article controller에 저장 할 수 있게 만들었음]
ex)
@PostMapping("/create")
public String wineCreate(@Validated @RequestParam("files") List<MultipartFile> files,
@Valid WineForm wineForm, BindingResult bindingResult,
@Valid TasteForm tasteForm, BindingResult tasteBindingResult) throws Exception {
if (bindingResult.hasErrors() || tasteBindingResult.hasErrors()) {
return "wineArticle_form";
}
Taste taste = tasteService.create(tasteForm.getSweet(), tasteForm.getBody(), tasteForm.getAcidity(), tasteForm.getTannin());
// 이미지
List<Board> boards = boardService.addBoard(files);
for (Board board : boards) {
this.wineService.create(wineForm.getWineName(), wineForm.getWineNameE(), wineForm.getCountry(),
wineForm.getList(), wineForm.getPrice(), wineForm.getKind(), wineForm.getFood(),
wineForm.getScore(), board, taste);
}
return "redirect:/";
}
파일을 POST형태로 받으면 이를 저장하고 GET 형태로는 RequestParam(쿼리파리미터)로 값을 받아서
해당 사진을 가져오는 방식입니다.
그리고 사진과 같은 파일들은 기본적으로 static으로 적용되어있습니다.
하지만 images는 외부에 있기에 따로 설정하였습니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("file:///C:/Users/User/IdeaProjects/img_test/");
}
}
html를 사용해서 POST를 사용하였습니다. [여기는 아직 오류가 나서 현재진행중]
<form th:object="${wineForm}" method="post" enctype="multipart/form-data" action="http://localhost:8080/article/create">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<div class="mx-auto">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-dark" for="files">Upload
file</label>
<input class="block w-1/2 text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none"
aria-describedby="file_input_help" name="files" id="files" type="file"
accept="image/jpeg, image/png, image/gif">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-700" id="file_input_help">SVG, PNG, JPG or GIF (MAX.
800x400px).</p>
</div>
~~~ 생략
파일을 선택한 후, 전송합니다.
해당 파일은 현재 프로젝트 파일에 images라는 이름의 디렉토리를 생성함과 동시에
파일을 요청한 날짜 이름으로 된 디렉토리안에 계산된 파일명으로 저장이 되었을 겁니다.
그리고 DB에 역시 해당 값이 잘 저장되었음을 확인할 수 있습니다.