파일을 업로드하려면 바이너리 데이터를 전송해야 하는데 폼을 전송할 때는 파일만 전송하는 것이 아니라 문자와 바이너리 데이터를 동시에 전송하는 경우가 많다.

 

HTTP는 multipart/form-data 전송 방식으로 다른 종류의 여러 파일와 폼의 내용을 함께 전송할 수 있는데 서블릿 컨테이너는 멀티파트 데이터를 request 객체에 각각 나누어 담아 전송하고 request.getParts()로 받을 수 있다.

 

업로드 사이즈는 다음과 같이 제한할 수 있다.

spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

// 옵션을 끄면 서블릿 컨테이너는 멀티파트 처리를 하지 않고 request.getParts()는 비어서 전송된다.
// Default true
spring.servlet.multipart.enabled=false

스프링의 DispatcherServlet에서 MultipartResolver가 실행이 되고 서블릿 컨테이너가 전달하는 HttpSerlvetRequest를 자식 인터페이스인 MultipartHttpServletRequest로 변환해서 반환해서 멀티파트와 관련된 추가 기능을 할 수 있게 한다.

 

서블릿이 제공하는 Parts는 멀티파트 형식을 편리하게 읽을 수 있는 다양한 메서드를 제공하지만 HttpServletRequest를 사용해야 하고 스프링이 제공하는 MultipartFile 인터페이스가 훨씬 편리하기 때문에 잘 사용하지 않는다.

 


 

스프링 MultipartFile

 

@RequestParam MultipartFile file, @ModelAttribute MultipartFile file 처럼 컨트롤러 파라미터에 입력만 해주면 사용할 수 있다.

  • getOriginalFilename() : 업로드 파일 명
  • transferTo() : 파일 저장

 

1. 업로드 파일 정보를 보관할 클래스

 

@Data
public class UploadFile {
    // 클라이언트에서 업로드한 파일명
    private String uploadFileName;
    // 서버에서 관리할 파일명 (중복되지 않을 이름으로 보관해야한다.)
    private String storeFileName;
    
    public UploadFile(String uploadFileName, String storeFileName) {
        this.uploadFileName = uploadFileName;
        this.storeFileName = storeFileName;
    }
}

 

2. 파일 저장 등을 처리하는 클래스

 

@Component
public class FileStore {
    @Value("${file.dir}")
    private String fileDir;
    
    public String getFullPath(String filename) {
        return fileDir + filename;
    }
    
    public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException {
        List<UploadFile> storeFileResult = new ArrayList<>();
        
        for (MultipartFile multipartFile : multipartFiles) {
            if (!multipartFile.isEmpty()) {
                storeFileResult.add(storeFile(multipartFile));
            }
        }
        return storeFileResult;
     }
    
    public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
         if (multipartFile.isEmpty()) {
             return null;
         }
         
         String originalFilename = multipartFile.getOriginalFilename();
         String storeFileName = createStoreFileName(originalFilename);
         multipartFile.transferTo(new File(getFullPath(storeFileName)));
         
         return new UploadFile(originalFilename, storeFileName);
    }
       
    private String createStoreFileName(String originalFilename) {
       String ext = extractExt(originalFilename);
       String uuid = UUID.randomUUID().toString();
       
       return uuid + "." + ext;
    }
       
    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(".");
        
        return originalFilename.substring(pos + 1);
    }
}

 

설정 값 분리의 필요성 (@Value)

더보기

DB 연결 정보나 외부 API 주소, 파일 경로 같은 설정 값들을 클래스에 입력하는 대신 properties, yml 등으로 분리해서 관리하면 환경에 따라 유연하게 값을 설정해줄 수 있고 불필요한 컴파일, 직접 클래스 코드를 하나 하나 수정하는 등의 문제를 해결할 수 있다.

 

@Value

 

1. 프로퍼티 치환법

 

properties에 작성된 키 값을 ${}안에 넣어주면 Spring이 PropertyPlaceHolderConfigurer를 통해 초기화 작업 중에 해당 값을 실제 값으로 치환한다. PropertyPlaceHolderConfigurer는 팩토리 후처리기로써 빈 설정 메타정보가 모두 준비됐을 때 빈 메타정보 자체를 조작하는 역할을 한다. 하지만 대체할 위치를 치환자로 지정해두고 후처리기가 치환해주기를 기다리는 수동적인 방법으로 빈에서 직접 값을 꺼내는 SpEL 방법이 권장 된다.

 

