포스트

2022-05-18-TIL

2022-05-18-TIL

Today I Learned

Multipart File Upload in Spring Boot

FE에서 기본적으로 확장자 및 용량이 제한되어 오겠지만, BE에서도 용량에 대한 검증이 필요하다. 스프링 부트에서는 application.properties 나 application.yml로 설정이 가능하다. 하지만 주의할 점은 Apache Tomcat에서 파일 용량 제한이 걸려있을 수 있으므로, 모두 확인해야한다.

Spring Boot의 MultipartProperties에는 두 가지 한도가 있다. max-file-size는 파일 하나의 최대 크기이고 기본값은 1MB다. max-request-size는 multipart/form-data 요청 전체의 최대 크기이고 기본값은 10MB다. 파일 여러 개를 한 요청에 올리면 각각은 max-file-size 안이어도 합계가 max-request-size를 넘을 수 있으므로 두 값을 함께 정해야 한다(MultipartProperties 소스, v2.7.0).

1
2
3
4
5
spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 50MB

이 값들은 결국 서블릿 표준의 MultipartConfigElement로 전달되어 컨테이너(Tomcat)가 파싱 단계에서 검사한다. 한도를 넘으면 컨트롤러에 도달하기 전에 MaxUploadSizeExceededException이 발생하므로, 사용자에게 의미 있는 응답을 주려면 @ExceptionHandler나 @ControllerAdvice에서 따로 처리해야 한다. Tomcat 커넥터에도 maxSwallowSize(기본 2MiB) 같은 별도 설정이 있어서, 업로드가 중단된 요청의 남은 본문을 얼마나 읽어 버릴지 결정한다. 한도 초과 시 클라이언트가 응답 대신 연결 끊김을 받는다면 이 값을 의심해 볼 수 있다(Tomcat 9 HTTP Connector). 링크의 마지막 Stack Overflow 질문은 설정한 크기의 절반 정도에서 거부되는 현상을 다룬다.

  • https://spring.io/guides/gs/uploading-files/#:~:text=constrained%20as%20follows%3A-,spring.,file%20size%20cannot%20exceed%20128KB.
  • https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.html#getMaxRequestSize–
  • https://docs.oracle.com/javaee/7/api/javax/servlet/MultipartConfigElement.html?is-external=true
  • https://stackoverflow.com/questions/34177873/max-limit-of-multipartfile-in-spring-boot
  • https://andonekwon.tistory.com/4
  • https://velog.io/@jinho_pca/Spring-Boot-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9A%A9%EB%9F%89%EC%A0%9C%ED%95%9C-%EC%84%A4%EC%A0%95#:~:text=%EB%94%B0%EB%A1%9C%20%EC%84%A4%EC%A0%95%ED%95%98%EC%A7%80%20%EC%95%8A%EC%95%98%EB%8B%A4%EB%A9%B4%20default,bytes%20%EB%A1%9C%20%EC%95%BD%201MB%EC%9D%B4%EB%8B%A4.
  • https://velog.io/@jinho_pca/Spring-Boot-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C-%EC%9A%A9%EB%9F%89%EC%A0%9C%ED%95%9C-%EC%84%A4%EC%A0%95
  • https://velog.io/@park2348190/MultipartProperties%EC%9D%98-max-file-size-max-request-size%EC%9D%98-%EC%B0%A8%EC%9D%B4
  • https://stackoverflow.com/questions/64183581/spring-boot-file-multipart-accept-half-allowed-size

CDN 이미지 리사이징

CDN 엣지 서버 중에는 원본 이미지를 URL 뒤에 붙인 명령으로 변환해서 내려 주는 기능을 가진 것이 있다. STON 엣지 서버 문서의 예시를 보면 /origin.jpg/dims/resize/100x100, /dims/grayscale/true처럼 원본 경로 뒤에 /dims/...를 붙여 크기 조절, 흑백 변환 같은 작업을 지정한다. 원본 서버에는 이미지 하나만 두고 썸네일 크기별 파일을 따로 만들지 않아도 된다는 것이 장점이다.

