참으로 어렵다.
파일 업로드 구현을 위해서 찾아본 자료도 참 많은데, 실상 구현을 하려니 너무 다른 방법들로 구현된 경우가 많아서 어디를 참고해야 할지 고민이 되었다.

허나, 어쩌랴... 뭐 방법이야 다양하겠지만, 우선적으로 고려대상은 Spring의 레퍼런스로 잡았다.

이번에도 시나리오를 작성해 보자.(마음이 답답하다. 어찌 풀어가야 할지...)
1. 설정파일에서 크게 2가지를 먼저 추가해본다.
   가. 첨부파일용 CommonsMultipartResolver 설정
       (org.springframework.web.multipart.commons.CommonsMultipartResolver)
   나. 업로드 디렉토리 지정용 FileSystemResource 설정
       (org.springframework.core.io.FileSystemResource)
2. MultipartFile을 담아 둘 빈 작성
3. 업로드를 처리할 컨트롤러 작성
4. 실제 업로드를 수행할 업로드용 빈 작성
5. 설정 파일에서 빈 묶기

아... 눈 앞이 아른해진다.

1번부터 차근차근 진행해 보겠다. 물론 순서는 대략적으로 잡아보았다.
<bean id="multipartResolver"
          class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="maxUploadSize" value="100000000" />
    <property name="uploadTempDir" ref="uploadDirResource" />
</bean>

최대 10메가 바이트(정확히 10메가는 아니지만)까지 업로드가 가능하도록 설정하고 있다.
그리고, 업로드용 디렉토리를 지정하는데 이건 API를 보면 알겠지만 Resource를 받게 되어 있다.

uploadTempDir로 받는 uploadDirResource를 작성해 보자.
<bean id="uploadDirResource"
          class="org.springframework.core.io.FileSystemResource">
    <constructor-arg>
       <value>디렉토리를 지정합니다.</value>
    </constructor-arg>
</bean>

일단 빈을 이렇게 설정해 두자.

이제, MultipartFile을 담을 빈을 작성해 보자. 이 녀석은 이후에 컨트롤러에 commandClass로 넣어줄 거다.

public class FileUploadBean {  
    // Constructor
    public FileUploadBean() {}  
   
    private MultipartFile file;

    public MultipartFile getFile() {
        return file;
    }
    public void setFile(MultipartFile file) {
        this.file = file;
    }   
}
간단하다. 그냥 MultipartFile을 setter/getter로 공개하고 있다.
MultipartFile로 만든 이유는, 이게 아무래도 제일 접근하기 쉬웠다. 다른 녀석은 뭐 잡다하게 더 코딩이 늘어나서... 따로 캐스팅이 필요없어져서 편해서 사용했다.(더 고민이 필요한 부분이겠다. 아직도 이게 왜 필요한지 감감할 뿐이다.)

컨트롤러를 만들어보자.
SimpleFormController를 확장해서 만들건데, 이건 개발자가 어떻게 구성하느냐에 따라 많은 차이를 보인 부분이기도 하다.(여러 소스를 뒤져보니, 이 부분에서 너무 다양한 구현 방법이 있어서 혼란 그 자체였다.)

가장 심플하게 만들어 보는게 주 목적이니, 간단하게 onSubmit 메소드만 구현해야겠다.

protected ModelAndView onSubmit(
    HttpServletRequest request, HttpServletResponse response,
    Object command, BindException errors) throws ... {
    MultipartHttpServletRequest multipartRequest =
        (MultipartHttpServletRequest) request; // request를 casting 한다.
    MultipartFile file = multipartRequest.getFile("filex"); // 여기서 쓰인 filex는 html 폼에서
                                                                           // input type=file name=filex
                                                                           // 이거랑 맞아야 한다.
    // 이래 놨더니, file로 업로드되는 파일에 대한 갖가지 정보를
    // 뽑아 볼 수 있었다. API 참고
   
    uploadFile.setMultipartFile( file );  // uploadFile은 DI 된 것.
                                                    // 실제 업로드를 수행하는 빈이다.
    uploadFile.setRealUploadPath( realUploadPath );  // realUploadPath도
                                                                           // 빈설정 파일에서 DI 했다.
    int result = uploadFile.upload();  // uploadFile에게 업로드하라고 위임
   
    ....
    ....
    // result에 따른 에러 처리
    return new ModelAndView .... // 처리가 끝났으니 논리적인 뷰 이름을 지정해서 이동
}

