파일을 다루기 때문에 업로드할 파일이나 다운로드할 파일의 관련 설정을 먼저 해야한다(업로드 경로, 업로드 크기 설정, 다운로드 파일 크기 등...) 프로젝트 안에 application.properties나 application.yml이 있을 것이다. 관련 설정 포함 시켜주자.

 

1. 설정

spring.servlet.multipart.enabled=true
spring.servlet.multipart.file-size-threshold=2KB
spring.servlet.multipart.max-file-size=200MB
spring.servlet.multipart.max-request-size=215MB
upload-path=/User/upload

이때 주의할 점은 업로드 경로다. 절대주소에서 최상위 디렉토리(Root)만 빠진 주소다. 최상위 디렉토리를 프로젝트 폴더가 C에 있나 D에 있나로 결정된다. 그래서 프로젝트 폴더가 C/D 드라이브 등 잘 파악하고 하자. 

 

2. Controller - 파일 업로드 , 파일 멀티 업로드, 파일 다운로드 3개로 구성된다.

가끔 다운로드가 안되는 경우가 있다. 코드에 문제는 아니다. 다만 설정을 잘못했을 것이다...

RequestMapping 주소가 있기 때문에 파일 업로드시 다운로드 Uri에 RequestMapping이 추가 됐는지 확인하자.

package com.example.filedemo.controller;

import com.example.filedemo.domain.FileUpload;
import com.example.filedemo.service.FileStorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/file")

public class FileController {

    private static final Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileStorageService fileStorageService;

    @Autowired
    private FileService fileService;
    
    @PostMapping("/uploadfile")
    public File uploadFile(@RequestParam("file") MultipartFile file) {
        if(file.isEmpty()){
            //파일 업로드가 안됐을 시 처리
        }
        String fileName = fileStorageService.storeFile(file);
        //확장자만 추출하는 형태 ex) exe , png, jpg ...
        String fileExt = fileName.replaceAll("^.*\\.(.*)$", "$1");
        
        String fileOriginalName = StringUtils.cleanPath(file.getOriginalFilename());

        String fileDownloadUri = ServletUriComponentsBuilder.fromCurrentContextPath()
                .path("/file/downloadfile/")
                .path(fileName)
                .toUriString();

        return new File(fileName, fileOriginalName, fileExt, fileDownloadUri);
    }
    
	@PostMapping("/uploadmultiplefiles")
    public List<File> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
        return Arrays.asList(files)
                .stream()
                .map(file -> uploadFile(file))
                .collect(Collectors.toList());
    }
    
	@GetMapping("/downloadfile/{fileName:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {
        // Load file as Resource
        Resource resource = fileStorageService.loadFileAsResource(fileName);

        String contentType = null;
        try {
            contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
        } catch (IOException ex) {
            logger.info("Could not determine file type.");
        }

        // Fallback to the default content type if type could not be determined
        if(contentType == null) {
            contentType = "application/octet-stream";
        }

        return ResponseEntity.ok()
                .contentType(MediaType.parseMediaType(contentType))
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                .body(resource);
    }
}

 

3. 파일 업로드에 대한 Response 해줄 클래스다. DTO 쯤으로 생각한다. 별 내용없고 파일 이름, 다운로드 경로, 사이즈 등등...을 저장하는 클래스다. 아마 경우에 따라 필요하지 않아서 생성하지 않을수도 있지만 이번에는 생성하는 방식으로 개발했다.

package com.example.filedemo.domain;

@Getter
@Setter
@AllArgsConstructor
@ToString
public class FileUpload {
    private String fileName;
    private String fileOriginalName;
    private String fileDownloadUri;
    private String fileExt;
    private long size;
    
    public FileUpload(String fileName, String fileOriginalName, String fileExt, String fileDownloadUri) {
        this.fileName = fileName;
        this.fileOriginalName = fileOriginalName;
        this.fileExt = fileExt;
        this.fileDownloadUri = fileDownloadUri;
    }
}

4. 서비스

 

이제 직접적으로 파일 업로드/다운로드의 서비스 로직을 만들 차례다. 

만약 맨 처음 파일 경로가 안 읽어질수도 있다. 주의하자.

파일 업로드 시 파일 이름 등을 바꾸고 싶다면 storeFile()을 보면 된다.

