20211202 TIL - S3에 파일을 업로드해보자

JIHYE·2021년 12월 2일
1
post-thumbnail

52market Project

Spring을 이용해 S3 버킷에 사진 업로드하기

어제는 로컬에 사진을 저장하는 것을 진행했는데 가장 큰 문제가 로컬에 저장하면 다시 브라우저에서 불러 올 수 없다는 점이다
로컬에 저장이 가능해지면 궁극적으로 S3에 저장하는것이 목표였기 때문에 오늘 구현해보기로 마음 먹었다
먼저 S3 버킷을 생성해주고 Spring과 연결해주면 간단하게 구현이 가능하다고는 하는데...
나에게는 하나도 간단치가 않았다 😅

먼저 dependency를 추가해주어야한다

implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.0.1.RELEASE'

이제 SpringS3에 업로드를 해줄 준비가 되었다

그리고 S3 버킷을 생성하고나서 꼭 권한을 잘 챙겨야한다
다 구현해놓고 ACL 문제때문에 업로드가 안되는 경우가(나야 나🙋🏻‍♀️)생기기 때문이다

application.yml파일에 세팅을 해주어야한다

cloud:
  aws:
    credentials:
      accessKey: 나의 accessKey
      secretKey: 나의 secretKey
    s3:
      bucket: 나의 bucket 이름
    region:
      static: ap-northeast-2	// 서울리젼
    stack:
      auto: false

이렇게 세팅해주고 꼭 .gitignore에 추가해주어야 나의 소듕한 정보가 해커들의 표적이 되지않는다
aws가 모니터링을 하고있다고는 하지만 미리미리 조심하는것이 좋으니까 🙆🏻‍♀️

파일을 S3에 업로드하는 과정은

  1. 로컬에 임시 저장
  2. S3에 업로드
  3. 임시저장한 파일 삭제

이러한 과정을 거치는데,
그 전에 먼저 S3와 연결을 해야 이 모든 과정이 가능하다는 점
여기서 쓰인 코드들은 내 spring의 멘토님의 블로그 글을 참고하였다

먼저 Bean 주입을 해준다

@Configuration
public class AmazonS3Config {

    @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();
    }
}

Key들은 미리 application.yml에 준비해두었기때문에 @Value로 불러와준다

다음은 이제 업로더를 준비해준다

@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket;

    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)  // 파일 변환할 수 없으면 에러
                .orElseThrow(() -> new IllegalArgumentException("파일 변환 에러 MultipartFile -> File convert fail"));
        return upload(uploadFile, dirName);
    }

    private String upload(File uploadFile, String dirName) {
        Date date = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String newFormat = simpleDateFormat.format(date);
        String extension = newFormat + "_" + uploadFile.getName();
        String fileName = dirName + "/" + extension;   // S3에 저장된 파일 이름
        String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드

        return uploadImageUrl;

    }

    // S3로 업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        removeNewFile(uploadFile);
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    // 로컬에 저장된 이미지 지우기
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("파일 삭제 성공");
            return;
        }
        log.info("파일 삭제 실패");
    }
    // 로컬에 파일 업로드 하기
    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
        if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
            try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }
}

파일이름으로 다시 이미지를 불러와야하기 때문에 unique한 값이 필요한데, 일단은 지금시간을 받아온 뒤 파일명을 구성하였다

Service 코드도 변경해주고

@Transactional
    public Article uploadOrUpdate(ArticleRequestDto requestDto, String imagePath) {
        Long userId = requestDto.getUserId();
        User user = userRepository
                .findById(userId)
                .orElseThrow(
                        () -> new NullPointerException("잘못된 접근입니다."));

        String imageName = imagePath.split("/Article/")[1];

        Article article = Article.builder()
                .user(user)
                .title(requestDto.getTitle())
                .content(requestDto.getContent())
                .imagePath(imagePath)
                .imageName(imageName)
                .latitude(user.getLatitude())
                .longitude(user.getLongitude())
                .build();

        return articleRepository.save(article);
    }

imageName은 img 태그에서 alt부분에 들어가야하기때문에 imagePath에서 폴더명 이하로 불러와주었다.

Controller 코드도 변경을 해주었다

