Development/Java

SpringMVC POST 파일 처리와 POST 맵핑, Redirection #13

evagrim 2022. 9. 3. 10:07


< POST 파일 업로드 설정하기 >


post의 인코딩 타입을 마찬가지로 쿼리 문자열이 아닌 바이너리 데이터를 전송할 수 있도록
html과 server를 모두 multipart/form-data 로 바꿔줘야 한다


참고1 - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/commons/CommonsMultipartResolver.html

참고2 - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/commons/CommonsFileUploadSupport.html



[멀티파트 설정]


commons-fileupload 라이브러리 설치
[pom.xml]
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>



서버 DI 설정
[servlet-context.xml]
<bean id="multipartResolver
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- setting maximum upload size -->
<property name="maxUploadSize" value="314572800" />
</bean>

>> CommonsMultipartResolver 객체 생성해서 setMaxUploadSize DI 해주고 300Mb까지 허용

setMaxUploadSize(long maxUploadSize) 업로드 허용할 전체 파일 사이즈
setMaxUploadSizePerFile(long maxUploadSizePerFile) 업로드 허용할 개개의 파일 사이즈
setMaxInMemorySize(int maxInMemorySize) 업로드 시 사용할 임시 메모리 사이즈
setUploadTempDir(Resource uploadTempDir) 임시 메모리 사이즈를 초과할 경우 저장할 임시 디렉토리

MaxInMemorySize, UploadTempDir 은 딱히 지정해주지 않아도 톰캣이 알아서 한다
MaxUploadSize 만 설정해도 큰 문제는 없다

 


클라이언트 인코딩 타입 설정
[html]
<form method="post" action="reg" enctype="multipart/form-data">

 

 

 

서버 컨트롤러로 제대로 오는지 확인
@RequestMapping("reg")
@ResponseBody
public String reg(String title, String content, MultipartFile file, String category, String[] foods, String food) {

    long Size = file.getSize();
    String fileName = file.getOriginalFilename();

    System.out.printf("fileName:%s, fileSize:%d\n", fileName, Size);

    return String.format("title:%s<br>content:%s<br>category:%s<br>" , title, content, category);
}

 

 


< POST 물리경로 얻기와 파일 저장하기 >


저장 경로를 얻는 방법

@Autowired
ServletContext ctx;
또는
@Autowired
private HttpServletRequest request;

절대 경로 얻기
String realPath = ctx.getRealPath("/upload");
또는
String realPath = request.getServletContext.getRealPath("/upload");

저장하기
File saveFile = new File(realPath);
multipartFile.transferTo(saveFile);

함수 내에서만 사용할거라면 인자로 HttpServletRequest request 객체를 받아오면 되겠고
컨트롤러 내에 여러 함수에서 사용하겠다면 멤버로 서블렛컨텍스트 객체를 얻어놓고 사용하면 되겠다

 

 

ex)
// Spring Ioc 컨테이너는 ServletContext를 이미 가지고 있다 Autowired만 해서 쓰면 됨
@Autowired  
private ServletContext ctx;

@RequestMapping("reg")
@ResponseBody
public String reg(String title, String content, MultipartFile file, String category, String[] foods, String food, HttpServletRequest request) {
    long Size = file.getSize();
    String fileName = file.getOriginalFilename();

    // ServletContext ctx = request.getServletContext();
    String webPath = "/static/upload";
    String realPath = ctx.getRealPath(webPath);

    return "";

}

 

realPath : C:\project\jsp\workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\webprj\static\upload

콘솔에 나오는 경로는 보면 알겠지만 실제로 프로젝트를 진행하고 있는 경로가 아니라 배포를 위한 모든 작업을 마친 뒤의 배포시의 경로가 나오고 있는 것을 알 수 있다
실제 서비스 될 때는 위의 경로에 파일이 저장되고 사용된다

(그러니 테스트 할 때도 엉뚱한데서 찾지말고 배포 폴더에서 찾자)


단일 저장
@RequestMapping("reg")
@ResponseBody
public String reg(String title, String content, MultipartFile file, String category, String[] foods, String food, HttpServletRequest request) throws IllegalStateException, IOException {

    long Size = file.getSize();
    String fileName = file.getOriginalFilename();

    ServletContext ctx = request.getServletContext();
    String webPath = "/static/upload";
    String realPath = ctx.getRealPath(webPath);


    File savePath = new File(realPath);
    if(!savePath.exists())
    savePath.mkdirs();

    realPath += File.separator + fileName;
    File saveFile = new File(realPath);

    file.transferTo(saveFile);

    return "";

 

}





