ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] Spring Boot에서 S3 연동하기
    Server/Spring 2023. 8. 2. 01:45

    개발자 학습 로드맵을 만들어주는 RoadMaker 프로젝트를 개발하던 도중 로드맵의 썸네일을 등록하는 API가 필요해졌다.

     

    이전 회사에서 Express.js에서 S3와 multer를 이용해 multipart/form-data로 전달된 이미지를 S3에 저장하는 API를 만들어본 적은 있었는데, 이번에는 스프링부트로 해당 작업을 해보게 되어 은근히 반가웠다.

     

    이번 포스팅은 SpringBoot와 S3를 연동하는 것에 집중할 예정이기에 S3에 대해서 자세히 다루지는 않을 예정이다.

     

    1. 버킷

    1.1. 버킷 생성

    AWS 콘솔에 접근한 뒤 버킷을 만든다.

     

     

    ACL을 열어야 사용자가 저장된 이미지를 볼 수 있기 때문에 다 열어준다(안 그러면 Access Denied를 만나게 될 것이다).

     

     

    이후 '버킷 만들기' 버튼을 누르면 다음과 같이 roadmaker-images 버킷이 잘 만들어진 것을 확인할 수 있다.

     

     

    1.2. IAM 사용자 생성

    서버에 multipart/form-data 타입으로 이미지가 전달되었을 때, 이를 S3에 업로드하기 위해서는 권한이 필요하다.

    IAM 사용자를 생성하고, 액세스키를 csv로 저장해 두자.

    저장한 엑서스키는 Spring Cloud Configuration에서 사용하게 될 것이다.

     

    IAM > 사용자 > 사용자 추가를 누른 후, 사용자 이름을 입력한다.

     

     

    새로 만들어질 사용자에 'AmazonS3 FullAccess' 정책을 연결해 주자.

     

     

    이후 검토 및 생성 단계로 넘어가 사용자를 생성하고 생성된 사용자를 선택한다.

    보안 자격 증명 > 액세스 키 만들기를 눌러 액세스키를 생성하고 이를 csv로 저장해 둔다.

     

     

    2. 의존성 추가

    S3를 연동하기 위해 AWS에서 제공하는 sdk를 사용할 수도 있지만

    스프링에서 쉽게 aws를 사용할 수 있게 만들어진 spring-cloud-aws를 사용할 것이다.

     

    다음과 같이 의존성을 추가해 준다.

    dependencies {
        implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.0")
        implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3'
    }

     

    'implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.0")'는 BOM이다.

    BOM(Bill Of Materials) 파일은 프로젝트에서 사용하는 라이브러리들의 버전을 명시적으로 정의하고, 다른 프로젝트 또는 모듈에서 재사용 가능하게 한다.

     

    Spring Cloud AWS와 같은 큰 라이브러리는 내부적으로 여러 라이브러리들을 사용한다. 이 경우, 모든 하위 라이브러리의 호환되는 버전을 일일히 찾아서 명시해줘야 할 수 도 있다. 하지만 BOM을 사용하면, 이러한 복잡성을 줄이고 특정 버전의 라이브러리가 의존하는 모든 하위 라이브러리의 호환되는 버전을 자동으로 가져와준다.

     

    3. Configuration

    Spring Cloud AWS를 사용하기 위해서는 위에서 만든 액세스 키를 갖고 인증 설정을 해줘야 한다.

    인증 방식은 다양하지만 나는 application.yml에 있는 인증 정보를 읽어오는 기본적인 방법을 사용했다.

     

    더 자세한 인증 방법에 대한 내용은 다음 문서를 참조하기를 바란다(https://docs.awspring.io/spring-cloud-aws/docs/3.0.0/reference/html/index.html#spring-cloud-aws-core)

     

    application.yml에 다음과 같은 정보를 넣어준다.

    spring:
      servlet:
        multipart:
          max-file-size: 20MB # 최대 파일 사이즈
          max-request-size: 20MB # 최대 요청 사이즈
      cloud:
        aws:
          credentials:
            access-key: {ACCESS_KEY} # IAM에서 생성한 access-key
            secret-key: {SECRET_KEY} # IAM에서 생성한 secret-key
          region:
            static: ap-northeast-2 # 버킷 region
          s3:
            bucket: roadmaker-images # 버킷 이름
          stack:
            auto: false

     

    이제 Spring Cloud AWS를 사용할 준비가 되었다!

     

    4. ImageService 작성

    이미지를 업로드할 때 사용할 ImageService를 작성해 주겠다.

    @Service
    @RequiredArgsConstructor
    public class ImageServiceImpl implements ImageService {
        private final S3Template s3Template; // s3
    
        @Value("${spring.cloud.aws.s3.bucket}")
        private String bucketName;
    
        @Override
        @Transactional
        public UploadThumbnailResponse uploadThumbnail(Roadmap roadmap, MultipartFile image) throws IOException {
            String originalFilename = image.getOriginalFilename(); // 클라이언트가 전송한 파일 이름
            String extension = StringUtils.getFilenameExtension(originalFilename); // 파일 확장자
    
            String uuidImageName = UUID.randomUUID().toString() + "." + extension; // 파일 이름 중복 방지
    
    	// S3에 파일 업로드
            S3Resource s3Resource = s3Template.upload(bucketName, uuidImageName, image.getInputStream(), ObjectMetadata.builder().contentType(extension).build());
    
    	// 업로드 된 썸네일 url을 entity 필드에 삽입
            String imageUrl = s3Resource.getURL().toString();
            roadmap.setThumbnailUrl(imageUrl);
    
            return UploadThumbnailResponse.builder().url(imageUrl).build();
        }
    }
    • S3Template: S3를 다루기 위한 S3 Client를 한 단계 더 추상화하여, 더 편하게 사용할 수 있게 한 클래스. 파일을 업로드하거나 읽는 등의 작업을 처리할 수 있다.
    • StringUtils.getFilenameExtension: 사용자가 올린 파일 이름에서 확장자를 추출하는 메서드.
    • UUID: S3는 중복되는 이름을 갖는 파일을 업로드하면 덮어쓴다. 중복을 방지하기 위해 UUID를 생성하고, 추출한 확장자를 붙여 파일 이름을 변경한다.
    • s3Template.upload: bucket 이름, 파일명, 파일스트림, 파일 확장자 정보를 넘겨 S3에 파일을 업로드한다.
    • s3Resource: 업로드 결과, 파일 URL을 반환하도록 한다.
    • UploadThumbnailResponse: 이미지 업로드에 대한 응답 DTO이다. 해당 DTO는 서비스 정책에 맞게 작성하면 된다.

     

    5. ImageController 작성

    이제 썸네일 생성 요청을 라우팅 할 Controller를 만들어주겠다.

    아무나 썸네일을 만들면 안 되고, 로그맵의 주인만 만들 수 있게 검사 로직을 추가해 줬다.

    imageService.uploadThumbnail 메서드가 리턴한 UploadThumbnailResponse를 ResponseEntity로 감싸 클라이언트에 반환해 준다.

    @RestController()
    @RequestMapping("/api/roadmaps")
    @RequiredArgsConstructor
    public class ImageController {
        private final ImageService imageService;
        private final RoadmapService roadmapService;
    
        @LoginRequired
        @PostMapping("/{roadmapId}/thumbnails")
        public ResponseEntity<UploadThumbnailResponse> uploadThumbnail(
            @PathVariable Long roadmapId,
            @RequestPart(value = "file") MultipartFile multipartFile,
            @LoginMember Member member
        ) throws IOException {
            Roadmap roadmap = roadmapService.findRoadmapById(roadmapId);
    
            if (!roadmap.getMember().getId().equals(member.getId())) {
                return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
            }
    
            UploadThumbnailResponse uploadThumbnailResponse = imageService.uploadThumbnail(roadmap, multipartFile);
    
            return new ResponseEntity<>(uploadThumbnailResponse, HttpStatus.CREATED);
        }
    }

     

    6. 결과

    다음과 같이 버킷에 이미지가 저장된 것을 확인할 수 있다.

     

     

    프론트엔드에서는 roadmap.thumbnailUrl을 img 태그의 src에 넣어줌으로써 유저에게 썸네일을 보여줄 수 있게 되었다.

     

    썸네일이 적용된 RoadMaker

     

    7. 마무리

    현재 로직에서는 이미지를 리사이징이 구현되어있지 않아, 유저가 업로드 한 이미지가 그대로 업로드된다.

    이미지를 리사이징 하지 않으면 화면을 불러오는 속도가 느려지고, 많은 리소스를 사용하게 된다.

     

    다음 시간에는 사용자가 이미지를 요청했을 때 리사이징 된 이미지가 캐싱되어 있는지 확인하고, 안되어있다면 리사이징하는 '온디맨드 이미지 리사이징'에 대한 글을 작성해 보겠다.

     

Designed by Tistory.