본문 바로가기

AWS
[AWS] S3 이미지 업로드(+ CKEditor5)

로컬 서버에 사진을 저장할 경우 다른 곳에서 실행할 때 사진이 안 나온다던가.. 하는 불편함을 해결하기 위해 클라우드에 파일 저장을 시도했다.

개인 프로젝트이기 때문에 과금은 안 될 걸로 예상하고 aws를 사용하기로 했다.
(다음 달에 한 번 보긴 해야 하지만, 사이트 배포해서 돌렸을 때도 금액이 얼마 안 나왔다.)


AWS 루트 계정을 만든 후 S3에 접속해서 버킷을 생성해준다.


버킷을 생성한 후에는 사용자를 생성해준다. 

IAM 콘솔로 이동하여 액세스 관리  >  사용자  > 사용자 생성


사용자가 생성되면 사용자명을 눌러서 아래 사진처럼 '보안 자격 증명'의 '액세스 키 만들기' 를 클릭하여 액세스 키를 생성한다.

생성한 액세스 키와 비밀 액세스 키는 기록해 두기.


이제 버킷 정책을 편집한다.

버킷 정책이 원래 비어있을 경우, 편집을 누른 후 '새 문 추가'를 눌러서 정책 추가한다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::버킷명/*"
        }
    ]
}

이제 AWS 웹 설정은 끝났고, 프로젝트로 돌아와서 설정해주면 된다.

나는 maven이기 때문에 pom.xml에 dependency를 추가해줬다.

<!-- AWS S3 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-aws</artifactId>
	<version>2.2.6.RELEASE</version>
</dependency>


* gradle은 build.gradle에 추가

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


버전 같은 경우 아래 사이트에서 확인하여 최신 버전으로 바꿔주면 된다.
https://central.sonatype.com/artifact/org.springframework.cloud/spring-cloud-starter-aws

 

Maven Central: org.springframework.cloud:spring-cloud-starter-aws

Discover spring-cloud-starter-aws in the org.springframework.cloud namespace. Explore metadata, contributors, the Maven POM file, and more.

central.sonatype.com


다음으로는 application.properties 파일에 aws 관련 설정을 추가한다.

액세스 키, 비밀 액세스 키는 절대절대절대 노출되면 안 된다..

github에 프로젝트를 올릴 경우 반드시 gitignore에 추가해주고 따로 파일을관리해야 한다.

cloud.aws.s3.bucket=
cloud.aws.credentials.access-key=
cloud.aws.credentials.secret-key=
cloud.aws.region.static=ap-northeast-2
cloud.aws.region.auto=false
cloud.aws.stack.auto=false

이제 S3 설정 파일을 생성한다.

@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 credentials = new BasicAWSCredentials(accessKey, secretKey);

        return (AmazonS3Client) AmazonS3ClientBuilder
                .standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }

}


aws 관련 설정은 application.properties에 정의해 둔 것을 @Value를 통해 가져온다.


이제 파일을 업로드하는 메소드를 정의해주면 된다.

@Service
@RequiredArgsConstructor
public class S3FileUploadService {

    private final AmazonS3 amazonS3;

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

    public String saveFile(MultipartFile multipartFile, String newFileName) throws IOException {

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(multipartFile.getSize());
        metadata.setContentType(multipartFile.getContentType());

        amazonS3.putObject(bucket, newFileName, multipartFile.getInputStream(), metadata);
        return amazonS3.getUrl(bucket, newFileName).toString();
    }

}


나는 파일명과 s3에 업로드된 url을 DB에 넣어 관리하기로 계획했기 때문에 s3에 파일을 저장할 때는 UUID를 이용해서 저장한다.

그래서 multipartFile뿐만 아니라 newFileName도 넘겨주고 newFileName으로 저장하게 구현했다.


이제 이걸 CKEditor와 연동하는 것이 핵심이다.

 mounted() {
    CustomEditor.create(document.querySelector('#editor'), {
      simpleUpload: {
        uploadUrl: this.$store.state.url + 'treasure/image',
      },
    })


uploadUrl 은 사진 저장을 처리할 서버의 주소로 지정하면 된다.


uploadUrl과 연결되는 메소드를 서버에 정의하면 첨부 성공!

@PostMapping("/treasure/image")
    public ResponseEntity<?> uploadImage(@RequestParam("upload") MultipartFile file) {
        try {

            // 원본 파일명
            String originName = file.getOriginalFilename();

            // 저장될 새 파일명 생성
            String newFileName = java.util.UUID.randomUUID().toString() + "@" + originName;

            //aws s3에 저장 후 이미지 url 반환 받기
            String imageUrl = s3Service.saveFile(file, newFileName);

            return ResponseEntity.ok(Map.of(
                    "uploaded", 1,
                    "fileName", newFileName,
                    "originName", originName,
                    "url", imageUrl
            ));

        } catch (Exception e) {
            return ResponseEntity.status(500).body(Map.of(
                    "uploaded", 0,
                    "error", Map.of("message", "파일을 업로드하지 못했습니다")
            ));
        }
    }


중복 등의 문제로 사진을 저장할 때 UUID를 사용했는데, 원본파일명을 알고 있어야 하기 때문에 고민하다가 UUID 뒤에 원본파일명을 붙였다.

구분자로 @를 사용해서 받아올 때 쉽게 파싱할 수 있다.


원하는 사진을 선택하여 첨부하면

정상적으로 첨부된다!!

image resize 플러그인을 추가했기 때문에 사진 사이즈는 자유롭게 조절 가능하다. 

AWS S3 버킷에서 확인해 보면 사진이 잘 저장된 것까지 확인할 수 있다.


com.amazonaws.SdkClientException: Failed to connect to service endpoint

AWS 관련 에러가 발생했다.  

AWS S3를 사용하는데, 실행 환경은 AWS 가 아니라서 발생하는 에러이다.

VM  optiond에 EC2 메타데이터를 사용하지 않도록 설정한다.

-Dcom.amazonaws.sdk.disableEc2Metadata=true


이렇게 해주면 위의 에러 메시지는 사라진다.


하지만..

com.amazonaws.AmazonClientException: EC2 Instance Metadata Service is disabled

라는 예외가 또 발생한다..

이것도 역시 AWS S3를 AWS EC2 인스턴스 환경 아닌 곳에서 실행해서 발생하는 예외이다.

에러가 아닌 예외 메시지이기 때문에 안 보이게 하려고 application.properties에 아래 코드를 추가했다.

logging.level.com.amazonaws.util.EC2MetadataUtils=error

CKEditor는 이미지 첨부와 동시에 클라우드에 저장하고, 이미지를 삭제할 때는 별다른 처리 방법을 제공하지 않는다.

따라서 게시글을 작성할 때 최종적으로 첨부된 사진과 한 번이라도 첨부했던 사진 목록을 비교하여 최종적으로 첨부되지 않고 삭제된 사진을 클라우드에서 삭제해주어야 한다.

안 해도 상관은 없지만.. 개인정보 문제이기도 하고, 용량도 차지하므로 비효율적이니까 처리해주는 것이 좋을 것!