2. SpEL (Spring Expression Language)

 

SpEL은 기본적으로 #{} 안에 표현식을 넣도록 되어있는데, user.name이라는 표현식은 이름이 user인 빈의 name 프로퍼티를 의미한다. SpEL은 다른 빈의 프로퍼티에 접근가능할 뿐만 아니라 메소드 호출도 가능하고 다양한 연산도 지원하며 클래스 정보에도 접근할 수 있다. 심지어 생성자를 호출해서 객체를 생성할 수도 있다. 

 

 

https://mangkyu.tistory.com/167

 

  • UploadFile : 클라이언트에서 업로드한 파일명과 서버에 저장할 파일명을 가지고 있는 클래스
  • List<UploadFile> storeFiles : multipartFile 을 하나씩 storeFile()을 통해 서버에 저장하고 List에 UploadFile을 add
  • UploadFile storeFile : UUID 랜덤값을 생성하고 확장자를 추출해서 서버 경로에 파일을 저장하고 UploadFile을 반환

 

3. 폼 전송 객체

 

@Data
public class ItemForm {
    private Long itemId;
    private String itemName;
    private List<MultipartFile> imageFiles;
    private MultipartFile attachFile;
}

 

 

4. 컨트롤러

 

@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemRepository itemRepository;
    private final FileStore fileStore;
  
    @GetMapping("/items/new")
    public String newItem(@ModelAttribute ItemForm form) {
        return "item-form";
    }
      
    @PostMapping("/items/new")
    public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes
redirectAttributes) throws IOException {
        UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
        List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

        //데이터베이스에 저장
        Item item = new Item();
        ...
        itemRepository.save(item);
        
        redirectAttributes.addAttribute("itemId", item.getId());
        return "redirect:/items/{itemId}";
    }

    @GetMapping("/items/{id}")
    public String items(@PathVariable Long id, Model model) {
        Item item = itemRepository.findById(id);
        model.addAttribute("item", item);
        return "item-view";
    }
    
    @ResponseBody
    @GetMapping("/images/{filename}")
    public Resource downloadImage(@PathVariable String filename) throws
MalformedURLException {
        return new UrlResource("file:" + fileStore.getFullPath(filename));
}

    @GetMapping("/attach/{itemId}")
    public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId)
throws MalformedURLException {
        Item item = itemRepository.findById(itemId);
        String storeFileName = item.getAttachFile().getStoreFileName();
        String uploadFileName = item.getAttachFile().getUploadFileName();
        UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));

        String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
        String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";

        return ResponseEntity.ok()
                             .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                             .body(resource);
     }
}

 

  • Resource downloadImage : <img> 태그로 이미지를 조회할 때, UrlResource로 이미지 파일을 읽어서 이미지 바이너리를 반환
  • ResponseEntity<Resource> dounloadAttach : 파일을 다운로드 할 때 클라이언트가 업로드했던 파일명으로 생성

 

 

5. View

 

<form th:action method="post" enctype="multipart/form-data">
    <ul>
        <li>상품명 <input type="text" name="itemName"></li>
        <li>첨부파일<input type="file" name="attachFile" ></li>
        <li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
    </ul>
    <input type="submit"/>
</form>
<div class="container">
    <div class="py-5 text-center">
    <h2>상품 조회</h2> </div>
    상품명: <span th:text="${item.itemName}">상품명</span><br/>
    첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|"
th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
    <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/$
{imageFile.getStoreFileName()}|" width="300" height="300"/>

</div>

 

 

 

[참고] 인프런 김영한님 강의를 공부한 내용입니다.

Converter

 

HTTP 요청 파라미터는 모두 문자로 처리되어서 다른 타입으로 받고 싶은 경우 직접 변환을 해야 되는데 스프링은 용도에 따라 다양한 방식의 타입 컨버팅을 제공한다. 하지만 수많은 타입 컨버터를 하나하나 찾는 것은 너무 불편하기 때문에 스프링은 추가로 컨버터를 편리하게 사용할 수 있는 ConversionService를 제공한다.

 