보통 파일이름을 오리지널 그대로 올리는 경우가 있는데 그렇게 해선 안된다. 이름이 겹치면 올라가지 않을뿐더라 나중에 보안에 문제가 생길 수 있다. 원래 파일이름을 통신을 통해 보내서 DB에 저장하고 파일이름 설계에 의한 이름 + 타임스태프로 해주는 경우가 많으니 참고바란다. 우선 난 타임스탬프 + _파일이름으로 해놓을 예정이다. 물론 Response에서 오리지널 이름과 변환 이름 둘 다 반환한다.

package com.example.filedemo.service;

import com.example.filedemo.property.config.Properties;
import com.example.filedemo.exception.FileStorageException;
import com.example.filedemo.exception.MyFileNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class FileStorageService {

    private final Path fileStorageLocation;

    @Autowired
    public FileStorageService(Properties Properties) {
        this.fileStorageLocation = Paths.get(Properties.getUploadPath())
                .toAbsolutePath().normalize();

        try {
            Files.createDirectories(this.fileStorageLocation);
        } catch (Exception ex) {
            throw new FileStorageException("Could not create the directory where the uploaded files will be stored.", ex);
        }
    }

    public String storeFile(MultipartFile file) {
        SimpleDateFormat sdf = new SimpleDateFormat ("yyyyMMddhhmmss_");
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        String timeStamp = sdf.format(timestamp);
        
        String fileName = timeStamp + StringUtils.cleanPath(file.getOriginalFilename());

        try {
            // Check if the file's name contains invalid characters
            if(fileName.contains("..")) {
                throw new FileStorageException("Sorry! Filename contains invalid path sequence " + fileName);
            }

            // Copy file to the target location (Replacing existing file with the same name)
            Path targetLocation = this.fileStorageLocation.resolve(fileName);
            Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);

            return fileName;
        } catch (IOException ex) {
            throw new FileStorageException("Could not store file " + fileName + ". Please try again!", ex);
        }
    }

    public Resource loadFileAsResource(String fileName) {
        try {
            Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
            Resource resource = new UrlResource(filePath.toUri());
            if(resource.exists()) {
                return resource;
            } else {
                throw new MyFileNotFoundException("File not found " + fileName);
            }
        } catch (MalformedURLException ex) {
            throw new MyFileNotFoundException("File not found " + fileName, ex);
        }
    }
}

 

5. 예외 클래스

 

파일을 업로드나 다운로드할 때 예외가 발생을 경우를 처리해 줄 예외클래스를 만들어준다.

package com.example.filedemo.exception;

public class FileStorageException extends RuntimeException {
    public FileStorageException(String message) {
        super(message);
    }

