AWS S3 이미지 업로드 - 배포한 사이트에 이미지 업로드

박영준·2023년 7월 17일
0

Spring

목록 보기
43/58

S3 버킷 만들기

참고: AWS S3 이미지 업로드 - S3 버킷 만들기


배포한 사이트에 이미지 업로드

S3 버킷을 만들어서 이미지를 업로드해서 연결을 확인했고,
이번엔 서버를 열어서 해당 서버로 이미지를 업로드할 것이다.
(Postman 으로 해당 API 로 요청을 보내면, 미리 생성해둔 버킷에 이미지가 업로드된 것을 확인 할 수 있음)

1. 코드 작성

1) 세팅하기

(1) application-aws.properties

// 액세스 키, 비밀 액세스 키
cloud.aws.credentials.accessKey=AWS 액세스 키
cloud.aws.credentials.secretKey=AWS 비밀 액세스 키

// 버킷 이름
cloud.aws.s3.bucket=버킷 이름

// 버킷 생성시 선택한 AWS 리전
cloud.aws.region.static=ap-northeast-2

// 설정한 CloudFormation 이 없으면 프로젝트 시작이 안되니, 해당 내용을 사용하지 않도록 false 를 등록
cloud.aws.stack.auto=false

// 파일 업로드 크기 설정
spring.servlet.multipart.max-file-size=20MB		// 파일 하나당 크기
spring.servlet.multipart.max-request-size=20MB	// 전송하려는 총 파일들의 크기

// MySQL 설정
spring.datasource.url=jdbc:mysql://엔드포인트:3306/초기 데이터베이스 이름
spring.datasource.username=마스터 사용자 이름
spring.datasource.password=마스터 암호

환경변수를 설정해준다.

(2) build.gradle

// aws
implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.3.1'
// 이걸로 해도 무방
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

Spring-Cloud-AWS 의존성을 추가해준다.

2) AwsConfig

@Configuration
public class AwsConfig {

	// IAM 계정의 액세스 키
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

	// IAM 계정의 비밀 키
    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

	// AWS 리전 이름
    @Value("${cloud.aws.region.static}")
    private String region;

	// AmazonS3Client 빈을 생성
    @Bean
    public AmazonS3Client amazonS3Client() {
    	// BasicAWSCredentials : accessKey 와 secretKey 를 기반으로 인증 정보를 생성
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()					// AmazonS3ClientBuilder.standard() : S3 클라이언트를 구성하기 위한 빌더 객체를 생성
                .withRegion(region)													// 클라이언트가 작업할 AWS 지역을 설정
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))		// withCredentials() : 액세스 키 및 비밀 키를 제공하는 AWSStaticCredentialsProvider 설정
                .build();															// 이 메서드를 호출하여 최종적으로 AmazonS3Client 인스턴스를 생성하고 반환
    }
}
  • AWS 액세스 키, 비밀 키, 버킷, 지역(region)에 대한 설정을 담은 클래스다.

    • AmazonS3Client 인스턴스를 다른 컴포넌트에서 주입해서, AWS S3 서비스에 대한 작업을 수행할 수 있다
  • @Value 를 통해, application-aws.properties 파일에서 설정 값을 주입받는다

3) Controller

	// 게시글 작성
    @PostMapping
    public ResponseEntity<PostResponseDto> createPost(@RequestPart(value = "image", required = false) MultipartFile multipartFile, @RequestPart PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok(postService.createPost(multipartFile, requestDto, userDetails.getUser()));
    }

게시글 작성에 이미지를 포함시킬 수 있다.
@RequestPart 를 통해, MultipartFile 과 json 타입의 데이터(PostRequestDto) 를 하나의 API 로 함께 보낼 수 있다.

required = false

  • 이미지를 넣지 않고 게시글을 작성할 경우, 이미지 대신 null 값이 들어가도록 null 값을 허용해준다.
    • 물론 이를 위해서는 Post 엔티티에서 image 필드에 @Column(name = "image", nullable = false) 에서 nullable 을 적용하지 않아야 한다.

아래 코드에서 @ModelAttribute 사용 이유?

  • 참고: @ModelAttribute
        // 게시글 작성
        @PostMapping
        public ResponseEntity<PostResponseDto> createPost(@RequestPart(value = "image", required = false) MultipartFile multipartFile, @ModelAttribute PostRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
            return ResponseEntity.ok(postService.createPost(multipartFile, requestDto, userDetails.getUser()));
        }

4) Service

(1)

@Service
@RequiredArgsConstructor
public class PostService {
    private final UserService userService;
    private final PostRepository postRepository;
    private final PostLikeRepository postLikeRepository;
    private final AmazonS3 amazonS3;
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    // 게시글 작성
    public PostResponseDto createPost(MultipartFile multipartFile, PostRequestDto requestDto, User user) {
        // 이미지 s3 업로드 후에 image url 반환
        String image = null;
        if (multipartFile != null) {
            image = uploadImage(multipartFile);
        }

        Post post = new Post(requestDto, image, user);
        postRepository.save(post);
        return new PostResponseDto(post);
    }
    