ConversionService 인터페이스는 단순하게 canConvert(), convert()로 되어 있고 직접 만든 컨버터를 등록할 수도 있는데

WebMvcConfigurer가 제공하는 addFormatters()를 통해 직접 만든 컨버터를 등록하면 스프링이 내부에서 사용하는 ConversionService에 추가가 되는데 직접 등록한 컨버터의 경우 스프링 기본 컨버터들보다 높은 우선 순위를 가진다.

 

 

ConversionService 적용 예시

 

1) 스프링MVC가 제공하는 @RequestParam 의 경우 컨트롤러의 파라미터를 처리하는 ArgumentResolver인 RequestParamMethodArgumentResolver에서 ConversionService를 사용해서 타입을 변환한다.

 

2) 타임리프는 변수 표현식에 괄호를 하나 더 넣어서 ${{...}}를 사용하면 ConversionService를 적용해서 변환된 결과를 출력해주는데 th:field의 경우 자동으로 ConversionService를 적용해준다.

 

 

Formatter

 

Converter는 타입을 변환하는 범용 기능을 제공하는데 실제로는 단순 범용적인 변환보다 숫자 1000을 문자 "1,000"으로 쉼표를 넣어 출력하거나 날짜 객체를 "22-06-17" 같이 출력하는 등의 처리를 해야 되는 경우가 많다. 이렇게 객체를 특정 문자 포멧에 맞추어 출력하는데 특화된 기능을 하는 것이 Formatter이다. (문자 변환에 특화)

 

하지만 Formatter도 크게 보면 객체를 문자로, 문자를 객체로 변환하는 정교한 컨버터일 뿐인데 포맷터를 지원하는 CovnersionService를 사용하면 포맷터도 컨버전 서비스에 추가할 수 있다.

 

스프링 부트는 포맷터를 추가할 수 있는 DefaultFormattingConversionService 를 상속 받은 WebConversionService를 내부적으로 사용한다. (Web)

 

스프링은 자바에서 기본으로 제공하는 타입들에 대해 수많은 포맷터를 기본으로 제공하는데 포맷터는 기본 형식이 지정되어 있기 때문에 객체의 각 필드마다 다른 형식으로 포맷을 지정하기가 어려워서 애노테이션 기반의 매우 유용한 포맷터 2개를 기본으로 제공한다.

 

  • @NumberFormat(pattern = "###,###") : 숫자 관련 형식 지정 포맷터 사용
  • @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") : 날짜 관련 형식 지정 포맷터 사용

 

주의)

메시지 컨버터(HttpMessageConverter)에는 컨버전 서비스가 적용되지 않는데 특히 객체를 JSON으로 변환할 때 쓰이는 메시지 컨버터는 내부에서 Jackson 같은 라이브러리를 사용하는데 이때 객체를 JSON으로 변환하는 것은 이 라이브러리에 달린 것이기 때문에 JSON 결과로 만들어지는 숫자나 날짜 포맷을 변경하고 싶은 경우 Jackson 라이브러리가 제공하는 설정에 따라 포맷을 지정해야 한다.

 

컨버전 서비스는 @RequestParam, @ModelAttribute, @PathVariable, 뷰 템플릿 등에서 사용 가능

 

 

HTML은 HTTP 상태 코드에 따른 오류 페이지로 대부분 해결이 되지만 API의 경우에는 각 오류 상황에 맞는 오류 응답 스펙을 정하고, JSON으로 데이터를 반환해야 하기 때문에 더 복잡하다.

 

BasicErrorController

스프링부트가 제공하는 BasicErrorController에는 errorHtml(), error() 두개의 메서드가 있는데 클라이언트 요청이 Accept 헤더 값이 text/html인 경우에는 errorHtml()이 호출이 되고 그 외에는 error()가 호출이 되어 ResponseEntity로 HTTP Body에 JSON 데이터를 반환한다. 하지만 API 오류 처리는 각가의 경우에 따라 응답 결과를 다양하게 출력해야 되기 때문에 BasicErrorController보다는 @ExceptionHandler를 사용하는 것이 좋다.

 

HandlerExceptionResolver -> ExceptionResolver -> @ExceptionHandler

 