< POST 다중 파일 업로드 처리하기 >


name을 동일하게 만들어 배열로 받는다

[html]
                                <tr>
                                    <th>첨부파일</th>
                                    <td colspan="3" class="text-align-left text-indent"><input type="file"
                                            name="files" /> </td>
                                </tr>
                                <tr>
                                    <th>첨부파일</th>
                                    <td colspan="3" class="text-align-left text-indent"><input type="file"
                                            name="files" /> </td>
                                </tr>


배열로 받아 for문으로 돌린다

@RequestMapping("reg")
@ResponseBody
public String reg(String title, String content, MultipartFile[] files, String category, String[] foods, String food, HttpServletRequest request) throws IllegalStateException, IOException {

    for(MultipartFile file : files) {

        if(file.getSize() == 0) continue;
        // 파일이 0개 또는 한개만 올라올 경우

        long Size = file.getSize();
        String fileName = file.getOriginalFilename();
        ServletContext ctx = request.getServletContext();
        String webPath = "/static/upload";
        String realPath = ctx.getRealPath(webPath);

        File savePath = new File(realPath);
        if(!savePath.exists())
        savePath.mkdirs();

        realPath += File.separator + fileName;
        File saveFile = new File(realPath);

        file.transferTo(saveFile);
    }


    return "";


}


if(file.getSize() == 0) continue;
파일을 한개나 안올릴수도 있으니까 조건처리 해줘야 한다
(안하면 destination files 어쩌구 하면서 있지도 않은거 작업한다고 뭐라 함)





< POST 매핑과 Redirection >


POST를 통해 파일을 주고받을 때는 해당 맵핑 주소에서 다음과 같은 에러 메시지를 볼 수 있다
Request processing failed; nested exception is java.lang.NullPointerException: Cannot read the array length because "files" is null

함수에서 파일 저장에 files를 받아 활용하고 있기 때문인데 GET 요청 시에도 POST submit (데이터 전송)이 필요한 해당 코드가 실행되기 때문이다
클라이언트의 GET 요청시에만 이루어질 함수, POST 요청시에만 이루어질 함수 등으로 구분해 줄 필요가 있다

메소드 내에서 NullPointetrException이 뜨는 코드를
if (request.getMethod().equals("POST")) { }
조건문으로 감싸주는 방법도 있고


서블렛의 doGet, doPost 메소드를 사용할 때처럼 두 종류로 구분하는 방법이 있다


Spring 3.0 이전 버전

@RequestMapping("list")
public String list() {

return "admin.board.notice.list";
}

@RequestMapping(value="reg", method=RequestMethod.GET)
public String reg() {

return "admin.board.notice.reg";
}


@RequestMapping(value="reg", method=RequestMethod.POST)
public String reg() {
// 리디렉션 : list 페이지로
return "redirect:list";
// 포워딩 : return "admin/notice/reg";
}

post는 데이터 전송 후 새로운 페이지로 redirect하는 것이 기본인데, 클라이언트의 새로고침이나 뒤로가기 등을 통한 post 재요청을 막기 위해서도 필요하다 

( 뒤로가기나 새로고침은 브라우저의 cashe를 사용해 요청되는데(양식 재제출 요청) 이때 post 요청시의 데이터가 cashe에 남아있고 컨트롤러에서는 해당 cashe 데이터에 대한 처리가 없다면 중복된 데이터가 DB에 등록되는 일이 발생한다. DB에 넣기전에 DB 데이터와 동일한지 조건처리 하는 방법도 있으나 DAO 에서 해야할 일을 controller 단에서부터 굳이 넘겨와서 하는 MVC 모델 자체를 깨버리는 문제도 있고 transaction 처리도 생각하면 그냥 평범하게 Redirect 하는 게 기본이다 )


Spring 3.0 이후 버전

@RequestMapping("list")
public String list() {

return "admin.board.notice.list";
}

@GetMapping("reg")
public String reg() {

return "admin.board.notice.reg";
}


@PostMapping("reg")
public String reg() {
// 리디렉션 : list 페이지로
return "redirect:list";
// 포워딩 : return "admin/notice/reg";
}


어노테이션이 늘어나서 편해짐