본문 바로가기

Server Programming/Spring Boot Backend Programming

7장-1. 다중 파일 업로드 처리 (+MultipartFile)

반응형

요구사항

  • 게시물 목록에 썸네일 이미지 추가
  • 게시물 등록시에 이미지 업로드
  • 게시물 조회시 이미지 출력

 

첨부파일의 처리

  • 브라우저의 데이터를 파일 형태로 서버에 보관
  • 업로드한 데이터의 부가 정보를 처리

파일 업로드시 고려사항

  • UUID : 동일 파일명을 고유 파일명으로 처리
  • Thumbnailator : 썸네일 파일 처리
  • JSON 데이터로 업로드 결과 반환
  • GET 방식으로 업로드 파일 조회
  • DELETE 방식으로 업로드 파일 삭제

 


MultipartFile API를 이용한 파일 업로드

컨트롤러에서 파라미터로 전달해 업로드 처리를 수행하는 것보다

Swagger UI로 테스트하기 위해서 dto 패키지에 UploadFileDTO를 별도의 DTO로 선언

 

1. apllication.properties에 파일 관련 설정 추가

spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=10MB

org.zerock.upload.path='파일 저장 경로'

 

2. 업로드 처리를 위한 UploadFileDTO 추가

: /dto/upload/UploadFileDTO

-다중 파일을 업로드하므로 List에 담아서 처리한다.

 

package org.zerock.b01.dto.upload;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Data
public class UploadFileDTO {
    private List<MultipartFile> files;
}

 

3. 컨트롤러와 Swagger UI 테스트

 

(1) 파일 업로드를 담당하는 UpDownController 컨트롤러 작성

-JSON으로 처리하기 위해 @RestController 어노테이션 사용

-파일 업로드와 파일을 보여주는 메소드 작성

  • 메소드 명 : upload
  • HTTP 메소드 : POST / MULTIPART_FORM_DATA_VALUE
  • 파라미터 : UploadFileDTO
  • 리턴타입 : String
@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    
    return null;
}

 

 

(2) Swagger UI를 통해 파일 업로드 확인

-파일 업로드 할 수 있는 메서드 작동 확인

 

4. 실제 파일 업로드를 위해 경로 설정 및 다중 파일 처리

-application.properties의 설정 정보를 @Value를 이용해 처리한다.

-DTO를 이용해 파일이 여러개인 경우를 처리하도록 forEach문 작성

//스프링 설정에 추가한, 파일 경로를 @Value를 이용해 변수로 설정
@Value("${org.zerock.upload.path}")
private String uploadPath;

@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    //업로드한 파일이 존재한다면
    if(uploadFileDTO!=null){
        //파일을 하나씩 가져온다.
        uploadFileDTO.getFiles().forEach(multipartFile -> {
            //업로드한 파일의 파일명을 로그로 출력
            log.info(multipartFile.getOriginalFilename());
        });
    }

    return null;
}

파일 업로드시 고려사항

  • UUID : 동일 파일명을 고유 파일명으로 처리
  • Thumbnailator : 썸네일 파일 처리
  • JSON 데이터로 업로드 결과 반환
  • GET 방식으로 업로드 파일 조회
  • DELETE 방식으로 업로드 파일 삭제

 

1. UUID를 이용해, 동일 파일명 문제 해결

-파일을 서버에 저장할 때, 동일 파일명인 경우 에러가 발생한다

-16자리 코드값을 생성하는 java.util.UUID를 이용해 고유한 파일명을 만들어 낸다 : UUID +_+파일명

 

//스프링 설정에 추가한, 파일 경로를 @Value를 이용해 변수로 설정
@Value("${org.zerock.upload.path}")
private String uploadPath;

@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    //업로드한 파일이 존재한다면
    if(uploadFileDTO!=null){
        //파일을 하나씩 가져온다.
        uploadFileDTO.getFiles().forEach(multipartFile -> {
            //원본파일명 변수로 저장하고, 로그로 출력
            String originalName=multipartFile.getOriginalFilename();
            log.info(originalName);

            //UUID 생성
            String uuid = UUID.randomUUID().toString();

            //파일 경로 변수 생성
            Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);

            //파일 업로드 예외처리
            try{
                //파일을 지정한 경로에 업로드하는 메서드
                multipartFile.transferTo(savePath);
            }catch(IOException e){
                e.printStackTrace();
            }

        });
    }

 

2. Thumbnailator 라이브러리 사용해, 썸네일 이미지 처리

-이미지의 경우 서버의 부하를 줄이고, 가독성을 위해 섬네일을 생성해 사용한다.

 

