ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 리엑트 및 Spring 레거시에서 파일 업로드 및 다운로드
    Language/Java 2023. 3. 18. 11:40

     

    오래된 레거시 버전의 스프링(spring 3.x)으로 인하여 기존에 알던 방식으로 사용할 수 없는 부분들이 많이 생겼고, 호랑이 담배피던 시절의 기억들을 떠올리면서 작업을 진행하게 되었다.

     

    처음에는 피그마보고 리엑트에서 퍼브리싱 및 컴포넌트 만들고 백엔드에서 기존에 공통으로 사용하는 파일업로드 넣고 디비 설계하고 넣기만 하면 될줄 알았는데.. 하루면 되겠지 했는데 꼬박 하루 반나절이나 작업한것 같다.. 맙소사!!

     

    그래서 공통으로 파일업로드 다운로드 하는것을 만들어 놓아야 나중에 내가 편하겠다고 생각하게 되었다.

    암튼 다음에도 레거시 프로젝트에서 시간을 절약하기 위해서 간단하게나마 정리해 두어야 겠다.

    다음 분기에는 꼭꼭 부트로 업그레이드 합시다..!!

     

    파일 업로드

    context 설정

    다시 xml에서 스프링 설정을 하게 될 줄이야... 맙소사..OTL..

    	...
    	<bean id="exceptionResolver" class="kr.gobang.core.base.ExceptionResolver" />
    	<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    		<property name="defaultEncoding" value="UTF-8"/>
    		<property name="maxUploadSize" value="10000000"/>	<!-- 10MB -->
    		<property name="maxInMemorySize" value="10000000"/>
    	</bean>
    	...

    기존 레거시 코드가 FileItem으로 되어 있는데 이렇게 설정을 변경해 주면 기존에 멀티파트로 받던 부분들이 동작을 안한다.

    FileItem으로 된 부분들을 MultipartFile로 받아서 교체해주는 작업이 별도로 필요하였다.

    그부분이 싫다고 지저분한 코드로 개발하고 싶진 않았다.필자는 

    처리되는 로직을 오버로딩 하여서 MultipartFile으로 교체 시켜 주었다. (FileItem, MultipartFile 거의 동일하여서)

     

    컨트롤러 레이어

    별다른건 없다 MultipartFile로 받게끔 해줬다.

      @RequestMapping(value = "/contracts/{contractId}/upload", method = RequestMethod.POST)
      public ModelAndView fileUpload(@PathVariable(value = "contractId") String contractId,
          @RequestParam(value = "file") List<MultipartFile> files,
          HttpServletRequest request) throws Exception {
    
        ...
        contractService.fileUpload(contractId, memberDTO.getID(), files);
        ...
        
        JsonModelAndView modelAndView = new JsonModelAndView();
        modelAndView.success();
        return modelAndView;
      }

     

    서비스 레이어

    이것도 별건 없다 그냥 유니크하게 s3에 올릴거 설정하고 다운로드때 필요한 정보들만 담아 두는 부분이다.

    s3로 putObject해서 넣는 부분이니 그부분은 생략하겠다.

      @BaseTransactional
      public String fileUpload(List<MultipartFile> files, int limitedFileCount, String fileSeq, String prefixPath, String procMemberId) throws IOException, CheckedException {
        // 업로드 갯수 제한
        ...
        
        for (MultipartFile multipartFile : files) {
          if (!multipartFile.isEmpty()) {
            // 파일 s3 업로드
            String fileName = multipartFile.getOriginalFilename();
            String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1);
            String s3FileName = UUID.randomUUID() + "." + fileExtension;
    
            String s3Path = prefixPath + s3FileName;
            AwsS3 s3 = new AwsS3(GobangConstants.S3_BUCKET_NAME);
            File orgFile = GobangUtils.uploadToTarget(s3, GobangUtils.multipartFileToFile(multipartFile), s3Path);
    
            // 파일 데이터 등록
            Map<String, String> insAttachFile = new HashMap<String, String>();
            insAttachFile.put("FILE_SEQ", fileSeq);
            insAttachFile.put("FILE_NAME", fileName);
            insAttachFile.put("FILE_EXTENSION", fileExtension);
            insAttachFile.put("CONTENT_TYPE", multipartFile.getContentType());
            insAttachFile.put("S3_PATH", s3Path);
            insAttachFile.put("ORDER_SEQ", "1");
            insAttachFile.put("REG_MEMBER_ID", procMemberId);
    
            attachFileDao.insert(insAttachFile);
          }
        }
    
        return fileSeq;
      }

     

    파일 다운로드

    사실 이부분에서 조금 헤멧는데 기존에 리엑트 설정 및 자바 설정을 분석하는데 시간이 허비되었다.

     

    컨트롤러 레이어

    여기서 react등 도메인이 틀리다면 CORS에서 추가된 헤더값은 리턴해주지 않는다.

    JWT를 쓴다면 많이 접했을 부분인데 필자는 다운로드 할때만 필요한 부분이라 아래와 같이 `Content-Disposition` 설정만 리턴이 되도록 추가만 해줬다.

      @RequestMapping(value = "/contracts/files/{attachFileId}/download", method = RequestMethod.GET)
      @ResponseBody
      public ResponseEntity<byte[]> download(@PathVariable(value = "attachFileId") String attachFileId, HttpServletRequest request, HttpServletResponse response) throws Exception {
        MemberDTO memberDTO = memberSessionManager.getMember(request, false);
        attachFileId = memberDTO.decrypt(attachFileId);
    
        AttachFileDownloadDTO attachFileDownloadDTO = attachFileService.getAttachFileBytesById(attachFileId);
    
        // CORS 헤더 허용 (React에서 파일명 받아야 하니깐 추가)
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
    
        // 헤더 설정
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(GobangUtils.getContentType(attachFileDownloadDTO.getContentType()));
        httpHeaders.setContentLength(attachFileDownloadDTO.getBytes().length);
        httpHeaders.setContentDispositionFormData("attachment", GobangUtils.getEncodeFiname(attachFileDownloadDTO.getFileName()));
    
        return new ResponseEntity<>(attachFileDownloadDTO.getBytes(), httpHeaders, HttpStatus.OK);
      }

     

    서비스 레이어

    디비에 저장된 파일 정보를 토대로 s3에서 가져오는 부분이다.

      public AttachFileDownloadDTO getAttachFileBytesById(String attachFileId) throws IOException {
        Map<String, String> attachFile = this.getAttachFileById(attachFileId);
    
        AwsS3 s3 = new AwsS3(GobangConstants.S3_BUCKET_NAME);
        AttachFileDownloadDTO attachFileDownloadDTO = new AttachFileDownloadDTO(attachFile.get("FILE_NAME"), attachFile.get("CONTENT_TYPE"), attachFile.get("S3_PATH"));
        attachFileDownloadDTO.setBytes(GobangUtils.getBytesByPath(s3, attachFileDownloadDTO.getS3Path()));
    
        return attachFileDownloadDTO;
      }

     

    유틸

      /**
       * s3 path(key)를 기준으로 파일 객체의 bytes를 가져온다.
       *
       * @param s3
       * @param path
       * @return
       * @throws IOException
       */
      public static byte[] getBytesByPath(AwsS3 s3, String path) throws IOException {
        return s3.getBytesByPath(path);
      }
    
      /**
       * 파일명 엔코딩
       *
       * @param fileName
       * @return
       * @throws java.io.UnsupportedEncodingException
       */
      public static String getEncodeFiname(String fileName) throws java.io.UnsupportedEncodingException {
        return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
      }
    
      /**
       * 컨텐트 타입
       *
       * @param keyname
       * @return
       */
      public static MediaType getContentType(String keyname) {
        String[] arr = keyname.split("\\.");
        String type = arr[arr.length - 1];
    
        switch (type) {
          case "txt":
            return MediaType.TEXT_PLAIN;
          case "png":
            return MediaType.IMAGE_PNG;
          case "jpg":
          case "jpeg":
            return MediaType.IMAGE_JPEG;
          default:
            return MediaType.APPLICATION_OCTET_STREAM;
        }
      }

     

    테이블

    JPA를 사용했다면 좀더 깔끔했을거라고 본다.

    CREATE TABLE IF NOT EXISTS `gobang`.`ATTACH_FILE` (
      `ID` INT NOT NULL AUTO_INCREMENT COMMENT '파일 번호',
      `FILE_SEQ` INT NOT NULL COMMENT '파일 시퀀스',
      `FILE_NAME` VARCHAR(100) NOT NULL COMMENT '파일 이름',
      `FILE_EXTENSION` VARCHAR(4) NULL COMMENT '파일 확장자',
      `CONTENT_TYPE` VARCHAR(20) NOT NULL COMMENT '컨텐트 타입',
      `S3_PATH` VARCHAR(300) NULL COMMENT 'S3 파일 경로',
      ...
    ENGINE = InnoDB
    ;

     

    리엑트

    리엑트에서 blob으로 호출하고 콜백 받은 부분을 조금만 수정해 줬다.

     

    호출부

    Request.download(`/contracts/files/${attachFileId}/download`);

     

    공통부

    blob으로 호출하고 프로미스에서 콜백할때 filename도 받아 올수 있게 dict로 리턴하고 받아서 처리하는 부분만 간단히 수정하였다.

    export class Request {
      static request(uri, param, method, headers, options) {
      
        ...
    
        const promise = Axios(Common.REQUSET_PREFIX + uri, options);
        const retPromise = new Promise((resolve, reject) => {
          promise
            .then((response) => {
              var data = response.data;
              
              ...
              
              if (data.RESULT_CD === 'SUCC_0001') {
                resolve(data, response);
              } else if (response.config.responseType === 'blob') {
                resolve({ data: data, response: response });
              } else {
                reject([data, response]);
              }
            })
            .catch((error, a, b, c) => {
              reject(error);
            })
            .finally(() => {
              Loading.hide();
            });
        });
        return retPromise;
      }
    
      static download(uri, filename) {
        Request.request(uri, null, 'GET', null, { responseType: 'blob' }).then(({ data, response }) => {
          console.log('response : ', response);
          if (Util.isEmpty(filename) === false) {
            fileDownload(data, filename);
          } else {
            if (response.headers['content-disposition'] === undefined) {
              filename = '파일명 미지정';
            } else {
              filename = response.headers['content-disposition'].split('filename=')[1].replace(/"/g, '');
              filename = decodeURIComponent(filename);
            }
    
            fileDownload(data, filename);
          }
        });
      }
      
      ...
      
      static put(uri, param) {
        return Request.request(uri, param, 'PUT');
      }
    
      static delete(uri, param) {
        return Request.request(uri, param, 'DELETE');
      }
    
      static upload(uri, param) {
        return Request.request(uri, param, 'POST', { 'Content-Type': 'multipart/form-data' });
      }
    }

     

     

    스프링 버전이 너무 낮다 보니 한계가 많이 생긴다.

    빠른 시일내에 프레임웍을 교체하고 많은 부분들 바꿔야 할것 같다.

    작업한 백엔드에서는 DTO도 못써서 너무너무 아쉽다.. 근데 썻다.. 몰라.. 롬복도 못써서..아놔..

    바꿀게 너무 많다.. ㅠㅠ

    'Language > Java' 카테고리의 다른 글

    Java Enum 적용  (0) 2024.01.15
    문자열 날짜 포멧 변경  (0) 2023.04.18
    JAVA/Stream  (2) 2022.11.29
    Spring Boot/어플리케이션 실행할때 JPA 스키마 생성 및 ddl, dml sql 실행  (0) 2022.08.18
    Mac OpenJDK 17 설치  (1) 2022.07.24

    댓글

Designed by Tistory.