프로젝트/뉴스피드

[Spring boot + AWS S3] 이미지 저장하기(프로필, 게시글 이미지)

writtenbyrla 2024. 2. 15. 22:14

💡 프로젝트 환경

  • spring boot 3.2.2
  • spring-cloud-starter-aws:2.2.6.RELEASE

 

 

1-1. AWS S3 버킷 만들기

1) aws 콘솔에서 회원가입 후 S3 검색해 버킷을 만듦

https://aws.amazon.com/ko/console/

 

AWS Management Console

AWS Support 플랜은 AWS로 성공하는 데 도움이 되는 다양한 도구, 프로그램 및 전문 지식에 대한 액세스의 조합을 제공합니다.

aws.amazon.com

실무에서는 보통 액세스 차단을 한다고 함, 테스트를 위해서 퍼블릭 상태로 둠

2) 버킷 정책 생성

권한 - 버킷 정책 편집 - 정책 생성기 클릭

Actions에는 GetObject 선택해주면 됨

현재 이미 정책을 생성해놓아서 유효하지 않은 ARN으로 보이긴 하지만, 실제로는 버튼이 활성화될 것이다.

버튼을 클릭하면 json 형태로 생성된 정책을 확인할 수 있으며, 이를 정책편집기에 붙여넣기 하고 변경사항을 저장해주면 된다.

 

 

1-2. 사용자 생성

root 사용자로 이용해도 되지만 IAM 사용자를 생성해서 이용하도록 한다. 

오른쪽 상단 계정정보에서 보안자격 증명 - 액세스 관리 - 사용자로 이동하여 사용자 생성 버튼 클릭

권한 설정 시 기존 정책 직접 연결 - 정책 필터: AmazonS3FullAccess 체크

생성 후 액세스 키 ID와 비밀 액세스 키가 발급 되는데 스프링 부트 연동 시 필요하므로 반드시 따로 기록하여 관리해야 한다.

 

 

2. 스프링부트에 AWS 설정 세팅

1) application.yml

연결을 위해 yml 파일에 버킷과 사용자 정보를 넣어줘야 한다.

반드시 gitignore을 이용해서 외부에 노출되지 않도록 조심 또 조심!!

accessKey와 secretKey는 IAM 사용자 생성 시 발급받은 키로 넣어주면 된다.

cloud:
  aws:
    stack:
      auto: false
    credentials:
      access-key: {accessKey}
      secret-key: {secretKey}
    region:
      static: ap-northeast-2
    s3:
      bucket: {버킷 이름}

 

2) config

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

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

 

3) controller -> service로 변경(필요한 controller에서 꺼내쓸 수 있게)

처음에 controller로 api를 만들어 테스트 해보았더니 잘 되었다!

나는 버킷에서 폴더를 또 분리했기 때문에 url에 폴더명까지 붙여줬고,

같은 파일명이 s3에 올라가게 되면 중복으로 인식해 하나만 남기 때문에 중복 방지를 위해 uuid를 생성하여 파일명에 같이 붙여주었다.

@RestController
@RequiredArgsConstructor
public class FileController {

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

    @PostMapping("/upload")
    public List<String> uploadFiles(@RequestPart("files") List<MultipartFile> files) {
        List<String> uploadedFileUrls = new ArrayList<>();

        try {
            for (MultipartFile file : files) {
                String fileName = file.getOriginalFilename();
                String uuid = UUID.randomUUID().toString();
                String fileUrl= "https://" + bucket  + ".s3.ap-northeast-2.amazonaws.com/post/" + uuid + "_" + fileName;
                ObjectMetadata metadata = new ObjectMetadata();
                metadata.setContentType(file.getContentType());
                metadata.setContentLength(file.getSize());
                amazonS3Client.putObject(bucket, "post/"+ uuid + "_" + fileName, file.getInputStream(), metadata);
                uploadedFileUrls.add(fileUrl);
            }
            return uploadedFileUrls;
        } catch (IOException e) {
            throw new HttpException(false, "이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST);
        }

    }
}

 

하지만 프로필 이미지, 포스트에 쓸거라 service단으로 커스텀하여 옮겨놓고 작업했음!

@Service
@RequiredArgsConstructor
@Slf4j
public class FileUploadService {

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

