티스토리 뷰

728x90
반응형

 

들어가기전

헤커톤을 통해 진행했던 사이드 프로젝트에서 프론트와 백엔드로 분리된 서버간의 파일 데이터를 처리하는 과정에서 발생했던 이슈에 대한 회고입니다.

 


사용기술

  • Front : next.js, 
    • 통신 방식 : axios
  • Back : Spring Data JPA
    • Rest ful 규격 준수

프로세스 설명

클라이언트의  Axios 통신 방식을 통해 formdata에 챌린지 정보가 담긴 json 데이터와 섬네일로 사용할 이미지 파일 정보를 등록하는 프로세스 입니다.

 

그리고 저희 시스템은 파일 정보에 대한 내용을 챌린지라는 테이블에 이미지 컬럼으로 가지고 있는 것이 아닌 file 정보만을 따로 관리하는 테이블을 구축하여 진행하고자 했습니다.

 

파일 관련된 테이블을 따로 구축한 이유는 챌린지 테이블 뿐만 아닌 다른 테이블 또한 이미지 관련 처리를 해야하는 시점에 있었기에 파일들을 각각 관리하기 보단 한곳에 모아 관리하는 것이 편할 것이라고 판단했습니다.

 

이렇게 관리했을 때 로그 형식으로 관리가 되고 데이터베이스에서 파일 데이터에 대한 정보가 세부적으로 관리가 되어있어 가독성이 좋을 것이라고 판단하였습니다.

 

파일 등록 프로세스

파일 등록 프로세스

 


Axios를 통한 파일 처리에 대한 백엔드 처리

@RequestPart를 이용하여 파일 과 데이터를 처리하는 방법