STON 문서의 Hash-Suffix 설정은 이런 변환 URL을 원본 서버 부하 분산에서 어떻게 다룰지에 관한 것이다. 여러 원본 서버에 해시로 요청을 나눌 때 /dims/ 이후를 해시 계산에서 빼면, 같은 원본 파일에 대한 변환 요청이 같은 서버로 모인다(STON 문서: 원본 서버). 세 번째 링크는 내부 위키라서 내용을 다시 확인하지 못했다.

  • http://dims.skcdn.com/sample.html
  • https://ston.readthedocs.io/ko/latest/admin/origin.html?highlight=%2Fdims%2Fresize#hash-suffix
  • https://music-flo.atlassian.net/wiki/spaces/MCP/pages/2031059049/FLO#resizing

Swagger Multiple Examples

OpenAPI 3에서는 요청 본문 예시를 여러 개 둘 수 있다. 미디어 타입 아래에 단수형 example 대신 복수형 examples를 쓰고, 이름을 키로 해서 summary와 value를 넣는다. 같은 예시를 여러 곳에서 쓰면 components/examples에 정의해 두고 $ref로 참조한다. Swagger UI에서는 드롭다운으로 예시를 골라 요청을 보내 볼 수 있다(Swagger: Adding Examples).

1
2
3
4
5
6
7
8
9
10
11
12
requestBody:
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/Order'
      examples:
        card:
          summary: 카드 결제
          value: { payType: CARD, amount: 10000 }
        transfer:
          summary: 계좌 이체
          value: { payType: TRANSFER, amount: 10000 }
  • https://swagger.io/docs/specification/describing-request-body/
  • https://swagger.io/docs/specification/adding-examples/

Java Optional

Optional은 값이 없음을 확실히 하기 위해서 만든 타입이지, 타입으로 선언하여 끌고 다니면서 로직상에서 여기저기에 사용하기 위함이 아니다. 버그의 가능성만 키울뿐이다. 따라서 .orElse 메서드로 조기에 처리해주는게 바람직하다.

JDK 문서의 API Note도 같은 의도를 밝힌다. Optional은 “결과 없음”을 표현할 필요가 분명한 메서드 반환 타입으로 쓰도록 만든 것이고, Optional 타입의 변수 자체는 절대 null이어서는 안 된다고 적혀 있다(Optional Javadoc, Java 17). 그래서 필드나 메서드 파라미터로 Optional을 받는 것은 권장되지 않는다.

orElse를 쓸 때 하나 주의할 점이 있다. orElse(createDefault())는 값이 있어도 인자를 먼저 평가하므로 createDefault()가 항상 호출된다. 기본값 생성 비용이 크거나 부수 효과가 있다면 orElseGet(this::createDefault)처럼 Supplier를 넘겨서 값이 없을 때만 실행되게 해야 한다.

  • https://homoefficio.github.io/2019/10/03/Java-Optional-%EB%B0%94%EB%A5%B4%EA%B2%8C-%EC%93%B0%EA%B8%B0/
  • https://www.baeldung.com/java-optional-return
  • https://ducmanhphan.github.io/2019-12-06-Best-practice-for-Optional-in-Java/#using-ifpresent()-or-ifPresentOrElse()-methods

Custom Exception in .orElseThrow()

orElseThrow의 시그니처는 <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)다. 예외 객체 자체가 아니라 예외를 만드는 Supplier를 받으므로, 생성자 인자가 없으면 메서드 참조로, 인자가 있으면 람다로 넘긴다.

1
2
User user = userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id));

UserNotFoundException::new는 인자 없는 생성자에만 맞는다. 생성자에 값을 넘겨야 하는 경우 메서드 참조로는 표현할 수 없다는 것이 링크 질문의 요지다.

  • https://stackoverflow.com/questions/22917138/java-8-method-references-provide-a-supplier-capable-of-supplying-a-parameterize
참고한 자료외부 출처 4

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다