    // 유저 프로필 이미지 업로드
    public String uploadProfile(MultipartFile file) {
        try {
            String fileName=file.getOriginalFilename();
            String uuid = UUID.randomUUID().toString();
            ObjectMetadata metadata= new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(file.getSize());
            amazonS3Client.putObject(bucket,"profile/"+ uuid + "_"+fileName,file.getInputStream(),metadata);
            return amazonS3Client.getUrl(bucket, "profile/"+ uuid + "_"+fileName).toString();

        } catch (IOException e) {
            throw new HttpException(false, "프로필 이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST);        }
    }


    // 게시글 멀티미디어 저장
    public List<String> uploadFiles(List<MultipartFile> files) {
        List<String> uploadedFileUrls = new ArrayList<>();

        try {
            for (MultipartFile file : files) {
                String fileName = file.getOriginalFilename();
                String uuid = UUID.randomUUID().toString();
                String fileUrl= "https://" + bucket  + ".s3.ap-northeast-2.amazonaws.com/post/" + uuid + "_" + fileName;
                ObjectMetadata metadata = new ObjectMetadata();
                metadata.setContentType(file.getContentType());
                metadata.setContentLength(file.getSize());
                amazonS3Client.putObject(bucket, "post/"+ uuid + "_" + fileName, file.getInputStream(), metadata);
                uploadedFileUrls.add(fileUrl);
            }
            return uploadedFileUrls;
        } catch (IOException e) {
            throw new HttpException(false, "이미지 등록에 실패하였습니다.", HttpStatus.BAD_REQUEST);
        }

    }
}

 

 

 

3. controller에서 필요한 로직 작성

1)  RequestBody로 받는 updateDto 와 RequestParam으로 받는 file 별도 로직 처리

// 프로필 이미지 수정
@PatchMapping("/{userId}/profileImg")
public ResponseEntity<UserResponseDto> uploadProfileImg(@PathVariable Long userId, @RequestParam("file") MultipartFile file) {
    String profileUrl = fileUploadService.uploadProfile(file);
    UserUpdateDto updateDto = new UserUpdateDto(userId, profileUrl);
    mypageService.updateProfileImg(userId, updateDto);
    UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 이미지 수정 완료");
    return ResponseEntity.status(HttpStatus.OK).body(response);
}
// 기본정보 수정
@PatchMapping("/{userId}/profile")
public ResponseEntity<UserResponseDto> updateProfile(@PathVariable Long userId,
                                                     @RequestBody @Valid UserUpdateDto userUpdateDto){

    mypageService.updateProfile(userId, userUpdateDto);
    UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 수정 완료");
    return ResponseEntity.status(HttpStatus.OK).body(response);
};

 

👉 프로필 이미지의 경우 분리해도 괜찮지만 게시글에 다중 이미지 처리할 땐 하나의 메소드로 합쳐져야 한다고 판단해서 하나의 컨트롤러로 합침

 

 

2) RequestPart로 동시에 파라미터값 받아서 db값 수정 + S3 업로드 한번에 처리

@PatchMapping("/{userId}/profile")
public ResponseEntity<UserResponseDto> updateProfileTest(@PathVariable Long userId,
                                                     @RequestPart(value = "data") @Valid UserUpdateDto userUpdateDto,
                                                     @RequestPart(name = "files") MultipartFile file){
    
    String profileUrl = fileUploadService.uploadProfile(file);
    userUpdateDto.setProfileImg(profileUrl);
    mypageService.updateProfile(userId, userUpdateDto);
    UserResponseDto response = UserResponseDto.res(HttpStatus.OK.value(), "프로필 수정 완료");
    return ResponseEntity.status(HttpStatus.OK).body(response);
};

 

 

 

4. post맨 테스트

프로필 수정(닉네임, 소개, 프로필 이미지) 할 때 

아래와 같이 data에 json형태의 객체 그대로를 넣고 files에 파일 업로드해서 한번에 보내면 

db에도 유저 정보가 수정되면서 s3에 들어가면 파일이 잘 들어와있다!

 

 

 

 

 

+++ 나의 프로젝트에서는 멀티미디어 파일 첨부하는 것이 선택사항이기 때문에

반드시 하나의 api로 관리할 필요가 없다고 느껴져서 멀티미디어와 게시글 관련 로직을 모두 분리하였다.

만약 쇼핑몰처럼 반드시 첨부가 되어야 할 상황에서는 같은 api로 작성하는 것도 좋을 것 같다!