트러블슈팅 - S3에 업로드한 이미지가 다운로드만 뜨는 문제 해결하기

J_log·2025년 4월 11일
0

문제 상황

Spring Boot에서 게시글 생성 시 파일(이미지, 동영상)을 첨부해 AWS S3에 업로드하도록 구현했다.
하지만 Postman으로 테스트하던 중 다음과 같은 에러가 발생했다.

2025-04-12T01:19:02.529+09:00  WARN 47568 --- [gigsync] [nio-8080-exec-7] 
.w.s.m.s.DefaultHandlerExceptionResolver : 
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: 
Content-Type 'application/octet-stream' is not supported]

파일 업로드 자체가 실패했고, 이후 S3 버킷을 확인해도 업로드된 파일은 application/octet-stream 타입으로 설정되어 있었으며 브라우저에서 바로 열 수 없고 다운로드만 되는 문제가 발생했다.

원인 분석

이 문제는 크게 두 가지 원인으로 나눌 수 있었다.

  1. Spring이 application/octet-stream 요청을 처리하지 못함
  • @RequestPart로 JSON (BoardRequestDto)과 파일(List<MultipartFile>)을 동시에 받는 구조였는데, Postman에서 JSON을 잘못 설정하거나 Content-Type을 생략하면 Spring이 인식하지 못해 위 에러가 발생함
  1. S3에 잘못된 Content-Type으로 업로드됨
  • Multipart 업로드 요청 시 Content-Type이 지정되지 않으면 S3에서 자동으로 application/octet-stream으로 처리됨 -> 이 경우, 이미지도 파일처럼 다운로드되며 웹에서 미리보기가 되지 않음

해결 방안

Postman에서 요청을 multipart/form-data로 정확히 설정하고 @RequestPart에는 application/json 형태의 문자열을 전달하였다. 업로드 파일의 Content-Type을 MultipartFile.getContentType()으로 명시하고, 이미지 업로드 시 허용된 확장자만 가능하도록 설정했다.

Postma API 요청

수정된 FileService.java

@Service
@RequiredArgsConstructor
public class FileService {

    private static final List<String> ALLOWED_IMAGE_EXTENSIONS = List.of("jpg", "jpeg", "png", "gif");

    private final S3Client s3Client;

    @Value("${cloud.aws.s3.bucketName}")
    private String bucketName;

    public List<BoardFile> uploadFiles(List<MultipartFile> files, Board board) {
        List<BoardFile> result = new ArrayList<>();

        for (MultipartFile file : files) {
            String originalFilename = file.getOriginalFilename();
            String extension = getExtension(originalFilename);

            if (isImage(file) && !ALLOWED_IMAGE_EXTENSIONS.contains(extension.toLowerCase())) {
                throw new IllegalArgumentException("지원하지 않는 이미지 형식입니다: " + extension);
            }

            String fileName = UUID.randomUUID() + "_" + originalFilename;
            String url = uploadToS3(file, fileName);

            result.add(BoardFile.builder()
                    .fileName(fileName)
                    .fileUrl(url)
                    .board(board)
                    .build()
            );
        }

        return result;
    }

    private String uploadToS3(MultipartFile file, String fileName) {
        try {
            PutObjectRequest putObjectRequest = PutObjectRequest.builder()
                    .bucket(bucketName)
                    .key(fileName)
                    .acl("public-read")
                    .contentType(file.getContentType())
                    .build();

            s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
            return "https://" + bucketName + ".s3.amazonaws.com/" + fileName;
        } catch (IOException e) {
            throw new RuntimeException("파일 업로드 중 오류 발생", e);
        }
    }

    private boolean isImage(MultipartFile file) {
        String contentType = file.getContentType();
        return contentType != null && contentType.startsWith("image");
    }

    private String getExtension(String filename) {
        if (filename == null || !filename.contains(".")) {
            throw new IllegalArgumentException("파일 이름에 확장자가 없습니다.");
        }
        return filename.substring(filename.lastIndexOf('.') + 1);
    }
}

결과 확인

브라우저에서 바로 이미지 미리보기 가능

동영상도 별도 설정 없이 S3 주소에서 재생 가능

확장자 검증으로 잘못된 파일 업로드를 방지하고 S3 URL을 게시판 조회 시 바로 사용할 수 있게 되었다.

이번 이슈는 흔하지만 초반에 놓치기 쉬운 Content-Type과 multipart/form-data 처리에 대한 실수였다. Spring Boot와 S3 연동 시에는 아래 항목들을 꼭 챙기자 !

  • @RequestPart를 사용할 때는 multipart/form-data 필수
  • 파일은 적절한 Content-Type으로 업로드해야 브러우저에서 랜더링됨
  • 이미지 확장자 검증은 보안/UX 차원에서 중요
  • Postman에서도 요청 설정을 정확히 해야 Spring이 인식함

0개의 댓글