AWS S3 적용하기 - IAM & 프로젝트 설정

chrkb1569·2023년 5월 10일
3

개인 프로젝트

목록 보기
14/28
post-custom-banner

전에 올린 게시글에서 S3 설정만 해놓으면 초기 설정은 모두 끝난 것이라고 설명하였으나...
S3를 사용하기 위해서는 IAM을 통하여 Key값을 발급 받아야한다고합니다.

오늘은 지난번에 했었던 S3 작업에 이어 IAM 설정을 진행해보도록 하겠습니다.

IAM?

일단 들어가기에 앞서, IAM이 무엇이길래 S3를 사용하기 위해서 설정해야하는지 궁금해 하실 수 있으실겁니다.

IAM은 AWS Identity and Access Management의 약자로, AWS에서 제공하는 서비스에 대한 엑세스를 안전하게 제어할 수 있는 서비스라는데, '정책'이라는 것을 통해서 서비스를 이용할 수 있고, 없고를 결정한다고합니다.

우리가 버킷을 생성할 때, 버킷 정책 생성기를 통하여 버킷 관련 정책을 설정하였는데, 그것과 비슷하다고 생각하면 될 것 같습니다.

이러한 정책을 통하여 어떠한 서비스를 사용할 수 있고, 어떠한 범위까지 사용할 수 있는지 결정하였다면, 다음으로는 이러한 서비스를 누가 사용할 수 있는지를 정해야하는데, 바로 키를 통하여 결정할 수 있게됩니다.

즉, IAM을 통하여 AWS 서비스에 대한 정책을 결정하고, 키를 발급받으면 키를 보유하고 있는 누구나 서비스를 이용할 수 있는 권한을 갖는다 보면 좋을 것 같습니다.
그럼 IAM이 무엇인지에 대하여 간략하게 알아보았으니, 이제부터는 IAM 설정을 해보도록 하겠습니다.

IAM 설정

일단 AWS IAM으로 들어와서 좌측 메뉴의 사용자로 들어가줍니다.

그리고 사용자 추가를 통하여 새로운 사용자를 등록해보도록 하겠습니다.

다음처럼 사용자 이름을 입력해준 뒤, 넘어가줍니다.

그럼 아까 설명하였던 권한을 설정하는 부분이 나오는데, 우리는 별도의 그룹에 속해있지 않으며, S3만을 위한 권한이 필요하기 때문에 직접 정책 연결을 선택해줍니다.

그럼 다음처럼 화면이 전환될텐데, 여기서 S3를 검색해보면,

뭐가 좀 뜰텐데 여기서 S3FullAccess를 선택해줍니다.
말 그대로 S3에 대한 모든 권한을 소유한다는 의미입니다.
그런데 저기보면 S3FullAccess를 보유한 엔터티가 1개가 존재한다고 뜨는데, 저건 조금있다가 설명해드리겠습니다.
그리고 다음으로 넘어가면,

이제까지의 설정들을 한번에 보여주는데, 바로 생성해줍니다.

그럼 이렇게 생성이 되었을텐데, 정작 중요한 키는 발급받지 않았다는 점이 함정입니다.
키를 발급 받아야하기 때문에 방금 생성한 사용자를 선택해줍니다.

그리고 보안 자격 증명 메뉴로 이동한 뒤, 아래로 내려가서 살펴보면 다음처럼 엑세스 키라는 메뉴를 볼 수 있습니다.

외부에서 S3를 사용하도록 만들기 위하여 엑세스 키를 생성해줍니다.

일단 다음과 같은 화면이 뜨는데, 저는 일단 AWS 외부에서 실행되는거니까 AWS 외부에서 실행되는 애플리케이션을 선택하였습니다.

그리고 다음처럼 키에 대한 설명을 적어줍니다.
선택 사항이니 그냥 넘어가셔도 상관 없습니다.