@PostMapping("/api/article/write")
    public ResponseEntity<Article> writeArticle (@ModelAttribute ArticleRequestDto requestDto,
                                                 @ModelAttribute MultipartFile file) throws IOException {
        String imagePath = s3Uploader.upload(file, "Article");
        Article article = articleService.uploadOrUpdate(requestDto, imagePath);
        return ResponseEntity.ok().body(article);
    }

imagePath를 받아오는 부분이 추가되었는데 파라미터로 받아온 파일은 s3Uploader.upload(file, "Article") 이곳을 거쳐 imagePath에 문자열로 Path값이 저장되고 게시글은 원래 구현되어있던 ArticleService를 거쳐서 리턴된다
s3Uploader.upload(file, "폴더명")으로 구현하였다
잘 구현되는지 확인하기위해 어제 만들었던 원시의 html파일에 불러오는 테스트까지 마치면 완-뵥!

근데 여기서 문제점 하나는 S3버킷의 접근 권한을 어디까지 줘야할까인데, 사용자에게 S3FullAccess를 줬는데도 업로드가 ACL때문에 안된다고해서 다른 권한도 오픈되어있는 상태라 이거는 좀 더 공부가 필요할듯하다

분명히 수정해야할 부분이 남아있고 예외처리를 해야할 상황이 훨씬 많지만 작은 기능부터 구현했으니 점점 범위를 넓혀나가면 될거라고 생각한다(이것이 agail...🤔)

모의면접의 날

오늘 모의면접이 있다는 것을 알았지만 사실 나는 오전 내내 S3 업로드와 씨름하느라 말그대로 빈손으로 면접에 임하게 되었다
주관식 시험 벼락치기처럼...(쭈그르처럼 들어갔다 😵)
불행인지 다행인지 면접관님이 좋은 이야기도 많이 해주시고, 더 많은 지식을 쌓아야겠다고 느끼기도 한 계기가 되었다
마지막으로 질문할 내용이 있냐고 물어보셨는데 거기에 사실 내가 캠프에 임하면서 하고있던 모든 고민이 담겨있었다 (화면에는 안보였겠지만 말하면서 살짝 눈물 그렁😥)

"팀원들 개개인의 능력이 워낙 출중하고 나는 그에 비해 너무나 개인기가 약하여 작은 기능들위주로 맡아서 구현하게 되는데, 이걸 극복하기위해 개인프로젝트를 하나 더 준비해야할까요?" 가 나의 질문이었다
면접관님의 대답은 개인프로젝트와 팀프로젝트 모두 잘하면 좋지만, 사실상 그건 힘들기때문에 작은 기능이라도 팀프로젝트에서 맡은바 최선을 다하되 기능을 구현함에 있어서 필요한 지식들은 채워나가면 좋을것 같다라는 이야기를 해주셨다
전에 매니저님과도 같은 이야기를 나눈적이 있었는데, 그 때 매니저님도 자기자신의 성장에 집중하라고 해주셨는데, 남과 비교하거나 작아지지말고 어제의 나보다 오늘의 나가 성장하면 되는거라고 말씀해주시면서 자기자신의 성장에 집중하란 이야기를 해주셨는데 비슷한 맥락이라고 느껴졌다

그리고 나의 애증의 양방향매핑 이야기를 들으시고는 스토리텔링을 잘 하여 면접때 트러블슈팅을 해결한 경험으로 이야기하면 좋을것 같다는 이야기도 해주셨다
마지막으로 기억에 남는 질문중에 httphttps중 어떤게 더 빠를까요?라고 질문하셨는데 궁금해서 찾아보았다
결론만 말하자면 https가 더 빠르다 😆

차이가 거의 없지만 https를 이용하면 암호화/복호화의 과정이 필요하기 때문에 http보다 속도가 느리다고한다
출처 : MangKyu's Diary

면접경험으로 나의 무지를 한번 더 뼈저리게 느끼기는 했지만 뭔가 더 올바른 방향으로 나아갈 수 있는 계기가 되었다고 생각한다
다음에 또 다시 이런 기회가 주어진다면 좀 더 잘 준비해서 임하면 좋을거라 생각한다

내일은 MVC Pattern에 대해 공부해봐야징📖

profile
초보개발자의 개발일기

0개의 댓글