Spring boot, ReactJS 파일 업로드 및 Mysql에 링크저장하기

Sieun Sim·2020년 5월 31일
1

Spring Boot

목록 보기
1/2

메뉴 등록 시 사진 등록이 필요하게 되었다.

apache의 commons-io 설치

        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

마지막 업데이트가 2년 이상 되긴 했는데 문제 없이 작동한다.


Controller

    @PostMapping(value = "/menu/upload")
    public Map<String, Object> upload(@RequestParam("file") MultipartFile multipartFile) {
        File targetFile = new File("src/main/resources/static/imgs/" + multipartFile.getOriginalFilename());
        try {
            InputStream fileStream = multipartFile.getInputStream();
            FileUtils.copyInputStreamToFile(fileStream, targetFile);
        } catch (IOException e) {
            FileUtils.deleteQuietly(targetFile);
            e.printStackTrace();
        }
        Map<String, Object> m = new HashMap<>();
        m.put("errorCode", 10);
        return m;
    }

자바는 File 클래스를 통해서 파일과 디렉터리를 다룬다. 그래서 File 인스턴스는 파일 일 수도 있고 디렉터리 일 수도 있다. File 인스턴스를 생성했다고 해서 파일이나 디렉터리가 바로 생성되는 것은 아니고, 출력스트림을 생성하거나 createNewFile()을 호출해야 한다.

자바에서의 `InputStream`은 abstract class로, BLOB(Binary Large OBject)과 CLOB(Character Large OBject) 모두 다룰 수 있다.

InputStream

This abstract class is the superclass of all classes representing an input stream of bytes.
Applications that need to define a subclass of InputStream must always provide a method that returns the next byte of input.

파일을 말그대로 흘러가면서 읽기 위한, c언어에서의 fp같은 느낌인 것 같다. 이 추상클래스를 구현하는 서브클래스들은 그 다음 바이트를 뱉는 메소드를 꼭 포함해야 한다고 한다. 내생각엔 read() 메소드가 그 예인 것 같다.

MultipartFile

A representation of an uploaded file received in a multipart request.
The file contents are either stored in memory or temporarily on disk. In either case, the user is responsible for copying file contents to a session-level or persistent store as and if desired. The temporary storage will be cleared at the end of request processing.

MultipartFile은 Handler의 매개변수 중 하나로, 사용자가 업로드한 파일을 핸들러에서 쉽게 다룰 수 있도록 도와준다. 이 매개변수를 사용하기 위해 MultipartResolver Bean이 등록되어 있어야 하는데, SpringMVC와는 달리 부트에서는 AutoConfigure 해준다고 한다. 이 파일은 당연하겠지만 메모리나 디스크에 잠시 저장되었다가 요청 프로세스가 끝날때 같이 사라진다. 위의 코드에서 이 MultipartFile을 이용해 파일스트림을 받아왔는데, multipartFile.getInputStream().getClass()를 호출해보면 java.io.FileInputStream 라는 것을 알 수 있다.

MultipartFile을 받아와서 그 FileInputStream을 얻고, commons-io의 라이브러리인 FileUtils의 FileUtils.copyInputStreamToFile(fileStream, targetFile);를 통해 빈 targetFile에 이 스트림을 복사해주었다.

try,catch문을 보면 실패할 시 이 FileUtils.deleteQuietly(targetFile);를 호출한다.


FileUtils.deleteQuietly(File)

Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
The difference between File.delete() and this method are:
A directory to be deleted does not have to be empty.
No exceptions are thrown when a file or directory cannot be deleted.

그게 폴더라 주의를 필요하든 말든간에 무조건 조용히 지워주는 메소드이다. 아마 모종의 이유로 IO 에러가 났을 때 쓰다말았다거나 등등 roll back 하기 위해 써주면 좋을 것 같다.

이제 이 targetFile은 실제 InputStream을 가지게 되면서 파일을 생성한다.


클라이언트(React) 코드

import React, { Component } from 'react';
import axios, { post } from 'axios';

const URL = 'http://localhost:8080/auth/login';

class Login extends Component {
    constructor(props) {
        super(props);
        this.state = {
            file: null
        }
    }

    
    fileUpload(file) {
        const url = 'http://localhost:8080/menu/upload';
        const formData = new FormData();
        formData.append('file', file)
        const config = {
            headers: {
                'content-type': 'multipart/form-data'
            }
        }
        return post(url, formData, config)
    }

