Spring

Spring Boot 파일(이미지) 업로드하기

오잎 클로버 2022. 1. 24. 09:00
728x90

#code 개발 중에 프로필 화면 구현이 필요하였기에 포스트 하기로 했습니다.

 

먼저 이미지를 저장하는 방식이 여러 방법이 있다고 한다... 그 사실을 몰라서 구글링을 통해 여러 가지 방법을 찾았는 데

대표적인 방법들만 설명하자면

첫 번째는 이미지 자체를 DB에 저장하는 방식 (BLOB 형식 그대로 사용)

BLOB이란?
Binary Large Object의 약자로서 2진으로 저장을 하며, 주로 소리, 사진 등 멀티미디어들을 가르킵니다.

많은 분들께서 가장 일반적인 방법이네, 요즘은 잘 사용되지않네, DB 병목 등 여러 문제가 있네 등

여러 의견이 분분한데.. 개인적으로 BLOB 방식을 잘 사용하지 않을 것 같습니다.

(이미지 하나 가져올 때마다 DB에 접근하여 가져와야하고, 이를 하지 않으려면 캐싱 관련 코딩을 또 해야 함.

굉장히 번거롭게 느껴지며, 또 입출력 시 반드시 프로그램을 통해서 처리를 해야 하기 때문에)

 

두 번째는 경로 저장 방식으로 DB에 간접적으로 저장하는 방식입니다.

장점은 위 방식과 다르게 더 효율적인 저장소에 따로 저장을 하고, 이에 맞는 주소나 접근에 필요한 정보를

DB에 저장하는 방식을 사용함으로써 보다 유동적이라는 점입니다.

단점은 저장소가 요구됩니다.

 

 

저 같은 경우에는 두 번째 방법을 채택하여 테스트를 해보았습니다.

Entity는 앞서 포스팅한 Entity 날짜 자동 저장에 설명했던 BaseTimeEntity를 사용하였습니다.

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Board extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @NotNull
    private long boardIdx;

    @NotNull
    private String originalFileName;

    @NotNull
    private String storedFileName;

    private long fileSize;

}

기존 파일명을 기록하기 위한 originalFileName, 그리고 저장한 파일명인 storedFileName

굳이 파일명을 나눈 이유는 기존 파일명이 이미 DB에 저장되어 있을 가능성은 높지만 따로 이름을 바꿔서 저장한 파일명이 같을 경우는 낮기 때문에 굳이 나눠서 저장하였습니다.

(14자리 수가 전부 같을 경우는... 굉장히 낮습니다.)

 

그리고 해당 파일을 조절하여 줄 클래스를 만들었습니다.

이름을 따로 뭐라고 지어야 될지 몰라 FileHandler라고 지었습니다.

@Component
public class FileHandler {

    public List<Board> parseFileInfo(
            Long boardID,
            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());

        // 파일이 빈 것이 들어오면 빈 것을 반환
        if (multipartFiles.isEmpty()) {
            return fileList;
        }

        // 프로젝트 폴더에 저장하기 위해 절대경로를 설정 (Window 의 Tomcat 은 Temp 파일을 이용한다)
        String absolutePath = new File("").getAbsolutePath() + "\\";

        // 경로를 지정하고 그곳에다가 저장
        String path = "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 = Board.builder()
                        .boardIdx(boardID)
                        .originalFileName(multipartFile.getOriginalFilename())
                        .storedFileName(path + "/" + new_file_name)
                        .fileSize(multipartFile.getSize())
                        .build();
                fileList.add(board);

                // 저장된 파일로 변경하여 이를 보여주기 위함
                file = new File(absolutePath + path + "/" + new_file_name);
                multipartFile.transferTo(file);
            }
        }

        return fileList;
    }

}

위 핸들러는 service에서 사용됩니다.

@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 Board addBoard(
            Board board,
            List<MultipartFile> files
    ) throws Exception {
        // 파일을 저장하고 그 Board 에 대한 list 를 가지고 있는다
        List<Board> list = fileHandler.parseFileInfo(board.getId(), files);

        if (list.isEmpty()){
            // TODO : 파일이 없을 땐 어떻게 해야할까.. 고민을 해보아야 할 것
        }
        // 파일에 대해 DB에 저장하고 가지고 있을 것
        else{
            List<Board> pictureBeans = new ArrayList<>();
            for (Board boards : list) {
                pictureBeans.add(boardRepository.save(boards));
            }
        }

        return boardRepository.save(board);
    }

    public List<Board> findBoards() {
        return boardRepository.findAll();
    }

    public Optional<Board> findBoard(Long id) {
        return boardRepository.findById(id);
    }

}

그 후 RestController가 이를 Post를 할 수 있도록 합니다.

 

 

@Slf4j
@RestController
@RequiredArgsConstructor
public class BoardController {
    
    private final BoardService boardService;

    @PostMapping("/board")
    public ResponseEntity<?> createBoard(
            @Validated @RequestParam("files") List<MultipartFile> files
    ) throws Exception {
        boardService.addBoard(Board.builder()
                .build(), files);

        return ResponseEntity.ok().build();
    }

    @GetMapping("/board")
    public String getBoard(@RequestParam long id) {
        Board board = boardService.findBoard(id).orElseThrow(RuntimeException::new);
        String imgPath = board.getStoredFileName();
        log.info(imgPath);
        return "<img src=" + imgPath + ">";
    }

}

파일을 POST형태로 받으면 이를 저장하고 GET 형태로는 RequestParam(쿼리파리미터)로 값을 받아서 

해당 사진을 가져오는 방식입니다.

 

그리고 사진과 같은 파일들은 기본적으로 static으로 적용되어있습니다.

하지만 images는 외부에 있기에 따로 설정하였습니다.

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations("file:///C:/Users/User/IdeaProjects/img_test/");
    }
}

원래는

new File("").getAbsolutePath() + "\\";

방식으로 처리하려고 했지만, 오류가 발생하여 그냥 전체 위치로 적용시켰습니다.

아마 앞에 file://라고 적지 않아 발생했던 오류 같습니다.

 

html를 사용해서 POST를 사용하였습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>파일 보내기 예제</title>
</head>
<body>
<form name="form" method="post" action="http://localhost:8080/board" enctype="multipart/form-data">
    <input type="file" name="files" multiple="multiple"/>
    <input type="submit" id="submit" value="전송"/>
</form>
</body>
</html>

html&amp;nbsp;

파일을 선택한 후, 전송합니다.

해당 파일은 현재 프로젝트 파일에 images라는 이름의 디렉토리를 생성함과 동시에

파일을 요청한 날짜 이름으로 된 디렉토리안에 계산된 파일명으로 저장이 되었을 겁니다.

그리고 DB에 역시 해당 값이 잘 저장되었음을 확인할 수 있습니다.

마지막으로 GET 요청을 하면

이미지가 잘 적용됩니다.

전체 소스코드는 해당 링크 참조해주세요.

링크

 

 

이상입니다.