그러면 키가 바로 생성이 될텐데...
여기서 가장 중요한 것은 키를 저장해주셔야합니다.
아까 왜 S3FullAccess를 가진 사용자가 하나 더 있는지 궁금하셨나요?

그건 깜빡하고 키파일을 저장 안 한 케이스입니다ㅋㅋ
아니 나중에 확인할 수 있을 줄 알았는데, 한 번 보여주고 끝이더라구요
그냥 저장하고 확인하는게 제일 마음에 편합니다.

키파일을 다운받았으면 이젠 진짜 끝입니다.
이제는 코드를 통해서 S3에 이미지를 저장하는 작업을 만들어보면 될 것 같습니다.

코드 작성

코드 작성하는 시간이 제일 마음이 편한 것 같네요

일단 가장 먼저 application.yml 파일부터 설정하고 시작하겠습니다.

spring:
  datasource:
    url: {데이터베이스 주소}
    username: {사용자이름}
    password: {사용자 비밀번호}
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      use-new-id-generator-mappings: false
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    database: mysql

cloud:
  aws:
    credentials:
      access-key: {다운받은 키 파일 중 Access Key ID}
      secret-key: {다운받은 키 파일 중 Secret Access Key}
    region:
      static: ap-northeast-2
    stack:
      auto: false

Repository의 경우에는 S3에 이미지가 저장될테니 필요성이 없을 수도 있지만, 그래도 이왕 데이터베이스를 연결한거, 데이터베이스에도 이미지 정보를 저장하도록 하겠습니다.

Entity

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.UUID;

@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    private String originName; // 이미지 파일의 본래 이름

    private String storedName; // 이미지 파일이 S3에 저장될때 사용되는 이름

    private String accessUrl; // S3 내부 이미지에 접근할 수 있는 URL

    public Image(String originName) {
        this.originName = originName;
        this.storedName = getFileName(originName);
        this.accessUrl = "";
    }

    public void setAccessUrl(String accessUrl) {
        this.accessUrl = accessUrl;
    }

    // 이미지 파일의 확장자를 추출하는 메소드
    public String extractExtension(String originName) {
        int index = originName.lastIndexOf('.');

        return originName.substring(index, originName.length());
    }

    // 이미지 파일의 이름을 저장하기 위한 이름으로 변환하는 메소드
    public String getFileName(String originName) {
        return UUID.randomUUID() + "." + extractExtension(originName);
    }
}

간단하게 설명을하자면, 이미지파일을 입력받으면 우리는 이 이미지 파일의 이름을 다른 이름으로 변경하여 저장할 것입니다.

우리에게 전송되는 이미지 파일들은 파일 고유의 이름을 가지고 있습니다.

그러나, 이 고유한 이름이 중복되는 상황이 발생할 수도 있는데, 우리는 이러한 현상을 방지하기 위하여 UUID를 새로운 이미지 파일의 이름으로 설정해주어 이름으로 인한 충돌을 방지하였습니다.

따라서, originName이 이미지 파일 본래의 이름을 나타내고,
storedName은 이미지 파일이 데이터베이스에 저장될 때 사용될 이름
accessUrl은 S3를 통하여 발급받게될 접근 URL입니다.

확장자를 추출하는 메소드 extractExtension의 경우에는 지원하는 이미지 파일의 확장자를 확인하기 위하여 생성하였으며, getFileName의 경우에는 기존 이미지 파일의 이름을 변경하기 위하여 생성하였습니다.

Repository

import minsub.S3Test.ImageStorage.domain.Image;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ImageRepository extends JpaRepository<Image, Long> {
}

다음으로는 Entity를 관리하기 위한 Repository입니다.

솔직히 S3를 사용해서 필요는 없지만, 그래도 일단 넣었습니다.

S3Config

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {

    @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()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }
}

다음으로는 S3를 사용하기 위하여 설정을 해주는 S3Config 파일입니다.
application.yml 파일에서 설정하였던 값들을 가져와, 이를 통하여 AmazonS3Client를 Bean 등록하는 작업을 수행합니다.

