티스토리 뷰

728x90

파일 업로드 처리

  • 웹 개발에서 사용자가 가잔 파일을 서버에 업로드하고 이를 처리하는 기능은 필수적으로 필요한 기능
  • 파일 업로드는 기능 자체로도 신경 써야 하는 일이 많지만, JPA를 이용해서 처리할 때도 생각보다 많은
    주의가 필요
  • 이번 장은 @ManyToOne이 아닌 @OneToMany를 이용해 특정 게시물에 속한 파일들의 정보를 처리한다

7.1 첨부파일과 @OneToMany

파일 업로드를 위한 설정

  • 서블릿 3이상 되면서 서블릿 API 자체에 파일 업로드를 처리할 수 있는 API를 제공하므로 추가적인 라이브러리가 필요하지 않다
  • application.properties 파일에 설정 추가
# 파일 업로드 기능을 위한 설정
spring.servlet.multipart.enabled=true
spring.servlet.multipart.location=C:\\upload
spring.servlet.multipart.max-request-size=100MB
spring.servlet.multipart.max-file-size=100MB

com.board.upload.path=C:\\upload

업로드 처리를 위한 DTO

  • 파일 업로드는 MultipartFile 이라는 API를 이용해서 처리
  • 컨트롤러에서 파라미터를 MultipartFile로 지정해주면 간단한 파일 업로드 처리는 가능하지만 SwaggerUI와 같은 프레임워크로 테스트하기 불편하기 때문에 dto 패키지에 별도의 DTO로 선언해서 사용하는 것이 좋다.
  • dto 패키지에 upload 하위 패키지를 생성하고 UploadFileDTO를 선언
@Data
public class UploadFileDto {

    private List<MultipartFile> files;
}

컨트롤러와 Swagger UI 테스트

  • 실제 파일 업로드 설정은 controller 패키지에 UpDown-Controller를 작성해서 처리
  • UpDownController는 파일 업로드와 파일을 보여주는 기능을 메소드로 처리
  • @RestController로 설정하고 파일의 업로드를 처리하기 위해 upload() 작성
  • controller -> UpDownController.class
@Log4j2
@RestController
public class UpDownController {

    @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;
    }
}
  • 실제 파일을 처리할 때는 파일의 저장 경로가 필요하므로 application.properties의 설정 정보는 @Value를 이용해 처리
    @Value("${com.board.upload.path}") // import 시에 springframwork로 시작하는 value
    private String uploadPath;

            //  파일이 올라오면
        if(uploadFileDto.getFiles() != null){
            uploadFileDto.getFiles().forEach(multipartFile -> {   //반복
                log.info(multipartFile.getOriginalFilename());
            }); // end each
        } //end if
  • @Value는 application.properties 파일의 설정 정보를 읽어서 변수의 값으로 사용 가능
  • uploadPath는 파일을 업로드 하는 경로로 사용

첨부파일 저장

  • 같은 이름의 파일이면 문제가 생기는데 이 문제를 해결하고자 가장 많이 사용하는 방법은 java.util.UUID를 이용해 새로운 값을 만들어 내는 방법 (UUID는 중복될 가능성이 거의 없는 코드 값을 생성)
    • UUID(16자리) + _ + 원래 파일명
 //// 첨부 파일 저장 - 중복 가능성이 거의 없는 코드 값을 생성하는 UUID
                String uuid = UUID.randomUUID().toString();

                Path savePath = Paths.get(uploadPath, uuid + "_" + originalName);

                try{
                    multipartFile.transferTo(savePath);  // 실제 파일 저장
                } catch (IOException e){
                    e.printStackTrace();
                }
  • MultipartFile의 transfer()를 이용 하면 간단히 파일 업로드 완료
  • transfer()은 예외처리 필수
  • properties에 업로드 경로를 c:\upload 했을 때 오류 발생
    • c:/ 로 하니 정상 작동

Thumbnail 파일 처리

  • 첨부파일이 이미지일 때는 용량을 줄여 작은 이미지 (이하 썸네일)을 생성하고 이를 나중에 사용하도록 구성
  • Thumbnailator 라이브러리를 이용
  • 썸네일 라이브러리 추가
  • implementation group: 'net.coobird', name: 'thumbnailator', version: '0.4.17'
  • 썸네일 이미지는 업로드하는 파일이 이미지일 때만 처리하도록 구성
    • 파일 이름 맨 앞에 's_'로 시작하도록 구성
  • upload() 메서드 수정해 섬네일 생성하도록 구성
// 이미지 라면 
try{

     if(Files.probeContentType(savePath).startsWith("image")){ 
        File thumbFile = new File(uploadPath, "s_" + uuid + "_" + originalName);

        Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200, 200);
    }
}