    ...
    
    // 게시글 삭제
    public MsgResponseDto deletePost(Long post_id, User user) {
        // 게시글이 있는지 & 사용자의 권한 확인
        Post post = userService.findByPostIdAndUser(post_id, user);

        // 이미지 s3 삭제
        if (post.getImage() != null) {
            amazonS3.deleteObject(bucket, post.getImage().substring(58));
        }

        postRepository.delete(post);

        return new MsgResponseDto("게시글을 삭제했습니다.", HttpStatus.OK.value());
    }
    
    ...

}

deleteObject(버킷 이름, 객체 키)

  • 버킷 > 업로드된 이미지 > 속성 > 키 에 적힌 값은 해당 이미지를 전송했을 때 앞에 달린 주소를 제외한 값이므로, substring 으로 잘랐다
    (Postman 에서 body로 반환된 이미지 데이터)

(2)

    ...
    
	// 이미지 업로드
    public String uploadImage(MultipartFile multipartFile) {
        String fileName = createFileName(multipartFile.getOriginalFilename());

        ObjectMetadata objectMetadata = new ObjectMetadata();		// ObjectMetadata 를 통해 파일에 대한 정보를 추가
        objectMetadata.setContentLength(multipartFile.getSize());		// multipartFil 의 크기 설정 (byte)
        objectMetadata.setContentType(multipartFile.getContentType());	// multipartFil 의 컨텐츠 유형 설정

        try(InputStream inputStream = multipartFile.getInputStream()) {
            amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)		// 객체를 S3에 업로드
                    .withCannedAcl(CannedAccessControlList.PublicRead));		// 업로드된 객체에 대한 공개 읽기 권한을 설정
        } catch(IOException e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "이미지 업로드에 실패했습니다.");
        }

        return amazonS3.getUrl(bucket, fileName).toString();		// 업로드된 객체(사진)의 URL을 반환
    }

	// 파일 이름 생성
    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 + ") 입니다.");
        }
    }
}

이미지 업로드

  • getOriginalFilename()

    • 업로드되는 파일에서 확장자를 포함한 파일의 이름을 반환
  • 업로드된 이미지는 공개 읽기 권한이 설정된 상태로 저장

  • 업로드된 이미지의 URL을 반환하여, 사용자가 이미지에 접근할 수 있다.

  • InputStream

    • 데이터를 byte 단위로 읽어들이는 통로
    • getInputStream() : 파일의 내용을 읽기 위한 InputStream 을 반환

파일 이름 생성

  • 주어진 파일 이름을 기반으로 고유한 파일 이름을 생성

  • UUID.randomUUID().toString()

    • 고유 식별자를 랜덤 생성
  • 문자열1.concat(문자열2)

    • 문자열1 과 문자열2 를 붙일 수 있다.
    • 따라서, "랜덤 생성된 고유 식별자 + 파일 확장자 명" 을 만들게 된다.
  • getFileExtension(파일 이름)

    • 주어진 파일 이름에서 파일 확장자를 추출

파일 확장자

  • fileName.lastIndexOf(".")
    • 마지막 점의 인덱스를 찾고, 해당 인덱스 이후의 부분을 반환
  • 파일 확장자가 없는 경우 StringIndexOutOfBoundsException 이 발생

2. 배포하기

참고: RDS, EC2 로 배포하기

(요약하자면) 다음 과정을 거쳐서 배포를 진행해준다.

RDS 구매 > RDS 포트 열기 > EC2 구매 및 접속 > build > Filezilla 로 업로드 > SpringBoot 작동시키기

3. 정적 웹 사이트 호스팅

1)

생성해둔 버킷으로 들어가기 > 속성 > 정적 웹 사이트 호스팅 > 편집

2)

정적 웹 사이트 호스팅 : 활성화 로 체크

인덱스 문서 : 프로젝트 > resources > templates > index.html 을 생성해주고, index.html 을 입력해서 이를 기본 페이지로 설정해준다

3)

생성해둔 버킷으로 들어가기 > 속성 > 정적 웹 사이트 호스팅 > 버킷 웹 사이트 엔드포인트 로 들어가면
배포해둔 사이트로 연결이 된다.

4. Postman 으로 테스트하기

게시글 작성 요청을 보내면

버킷에 사진이 업로드되어 있다.

단, form-data 형식으로 json 데이터를 전송하기 위해서는 json 타입인 것을 명시해줘야 한다.(application/json)


참고: Springboot로 S3 파일 업로드하기
참고: S3로 정적서버 배포
참고: 📁 AWS 정적 웹페이지 배포하기 - S3, CloudFront
참고: [Spring] Json with MultipartFile
참고: [Spring] Spring Boot AWS S3 사진 업로드 하는 법
참고: [SpringBoot] SpringBoot를 이용한 AWS S3에 여러 파일 업로드 및 삭제 구현하기

profile
개발자로 거듭나기!

0개의 댓글