    upload = (e) => {
        e.preventDefault();
        this.fileUpload(this.state.file).then((response) => {
            console.log(response.data);
        })
    }
    fileChange = (e) => {
        this.setState({ file: e.target.files[0] })
    }


    render() {
        return (
            < div >
                <h1>파일 업로드</h1>
                <form onSubmit={this.upload}>
                    <h1>File Upload</h1>
                    <input type="file" onChange={this.fileChange} name="file" />
                    <button type="submit">Upload</button>
                </form>
            </div>
        )

    }
}

export default Login;

이미지 저장과 동시에 mysql에 이미지 링크 저장하기

다른 방법도 있겠지만 그냥 이 서버의, 이 프로젝트의 resource 폴더에 정적으로 저장하고 또 접근하려 한다.

따로 찾아보고 구현한건 아니라서 틀린 방법일 수도 있지만, 그냥 메뉴 업로드 요청을 받아서 메뉴 객체의 새로운 인스턴스를 만드는 순간 랜덤한 uuid를 생성해 이 uuid로 저장했다. 그리고 MySQL에 이 uuid를 저장해 접근할 수 있게 했다. 

ip:port/menuimgs/~로 접근했을 때 바로 클래스패스의 menuimgs폴더로 접근하기위해 config 파일의 `addResourceHandler`에 추가해준다.


Config 파일

package com.lastpang.server.Config;

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 WebConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/menuimgs/**")
                .addResourceLocations("classpath:/menuimgs/");
    }

}

Controller

    @PostMapping(value = "/menu/upload")
    public Map<String, Object> upload(@RequestParam("storename") String storename, @RequestParam("file") MultipartFile multipartFile,
                                      @RequestParam("menuname") String menuname, @RequestParam(value = "price", required = false) Integer price,
                                      @RequestParam(value="desc", required = false) String desc, @RequestParam(value = "options", required = false) String options) {
        UUID uid = UUID.randomUUID();
        File targetFile = new File("src/main/resources/menuimgs/"+uid.toString());
        Menu menu = new Menu();

        try {
            System.out.println( multipartFile.getInputStream().getClass());
            InputStream fileStream = multipartFile.getInputStream();
            FileUtils.copyInputStreamToFile(fileStream, targetFile);
            String filename = multipartFile.getOriginalFilename();
            menu.setMenuImgUuid(uid.toString());
            menu.setPrice(price);
            menu.setDescription(desc);
            menu.setStore(storeRepository.findStoreByStoreName(storename));
            menu.setOptions(options);
            menu.setMenuName(menuname);
            menuRepository.save(menu);

        } catch (IOException e) {
            FileUtils.deleteQuietly(targetFile);
            e.printStackTrace();
        }

        Map<String, Object> m = new HashMap<>();
        m.put("errorCode", 10);
        return m;
    }

윽 더럽다.. 어떻게 잘하면 Menu 객체에 바로 캐스팅하면서 한번에 처리할 수도 있겠지만 Multipart 때문에 바로 처리할 수는 없다. 예쁘게 파싱해서 쓰거나 할수 있겠지만 일단 급해서 더럽게 구현만..

이제 http://ip:port/menuimgs/0e6de525-f54b-441a-a29d-7c43e1bd0e38 이런 주소로 정적 리소스인 메뉴 이미지에 접근할 수 있다. EC2에 올려서 쓸거라 이 링크 자체를 db에 저장해도 되겠지만 혹시나 ip나 port가 바뀔 수도 있으니 그런건 클라이언트에서 만들어도 무방할 것 같다.사실 uuid를 랜덤 말고 시간+메뉴라던지 더 유니크하게 만든 뒤 해싱하는게 나을 것 같긴 하지만..ㅎㅎ.

다른데서는 어떻게하는지 궁금한데 아마존 S3를 썼을때는 timestamp스럽게 생긴 156785736686 이런 숫자들을 봤던것도 같다. 나중에 다른 좋은 예시를 찾아봐야겠다


참고자료
https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html?is-external=true
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html
https://devbox.tistory.com/entry/Java-File-클래스 [장인개발자를 꿈꾸는 :: 기록하는 공간]
https://galid1.tistory.com/564

0개의 댓글