(1) 의존성 추가

implementation 'net.coobird:thumbnailator:0.4.17'

 

(2) 섬네일 이미지 생성을 위한 upload() 메서드 변경

-생성하는 파일 이름 규칙

-섬네일 이미지 생성하는 파일 형식 설정

//스프링 설정에 추가한, 파일 경로를 @Value를 이용해 변수로 설정
@Value("${org.zerock.upload.path}")
private String uploadPath;

@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    //업로드한 파일이 존재한다면
    if(uploadFileDTO!=null){
        //파일을 하나씩 가져온다.
        uploadFileDTO.getFiles().forEach(multipartFile -> {
            //원본파일명 변수로 저장하고, 로그로 출력
            String originalName=multipartFile.getOriginalFilename();
            log.info(originalName);

            //UUID 생성
            String uuid = UUID.randomUUID().toString();

            //파일 경로 변수 생성
            Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);

            //파일 업로드 예외처리
            //1. 파일 업로드
            //2. 섬네일 처리
            try{
                //1. 파일을 지정한 경로에 업로드하는 메서드
                multipartFile.transferTo(savePath);
                //2. 섬네일 처리
                //(1) 파일 형식이 이미지인지 확인
                if(Files.probeContentType(savePath).startsWith("image")){
                    //(2) 섬네일 파일 이름 규칙
                    File thumbFile = new File(uploadPath, "s_"+uuid+"_"+originalName);

                    //(3) 이름규칙에 따라 섬네일 이미지 생성
                    //: 4개의 파라미터 전달
                    Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200, 200);
                }


            }catch(IOException e){
                e.printStackTrace();
            }

        });
    }

 

3. JSON 데이터로 업로드 결과 반환

-여러 개 파일 업로드시 여러 개의 업로드 결과 반환

-결과를 반환하는 DTO 구성해 처리

 

(1) 업로드 결과를 반환하는 UploadResultDTO 클래스 작성

-/dto/upload/UploadResultDTO

-@Data, @Builder, @AllArgsConstructor, @NoArgsConstructor : 정보를 반환하는 DTO이기 때문에 사용하는 어노테이션

  • 멤버 변수
    • UUID 값
    • 파일 이름
    • 이미지 여부
  • 메서드
    • getLink() : 첨부파일의 경로 처리에 사용하는 메서드로, String을 반환하므로 JSON 문자열 처리시 link 속성이 된다.
package org.zerock.b01.dto.upload;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UploadResultDTO {
    private String uuid;
    private String fileName;
    private boolean img;

    //이미지 여부에 따라서 달라지는 경로
    public String getLink(){
        if(img){
            return "s_"+uuid+"_"+fileName;
        }else{
            return uuid+"_"+fileName;
        }
    }

}

 

(2) UpDownController의 upload() 메서드 변경

  • 메소드 명 : upload
  • HTTP 메소드 : POST / MULTIPART_FORM_DATA_VALUE
  • 파라미터 : UploadFileDTO
  • 리턴타입 : List<UploadResultDTO>

변경 전, upload()

//스프링 설정에 추가한, 파일 경로를 @Value를 이용해 변수로 설정
@Value("${org.zerock.upload.path}")
private String uploadPath;

@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    //업로드한 파일이 존재한다면
    if(uploadFileDTO!=null){
        //파일을 하나씩 가져온다.
        uploadFileDTO.getFiles().forEach(multipartFile -> {
            //원본파일명 변수로 저장하고, 로그로 출력
            String originalName=multipartFile.getOriginalFilename();
            log.info(originalName);

            //UUID 생성
            String uuid = UUID.randomUUID().toString();

            //파일 경로 변수 생성
            Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);

            //파일 업로드 예외처리
            //1. 파일 업로드
            //2. 섬네일 처리
            try{
                //1. 파일을 지정한 경로에 업로드하는 메서드
                multipartFile.transferTo(savePath);
                //2. 섬네일 처리
                //(1) 파일 형식이 이미지인지 확인
                if(Files.probeContentType(savePath).startsWith("image")){
                    //(2) 섬네일 파일 이름 규칙
                    File thumbFile = new File(uploadPath, "s_"+uuid+"_"+originalName);

                    //(3) 이름규칙에 따라 섬네일 이미지 생성
                    //: 4개의 파라미터 전달
                    Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200, 200);
                }


            }catch(IOException e){
                e.printStackTrace();
            }

        });
    }

    return null;
}

 

 

변경 후, upload()

-JSON 데이터로 처리