uploadFile을 어떤 이는 static으로 구성하기도 했는데, 난 그냥 외부에서 주입하도록
빈으로 선언했다. 잘 못한건가? -_-; 그렇게 하고파서...

uploadFile에게 일을 시켰으니, 어떻게 일을 처리할지 알아보자.
uploadFile의 upload 메소드만 보자.

public int upload() throws ... {
    // 업로드 파일에 대한 기초정보를 얻는다.
    // MultipartFile에 대한 API를 보면 얻을 수 있는 정보를 알 수 있다.
    String fileName = multipartFile.getName(); // multipartFile은 컨트롤러에서 전해줬다.
    String originalFileName = multipartFile.getOriginalFilename();
    String contentType = multipartFile.getContentType(); // 별걸 다 알아다 준다.
    long fileSize = multipartFile.getSize();

    // 업로드되는 파일 이름이 중복되면 안되므로 UUID를 써서 엄청 긴 이름을 주겠다.
    UUID randomUUID = UUID.randomUUID();

    // 파일 용량 체크
   ....

   // 파일 확장자 체크
   ...

  // 스트림을 설정한다. 결국은 스트림으로 파일을 전송한다.
  InputStream inputStream = null;
  OutputStream outputStream = null;

  try {
    if( fileSize > 0 ) {
      inputStream = multipartFile.getInputStream(); // 스트림을 얻어올 수 있다.
      File realUploadDir = new File(업로드될 디렉토리); // 업로드 디렉토리도 받아뒀다.
      if( !realUploadDir.exists() )
        realUploadDir.mkdirs();  // 업로드 되는 디렉토리가 없다면, 만들어준다.
     
      String organizedFilePath = 디렉토리 + "/" + randomUUID + "_" + originalFileName;
      outputStream = new FileOutputStream( organizedFilePath );
     
      // 버퍼를 지정해서 올린다.
      int readBytes = 0;
      byte[] buffer = new byte[8192];
     
      while((readBytes = inputStream.read(buffer, 0, 8192)) != -1 ) {
          outputStream.writer(buffer, 0, readBytes);
      }
    }
  } catch( ... ) {

  } finally {
    // 스트림을 반드시 닫는다.
    outputStream.close();
    inputStream.close();
  }
  return 결과값; // 이 결과값이 위의 컨트롤러에 전달된다.
}

길었지만, 다 되었다.
이 녀석을 설정 파일에서 빈으로 선언해주고, 컨트롤러에 DI해주면 되겠다.

그럼 이제, 지금까지 만든 컨트롤러와 파일업로드 처리 빈을 설정파일에 기록해 보자.

<bean id="uploadBean" class="company.project.mvc.common.util.UploadFile" />

<bean id="fileUpladController" class="company.project......아까만든 컨트롤러">
  <property name="commandClass">
    <value>company.project....FileUploadBean</value>
  </property>
  <property name="realUploadPath">
     <value>업로드 될 위치</value>
  </property>
  <property name="uploadFile" ref="uploadBean" />
  <property name="formView" value="폼의 논리적인 이름" />
  <property name="successView" value="성공시 갈  논리적인 뷰 이름" />
</bean>

이제 다 되었다. 묶는거 까지 다 되었다.

테스트를 해보는 일만 남았다.

less..

테스트 결과는?
너무 잘 된다. 다행이다. 더 생각해보고 싶은건, Commons에서 제공하는 MultipartUpload와 Spring과 어떻게 잘 엮어지는지 그게 궁금하다.
솔직히 원리를 100%다 이해는 못했다.

그래서, 챙피하다. 된걸로 만족한다면, 챙피한 일이다.

less..



더 생각하고, 더 이해해 보길 기대해 본다.
반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기