본문 바로가기
개발이야기

AWS S3 이미지 업로드 Spring으로 사용해보기

by janiiiiie 2021. 11. 14.
반응형

 

대부분의 이미지 서버를 구축할 때는 AWS S3를 보편적으로 사용합니다. 저희 또한, S3를 애플리케이션 이미지 업로드 및 다운로드 서버로 선택하게 되었습니다. 큰 이유가 있기 보다는.. 서버를 사용하는데는 항상 비용문제가 따라오기 마련인데 마침 공짜 크레딧이 생겨(!!) 무리 없이 이용할 수 있게 되었습니다. 

스프링으로 사용하지 않는 경우에는 보통 API Gateway - AWS Lambda - S3 식의 구조로도 많이 발견이 됩니다. 이는 트래픽이 많이 몰릴 수 있는 상황이거나 앞으로의 확장성을 많이 고려해야 할 때 사용하는 방식입니다. 

 

하지만 어드민 페이지를 구축하거나(보통 사진업로드가 많지않음) 저희처럼 MVP한 모델로 신규 서비스를 런칭하는 것이라면 크게 트래픽이 몰리지 않기 때문에 오히려 서버 관리 비용과 리소스만 늘릴 수 있습니다. API Gateway - Lambda 를 사용하느냐, 스프링을 통해 구현하느냐는 각 상황에 맞게 사용하는 것이 좋습니다. 

 

저희 서비스의 경우는 매우 MVP한 모델이며 트래픽이 있을지 없을지는 런칭 후 모니터링이 필요하기 때문에 지금 당장은 Spring을 통해서 구현을 하기로 내부적으로 논의를 마쳤습니다.

 

 

S3 버킷 생성하기

저는 이미 Elastic Beanstalk 으로 인해 S3 버킷이 생성이 되어 있는 상태였습니다. 만약 S3 단독으로 생성할 경우 Console -> Amazon S3 -> 버킷 생성을 클릭해 간단하게 만들 수있습니다. 버킷 생성 후, S3 접근을 위한 IAM 계정을 필수로 생성해주어야 합니다.

 

 

 

의존성 추가

S3와 연동하기 위해서는 AWS 서비스와 연결을 시켜주는 아래와 같은 의존성이 필요합니다. 현재 구축 시점(2021/10월) 에서는 아래 버전이 최신이었습니다. (Spring 2.4.x 버전과 호환성 문제 없었음)

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

 

 

AWS S3 config 

저의 경우 AWS S3 client 연결 정보를 config 파일을 통해 Bean으로 생성 후 사용을 하였습니다. 버킷 생성 후, 접근을 위한 IAM 계정을 생성했다면 access key, secret access key 등이 제공되었을텐데요, 그 정보를 받아오는 파일입니다.

@Configuration
public class AwsS3Config {

    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}

@Value 로 받아오는 외부주입 값들은 모두 아래와 같은 yml이나 properties 파일에서 설정 후 받아오면 됩니다. 

 

 

application-aws.yml

cloud:
  aws:
    s3:
      bucket: 버킷명
    credentials:
      access-key: IAM 계정 - access key
      secret-key: IAM 계정 - secret access key
    region:
      static: 현재 s3 버킷의 Region
    stack:
      auto: false

해당 정보는 절대로 깃허브 같은 외부로 공개되면 안되기 때문에 반드시 gitIgnore를 활용하여 커밋이 되지않도록 주의해야 합니다.

 

 

AwsS3Service (upload, delete, createFileName)

@Service
@RequiredArgsConstructor
public class AwsS3Service {

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

    private final AmazonS3 amazonS3;

    public List<String> uploadImage(List<MultipartFile> multipartFile) {
        List<String> fileNameList = new ArrayList<>();

        multipartFile.forEach(file -> {
            String fileName = createFileName(file.getOriginalFilename());
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(file.getSize());
            objectMetadata.setContentType(file.getContentType());

            try(InputStream inputStream = file.getInputStream()) {
                amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
            } catch(IOException e) {
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다.");
            }

            fileNameList.add(fileName);
        });

        return fileNameList;
    }

    public void deleteImage(String fileName) {
        amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
    }

    private String createFileName(String fileName) {
        return UUID.randomUUID().toString().concat(getFileExtension(fileName));
    }

    private String getFileExtension(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
        }
    }
}

저희는 프론트엔드 개발자 분들과 협의하여, upload/delete 기능을 구성하기로 하고 파일 resize나 확장자 validation check는 백엔드가 아닌 프론트 쪽에서 하기로 결정했습니다. 파일 업로드 후, s3에 업로드 된 파일명을 리스트 형태로 돌려주도록 설계하였습니다.

 