스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다. 컨트롤러 밖으로 던져진 예외를 해결하고, 동작을 변경하고 싶으면 HandlerExceptionResolver를 사용하면 된다.

DispatcherServlet에서 예외를 받아서 처리를 하고 정답 응답으로 바꿔 놓을 수 있다. (인터셉터의 postHandle()은 호출되지 않음)

 

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
    public ModelAndView resolveException(HttpServletRequest request,
        HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
                return new ModelAndView();
             }
         } catch (IOException e) {
             ...
         }
         return null;
     }
}

 

ModelAndView를 반환하는 이유는 예외를 처리해서 정상 흐름처럼 변경하는 것이 목적이기 때문이다.

DispatcherServlet은 HandlerExceptionResolver의 반환 값(ModelAndView)에 따라 다음과 같이 동작한다.

 

  • 빈 ModelAndView : 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴한다.
  • ModelAndView 지정 : ModelAndView에 View, Model 등의 정보를 담아서 반환하고 뷰를 렌더링한다.
  • null : 다음 ExceptionResolver를 찾아서 실행하고, 없을 경우 기존에 발생한 예외를 서블릿 밖으로 던진다

 

WAS까지 예외가 전달이 되고, WAS에서 오류 페이지 정보를 찾아서 다시 오류 페이지를 호출하는 과정은 너무 복잡한데 HandlerExceptionResolver를 통해 DispatcherServlet에서 예외를 처리하면 서블릿 컨테이너까지 예외가 전달되지 않아 WAS 입장에서는 정상 처리가 된 것처럼 만들 수 있다. 서블릿 컨테이너까지 예외가 올라가면 추가 프로세스가 복잡하게 실행되기 때문에 성능상 이점도 있는 것이다.

이제 RuntimeException을 상속 받은 예외 클래스를 생성하고 해당 예외를 처리하는 HandlerExceptionResolver를 만든 다음 WebMvcConfigurer에서 extendHandlerExceptionResolvers를 Override해서 resolver.add로 등록할 수 있다.

 

// WebMvcConfigurer 를 통해 등록
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
    resolvers.add(new MyHandlerExceptionResolver());
}

 


 

스프링 ExceptionResolver

 

위에서처럼 HandlerExceptionResolver를 직접 만들어 response에 데이터를 넣고, ModelAndView를 반환하는 것은 너무 불편하다.

스프링부트가 제공하는 ExceptionResolver는 HandlerExceptionResolverComposite에 다음과 같이 우선 순위 순으로 ExceptionResolver를 등록한다.

 

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

 

스프링이 제공하는 ExceptionResolver를 통해 @ControllerAdvice, @RestControllerAdvice로 예외 처리를 하나의 클래스로 묶고 @ExceptionHandler와 @ResponseStatus를 통해 예외 처리를 메서드로 편리하게 관리할 수 있다.

 

 

1. ExceptionHandlerExceptionResolver

스프링은 @ExceptionHandler를 이용한 편리한 예외 처리 기능을 제공하는데 웹 화면에 HTML 오류 화면을 제공하는 경우는 BasicErrorController를 사용하는 것이 편하지만 API 예외의 경우는 복잡하기 때문에 세밀하게 설정할 필요가 있다.

 

@ExceptionHandler 애노테이션을 선언하고 처리하고 싶은 예외를 지정하면 해당 컨트롤러에서 예외 발생 시 ExceptionHanlderExceptionResolver는 해당 예외를 처리할 수 있는 @ExceptionHandler가 있는지 확인하고 실행하여 예외를 처리한다. ErrorResult를 반환하며 @ResponseStatus와 같이 사용할 수 있다. @ExceptionHanlder에 예외를 지정하지 않으면 해당 메서드의 파라미터 예외를 사용하며 @ResponseStatus 대신 ErrorResult를 ResponseEntity로 감싸서 HTTP 응답 코드를 동적으로 변경하여 줄 수도 있다.

컨트롤러에 기존 메서드와 예외 처리 @ExceptionHanlder 메서드가 섞여있는데 @ControllerAdvice, @RestControllerAdvice를 사용하면 이를 분리할 수 있다. @ExceptionHandler를 적용할 컨트롤러 또는 패키지를 지정하거나 아무 것도 지정하지 않으면 모든 컨트롤러에 글로벌ㄹ 적용할 수 있다.

 

