WebFlux에서 이미지 응답과 파일 없음 404 처리하기
WebFlux 프로젝트에서 프론트엔드가 사용할 이미지를 서버에서 보내기로 했습니다. icon 폴더에는 SVG, line 폴더에는 PNG 파일이 있었습니다.
확장자에 맞는 응답 헤더를 보내야 했습니다. 없는 파일을 요청하면 500 대신 404를 반환해야 했습니다. 당시에는 파일을 읽기 전에 존재 여부를 확인하고 없으면 빈 Flux를 반환하도록 바꿨습니다.
전체 코드
컨트롤러
@GetMapping("/images/{imageType}/{imageName}")
public Mono<ResponseEntity<DataBuffer>> getImage(
@PathVariable String imageType,
@PathVariable String imageName) {
MediaType mediaType;
if (imageName.toLowerCase().endsWith(".png")) {
mediaType = MediaType.IMAGE_PNG;
} else if (imageName.toLowerCase().endsWith(".jpeg") || imageName.toLowerCase().endsWith(".jpg")) {
mediaType = MediaType.IMAGE_JPEG;
} else if (imageName.toLowerCase().endsWith(".svg")) {
mediaType = MediaType.valueOf("image/svg+xml");
} else {
mediaType = MediaType.IMAGE_PNG;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
Mono<DataBuffer> dataBufferMono = imageService.loadImage(imageType, imageName).reduce(DataBuffer::write);
return dataBufferMono.map(dataBuffer -> ResponseEntity.ok().headers(headers).body(dataBuffer))
.defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
}서비스
public Flux<DataBuffer> loadImage(String imageType, String imageName) {
log.info("{}", imageName);
ClassPathResource imageResource = new ClassPathResource("static/images/" + imageType + "/" + imageName);
if (!imageResource.exists()) {
log.warn("파일을 찾을 수 없음");
return Flux.empty();
}
return DataBufferUtils.read(
imageResource,
new DefaultDataBufferFactory(),
4096);
}서비스에서 파일 읽기
먼저 파일을 읽는 서비스부터 보겠습니다.
public Flux<DataBuffer> loadImage(String imageType, String imageName) {
log.info("{}", imageName);
ClassPathResource imageResource = new ClassPathResource("static/images/" + imageType + "/" + imageName);
if (!imageResource.exists()) {
log.warn("파일을 찾을 수 없음");
return Flux.empty();
}
return DataBufferUtils.read(
imageResource,
new DefaultDataBufferFactory(),
4096);
}ClassPathResource로 static/images 아래 경로를 찾습니다. 파일이 없으면 Flux.empty()를 반환하고 파일이 있으면 DataBufferUtils.read로 4096바이트 단위의 버퍼를 읽습니다. 마지막 버퍼는 이보다 작을 수 있습니다.
이 분기 없이 존재하지 않는 파일을 읽었을 때는 FileNotFoundException 때문에 500 응답이 발생했습니다.
컨트롤러에서 헤더와 상태 결정하기
@GetMapping("/images/{imageType}/{imageName}")
public Mono<ResponseEntity<DataBuffer>> getImage(
@PathVariable String imageType,
@PathVariable String imageName) {
MediaType mediaType;
if (imageName.toLowerCase().endsWith(".png")) {
mediaType = MediaType.IMAGE_PNG;
} else if (imageName.toLowerCase().endsWith(".jpeg") || imageName.toLowerCase().endsWith(".jpg")) {
mediaType = MediaType.IMAGE_JPEG;
} else if (imageName.toLowerCase().endsWith(".svg")) {
mediaType = MediaType.valueOf("image/svg+xml");
} else {
mediaType = MediaType.IMAGE_PNG;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
Mono<DataBuffer> dataBufferMono = imageService.loadImage(imageType, imageName).reduce(DataBuffer::write);
return dataBufferMono.map(dataBuffer -> ResponseEntity.ok().headers(headers).body(dataBuffer))
.defaultIfEmpty(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
}파일 이름의 확장자로 PNG, JPEG, SVG를 구분해 Content-Type을 설정합니다. 당시 사용한 파일은 PNG와 SVG였지만 코드에는 JPEG 분기도 있습니다. 그 외 확장자는 PNG로 처리했습니다.
서비스가 반환한 여러 버퍼는 reduce(DataBuffer::write)로 합칩니다. 저는 앞에서 합친 결과에 다음 버퍼를 하나씩 더하는 순서로 이해했습니다. 아래와 같습니다.
((1번 째 + 2 번 째) + 3번 째) + ....앞의 버퍼에 다음 버퍼 내용을 쓰는 과정을 반복해 하나의 DataBuffer를 얻습니다. 4096바이트씩 나눠 읽었지만 응답하기 전에는 전체 이미지를 한 버퍼에 모으는 셈입니다. 반환 타입도 Mono<ResponseEntity<DataBuffer>>이므로 파일을 조금씩 그대로 전송하는 Flux<DataBuffer> 응답과는 다릅니다.
이렇게 버퍼를 합칠 때는 사용하는 버퍼 구현에 맞춰 자원 해제도 살펴야 합니다. Spring이 제공하는 DataBufferUtils.join 문서도 참고할 수 있습니다.
서비스가 빈 Flux를 반환하면 reduce 결과도 비어 있으므로 defaultIfEmpty에서 404 응답을 만듭니다. 내용이 없는 파일도 읽힌 버퍼가 없으면 이 분기로 들어갈 수 있습니다.