upload

보통 이미지 업로드 기능을 자바로 사용할 시, MultipartFile 객체를 사용하여 이미지 정보를 들고 오는데요, 현재 빵집에 대한 사진은 여러장을 올릴 수 있기 때문에 List로 받아오고 있습니다.

 

들고온 파일들을 forEach문을 사용해 돌면서 파일 정보를 읽어오고 AmazonS3 라이브러리를 활용해 putObject를 통해서 손쉽게 업로드 해주었습니다. 업로드 할 때 한글명을 가진 파일의 경우 인식을 제대로 못해 외계어로 올라가는 경우가 많았는데요, 이 때문에 파일이름을 랜덤으로 생성해주는 createFileName 메서드를 따로 만들어 해당 이름으로 업로드 되도록 하였습니다. 

 

생성 된 파일명만을 돌려주기로 이야기가 되었기 때문에, 업로드 된 파일이름을 리스트로 응답값으로 반환하도록 구축했습니다. 

 

 

delete

삭제 기능은 매우 간단합니다. 클라이언트 쪽에서 가지고 있을 파일명들을 그대로 받아와서 해당 파일명과 일치하는 사진을 deleteObject를 통해서 삭제하도록 하였습니다. 

 

 

 

AwsS3Controller

이제 위의 기능을 사용할 수 있도록 controller를 만들면 끝입니다. 컨트롤러 코드는 일반적인 REST API 호출 구조이고 특별한 것은 없습니다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/s3")
public class AmazonS3Controller {

    private final AwsS3Service awsS3Service;

    /**
     * Amazon S3에 이미지 업로드
     * @return 성공 시 200 Success와 함께 업로드 된 파일의 파일명 리스트 반환
     */
    @ApiOperation(value = "Amazon S3에 이미지 업로드", notes = "Amazon S3에 이미지 업로드 ")
    @PostMapping("/image")
    public ResponseEntity<List<String>> uploadImage(@ApiParam(value="img 파일들(여러 파일 업로드 가능)", required = true) @RequestPart List<MultipartFile> multipartFile) {
        return ApiResponse.success(awsS3Service.uploadImage(multipartFile));
    }

    /**
     * Amazon S3에 이미지 업로드 된 파일을 삭제
     * @return 성공 시 200 Success
     */
    @ApiOperation(value = "Amazon S3에 업로드 된 파일을 삭제", notes = "Amazon S3에 업로드된 이미지 삭제")
    @DeleteMapping("/image")
    public ResponseEntity<Void> deleteImage(@ApiParam(value="img 파일 하나 삭제", required = true) @RequestParam String fileName) {
        awsS3Service.deleteImage(fileName);
        return ApiResponse.success(null);
    }
}

 

 

실제로 호출해서 직접 이미지 업로드/삭제 해보기

직접 로컬에서 실행시켜서 확인해보도록 합니다. 저는 Insomnia 라는 API 테스터 툴을 사용해보았습니다.

 

아무 사진이나 클릭해서 업로드 해봅시다

반환 받은 파일명과 일치한 사진이 S3 버킷에 잘 업로드가 되었습니다 :)

그럼, 해당 파일을 한번 삭제해보도록 하겠습니다.

 

 

삭제할 때는 저희 UI 플로우 상 하나씩 클릭하여 삭제할 것이기 때문에 리스트로 받아오도록 하지 않았습니다.

 

현재 파일명을 가지고 버킷의 동일한 이름의 파일이 잘 삭제 된 것이 확인되었습니다..!

 

 

이렇게 간단하게 이미지 업로드 구현을 저희 서비스에 추가를 해보았습니다. 만약 사용자가 늘어난다면 저희 서비스 또한 API Gateway - Lambda 로 버전업을 해야겠지만, 현재로써는 스프링으로도 무난하게 동작할 수 있을 것이라고 생각합니다. 해당 작업 PR을 블로그에 첨부할 예정이니 참고 바랍니다!

 

https://github.com/depromeet/bread-map-backend/pull/84

 

AWS s3 이미지업로드 구현 by Jane096 · Pull Request #84 · depromeet/bread-map-backend

삭제 시 필요한 파일명 현재 createFileName() 으로 임의의 파일명을 생성 중, 해당 파일 명을 리턴하여 그것을 저장하도록 함 이 파일명으로 이후 s3의 파일 삭제 테스트 에러 방지를 위해 setProperty에

github.com

 

https://github.com/Jane096

 

Jane096 - Overview

Make it count! Jane096 has 5 repositories available. Follow their code on GitHub.

github.com

 

반응형