Project/Team Project

AWS S3에 이미지, 파일 업로드 로직 구현하기

이동식이 2023. 2. 7. 00:21
728x90

Amazon S3 (Simple Storage Service)

어디서나 원하는 양의 데이터를 검색할 수 있도록 구축된 key 기반의 객체 스토리지이며 데이터를 저장 및 검색하는데 사용할 수 있는 고유한 객체 키를 할당한다.

 

파일 서버의 역할을 하는 서비스입니다. 트래픽이 증가하면 장비를 증설해야 하지만 S3이 이를 대신해 주기 때문에 트래픽에 따른 시스템적인 문제는 걱정 할 필요가 없다. 파일에 접근 권한도 지정할 수 있다!

 

모든 종류의 데이터를 원하는 형식으로 저장할 수 있다.

 

Amazon S3 기능

https://aws.amazon.com/ko/s3/features/

 

Amazon S3 기능 - Amazon Web Services

 

aws.amazon.com

 

Amazon S3의 용어

- 객체 : 저장되는 데이터(파일)

- 버킷 : 객체를 그룹핑한 최상위 디렉토리. 버킷 단위로 region을 지정하고 인증과 접속 제한을 걸 수 있다.

- 버전관리: 객체들의 변화를 저장. 삭제하거나 변경해도 변화를 모두 저장합니다.

- RSS : Reduced Redundancy Stroage. 데이터가 손실될 확률이 높은 형태의 저장 방식이지만 가격이 저렴하여 복원이 가능한 데이터를 저장하는 것이 적합합니다.

- Glacier - 매우 저렴한 가격으로 데이터를 저장할 수 있다.

 

AWS S3 버킷 생성

아래 글을 참고하여 버킷을 생성합니다.

https://dev.classmethod.jp/articles/for-beginner-s3-explanation/

 

초보자도 이해할 수 있는 S3(Simple Storage Service) | DevelopersIO

S3(Simple Storage Service)의 개념과 특징에 대해 정리해보았습니다.

dev.classmethod.jp

 

환경 셋팅

build.gradle

- dependency에 spring-cloud-aws를 추가합니다.

 

AWS 설정

application.yml

cloud:
  aws:
    credentials:
      accessKey: IAM 계정 access key
      secretKey: IAM 계정 secret access key
    s3:
      bucket: 버킷명
    region:
      static: 버킷의 resion // 서울은 ap-northeast-2
    stack:
      auto: false

- accessKey, secretKey : AWS 계정에 부여된 key값 (절대 노출되면 안됩니다. 꼭 .gitignore 설정 해주기)

- bucket : S3 서비스에 생성한 버킷 이름

- region.static : S3를 서비스 할 regioin (서울은 ap-northeast-2) 

- stack.auto : CloudFormation(서버 구성 자동화) 자동 실행 여부

 

https://docs.aws.amazon.com/ko_kr/general/latest/gr/rande.html

 

AWS 서비스 엔드포인트 - AWS 일반 참조

이 페이지에 작업이 필요하다는 점을 알려 주셔서 감사합니다. 실망시켜 드려 죄송합니다. 잠깐 시간을 내어 설명서를 향상시킬 수 있는 방법에 대해 말씀해 주십시오.

docs.aws.amazon.com

 

구현

controller

@Slf4j
@Controller
@RequestMapping("/profile")
@RequiredArgsConstructor
public class ProfileController {

    private ProfileService profileService;

	@PostMapping("/{memberId}/edit")
    	public String wrtieProfile(@PathVariable Long memberId, Authentication authentication, MultipartFile multipartFile) {
        	String userName = authentication.getName();
        	profileService.update(memberId, userName, multipartFile);
        	return "redirect:/profile/detail/{memberId}";
	    }

- MultipartFile : 파일 객체를 받기 위해서 사용합니다.

- IOException : S3에 파일을 업로드 할 때 IOEXception 예외를 던져줍니다.

 

service

@Service
@RequiredArgsConstructor
@Slf4j
public class ProfileService {

    private final AmazonS3Client amazonS3Client;
    private final MemberRepository memberRepository;
    private final MemberReviewRepository memberReviewRepository;
    private final ProfileRepository profileRepository;

    // ========== 유효성 검사 ==========
    public Member checkMemberId(Long targetMemberId) {
        Member member = memberRepository.findById(targetMemberId)
                .orElseThrow(() -> new ApplicationException(ErrorCode.USERNAME_NOT_FOUNDED));
        return member;
    }

    public Member checkMemberName(String userName) {
        Member member = memberRepository.findByUserName(userName)
                .orElseThrow(() -> new ApplicationException(ErrorCode.USERNAME_NOT_FOUNDED));
        return member;
    }

    // ========== 프로필 사진 업로드 ==========
    @Value("${cloud.aws.s3.bucket}")
    private String uploadFolder;

    @Transactional
    public ProfileResponse update(Long memberId, String userName, MultipartFile multipartFile) throws IOException {

    // 업로드 된 file의 존재 유무 확인
        if (multipartFile.isEmpty()) {
            throw new ApplicationException(ErrorCode.FILE_NOT_EXISTS);
        }

    // 존재하는 member인지 확인
        Member member = checkMemberId(memberId);


    // AmazonS3 라이브러리 ObjectMetadata 활용
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(multipartFile.getContentType());
        objectMetadata.setContentLength(multipartFile.getSize());

    // 사용자가 올린 파일 이름
        String uploadFileName = multipartFile.getOriginalFilename();

        int index;

        try {
            index = uploadFileName.lastIndexOf(".");
        } catch (StringIndexOutOfBoundsException e) {
            throw new ApplicationException(ErrorCode.WRONG_FILE_FORMAT);
        }

        if (!member.getUserName().equals(userName)) {
            new ApplicationException(ErrorCode.INVALID_PERMISSION);
        }

        String ext = uploadFileName.substring(index + 1);

    // UUID로 랜덤한 id와 파일 이름을 합쳐서 awsS3FileName 변수 생성 (중복 방지)
        String awsS3FileName = UUID.randomUUID() + "." + ext;

        String key = "profileImage/" + awsS3FileName;

        try (InputStream inputStream = multipartFile.getInputStream()) {
            amazonS3Client.putObject(new PutObjectRequest(uploadFolder, key, inputStream, objectMetadata)
                    .withCannedAcl(CannedAccessControlList.PublicRead));
        } catch (IOException e) {
            throw new ApplicationException(ErrorCode.FILE_UPLOAD_ERROR);
        }

        // db에 저장하기
        String fileUrl = amazonS3Client.getUrl(uploadFolder, key).toString();
        log.info("🔵 amazonS3Client.getUrl = {} ", fileUrl );
        // PostFile postFile = PostFile.save(uploadFileName, fileUrl, post);
        Profile profile = Profile.save(uploadFileName, key, member);
        profileRepository.save(profile);
        log.info("🔵 파일 등록 완료 ");
        return ProfileResponse.updateProfileImage(uploadFileName, awsS3FileName);
    }

 

참고

https://victorydntmd.tistory.com/334

https://victorydntmd.tistory.com/323

https://jane514.tistory.com/10