2. ResponseStatusExceptionResolver

@ResponseStatus 애노테이션으로 상태 코드와 reason을 적용하면 해당 예외가 발생하여 컨트롤러 밖으로 넘어갈 때, ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인하고 오류 코드, 메시지를 적용한다. 결국 내부적으로 response.sendError() 를 호출하기 때문에 WAS에서 다시 오류 페이지(/error)를 내부 요청한다.

@GetMapping("/")
public String responseStatusEx() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

 reason은 MessageSource에서 찾는 기능도 제공하기 때문에 다음과 같이 messages.properties를 통해 사용할 수 있다.

error.bad=잘못된 요청 오류입니다. 메시지 사용

 

@ResponseStatus 애노테이션은 코드를 수정할 수 없는 라이브러리 예외 코드나 조건에 따라 동적으로 변경하고 싶은 경우 사용하기 힘들기 때문에 이런 경우는 ResponseStatusException을 직접 throw 하는 것이 좋다.

 

 

3. DefaultHandlerExceptionResolver

 

스프링 내부에서 발생하는 스프링 예외를 해결하는데 파라미터 바인딩 시점에 타입이 맞지 않는 경우 내부적으로 발생하는 TypeMismatchException의 경우 서블릿 컨테이너로 전달되어 500 오류가 발생하지만 파라미터 바인딩은 대부분 클라이언트에서 잘못 호출해서 발생하기 때문에 400오류가 더 적절하다. 이런 경우에 DefaultHandlerExceptionResolver는 이것을 400 오류로 변경한다. DefaultHandlerExceptionResolver도 내부적으로 sendError를 사용하며 스프링 내부 오류를 어떻게 처리할지 수 많은 내용이 정의되어 있다.

 

 

 

 

자바의 main 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행되고 실행 도중 예외를 잡지 못해서 main 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 쓰레드가 종료된다.

 

웹어플리케이션은 사용자 요쳥별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다. 만약에 예외가 발생했는데 컨트롤러에서 예외를 잡지 못하고 WAS까지 예외가 전달이 되면 서블릿 컨테이너가 기본으로 제공하는 오류 화면이 호출된다.

 

서블릿과 서블릿 컨테이너

더보기

 CGI 웹 서버와 웹 애플리케이션 서버(WAS) 간에 데이터를 주고 받기 위한 규악으로 서버 프로그램은 CGI 규약에 따라 클라이언트의 데이터를 환경변수로 전달하고, 프로그램의 표준 출력 결과를 클라이언트에게 전송한다.

 서블릿은 자바 웹 서버가 동적인 페이지를 생성할 수 있도록 클라이언트의 요청을 처리하고, 결과를 반환하는 자바 웹 프로그래밍 기술로 흔히 자바로 구현된 CGI 라고 한다. 서블릿은 독립적으로 실행되지 않으며 서블릿 컨테이너에 의해 생명주기 관리가 되는데 대표적인 오픈 소스 서블릿 컨테이너로 톰캣(WAS)이 있다.

 

 

 

 서블릿 컨테이너는 클라이언트와 서버 간의 소켓 통신에 필요한 TCP/IP 연결, HTTP 프로토콜 해석 등의 네트워크 기반 작업을 추상화해 API로 제공하여 복잡한 과정을 생략하고 개발자가 비즈니스 로직에 집중하게 도와준다.

 

 서블릿 컨테이너는 서블릿 인터페이스(javax.servlet.Servlet)를 구현하여 GenericServlet 이라는 추상 클래스를 제공하는데 이는 service() 메서드를 제외하고 대부분의 서블릿에 필요한 메서드를 구현한 일종의 서블릿 어댑터이다.

 

 일반적으로 알고 있는 서블릿인 HttpServlet은 GenericServlet을 상속 받아 추상 메서드인 service()를 HTTP 프로토콜 요청 메서드에 적합하게 구현한 서블릿으로 서블릿 컨테이너는 웹 서버로부터 요청을 받으면 HTTP 프로토콜로 request, response 객체를 생성하여 서블릿을 호출하고 service()를 실행한다.

 

 

참고)

https://yangbongsoo.gitbook.io/study/servlet_container 

https://mangkyu.tistory.com/14

https://12bme.tistory.com/555

 

 

