
CKEditor5 커스텀 빌드를 사용하여 이미지 업로드를 구현하였다.
이미지 업로드 UI는 에디터에서 정상적으로 출력되었으나
실제 업로드 기능 사용 시 아래와 같은 오류를 만나게 되었다.
▼ 브라우저 console 로그
Failed to load resource: the server responded with a status of 500 ()
POST http://localhost:8088/day042_totalpractice/uploadImage.do 500 (Internal Server Error)
▼ 이클립스 console
java.lang.IllegalStateException:
어떤 multi-part 설정도 제공되지 않았기 때문에,
part들을 처리할 수 없습니다.
[ 오류가 의미하는 것 ]
이 오류는 CKEditor 문제도 아니고,
이미지 업로드 로직 문제도 아니다.
Servlet이 multipart/form-data 요청을 처리할 준비가 안 된 상태에서
request.getPart()를 호출했기 때문에 발생한 오류다.
[ 오류 발생 원인 ]
CKEditor의 SimpleUploadAdapter는
이미지를 업로드할 때 다음과 같은 요청을 보낸다.
Content-Type: multipart/form-data
즉, 일반 POST 요청이 아니라
파일이 포함된 multipart 요청이다.
그런데 Servlet은 기본적으로
multipart를 처리하지 못한다
Servlet은
- request.getParameter() → 기본 요청에서만 사용 가능
- request.getPart() → multipart 설정이 되어 있어야만 사용 가능
즉, request.getPart("upload"); 에서
무조건 예외가 발생한다.
그래서 터진 예외가 바로 IllegalStateException:
어떤 multi-part 설정도 제공되지 않았기 때문에,
part들을 처리할 수 없습니다.
예외를 풀어서 해석하면 아래와 같다.
“이 요청이 multipart 요청인 건 알겠는데,
이 서블릿에는 multipart를 처리하겠다는 설정이 없다.”
▼ 핵심 원인 한 줄 요약
multipart/form-data 요청을 처리하는
서블릿에 @MultipartConfig 설정이 없었기 때문
[ 착각 포인트 ]
❌ 잘못된 생각
“UploadImageAction에서 파일을 처리하니까
UploadImageAction에 설정하면 되는 거 아닌가?”
⭕ 실제 동작 구조
실제 요청을 받는 서블릿은 FrontController
UploadImageAction은 이미 request를 받은 이후에 실행됨
따라서
multipart 설정은
Action 클래스가 아니라
FrontController에 반드시 있어야 한다
[ 실제 해결 방법 ]
FrontController에 @MultipartConfig 추가
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB
maxFileSize = 1024 * 1024 * 10, // 10MB
maxRequestSize = 1024 * 1024 * 20 // 20MB
)
원래는 이렇게 웹어노테이션만 있었다.
@WebServlet("*.do")
또한 multipart 예외로 인해 UploadImageAction이
정상 종료되지 못했고,그 결과 ActionForward가
null인 상태에서 forward.isRedirect() 가 호출되며
NullPointerException이 추가로 발생했다.
그래서 FrontController에
아래 처리도 반드시 필요하다.
if (forward == null) {
return;
}
▼ 최종 정리 코드(오류 수정 완료(
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("*.do")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB
maxFileSize = 1024 * 1024 * 10, // 10MB
maxRequestSize = 1024 * 1024 * 20 // 20MB
)
public class FrontController extends HttpServlet {
private static final long serialVersionUID = 1L;
private ActionFactory factory;
public FrontController() {
super();
factory = new ActionFactory();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doAction(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doAction(request, response);
}
private void doAction(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String command = request.getRequestURI();
command = command.substring(request.getContextPath().length());
System.out.println("[로그] command : " + command);
Action action = factory.getAction(command);
System.out.println("[로그] action : " + action);
ActionForward forward = action.execute(request, response);
// ★★★★★ 이 줄이 핵심 ★★★★★
// 업로드처럼 응답을 이미 끝낸 경우
if (forward == null) {
return;
}
if (forward.isRedirect()) {
response.sendRedirect(forward.getPath());
} else {
RequestDispatcher dispatcher =
request.getRequestDispatcher(forward.getPath());
dispatcher.forward(request, response);
}
}
}