DevJong12

[Picasso][Spring] Object Storage 업로드 기능 구현 본문

프로젝트/Picasso

[Picasso][Spring] Object Storage 업로드 기능 구현

Jong12 2023. 10. 2. 20:46
728x90

목차

    개발 개요

    별다른건 없고..Naver Cloud의 Object Storage를 개별로 Upload Util을 만들걸 생각하니까 머리가 지끈거려와서 공통기능으로 추출해주고 사용하면 좋을 것 같다고 생각하여 제작하였다.

    추가적으로 한개의 버킷에서 dev/~, local/~ 형식으로 경로를 나눠가며 활용을 하고 싶었다. 
    이에 따른 코드를 공유해 보고자 한다.

    의존성 추가

    Common-io의 경우 확장자를 편하게 추출하기 위해 활용하였다.
    또한 amazone의 aws-java-sdk의 경우 Naver Cloud에서는 Object Storage에서 오픈소스로 제작을 한 부분이다  보니 그대로 사용이 가능하였다.
        //File
        api 'commons-io:commons-io:2.13.0'
    
        // Amazon AWS Java SDK S3
        api 'com.amazonaws:aws-java-sdk-s3:1.12.530'

    YML 속성 추가

    accessKey와 secretKey의 경우에는 Naver Cloud에서 마이페이지 - 인증키 관리에서 추가가 가능하다
    또한 해당속성들은 application.yml에 추가하였으며, 필자가 임의로 추가한 값으며 access-keysecret-key의 경우에는 실제로는 VM-Option을 활용하였다.
    naver:
      storage:
        end-point: https://kr.object.ncloudstorage.com
        region-name: kr-standard
        bucket-name: picasso-bucket
        access-key: ${accessKey}
        secret-key: ${secretKey}

     

    실제 사용한 방식


    YML Properties 값 주입 및 Properties 설정

    다음과 같이 두가지 설정을 진행하였다.
    @ConfigurationProperties에서 내가 지정한 항목의 하위 속성들을 다음과 같이 주입하는 설정을 진행하였으며, 해당 Naver ObjectStorageProperties가 Bean으로 등록이 될수 있도록 @Configuration 클래스를 생성해서 Spring이 Startup되는 과정에서 @EnableConfigurationProperties에 등록된 Properties가 Bean으로 생성 될 수 있도록 지정하였다.
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @Getter
    @AllArgsConstructor
    @ConfigurationProperties(prefix = "naver.storage")
    public class NaverObjectStorageProperties {
        private String endPoint;
        private String regionName;
        private String accessKey;
        private String secretKey;
        private String bucketName;
    }
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    import picasso.server.common.properties.NaverObjectStorageProperties;
    
    @Configuration
    @EnableConfigurationProperties(
        {
            NaverObjectStorageProperties.class,
        }
    )
    public class ConfigurationPropertiesConfig {
    }

    AmazonS3객체 Bean 등록

    일단 해당 클래스에서만 비밀스런 정보(Properties)를 모두 주입하여 객체를 생성한다.

    기본적으로 모든 클래스가 주입받게 될 객체는 storageObject()에서 생성된 AmazoneS3객체이다.
    AmazoneS3객체에서는 EndPoint와 Key의 설정에 대한 정보가 필요하다. 해당 객체를 내부의 메소드를 실행하여 정보를 받아 Bean을 생성해 등록한다.
    EndPoint의 경우에는 getEndpointConfig() 메소드에서 EndPoint와 RegionName을 설정해서 EndConfiguration을 반환한다.
    또한 인증키와 관련된 부분은 getEndpointConfig() 메소드에서 accessKey와 privateKey를 지정해 AWSStaticCredentialsProvider객체를 반환한다.
    import com.amazonaws.auth.AWSStaticCredentialsProvider;
    import com.amazonaws.auth.BasicAWSCredentials;
    import com.amazonaws.client.builder.AwsClientBuilder;
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.AmazonS3ClientBuilder;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import picasso.server.common.properties.NaverObjectStorageProperties;
    
    @Slf4j
    @RequiredArgsConstructor
    @Configuration
    public class NaverObjectStorageConfig {
        private final NaverObjectStorageProperties naverObjectStorageProperties;
    
        @Bean
        public AmazonS3 storageObject() {
            log.info("Create NaverObjectStorageConfig Bean");
            return AmazonS3ClientBuilder.standard()
                    .withEndpointConfiguration(this.getEndpointConfig())
                    .withCredentials(this.getCredentialsProvier())
                    .build();
        }
    
        private AwsClientBuilder.EndpointConfiguration getEndpointConfig() {
            log.info(
                    "Create EndPoint Object >>> EndPoint : {}, RegionName : {}",
                    this.naverObjectStorageProperties.getEndPoint(),
                    this.naverObjectStorageProperties.getRegionName()
            );
    
            return new AwsClientBuilder.EndpointConfiguration(
                    this.naverObjectStorageProperties.getEndPoint(),
                    this.naverObjectStorageProperties.getRegionName()
            );
        }
    
        private AWSStaticCredentialsProvider getCredentialsProvier() {
            log.info(
                    "Create CredentialsProvider  >>> AccessKey : {}, SecretKey : {}",
                    this.naverObjectStorageProperties.getAccessKey(),
                    this.naverObjectStorageProperties.getSecretKey()
            );
    
            return new AWSStaticCredentialsProvider(
                    new BasicAWSCredentials(
                            naverObjectStorageProperties.getAccessKey(),
                            naverObjectStorageProperties.getSecretKey()
                    )
            );
        }
    }

    경로를 지정시 활용한 Type

    필자의 경우 local/profile/${fileName}, dev/paint/${fileName} 식으로 파일 경로가 지정되기를 희망하였다. 이때 중간의 profile또는 paint등의 값을 들어갈 수 있게 생성한 EnumType이다.
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    public enum NaverObjectStorageUsageType {
        PROFILE("profile")
        , PAINT("paint");
    
    
        private String path;
    }

    파일 업로드 Util

    기본적으로 우리팀은 대부분 storageFileUpload(NaveObjectStorageUsageType, MultipartFile)메소드만 활용하였다.

    그러면 UsageType의 path로 지정된 value로 파일이 지정되고 파일의 Value가 아래의 이미지처럼 반환되도록 메소드를 완성하였다. 

     

    코드가 너무길어서 더보기로 보면 좋을 듯 하다.

    더보기
    import com.amazonaws.services.s3.AmazonS3;
    import com.amazonaws.services.s3.model.CannedAccessControlList;
    import com.amazonaws.services.s3.model.ObjectMetadata;
    import com.amazonaws.services.s3.model.PutObjectRequest;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.io.FilenameUtils;
    import org.springframework.core.env.Environment;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;
    import picasso.server.common.exception.FileDeleteException;
    import picasso.server.common.exception.FileIOException;
    import picasso.server.common.exception.FileUploadException;
    import picasso.server.common.properties.NaverObjectStorageProperties;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.util.UUID;
    
    
    @Slf4j
    @RequiredArgsConstructor
    @Component
    public class NaverObjectStorageUtil {
        private final AmazonS3 storageObject;
        private final NaverObjectStorageProperties naverObjectStorageProperties;
        private final Environment environment;
    
        /**
         * 사용처, MultipartFile만 제공하면 업로드가 가능하다.
         *
         * @param usageType
         * @param file
         * @return
         * @throws FileUploadException
         */
        public String storageFileUpload(NaverObjectStorageUsageType usageType, MultipartFile file){
            try {
                String filePath = getPath(usageType, getFileUUIDNameByMultipartFile(file));
    
                ObjectMetadata metadata = new ObjectMetadata();
                metadata.setContentType(file.getContentType());
                metadata.setContentLength(file.getBytes().length);
    
                uploadFile(
                        usageType, filePath, file.getInputStream(), metadata
                );
                log.info("File Upload : {}", filePath);
                return filePath;
            } catch (IOException e) {
                throw FileIOException.EXCEPTION;
            } catch (Exception e) {
                log.error(e.getMessage());
                throw FileUploadException.EXCEPTION;
            }
        }
    
        /**
         * 파일 삭제를 진행할 경우 사용한다
         *
         * @param filePath
         * @throws FileDeleteException
         */
        public void storageFileDelete(String filePath) throws FileDeleteException {
            try {
                storageObject.deleteObject(
                        naverObjectStorageProperties.getBucketName(),
                        filePath
                );
                log.info("File Delete : {}", filePath);
            } catch (Exception e) {
                throw FileDeleteException.EXCEPTION;
            }
        }
    
    
        /**
         * 실제 File을 NaverObject Storage에 Upload
         *
         * @param usageType
         * @param filePath
         * @param uploadTarget
         * @param metadata
         */
        private void uploadFile(NaverObjectStorageUsageType usageType, String filePath, InputStream uploadTarget, ObjectMetadata metadata) {
            storageObject.putObject(
                    new PutObjectRequest(
                            naverObjectStorageProperties.getBucketName(),
                            filePath,
                            uploadTarget,
                            metadata
                    ).withCannedAcl(CannedAccessControlList.PublicRead)
            );
        }
    
        /**
         * UUID.png 등으로 제작하기 위함
         *
         * @param file
         * @return
         */
        private String getFileUUIDNameByMultipartFile(MultipartFile file) {
            return UUID.randomUUID() +
                    "." +
                    FilenameUtils.getExtension(file.getOriginalFilename());
        }
    
        /**
         * ObjectStorage에 저장될 File Path
         *
         * @param usageType
         * @param fileName
         * @return
         */
        private String getPath(NaverObjectStorageUsageType usageType, String fileName) {
            return environment.getProperty("spring.profiles.active", "default") + "/"
                    + usageType.getPath() + "/"
                    + fileName;
        }
    
    
    }
     

    기능을 제작해보면서...

    공통 기능을 만드는데 정말 재밌게 작업을 했던 것 같다. 실제 로직자체에서 environment.getProperty를 사용해 기능구현을 하는등 써보고 싶은걸 사용해서 만들고 나니까 아직도 나는 개발이 정말 재밌다는게 몸소 체감할 수 있던 소스였었다.

    728x90
    Comments