//  의존성 설정 시 썸네일 최신 버전 아니면 오류가 발생한다. 이유는 모르겠다..

//썸네일
    // https://mvnrepository.com/artifact/net.coobird/thumbnailator
    implementation group: 'net.coobird', name: 'thumbnailator', version: '0.4.20'

업로드 결과의 반환처리

  • 여러 개의 파일이 업로드 되면 업로드 결과도 여러 개 발생 하게 되고 여러 정보를 반환해야 하므로
    별도의 DTO를 구성
  • dto 의 upload 패키지에 UploadResultDto 클래스 추가
  • UploadResultDto는 업로드된 파일의 UUID 값과 파일 이름 (fileName), 이미지 여부를 객체로 구성하고 getLink()를
    통해 첨부파일의 경로 처리에 사용
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
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;
        }
    }
}    
  • UploadResultDto의 getLink()는 나중에 JSON으로 처리될 때는 link라는 속성으로 자동 처리
  • UpDownController의 upload() List<UploadResultDto>를 반환하도록 수정
@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.getFiles() != null){

            // 업로드 결과의 반환처리
            // List<UploadResultDto>를 반환하도록 수정
            final List<UploadResultDto> uploadResultDtoList = 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 image = false;

                try{
                    multipartFile.transferTo(savePath);  // 실제 파일 저장

                    // 이미지 파일의 종류라면
                    if(Files.probeContentType(savePath).startsWith("image")){

                        // 업로드 결과의 반환 처리
                        image = true;

                        File thumbFile = new File(uploadPath, "s_" + uuid + "_" + originalName);

                        Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 200, 200);
                    }


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

                // 업로드 결과의 반환 처리
                uploadResultDtoList.add(UploadResultDto.builder().uuid(uuid)
                        .fileName(originalName)
                        .img(image).build()
                );


            }); // end each

            return uploadResultDtoList; 
        } //end if




        return null;
    }
}
  • 리스트를 쓰기 위해 반환타입을 제네릭으로 바꿔준다. public String -> public List<UploadResultDto>로 바꿔 준다.
  • 반환이 JSON 데이터가 생성 되고, getLink() 대신 link라는 속성이 생긴 것을 확인 할 수 있다.
[
  {
    "uuid": "31c30880-a776-4d89-a9bb-c9ba8c7e9f6a",
    "fileName": "dd.png",
    "img": true,
    "link": "s_31c30880-a776-4d89-a9bb-c9ba8c7e9f6a_dd.png"
  },
  {
    "uuid": "72701ddf-dc9a-4451-a035-2cd10124d799",
    "fileName": "mes.pdf",
    "img": false,
    "link": "72701ddf-dc9a-4451-a035-2cd10124d799_mes.pdf"
  }
]

첨부파일 조회

  • 첨부파일 조회는 가능하면 GET 방식으로 바로 가능하도록 설정
  • 첨부파일은 나중에 보안 문제가 발생하므로 코드를 통해서 접근 여부를 허용하도록 컨트롤러를 이용하는 것이 좋다
  • 첨부파일을 조회할 때는 '/view/파일이름' 으로 동작하도록 UpDownController에 작성
  • 조회테스트를 간단히 하기 위해 upload 폴더에 aaa.jpg 파일 하나 추가
  • UpDownController에 vieFileGET() 메소드 추가
// 첨부파일 조회
    @ApiOperation(value = "view 파일", notes = "GET 방식으로 첨부파일 조회")
    @GetMapping("/view/{fileName}")
    public ResponseEntity<Resource> viewFileGet(@PathVariable String fileName){

        Resource resource = new FileSystemResource(uploadPath + File.separator + fileName);

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

        try{
            headers.add("Content-Type", Files.probeContentType( resource.getFile().toPath()));
        } catch (Exception e){
            return ResponseEntity.internalServerError().build();
        }

        return ResponseEntity.ok().headers(headers).body(resource);

    }

첨부파일 삭제

  • 조회와 비슷한 DELETE 방식의 호출하는 형태로 첨부파일 삭제 구현
  • 첨부파일 삭제 할 때 이미지 라면 섬네일이 존재할 수도 있으므로 같이 삭제하도록 구현
  • UpDownController에는 removeFile() 메서드 작성
// 첨부파일 삭제
    @ApiOperation(value = "remove 파일", notes = "DELETE 방식으로 파일 삭제 ")
    @DeleteMapping("/remove/{fileName}")
    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();
            }

            log.info("삭제된 파일 이름은 : " + resourceName);

        } catch (Exception e){
            log.error(e.getMessage());
            e.printStackTrace();

        }
        resultMap.put("result", removed);

        return resultMap;

    }
  • 프로젝트를 개발할 때 파일 업로드는 필수로 필요한 기능이고, 여러 곳에서 사용할 수 있어야 한다.
728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함