106DAYS) [Main-Project] AWS 및 IAM 계정 생성 및 S3를 활용한 이미지 업로드 시도중, 3주차 멘토링

nacSeo (낙서)·2023년 3월 21일
0

AWS 계정 생성부터 구글링을 참고하였는데 최신 버전과 차이가 있어 나에게 알맞는 설정을 하는데 진땀을 뺐다 ^^,,
우선 루트 계정을 만들었고, IAM 계정을 생성하여 S3 버킷을 사용할 수 있는 권한을 부여하였다. (루트 계정 그대로 사용하는 것보다 IAM 계정을 파서 권한을 부여하여 사용하는 방식이 안전하고 일반적이라고 .. :0)
그리고 여러 시행 착오를 끝으로 버킷 생성 완료!

계정 및 S3 세팅을 마치고 코드를 구현했다.
우선, build.gradle에 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'을 추가해주고, aws 관련 yml파일들을 만들어주었다.

application-aws.yml

cloud:
  aws:
    s3:
      bucket: <AWS S3 버킷 이름>
    region:
      static: ap-northeast-2  # AWS 리전 정보
    stack:
      auto: false
    credentials:
      instanceProfile: true  # AWS CLI에서 aws configure list 정보를 반영할건지 여부

application-credentials.yml

cloud:
  aws:
    credentials:
      accessKey: <AWS IAM Access Key>
      secretKey: <AWS IAM Secret Key>

application.yml

spring:
  profiles:
    include:
      - aws
      - credentials
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  servlet:
    multipart:
      maxFileSize: 50MB
      maxRequestSize: 50MB
logging:
  level:
    org:

application.yml에는 profiles로 aws와 credentials를 추가해주고, servlet을 통해 multipart 파일 사이즈를 설정해주었다.

참고로 보안을 위해 .gitignore에 키 정보가 담겨있는 credentials yml파일을 추가!

### s3 개인 정보 보호 ###
!**/src/main/resources/application-credentials.yml

다음으로 본격적으로 S3 관련 로직을 추가했다.

S3Component


@Getter
@Setter
@ConfigurationProperties(prefix = "cloud.aws.s3")
public class S3Component {
    private String bucket;
}

Component로 S3 bucket을 추가해줬다.

S3Config

@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;

    public AmazonS3 amazonS3Client() {
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

yml 파일에 등록한 aws accessKey, secretKey, region 값을 적용시키고 계정 정보를 등록하는 로직을 작성하였다.

AWSS3UploadService

@RequiredArgsConstructor
@Component
public class AWSS3UploadService implements UploadService {
    private final AmazonS3 amazonS3;
    private final S3Component component;

    @Override
    public void uploadFile(InputStream inputStream, ObjectMetadata objectMetadata, String fileName) {
        amazonS3.putObject(new PutObjectRequest(component.getBucket(), fileName, inputStream, objectMetadata)
                .withCannedAcl(CannedAccessControlList.PublicRead));
    }

    @Override
    public String getFileUrl(String fileName) {
        return amazonS3.getUrl(component.getBucket(), fileName).toString();
    }
}

build.gradle에 추가한 AmazonS3와 위에 작성한 S3Component를 DI받아 MultipartFile로 받아온 이미지 파일을 inputStream, objectMetadata, fileName으로 나눠서 s3에 업로드할 수 있는 로직을 구현하였다.

FileUploadService

@RequiredArgsConstructor
@Service
public class FileUploadService {
    private final UploadService s3Service;

    // Multipart를 통해 전송된 파일을 업로드하는 메소드
    public String uploadImage(MultipartFile file) {
        String fileName = createFileName(file.getOriginalFilename());
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(file.getSize());
        objectMetadata.setContentType(file.getContentType());
        try (InputStream inputStream = file.getInputStream()) {
            s3Service.uploadFile(inputStream, objectMetadata, fileName);
        } catch (IOException e) {
            throw new IllegalArgumentException(
                    String.format("파일 변환 중 에러가 발생하였습니다 (%s)", file.getOriginalFilename()));
        }
        return s3Service.getFileUrl(fileName);
    }

    // 기존 확장자명을 유지한 채, 유니크한 파일의 이름을 생성하는 로직
    private String createFileName(String originalFileName) {
        return UUID.randomUUID().toString().concat(getFileExtension(originalFileName));
    }

    // 파일의 확장자명을 가져오는 로직
    private String getFileExtension(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e) {
            throw new IllegalArgumentException(
                    String.format("잘못된 형식의 파일 (%s) 입니다", fileName));
        }
    }
}

위에도 주석으로 설명이 있지만 Multipart를 통해 전송된 파일을 업로드하는 메소드, fileName을 기존 확장자명을 유지한 채, 유니크한 파일의 이름을 생성하는 java util인 UUID를 활용한 로직, 파일 확장자명을 가져오는 로직 등을 구현하였다.

UploadService

public interface UploadService {
    void uploadFile(InputStream inputStream, ObjectMetadata objectMetadata, String fileName);

    String getFileUrl(String fileName);
}

그리고 어제 잘못 작성했던 reviewService의 createReview 매개변수로 Multipart를 받아오는 거나 Mapper에 review.setReviewImage(postDto.getReviewImage());로직 등을 지우고, Controller에는 body와 image라는 이름으로

public ResponseEntity postReview(@RequestPart("body") ReviewDto.Post postDto,
                                     @RequestPart("image") MultipartFile reviewImage,
                                     Principal principal) {
                                     ...
}

로 변경하였다.

마지막으로 추가한 S3 로직을 사용하기 위해 reviewCompositeService에 FileUploadService를 DI 받고 createReview 메서드에

public Review createReview(Review creatingReview, MultipartFile reviewImage, String email) {
        Member member = memberService.findLoginMemberByEmail(email);

        // TODO: s3 저장하는 로직
        fileUploadService.uploadImage(reviewImage);

        creatingReview.setMember(member);

        Review createdReview = reviewService.createReview(creatingReview);

        return createdReview;
    }

다음과 같이 코드를 추가해주었다.

그렇지만 과정에서 많은 에러가 발생 ^^,, 내일 직접 팀원들과 만나 작업하기로 했는데 해당 에러들을 해결하면 에러핸들링을 작성해보도록 하겠다!

밤에는 3주차 멘토링 시간이 있었다.
멘토링을 통해 피드백을 받고 주요 의사결정 사항들은 다음과 같았다.

프로젝트 관련 외적으로도 멘토분께서 취업 전략이나 테크 블로그 활용법, 기술면접 팁 등등 여러 좋은 정보들을 제공해주셨다. 지금 현업에서 열심히 활동하고 계신 개발자셔서 많이 와닿고 소소한 팁같은 부분도 신경주셔서 너무 감사했다!! 🙇‍♂️

profile
백엔드 개발자 김창하입니다 🙇‍♂️

0개의 댓글