오류가 발생했을 때, HttpServletResponse가 제공하는 response.sendError(HTTP StatusCode, ErrorMessage)를 사용해서 서블릿 컨테이너에게 오류가 발생했다는 것을 전달할 수도 있다. 서블릿 컨테이너는 클라이언트에게 응답을 보내기 전 response 객체에 sendError()가 호출되었는지 확인하고 오류 페이지를 보여준다.


 

서블릿 컨테이너가 제공하는 기본 예외 처리 화면 대신 서블릿이 제공하는 오류 화면 기능을 사용하면 고객 친화적으로 오류 화면을 보여줄 수 있다. WebServerFactoryCustomizer의 customize를 오버라이드해서 오류 페이지를 등록할 수 있는데 HTTP 상태 코드와 오류 페이지 url로 ErrorPage를 생성해서 등록하고 오류 페이지를 처리할 컨트롤러를 생성하면 된다. (스프링 부트 자동화)

 

WAS는 ErrorPage를 확인하고 오류 정보를 request의 attribute에 담아서 오류 페이지 url로 다시 요청을 하는데 서버 내부에서 추가적인 호출을 하는 것이기 때문에 클라이언트는 알 수가 없다. 오류 페이지를 요청할 때 필터, 서블릿, 인터셉터가 전부 다시 호출되는데 서블릿은 해당 요청이 정상 요청인지, 오류 요청인지 알 수 있는 추가 정보로 DispatcherType(ENUM)을 제공한다.

(DispatcherType = FORWARD, REQUEST, ERROR ..)

// 필터를 등록할때 추가로 DispatcherType 설정 가능
// 기본 값이 REQUEST라서, 별도의 설정이 없으면 정상 요청시에만 필터를 적용한다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);

필터는 위와 같이 등록 시 설정을 해주면 되지만 기본 설정이 오류 요청에는 필터를 적용하지 않는 것이라 문제가 없다. 인터셉터는 서블릿이 아닌 스프링이 제공하는 기능이기 때문에 DispatcherType을 이용할 수 없고 excludePathPatterns에 오류 페이지 경로를 넣어주면 된다.

 

스프링 부트는 ErrorPage를 '/error' 라는 기본 경로로 자동으로 등록하고 BasicErrorController라는 스프링 컨트롤러를 생성한다.

BasicErrorController는 기본 로직이 전부 등록되어 있기 때문에 해당 컨트롤러가 제공하는 규칙에 따라 오류 페이지를 등록만 해두면 된다. 그리고 오류 관련 정보를 model에 담아서 전달하기 때문에 뷰 템플릿은 이 값을 활용해서 출력할 수 있다.

옵션으로 오류 관련 정보를 보낼지 설정이 가능한데 실무에서는 오류 정보를 전송하는 것은 위험하기 때문에 사용하지 않는 것이 좋다.

<ul>
	<li>오류 정보</li>
	<ul>
        <li th:text="|timestamp: ${timestamp}|"></li>
        <li th:text="|path: ${path}|"></li>
        <li th:text="|status: ${status}|"></li>
        <li th:text="|message: ${message}|"></li>
        <li th:text="|error: ${error}|"></li>
        <li th:text="|exception: ${exception}|"></li>
        <li th:text="|errors: ${errors}|"></li>
        <li th:text="|trace: ${trace}|"></li>
	</ul>
	</li>
</ul>
server.error.include-exception=true
// never : 사용 x, always: 항상 사용, on_param : 파라미터가 있을 때 사용
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

 

 

  • 정적 리소스
    • resources/static/error/404.html (구체적인 것이 우선 순위)
    • resources/static/error/4xx.html (400대 오류 디폴트 처리)

 

  • 뷰 템플릿 (정적 리소스보다 우선 순위가 높다.)
    • resources/templates/error/

'Spring' 카테고리의 다른 글

[스프링 MVC2] 스프링 타입 컨버터  (0) 2022.06.17
[스프링 MVC2] API 예외 처리  (0) 2022.06.17
[스프링 MVC2] 스프링 인터셉터  (0) 2022.06.16
[스프링 MVC2] 서블릿 필터  (0) 2022.06.16
[스프링 MVC2] 쿠키와 세션  (0) 2022.06.16

+ Recent posts