@PostMapping(value="/challenge/ins", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> insertChallenge(final @Valid @RequestPart ChallengeCreateRequest challengeCreateRequest,
                                           @RequestPart("_file") MultipartFile multipartFile)

 

  • 위 코드를 보게 되면 ChallengeCreateRequest라는 객체와 MultipartFile을 받는 형태로 구축하게 됩니다.
  • Content-type은 Multipart/formdata만을 허용하도록 consumes를 설정합니다.
  • 어노테이션은 @RequestPart로 받아야만 정상적으로 받게 됩니다.
  • 이렇게 되면 Axios로 전달하는 값을 받을 수 있게 됩니다.

이런 처리 방식은 장점으로는 되게 간단하게 파라미터 값을 처리할 수 있고 어떤 파라미터를 받아 처리하는지 알 수 있어 직관적으로 확인이 쉽습니다.

 

하지만 현 시스템은 파일 처리가 많이 사용되는 시스템입니다. 그렇게 될 경우 위 같은 방식은 반복되는 코드가 많아지고 아무리 파일 등록에 대한 utill이 작업이 된다 할지라도 Controller단에서 MultipartFile을 가공하는 작업이 많아지면 코드 가독성 및 효율적인 면에서 떨어진다고 판단했습니다.

 

그래서 생각한 방법은 API단계에서 파일 처리를 하지 않고 service 단에서 자동으로 파일 등록 처리될 수 있는 방식으로 진행하고자 했습니다. 즉, Controller단에서 따로 MultipartFile을 파라미터로 받지 않아도 처리될 수 있도록 하고자 했습니다.

 

변경한 파일 처리 API

 @FileUploadAction()
 @PostMapping(value="/challenge/ins", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
 public ResponseEntity<?> insertChallenge(final @Valid @RequestPart ChallengeCreateRequest challengeCreateRequest) throws Exception {

 

파일 처리 부분을 따로 분리하여 공통 서비스로 처리를 하게 되니 위와 같이 깔끔한 코드로 변경되었습니다.

 

파일 처리 자동화 단계

1. 파일 데이터를 ThreadLocal에 담기위한 인터셉터 작업

// 어노테이션 FileUploadAction 어노테이션일 경오에만 인터셉터가 타도록 수정
if(handler instanceof HandlerMethod handlerMethod) {
    Method method = handlerMethod.getMethod();
    if(method.isAnnotationPresent(FileUploadAction.class)){
        System.out.println("------------------------ cms file support intercepter ----------------------------");
        CmsFileThreadLocalHolder.setRequest(request);
    }
}

 

  • 위 코드를 보게 되면 interceptor 단계에서 처리하는 코드 입니다. 해당 인터셉터를 타게 되면 HttpServletRequestCmsFileThreadLocalHolder라는 곳에 담습니다.  CmsFileThreadLocalHolder의 역할은 ThreadLocal안에 HttpServletRequest를 MultipartHttpServletRequest로 캐스팅하여 해당 스레드가 종료되기 전까지 관리하게 됩니다.
  • 이렇게 ThreadLocal에 담아 처리하는 이유는 추후 service단에서 file에 대한 service 처리를 하기위해서 담습니다.

2. @FileUploadAction 파일 전용 어노테이션 커스텀 적용

  • 1번에서의 코드를 보면 FileUploadAction이라는 어노테이션이 있습니다. 이 어노테이션은 Controller 단에서 파일이 처리되는 API에 대해서만 처리하기 위한 어노테이션입니다.
  • 해당 어노테이션을 설정하여, 리플렉션을 통해 파일 처리를 하는 API임을 알려 불필요한 ThreadLocal 생성을 방지합니다.

3. getMultiFileMap()를 이용하여 파일 업로드 처리

MultipartHttpServletRequest multirequest = CmsFileThreadLocalHolder.getRequest();
MultiValueMap<String, MultipartFile> checkFiles = multirequest.getMultiFileMap();

 

  • 파일을 처리하는 service에서 위의 코드를 통해 파일을 처리해줍니다.
  • multirequest에 ThreadLocal에 담아두었던 정보를 가져옵니다. 
  • multirequest의 getMultiFileMap() 메소드를 통해 파일 정보들을 가져와 업로드 처리를 합니다.

 

위 과정들을 통해 파일들을 쉽게 처리할 수 있게 되었습니다. 그러나 다른 이슈가 발생하게 되었습니다..

분명 Postman으로 테스트 했을 때는 파일이 정상적으로 등록되는 것을 확인했습니다. 하지만 클라이언트와의 통신을 통해 파일을 등록하고자할 경우 에러가 발생하게 되었습니다.


 

이슈 발생

Postman으로 테스트했을 때에는 이상이 없었습니다. 하지만 Axios를 이용한 파일을 처리하는 과정에서 에러가 발생하게 되었습니다.

 

발생 구간은 파일에 대한 정보가 MultipartHttpServletRequest 까지 받아지는 것을 확인하였지만 파일에 대한 확장명을 체크하는 과정에서 에러가 발생하게 되었습니다.

 

원인

 MultiValueMap<String, MultipartFile> checkFiles = multirequest.getMultiFileMap();

 

  • 원인은 getMultiFileMap()을 통해 파일 정보들을 가져오려고 하였지만, 파일 정보 뿐만 아니라 JSON 정보도 포함되어 넘어오는 과정에 있어 확장명 체크하는 부분에서 파일이 아니기에 에러가 발생하게 되었습니다.
  • getMultiFileMap 파일 정보를 확인해보니
    • FileName : Blob
    • Content-type : application/json
    • FileName : image.jpg
    • Content-type : image/jpeg

getMultiFileMap에 Json 정보와 file 정보 둘다 포함이 된 상태로 넘어오게 되었습니다. 그렇다보니 파일 확장명을 체크하는 부분에서 JSON 데이터에 대한 지속적인 에러가 발생했었습니다.

 

그렇다면 왜?? MultiFileMap에 파일 정보 뿐만 아니라 json 값도 포함이 되었을까요??

 

클라이언트에서 통신한 코드

 const formData = new FormData();

// ChallengeCreateRequest 데이터를 formData에 추가
formData.append('challengeCreateRequest', new Blob([JSON.stringify(challengeData)], { type: 'application/json' }));

// 파일 추가 (여기서는 'file'이 업로드할 파일 객체)
if (file) {
formData.append('_file', file);
}

try {
const response = await axios.post('http://localhost:8080/challenge/ins', formData, {
  headers: {
    'Content-Type': 'multipart/form-data',
    'Authorization': `Bearer ${token}`,  // 토큰을 헤더로 전달
  },
});

console.log(response.data);  // 서버에서 받은 응답
} catch (error) {
console.error('Error uploading challenge:', error);
}

 

  • 위 코드와 같이 클라이언트에서는 Json 정보와 파일 정보를 Formdata에 담아서 전달하고자 했습니다.
  • axios 같은 경우  multipart/formdata로 전송하기 위해서는 파일이 아닌 데이터는 new Blob로 감싸서 Content-type을 지정해주고 전달해야했습니다.
  • 만약 백엔드에서"@RequestPart를 이용하여 파일 과 데이터를 처리하는 방법" 부분에 대한 처리를 했다면 이상없이 파일을 받을 수 있었겠지만 MultipartHttpServletRequest를 이용한다면 조금 더 신경을 써줘야했습니다.

클라이언트에서 multipart/form-data 형식으로 요청을 보내면, 요청 본문에 포함된 모든 데이터(파일과 일반 폼 필드 포함)는 MultipartHttpServletRequest에서 관리가 됩니다. getMultiFileMap() 메소드 같은 경우 파일 업로드에 관련된 모든 파라미터를 반환하는 역할을 합니다. 그렇기 때문에 요청에 포함된 모든 파일 뿐만 아니라, ChallengeCreateRequest와 같은 일반적인 필드 또한 포함하게 됩니다.

그리고 axios를 통해 ChallengeCreateRequest를 처리한 부분을 보게되면, new Blob로 감싸서 전달하기 때문에 파일명이 "Blob"로 나타나게 됩니다..


해결방안

이전 까지는 MultiFileMap이라는 메소드는 파일 형식만이 처리하는 줄 알았습니다. 어떻게 보면 말그대로 파일 형식만을 처리하는 것이 맞긴하네요… 단지 클라이언트에서 전달하는 방식에서 Json을 파일 형식에 감싸서 던졌기 때문에 MultiFileMap은 그걸 온전히 받아낸 죄ㅠㅠ 밖에 없네요..

이러한 이유로 Content-type을 통해 파일 타입을 체크하고 파일에 해당하는 경우에만 파일 등록이 처리 될 수 있도록 필터를 거쳐주면 됩니다. Content-type의 조건은 아무래도 application을 포함하는 경우에 대한 내용들은 다 거르면? 되겠죠??

이러한 점을 돌아본다면, 아래와 같은 코드가 될 것 입니다.

//첨부 파일 가능한지 먼저 체크
for(Map.Entry<String, List<MultipartFile>> entry : checkFiles.entrySet()){
    MultipartFile multiFile = entry.getValue().get(0);
    String saveFileName = multiFile.getOriginalFilename();
    //application이 아닌 Content-type만을 잡을 수 있도록 처리
    if(!Objects.requireNonNull(multiFile.getContentType()).contains("application")) {
        logger.info("Intercepted file: {}, ContentType: {}",  multiFile.getOriginalFilename(), multiFile.getContentType());
        //첨부 파일 확장명 체크
        if (!fileUploadUtil.validateUploadFile(saveFileName, fileUploadUtil.ALLOW_EXTS)) {
            throw new DoNotMatchExtException("해당 파일은 첨부할 수 없는 파일 형식입니다.");
        }
        files.add(entry.getKey(), multiFile);
    }
}

 

마치면서…

파일 처리에 있어서 단순히 파라미터로 받아 처리만 해주면 되겠지?? 라는 생각을 했었습니다.

예를들어 jsp라던지.. 프론트와 백엔드가 분리되어있지 않았다면 좀 더 수월하겠지만, 분리된 시점에서의 파일 처리는 여러 이슈가 있을 수 있겠다는 생각이 들었습니다.

서버 처리에 대해서만 생각했었지 클라이언트에서의 처리에 대해서는 생각해보지 않았었는데.. 코드를 작성할 때는 나뿐만 아닌 연관된 기술들에 대해서도 신경을 써야겠구나.. 라는 생각을 하게 되는 시간이 되었습니다.

그리고 MultipartHttpServletRequest를 좀 더 알고 쓰자...^^

 

자세한 코드는 아래 git 저장소를 통해 확인해보세요.

https://github.com/FTHON-6TEAM/Challenger-Be

 

GitHub - FTHON-6TEAM/Challenger-Be: 이번주 챌린지 백엔드 서버

이번주 챌린지 백엔드 서버. Contribute to FTHON-6TEAM/Challenger-Be development by creating an account on GitHub.

github.com

 

728x90
반응형
250x250
반응형
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함