Service

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.ObjectMetadata;
import lombok.RequiredArgsConstructor;
import minsub.S3Test.ImageStorage.ImageRepository;
import minsub.S3Test.ImageStorage.domain.Image;
import minsub.S3Test.ImageStorage.dto.ImageSaveDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ImageService {

    private static String bucketName = "chrkb156920230508";

    private final AmazonS3Client amazonS3Client;
    private final ImageRepository imageRepository;

    @Transactional
    public List<String> saveImages(ImageSaveDto saveDto) {
        List<String> resultList = new ArrayList<>();

        for(MultipartFile multipartFile : saveDto.getImages()) {
            String value = saveImage(multipartFile);
            resultList.add(value);
        }

        return resultList;
    }

    @Transactional
    public String saveImage(MultipartFile multipartFile) {
        String originalName = multipartFile.getOriginalFilename();
        Image image = new Image(originalName);
        String filename = image.getStoredName();

        try {
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentType(multipartFile.getContentType());
            objectMetadata.setContentLength(multipartFile.getInputStream().available());

            amazonS3Client.putObject(bucketName, filename, multipartFile.getInputStream(), objectMetadata);

            String accessUrl = amazonS3Client.getUrl(bucketName, filename).toString();
            image.setAccessUrl(accessUrl);
        } catch(IOException e) {

        }

        imageRepository.save(image);

        return image.getAccessUrl();
    }
}

다음으로는 이미지 저장 로직을 담당하는 ImageService입니다.

MultipartFile의 이미지 파일을 입력받아, 이를 S3에 저장하는 로직을 수행합니다.

ImageSaveDto

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class ImageSaveDto {
    private List<MultipartFile> images = new ArrayList<>();
}

사용자로부터 MultipartFile을 받아오는 DTO입니다.
@Setter 어노테이션의 경우, MultipartFile형태의 이미지를 받아오기 위하여 선언하였습니다.

Controller

import lombok.RequiredArgsConstructor;
import minsub.S3Test.ImageStorage.dto.ImageSaveDto;
import minsub.S3Test.ImageStorage.service.ImageService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ImageController {

    private final ImageService imageService;

    @PostMapping("/image")
    @ResponseStatus(HttpStatus.OK)
    public List<String> saveImage(@ModelAttribute ImageSaveDto imageSaveDto) {
        return imageService.saveImages(imageSaveDto);
    }
}

사용자로부터 MultipartFile 이미지를 받아오는 Controller입니다.
DTO로 데이터를 받아오고, 이를 통하여 로직을 수행합니다.

PostMan을 통한 결과 확인

PostMan을 사용할 경우, Form-data 방식의 데이터 요청도 수행할 수 있는데,

다음처럼 이미지를 첨부하여 요청할 경우,

우리가 생성하였던 S3 내부에 이미지가 저장되는 것을 확인할 수 있습니다.

결과화면은 다음과 같은데, 반환되는 URL을 통하여 다음처럼 우리가 첨부한 사진에 접근할 수 있습니다.

일단은 코드와 동작확인까지만 빠르게 설명하였는데, 다음 시간에 코드에 대하여 자세하게 설명하는 시간을 갖도록 하겠습니다.

post-custom-banner

4개의 댓글

comment-user-thumbnail
2024년 2월 2일

글 재밌게 잘 쓰시네요!!
글 보면서 공부하고 있는데 고충을 같이 나누는 느낌이라 재밌게 하고 있습니다.
포스팅 감사합니다.

1개의 답글
comment-user-thumbnail
2024년 3월 20일

하하.. 블로그 글 보면서 따라해보다가
access 키만 저장하고 secret 키를 저장안해서 또 만들게 됬네요...ㅜ_ㅠ

좋은 글 감사합니다!!

1개의 답글