ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] AWS S3를 이용하여 파일 업로드
    Server/Spring Boot 2020. 9. 8. 22:29
    728x90
    반응형

    AWS S3 설정과 키 발급은 다 되어 있다고 가정하고 글을 진행하려 한다. 

     

    pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-aws</artifactId>
        <version>2.0.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-aws-context</artifactId>
        <version>1.2.1.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-aws-autoconfigure</artifactId>
        <version>1.2.1.RELEASE</version>
    </dependency>

    먼저 프로젝트는 Maven으로 진행되며 pom.xml에 위의 의존성을 추가해주자.

     

     

    application.properties

    cloud.aws.credentials.accessKey=엑세스 키 ID (AWS S3에서 발급 받은 키)
    cloud.aws.credentials.secretKey=비밀 엑세스 키 (AWS S3에서 발급 받은 키)
    cloud.aws.stack.auto=false
    
    # AWS S3 Service bucket
    cloud.aws.s3.bucket=버킷이름 (자신이 설정한 버킷이름)
    cloud.aws.region.static=ap-northeast-2 (버킷 지역(서울은 ap-northeast-2))
    
    # AWS S3 Bucket URL
    cloud.aws.s3.bucket.url=https://s3.ap-northeast-2.amazonaws.com/버킷이름

    application.properties 파일에는 위의 코드를 추가하자. 먼저 위에 accessKey, secretKey는 AWS IAM설정을 하면서 발급받았던 키를 적어주면 된다. 그리고 AWS S3 버킷이름을 설정했던 이름을 적어주면 되는 것이다. 위의 accessKey, secretKey는 외부에 노출되면 안되기 때문에 꼭 .gitignore를 해놓길 바란다.

     

     

     

    File Upload 시나리오

    • 클라이언트에게 Multipart-form/data 형식으로 파일을 전송 받는다. 이 때 파일의 데이터 타입은 MultipartFile이다.
    • FIleUploadServer/S3FileUploadService를 통해 파일을 S3에 업로드하고, 파일이 저장된 URL을 DB에 저장한다.
    • 클라이언트가 파일을 요청 시 파일이 아닌 파일이 저장된 경로를 반환한다.
    • 따라서 클라이언트로부터 데이터를 받을 때는 MultipartFile 데이터 타입으로 받지만, 반환할 땐 String 타입으로 반환한다. 

     

     

    S3FileUploadService

    import com.amazonaws.AmazonClientException;
    import com.amazonaws.services.s3.AmazonS3Client;
    import com.amazonaws.services.s3.model.PutObjectRequest;
    import com.amazonaws.services.s3.transfer.TransferManager;
    import com.amazonaws.services.s3.transfer.Upload;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.UUID;
    
    @Slf4j
    @Service
    public class S3FileUploadService {
    
        // 버킷 이름 동적 할당
        @Value("${cloud.aws.s3.bucket}")
        private String bucket;
    
        // 버킷 주소 동적 할당
        @Value("${cloud.aws.s3.bucket.url}")
        private String defaultUrl;
    
        private final AmazonS3Client amazonS3Client;
    
        public S3FileUploadService(AmazonS3Client amazonS3Client) {
            this.amazonS3Client = amazonS3Client;
        }
    
        public String upload(MultipartFile uploadFile) throws IOException {
            String origName = uploadFile.getOriginalFilename();
            String url;
            try {
                // 확장자를 찾기 위한 코드
                final String ext = origName.substring(origName.lastIndexOf('.'));
                // 파일이름 암호화
                final String saveFileName = getUuid() + ext;
                // 파일 객체 생성
                // System.getProperty => 시스템 환경에 관한 정보를 얻을 수 있다. (user.dir = 현재 작업 디렉토리를 의미함)
                File file = new File(System.getProperty("user.dir") + saveFileName);
                // 파일 변환
                uploadFile.transferTo(file);
                // S3 파일 업로드
                uploadOnS3(saveFileName, file);
                // 주소 할당
                url = defaultUrl + saveFileName;
                // 파일 삭제
                file.delete();
            } catch (StringIndexOutOfBoundsException e) {
                url = null;
            }
            return url;
        }
    
        private static String getUuid() {
            return UUID.randomUUID().toString().replaceAll("-", "");
        }
    
        private void uploadOnS3(final String findName, final File file) {
            // AWS S3 전송 객체 생성
            final TransferManager transferManager = new TransferManager(this.amazonS3Client);
            // 요청 객체 생성
            final PutObjectRequest request = new PutObjectRequest(bucket, findName, file);
            // 업로드 시도
            final Upload upload =  transferManager.upload(request);
    
            try {
                upload.waitForCompletion();
            } catch (AmazonClientException amazonClientException) {
                log.error(amazonClientException.getMessage());
            } catch (InterruptedException e) {
                log.error(e.getMessage());
            }
        }
    }
    

    전체코드는 위와 같은데 하나씩 어떤 의미를 갖고 있는지 이해해보자.

     

    import org.springframework.beans.factory.annotation.Value;
    
    @Service
    public class S3FileUploadService {
    
        // 버킷 이름 동적 할당
        @Value("${cloud.aws.s3.bucket}")
        private String bucket;
    
        // 버킷 주소 동적 할당
        @Value("${cloud.aws.s3.bucket.url}")
        private String defaultUrl;
    }
    

    먼저 위의 코드에서 @Valueapplication.properties파일의 있는 값을 가져와 변수에 할당해주는 어노테이션이다.  (여기서 @Value는 lombok 어노테이션이 아니라는 것을 생각하자)

     

    @Service
    public class S3FileUploadService {
    
        private final AmazonS3Client amazonS3Client;
    
    	// 생성자 의존성 주입
        public S3FileUploadService(AmazonS3Client amazonS3Client) {
            this.amazonS3Client = amazonS3Client;
        }
    
    }
    

    그리고  AmazonS3Client는 S3 전송객체를 만들 때 필요한 클래스이다. 따라서 위의 클래스를 생성자를 통해서 의존성 주입을 받는다.

     

    @Service
    public class S3FileUploadService {
    
        public String upload(MultipartFile uploadFile) throws IOException {
            String origName = uploadFile.getOriginalFilename();
            String url;
            try {
                // 확장자를 찾기 위한 코드
                final String ext = origName.substring(origName.lastIndexOf('.'));
                // 파일이름 암호화
                final String saveFileName = getUuid() + ext;
                // 파일 객체 생성
                // System.getProperty => 시스템 환경에 관한 정보를 얻을 수 있다. (user.dir = 현재 작업 디렉토리를 의미함)
                File file = new File(System.getProperty("user.dir") + saveFileName);
                // 파일 변환
                uploadFile.transferTo(file);
                // S3 파일 업로드
                uploadOnS3(saveFileName, file);
                // 주소 할당
                url = defaultUrl + saveFileName;
                // 파일 삭제
                file.delete();
            } catch (StringIndexOutOfBoundsException e) {
                url = null;
            }
            return url;
        }
    }
    

    그리고 update의 메소드를 살펴봤을 때, 매게변수를 보면 MultipartFile 인터페이스가 존재한다. MultipartFile 인터페이스는 업로드 한 파일 및 파일 데이터를 표현하기 위한 용도로 사용된다. 

    String origName = uploadFile.getOriginalFilename();

    따라서 getOriginalFileName()의 메소드를 통해서 파일이름을 구할 수 있다. 

     

    final String ext = origName.substring(origName.lastIndexOf('.'));

     

    사진 파일의 형식은 abc.jpg, abc.png 등등의 형식일 것이다. 따라서 파일이름에서 확장자(jpg, png)를 구하기 위한 코드이다.

     

     final String saveFileName = getUuid() + ext;

    그리고 위의 코드를 보면 ext 변수에는 파일의 확장자가 들어있을 것이다. 그리고  getUuid() 메소드가 나오는데 그것은 아래의 코드와 같다.

     

    @Service
    public class S3FileUploadService {
    
        private static String getUuid() {
            return UUID.randomUUID().toString().replaceAll("-", "");
        }
    }
    

    UUID는 범용 고유 식별자이기 때문에 완벽한 고유값이라는 보장은 없지만 실제 사용시에 중복될 가능성이 거의 없기 때문에 파일 업로드시 임시 파일명으로 사용한다. 따라서 saveFileName 변수에는 임시 파일명 + 확장자로 값이 들어가게 된다.

    File file = new File(System.getProperty("user.dir") + saveFileName);

    그리고 System.getProperty를 통해서 시스템 환경 값을 가져올 수 있다. 위의 경우에는 "user.dir"을 통해서 현재 작업의 디렉토리를 구할 수 있다. 따라서 현재작업 디렉토리 + 임시파일명을 가지고 File클래스를 만드는 것이다.  

     

    // 파일 변환
    uploadFile.transferTo(file);
    // S3 파일 업로드
    uploadOnS3(saveFileName, file);
    // 주소 할당
    url = defaultUrl + saveFileName;
    // 파일 삭제
    @Service
    public class S3FileUploadService {
    
        private void uploadOnS3(final String findName, final File file) {
            // AWS S3 전송 객체 생성
            final TransferManager transferManager = new TransferManager(this.amazonS3Client);
            // 요청 객체 생성
            final PutObjectRequest request = new PutObjectRequest(bucket, findName, file);
            // 업로드 시도
            final Upload upload =  transferManager.upload(request);
    
            try {
                upload.waitForCompletion();
            } catch (AmazonClientException amazonClientException) {
                log.error(amazonClientException.getMessage());
            } catch (InterruptedException e) {
                log.error(e.getMessage());
            }
        }
    }
    

    위의 코드의 uploadOnS3 메소드는 AWS S3에 전송하기 위한 메소드이다. 주석을 보면 알 수 있듯이, AWS 전송객체와 요청객체를 만들고 업로드를 시도한다.

     

     

    그리고 이제 PostMan을 이용해서 파일 업로드하는 예제를 진행해보자.

    import lombok.Data;
    import org.springframework.web.multipart.MultipartFile;
    
    @Data
    public class ProfileModel {
        private int profileIdx;
        private String profileName;
        // 프로필 사진 URL
        private String profileURL;
    }

    프로필 객체를 담을 Model 클래스를 하나 만들어주자. 그리고 Controller, Service 계층을 만들어 간단한 예제코드로 예제를 진행해보자.

     

    import com.example.demo.model.DefaultRes;
    import com.example.demo.model.ProfileModel;
    import com.example.demo.service.ProfileService;
    import com.example.demo.utils.ResponseMessage;
    import com.example.demo.utils.StatusCode;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    
    @Slf4j
    @RestController
    @RequestMapping("profile")
    public class ProfileController {
    
        public static final DefaultRes FAIL_DEFAULT_RES = new DefaultRes(StatusCode.INTERNAL_SERVER_ERROR, ResponseMessage.INTERNAL_SERVER_ERROR);
    
        private final ProfileService profileService;
    
        public ProfileController(ProfileService profileService) {
            this.profileService = profileService;
        }
    
        @PostMapping("/save")
        public ResponseEntity saveProfile(ProfileModel profileModel, @RequestPart(value = "profile", required = false) final MultipartFile multipartFile) {
            try {
                return new ResponseEntity(profileService.profileSave(profileModel, multipartFile), HttpStatus.OK);
            } catch (Exception e) {
                log.error(e.getMessage());
                return new ResponseEntity(FAIL_DEFAULT_RES, HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    }

    위와 같이 Controller에 경로맵핑, Service 계층을 주입받아 사용하자. 그리고 Content-Type은 Multipart/form-data를 선택하여 아래와 같이 PostMan을 통하여 테스트해보자.

     

    위와 같이 profileName, profile(사진) 업로드를 통하여 요청을 보내을 보내면 된다. 그 전에 Service 계층의 코드를 한번보자.

     

    package com.example.demo.service;
    
    import com.example.demo.mapper.ProfileMapper;
    import com.example.demo.model.DefaultRes;
    import com.example.demo.model.ProfileModel;
    import com.example.demo.utils.ResponseMessage;
    import com.example.demo.utils.StatusCode;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    @Service
    public class ProfileService {
    
        private final S3FileUploadService s3FileUploadService;
        private final ProfileMapper profileMapper;
    
        public ProfileService(S3FileUploadService s3FileUploadService, ProfileMapper profileMapper) {
            this.s3FileUploadService = s3FileUploadService;
            this.profileMapper = profileMapper;
        }
    
    
        public DefaultRes profileSave(ProfileModel profileModel, MultipartFile multipartFile) {
            try {
                if (multipartFile != null) {
                    profileModel.setProfileURL(s3FileUploadService.upload(multipartFile));
                }
    
                profileMapper.saveProfile(profileModel);
                return DefaultRes.res(StatusCode.OK, ResponseMessage.SUCCESS_PROFILE_REGISTER);
            } catch (Exception e) {
                return DefaultRes.res(StatusCode.DB_ERROR, ResponseMessage.DB_ERROR);
            }
        }
    }
    

    PostMan을 통해서 받아왔던 사진을 MultipartFile 객체로 받았었다. 따라서 위에서 작성했던 S3FileUploadService 클래스의 upload메소드를 통하여 MultipartFile 객체의 AWS S3에 업로드된 URL의 값을 받는다. 반환된 URL 값은 ProfileModel 클래스에 넣고 insert쿼리를 통하여 프로필을 등록하자.

     

    import com.example.demo.model.ProfileModel;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface ProfileMapper {
    
        // 프로필 등록
        @Insert("INSERT INTO profile (profileName, profileURL) VALUES (#{profileName}, #{profileURL})")
        void saveProfile(ProfileModel profileModel);
    }

     Mapper는 위와 같이 간단하게 작성을 하였다.

     

    profileIdx profileName profileURL
    1 프로필이름 s3.ap-northeast-2.amazonaws.com/test/s2389djsdji1598d148d002.jpg

    INSERT 쿼리를 실행하면 위와 같이 DB에 위와 같이 저장이 된다.

     

    그리고 PostMan을 통하여 요청하면 아래와 같이 응답이 잘 오는 것도 확인할 수 있다.

     

    반응형

    댓글

Designed by Tistory.