@ApiOperation(value = "Upload POST", notes="POST 방식으로 파일 업로드")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public List<UploadResultDTO> upload(UploadFileDTO uploadFileDTO){
    log.info(uploadFileDTO);
    
    
    //업로드한 파일이 존재한다면
    if(uploadFileDTO!=null){
        
        //리턴할 객체 선언
        final List<UploadResultDTO> list = new ArrayList<>();
        
        //파일을 하나씩 가져온다.
        uploadFileDTO.getFiles().forEach(multipartFile -> {
            //원본파일명 변수로 저장하고, 로그로 출력
            String originalName=multipartFile.getOriginalFilename();
            log.info(originalName);

            //UUID 생성
            String uuid = UUID.randomUUID().toString();

            //파일 경로 변수 생성
            Path savePath = Paths.get(uploadPath, uuid+"_"+originalName);
            
            
            //이미지 여부를 판단하는 boolean
            //: 이미지 여부에 따라 getLink() 값이 결정되기 때문에
            boolean image=false;

            //파일 업로드 예외처리
            //1. 파일 업로드
            //2. 섬네일 처리
            try{
                //1. 파일을 지정한 경로에 업로드하는 메서드
                multipartFile.transferTo(savePath);
                //2. 섬네일 처리
                //(1) 파일 형식이 이미지인지 확인
                if(Files.probeContentType(savePath).startsWith("image")){
                    //(2) 섬네일 파일 이름 규칙
                    File thumbFile = new File(uploadPath, "s_"+uuid+"_"+originalName);

                    //(3) 이름규칙에 따라 섬네일 이미지 생성
                    //: 4개의 파라미터 전달
                    Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200, 200);
                }


            }catch(IOException e){
                e.printStackTrace();
            }
            
            //빌더패턴을 이용해 list에 추가
            list.add(UploadResultDTO.builder()
                            .uuid(uuid)
                            .fileName(originalName)
                            .img(image).build());
        });
        
        return list;
    }

    return null;
}

 

4. GET 방식으로 업로드 파일 조회

-컨트롤러에서 첨부파일의 보안 문제를 해결하기 위해 코드를 통해 접근 여부를 허용

-'/view/파일이름'을 통해 조회가 가능하도록 구성

 

(1) 파일 조회를 위한 viewFileGET() 메소드 작성

@ApiOperation(value=" ", notes = "GET방식으로 업로드 파일 조회")
@GetMapping("/view/{fileName}")
//import org.springframework.core.io.Resource;
//ResponseEntity는 HttpHeader와 HttpBody를 포함하는 HttpEntity를 상속받아 구현한 클래스

public ResponseEntity<Resource> viewFileGet(@PathVariable String fileName){
    Resource resource = new FileSystemResource(uploadPath+File.separator+fileName);

    String resourceName = resource.getFilename();
    HttpHeaders headers =  new HttpHeaders();

    try{
        //파일의 형식을 header에 추가
        headers.add("Content-Type", Files.probeContentType(resource.getFile().toPath()));
    }catch (Exception e){
        //e.printStackTrace();
        return ResponseEntity.internalServerError().build();
    }

    //헤더에 헤더 넣고, 본문에 리소스 넣어서 리턴
    return ResponseEntity.ok().headers(headers).body(resource);
}

 

5. DELETE 방식으로 업로드 파일 삭제

-파일이 이미지인 경우 섬네일도 함께 삭제

-UpDownController에 removeFile() 메서드 작성

@ApiOperation(value="remove 파일", notes = "DELETE 방식으로 업로드 파일 삭제")
@DeleteMapping(value="/remove/{fileName}")
//이미지면 섬네일도 함께 삭제하기 위해 Map 자료구조이용
public Map<String, Boolean> removeFile(@PathVariable String fileName){
    Resource resource=new FileSystemResource(uploadPath+File.separator+fileName);
    
    String resourceName= resource.getFilename();
    
    Map<String, Boolean> resultMap = new HashMap<>();

    boolean removed=false;
    
    try{
        String contentType = Files.probeContentType(resource.getFile().toPath());
        //먼저 파일을 지우고
        removed = resource.getFile().delete();
        
        //해당 파일이 이미지라면, 섬네일도 지운다.
        if(contentType.startsWith("image")){
            //이름 규칙에 따라 섬네일 파일 선택
            File thumbnailFile = new File(uploadPath+File.separator+"s_"+fileName);
            
            thumbnailFile.delete();
        }
        
    }catch (Exception e){
        log.error(e.getMessage());
    }
    //에러가 발생하지 않았다면 결과 반환
    resultMap.put("result", removed);
    
    return resultMap;
}

 

 

반응형