스프링 부트 AWS S3 이미지 올리기

췌누의 개발·2024년 8월 16일
post-thumbnail

행운복권 프로젝트를 진행하면서 유저의 프로필 이미지와 앱 로고를 저장해서 api 통신 시 클라이언트로 이미지를 제공하고자 한다.

AWS S3 버켓 생성 및 환경설정(참고)

https://celdan.tistory.com/36

AWS S3 버킷 생성 및 환경설정이 됐다는 가정하에 이야기를 진행하고자 한다.

yml 파일에 S3 관련 설정들을 환경 변수로 세팅하여 외부 노출을 막았다. 노출되는 순간 바로 이메일이 날아온다... 조심해야 한다

implementation 'com.amazonaws:aws-java-sdk-s3control:1.12.364'

먼저 dependency를 추가 해야한다.


S3Config

@Configuration
public class S3Config {

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

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

    @Bean
    public AmazonS3 getS3ClientBean() {
        AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(Regions.AP_NORTHEAST_2)
                .build();
    }

}

S3Config 설정을 해준다. S3Client 사용하기 위해 Bean으로 등록해주었다.


ErrorCode

@Getter
@AllArgsConstructor
public enum ErrorCode {

    BAD_FILE_EXTENSION(404,  "FILE extension error"),
    FILE_EMPTY(404,  "FILE empty"),
    FILE_UPLOAD_FAIL(404,  "FILE upload fail"),
    FILE_OVER_SIZE(404,  "FILE 크기가 10mb를 초과 하였습니다"),
   
    // .........

    private int status;
    private String reason;
}

예외 처리를 위해 메시지를 따로 보관하였다.


ImageController

@Tag(name = "업로드", description = "업로드 관련 API")
@RequiredArgsConstructor
@RequestMapping("/api/v1/images")
@RestController
@Slf4j
public class ImageController {

    private final ImageService imageService;

    @Operation(summary = "사진 업로드")
    @PostMapping("/upload")
    public UploadImageResponse uploadImage(
            @Parameter(name = "file",
                    description = "multipart/form-data 형식의 이미지를 input으로 받습니다.",
                    required = true)
            @RequestPart MultipartFile file) {

        log.info("file = {}",file);
        return imageService.uploadImage(file);
    }
}

@RequestPart를 사용하여 사진 파일을 multipart/form-data 형식으로 받도록 구현했다.


ImageService

@RequiredArgsConstructor
@Service
@Slf4j
public class ImageService implements ImageUtils{

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

    @Value("${aws.s3.base-url}")
    private String baseUrl;

    private final AmazonS3 amazonS3;

    public UploadImageResponse uploadImage(MultipartFile file) {
        String url = upload(file);
        return new UploadImageResponse(url);
    }

    public String upload(MultipartFile file) {

        if (file.isEmpty() && file.getOriginalFilename() != null){
            throw FileEmptyException.EXCEPTION;
        }

        if (file.getSize() / (1024 * 1024) > 10) {
            throw FileOversizeException.EXCEPTION;
        }

        String originalFilename = file.getOriginalFilename();
        String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

        if (!(ext.equals("jpg")
                || ext.equals("HEIC")
                || ext.equals("jpeg")
                || ext.equals("png")
                || ext.equals("heic"))) {
            throw BadFileExtensionException.EXCEPTION;
        }

        String randomName = UUID.randomUUID().toString();
        String fileName = SecurityUtils.getCurrentUserId() + "|" + randomName + "." + ext;

        try {
            ObjectMetadata objMeta = new ObjectMetadata();
            byte[] bytes = IOUtils.toByteArray(file.getInputStream());
            objMeta.setContentType(file.getContentType());
            objMeta.setContentLength(bytes.length);
            amazonS3.putObject(
                    new PutObjectRequest(bucket, fileName, file.getInputStream(), objMeta)
                            .withCannedAcl(CannedAccessControlList.PublicRead));
        } catch (IOException e) {
            throw FileUploadFailException.EXCEPTION;
        }
        return baseUrl + "/" + fileName;
    }

    @Override
    public void delete(String profilePath) {
        String objectName = getBucketKey(profilePath);
        amazonS3.deleteObject(bucket, objectName);
    }

    public String getBucketKey(String profilePath){
        return profilePath.substring(profilePath.lastIndexOf('/') + 1);
    }
}

이미지를 업로드하는데 핵심 로직이다. 조금 더 자세히 살펴보도록 하겠다.


if (file.isEmpty() && file.getOriginalFilename() != null) {
     throw FileEmptyException.EXCEPTION;
}

if (file.getSize() / (1024 * 1024) > 10) {
     throw FileOversizeException.EXCEPTION;
}

파일이 비어있거나, 파일 최대 크기에 대해서 예외 처리를 진행했다.

String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);

if (!(ext.equals("jpg")
            || ext.equals("HEIC")
            || ext.equals("jpeg")
            || ext.equals("png")
            || ext.equals("heic"))) {
     throw BadFileExtensionException.EXCEPTION;
}

String randomName = UUID.randomUUID().toString();
String fileName = SecurityUtils.getCurrentUserId() + "|" + randomName + "." + ext;

받아온 파일에 확장자를 확인하여 예외 처리를 진행했다. 또한 S3에 사진 파일을 올릴 때 어떤 유저가 이미지를 올렸는지는 파악하고자 uuid 랜덤 값과 SecurityContext에서 유저의 id 값을 가져와 파일을 업로드할 수 있도록 구현했다.


try {
    ObjectMetadata objMeta = new ObjectMetadata();
    byte[] bytes = IOUtils.toByteArray(file.getInputStream());
    objMeta.setContentType(file.getContentType());
    objMeta.setContentLength(bytes.length);
    amazonS3.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), objMeta)
    .withCannedAcl(CannedAccessControlList.PublicRead));
    } catch (IOException e) {
         throw FileUploadFailException.EXCEPTION;
    }
    return baseUrl + "/" + fileName;

ObjectMetadata 객체를 생성하여 파일의 콘텐츠 타입 및 길이를 설정한다.
amazonS3.putObject 메서드를 통해서 Amazon S3에 파일을 업로드한다.

이제 포스트 맨으로 api를 테스트 해보자

성공적으로 통신을 완료했고 이미지 url을 응답 스펙에 맞추어 잘 보내주는 것을 확인했다.

AWS S3에 이미지가 잘 업로드됐다. 우리가 원하는 대로 uuid와 유저의 id 값을 조합하여 url을 구성한 것을 확인했다.

이미지가 잘 저장된 것을 확인할 수 있었다.

프로젝트 링크를 통해서 참고하시면 좋을 것 같습니다! 감사합니다. 도움이 되셨으면 좋겠습니다.👋🏼

행운 복권 깃허브 링크
https://github.com/Uttug-Seuja/luck-lottery-server

profile
아샷추를 좋아합니다

1개의 댓글

comment-user-thumbnail
2024년 8월 17일

개발할때 가장 멋있는 남자..

답글 달기