    public FileStorageException(String message, Throwable cause) {
        super(message, cause);
    }
}
package com.example.filedemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyFileNotFoundException extends RuntimeException {
    public MyFileNotFoundException(String message) {
        super(message);
    }

    public MyFileNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

6. 테스트

 

백엔드 개발이 완료 됐으니 API를 Postman으로 테스트를 해보자.

지금 같은 경우 Controller에서 RequestMapping을 해주었다. 그렇기 때문에 주소가 /localhost:8080/api/file/uploadfile이 된다.

'IT > Spring' 카테고리의 다른 글

[Spring Boot] Spring Boot 입문 - 프로젝트 세팅  (0) 2021.06.22
[Spring] Spring Framework 이클립스 세팅  (0) 2021.06.03

급하게 회사에서 외주 프로젝트가 들어왔다. 한번쯤은 스프링으로 해보는 것도 나쁘지 않다고 생각했는데... 너무 두렵다... 아무튼 예전에 egov랑 스프링을 조금 해본 정도로 프로젝트를 진행한다. 개념은 아예 없기 때문에 블로그 쓰면서 스터디 하면서 정리할 예정이다. 너무 쉽게 생각했다...

 

IDE

우선 IDE인데 이클립스를 하려고 했는데 나의 상황이 사용중인 IDE가 안드로이드 스튜디오(안드로이드), VS Code(Node.js) 인데 스프링 하려고 이클립스까지 쓰면 IDE는 총 3개 사용하는 것이다. 아마 PC 바꾸면 대참사가 날 것이다. 그래서 IntelliJ IDE를 사용해보려고 한다. 아마 자기가 학생이면 잘 모를수도 있다.  IntelliJ도 학생은 무료로 사용할 수 있다. 그렇기 때문에 한번 쯤 써보는 것도 좋다. 굉장히 많은 툴들을 지원하고 JAVA, PHP, Node.js, Android...등 많은 언어들을 지원하기 때문에 나로썬 써보는 게 좋다고 생각했다. 그래서 대부분 IDE를 IntelliJ로 진행해보려한다.

 

프로젝트 세팅은 스프링 페이지에서 만들어서 IDE에 Import하는 방법과 IDE에서 생성하는 방법이 있다. 물론 두 방법 다 과정은 같기 때문에 IDE에서 생성해보려고 한다.

 

1. New > project > New Project 에서 왼쪽 탭에 Spring Initalizr을 누르면 위 이미지처럼 나온다. 물론 탭에 없다면 맨 아래 Empty Project에서 찾아보기 바란다...

2. 스프링 프로젝트의 세팅 - Group과 Artifact, package name 등을 입력하고 빌드도구랑 언어는 우선은 Maven, Java를 선택

패키징은 Jar와 War중 선택이 가능한데 Jar를 선택하면 스프링부트 내장톰캣(Embeded Tomcat)을 사용하여 Stand Alone으로 WAS를 구동시키고. 외부 WAS에 Deploy해서 사용하는 환경이시라면 War를 선택하여 내보내면 된단다...


3. Next를 눌러서 다음으로 DI 세팅이다. 내가 필요한 DI를 선택하면 빌드에 자동으로 항목이 추가되고 리포지토리에서 내려받아진다. 물론 나중에 따로 추가도 가능하다.

4. Finish를 누르면 끝난다. 생성된 스프링부트 프로젝트의 구조인데 주요 파일로는 스프링부트 컨테이너를 실행시키기 위한 src폴더내의 main가 있고 resources 폴더에는 web을 선택했기 때문에 html, css, js 등의 정적 컨텐츠가 위치할 static 폴더가 자동으로 생성됐으며 여러가지 설정 정보를 위한 application.properties 파일도 생성됐다. 

 

5. 우선 국룰을 통해서 프로젝트를 만들었으면 실행을 해야한다고 생각한다. 이클립스 자바 할 때처럼 Run을 해보자...당연히 바로 안된다. 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2021-06-25 16:08:48.400 ERROR 238940 --- [  restartedMain] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


Process finished with exit code 0

 

뭐라고 주절주절 하는데 아래 Action: 을 보면 뭐라 써있다. 대충 읽어보면 아까 DI 세팅할 때 DB를 같이 추가 했는데 DB에 대한 기본 설정을 하지 않아 발생한 문제다.

2가지 방법으로 해결했다. 

 

 

5-1 application.properties에 DB 세팅을 해준다. 자신의 설정에 맞게 해주면 된다.

5-2 우선 어노테이션을 통해 Auto Configuration에서 DataSource 관련 설정 로드를 WAS 구동시 하지 않도록 exclude하는 방법이 있다.

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class}) (붙여서 하면 자꾸 클래스 부분이 주석처리 되어 에러나니깐 @SpringBootApplication을 엔터 몇번 치고 복붙하자)

6. 그러면 이제 세팅이 완료 됐으면 빌드 후 다시 실행해보면 정상적으로 작동한다. 맨 아래 Started DemoApplication in 2.162 seconds (JVM running for 3.37) 나온다면 정상 작동을 한 것이다. 

7. 그럼 정말 정상 작동하는지 브라우저에서 확인을 해봐야 하지 않나... 우선 index.html을 간단하게 만들어서 테스트해보자.

리소스 정적 폴더에 index.html을 만들면 바로 intellij에서는 해당하는 html 파일을 바로 브라우저를 킬 수 있게 되어 있지만 브라우저에서 https://localhost:8080에 접속하면 index.html에 작성한 페이지가 나온다. 물론 internet explorer은 https가 안된다. 주의하자!

 

 

'IT > Spring' 카테고리의 다른 글

[Spring Boot] 파일 업로드 / 다운로드 REST API  (0) 2021.07.13
[Spring] Spring Framework 이클립스 세팅  (0) 2021.06.03

오랜만에 이클립스 설치하고 JDK 다시 설치하고 하니깐 여간 귀찮다... 스프링도 세팅을 빨리 하고 좀 스터디해서 스프링에 대한것도 올려야겠다.

 

우선 이클립스 키면 스프링을 프로젝트를 생성할 수 없다. 그렇기 때문에 스프링을 추가해줘야한다. 

대부분에 이클립스 플러그인이나 툴들을 Eclipse Marketplace 에서 받으면 편하게 적용할 수 있다.

Help > Eclipse Marketplace 들어와서 스프링을 찾아주면된다.

설치됐는지 확인을 해보면 프로젝트 생성할 때 스프링이 있는지 확인하면 된다.

+ Recent posts