블로그 이미지

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 이미지 서버 구현하기 (1)
    카테고리 없음 2022. 9. 26. 03:39

    속닥속닥 을 운영하면서 게시물에 사진을 올리는 기능이 있으면 좋겠다는 피드백을 많이 받았다. 만들어야지 만들어야지 했는데, 다른 태스크로 인해 우선순위가 뒤로 밀려있다가 드디어 만들게 되었다!

    S3를 사용했다면 더 간단하게 구현했을테지만, S3의 사용이 제한되어 있었기 때문에 정적 리소스를 반환하는 이미지 서버를 직접 구축하게 되었다.

    간단히 구현 사항을 요약해 보자면

    1. 이미지 업로드 API를 통해 이미지를 저장한다.
    2. 이미지 네임을 기반으로 조회 요청을 받으면 해당 정적 이미지 파일을 돌려준다.

    추상화시킨 구현 내용을 보면 굉장히 간단하다!

    차근차근 구현해보자

    이미지 업로드

    먼저, 이미지를 저장하는 것부터 구현한다. 클라이언트가 서버에게 이미지를 보낼때는 데이터가 크거나, 이미지에 대한 설명을 같이 보낼수도 있기 때문에 Content-type을 mutlpart/form-data 타입으로 지정해 전송한다. (Multipart 에 대한 설명은 Multipart 에 정리해두었다.)

    스프링 MVC에서는 multipart 타입으로 데이터를 받기 위해 @RequestPart 를 이용한다. 

     

    여담으로, Spring 에서는 MultipartFile 타입을 request 파라미터로 받기 위해서는 MultipartResolver Bean이 등록되어 있어야 한다. Spring MVC에서는 이를 자동으로 등록해주지 않으나, Spring Boot를 사용할 경우 MultipartResolver Bean이 등록되어 있지 않다면 자동으로 등록해준다.

    MultipartAutoConfiguration.class

     

    이제 코드를 보며 이야기해보자.

    @RestController
    @RequestMapping("/images")
    public class ImageController {
    
        private final ImageService imageService;
        
        public ImageController(ImageService imageService) {
            this.imageService = imageService;
        }
        
        @PostMapping
        public ResponseEntity<ImageResponse> upload(@RequestPart MultipartFile image) {
            ImageResponse imageResponse = imageService.uploadImage(image);
    
            return ResponseEntity.ok(imageResponse);
        }
    }

    @RequestPart 는 name 속성을 통해 key 값을 지정해 바인딩 해줄 수 있는데, 지정하지 않으면 디폴트로 변수명을 key 값으로 갖기 때문에 특별히 이유가 있지 않다면 따로 지정해주지 않아도 된다.

     

    @Service
    public class ImageService {
    
        private final String imageDir;
    
        public ImageService(@Value("${image-dir}") String imageDir) {
            this.imageDir = imageDir;
        }
        
        public ImageResponse uploadImage(MultipartFile image) {
            final String extension = image.getContentType().split("/")[1];
            final String imageName = UUID.randomUUID() + "." + extension;
    
            try {
                final File file = new File(imageDir + imageName);
                image.transferTo(file);
            } catch (Exception e) {
                throw new FileIOException();
            }
    
            return new ImageResponse(imageName);
        }
    }

    ImageService의 코드를 살펴보자. 먼저, 저장 경로를 뜻하는 imageDir 을 @Value 어노테이션을 통해 주입받고 있는데, 이유는 다음과 같다.

    • 정적 이미지가 어디에 존재하는지 감추고 싶다.
    • 로컬에서 구동할때와 ec2 인스턴스에서 구동할 때의 저장 경로를 달리하려는데, 매번 저장 경로 코드를 바꾸기가 번거롭다.

    따라서 imageDir 은 application.yml 에서 주입하는 방식을 사용했다.

    다음으로, 이미지의 이름을 지정해주어야 한다. 이미지 이름을 저장하는 방식은 {UUID}.{extension} 으로 정했는데, 클라이언트가 이미지를 요청하는 방식이 {server_url}/path/{UUID}.{extension} 이기 때문에 server url 까지 같이 저장해줄까? 하는 생각을 했었다. 하지만, 조금 더 생각해보고 다음과 같은 이유로 생각을 바꾸었다.

    • 만약, 서버의 도메인 네임이 수정된다고 하면, DB에 이미 들어가있는 이미지 이름 데이터들을 다 수정할 것인가? 변경에 자유롭지 못해진다.
    • 도메인이 변경되거나, 도메인 변경과 함께 이미지 서버 자체가 통째로 변경된다 해도, 이미지 파일들만 잘 마이그레이션 한다면 프론트 측에서 이미지 리소스를 받을 때 prefix에 도메인 네임만 변경함으로써 쉽게 변경이 가능하다.

    그렇게 이미지 네임을 설정하고, 이제 이미지를 지정한 경로에 저장할 차례다.

    저장 디렉토리와 이미지 네임을 합친 경로로 File 객체를 생성해준다. 다음으로 요청으로 받은 이미지 데이터를 경로에 저장하면 되는데, 스프링에서는 아주 편리하게도 MultipartFile 객체에서 transferTo 메서드로 이를 해결해준다. MultipartFile 객체가 transferTo 메서드의 파라미터로 Path 객체를 넘겨주면 (File 객체를 넘겨도 된다) 해당  경로로 파일을 잘 복사해준다.

    이렇게 간단히 지정한 경로로 이미지 파일을 저장해줄 수 있다!

     

    이미지 조회

    이제 이미지 조회 기능을 구현해 볼 차례이다.

    정적 리소스에 대한 요청이 들어오면, Spring Boot 에서는 자동으로 resources/static 디렉토리에 있는 파일을 찾아 반환해준다. 그런데, 이미지 파일들을 프로젝트 패키지 내에 보관하면 어떻게 될까? 프로젝트를 다시 클론받으면 해당 파일들은 사라질거고, 만약 CI/CD 파이프라인이 구성되어 있어 jar 파일만 서버에 오게 된다면 어플리케이션을 재시동할 때마다 이미지 파일들은 날아가게 될 것이다.

    그렇기 때문에 프로젝트 외부 경로로 저장을 해 준 것이다. 그럼, 이 외부 경로의 정적 리소스를 어떻게 접근하도록 할까?

    ResourceHandlerRegistry 를 이용하면 된다!

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/images/**")
                    .addResourceLocations("file://" + imageDir);
        }
    }

    addResourceHandlers 메서드를 오버라이딩해 사용하면 된다. registry의 addResourceHandler 메서드를 통해 어떤 경로의 요청에 대해 핸들링 할 것인지 지정해줄 수 있고, addResourceLocations 를 체이닝해 이미지를 존재하는 어플리케이션 외부 경로를 매핑해줄 수 있다.

     

    부가적으로

    이렇게 구현하면 로컬에서 정상적으로 이미지 업로드, 조회가 되는 것을 확인할 수 있다. 그런데, 나는 서버에 배포를 해야 하는 상황이다. 또한, 현재 클라이언트는 https 를 통해 속닥속닥 메인 서버와 통신하고 있다. 이대로 배포하면 클라이언트는 https 를 사용하는데, 이미지 서버에 요청할 때는 http 를 통해 요청하게 된다. 그럴 경우, mixed content 경고가 발생하게 되는데, https를 통해 접속한 사이트에서 http를 통해 자원을 요청할 때 발생하는 경고이다. 크롬은 mixed content 요청인 경우에 안전하지 않은 콘텐츠라며 차단해버린다. 때문에 이미지 서버도 HTTPS 를 설정해주도록 하자!

    HTTPS 설정은 같은 팀원인 크리스가 속닥속닥 기술블로그에 작성해놓은 글을 참고했다.

     

    진짜 끝?

    이대로 끝내도 되는걸까? 싶은 찰나에... 또 다른 문제를 직면했다.

    용량이 높은 이미지인 경우 브라우저에서 이를 받아오는데 8초가 넘게 걸리는 경우도 있다는 것이다. 다음 포스팅에서는 이를 최적화하는 과정을 써보도록 하겠다!

    댓글

Designed by Tistory.