[SpringBoot] AWS S3로 이미지 업로드하기

Cherry·2022년 7월 21일
13
post-thumbnail

프로젝트 시작하기 전에는 스프링부트 1도 몰랐고 어떻게 돌아가는지도 몰랐는데 이번에 이미지 s3로 업로드하는 api만들면서 조금 감이 온것 같다. 진짜 에러의 에러 연속을 만나면서 나 스프링부트랑 안맞나....? 생각하면서 진짜 던지고 싶었는데 지금 에러 다 해결해서 기분이 너무 좋아서 쓰는 글이다ㅎㅎㅎ

우선 aws에서 s3 생성부터 하자

AWS S3 Bucket 생성


위에 사진대로 생성해준다

IAM 사용자 권한 추가

S3에 접근하기 위해서는 IAM 사용자에게 S3 접근 권한을 주고, 그 사용자의 액세스 키, 비밀 엑세스 키를 사용해야 한다.


설정후에 사용자 추가를 하면 액세스 키, 비밀 엑세스 키가 보여지는데 이 키들은 현재 화면에서 밖에 볼 수 없다. 즉, .csv 파일을 다운받아 로컬에 꼭 가지고 있어야 한다. 꼭 무조건,,,,

Spring Boot로 파일 업로드

위에 s3 버킷 설정을 다해주면 이제 스프링부트 프로젝트만 수정해주면 된다.

build.gradle에 의존성 추가

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

Spring Boot는 gradle 기반으로 만들었고 위의 의존성을 추가하해준다.

application/properties 작성하기

application.properties에 다음과 같이 아까 생성했던 s3의 정보와 IAM 사용자에 대한 정보를 등록해준다.
이때 주의해야 하는 점은 해당 정보를 깃허브에 절대 업로드하면 안된다는 점이다!!! 퍼블릭에 올리면 잘못해서 해킹당해 과금당할 수가 있다고 한다. 조심 또 조심해야된다ㅜㅜ 나는 /gitignore에 넣어서 애초에 git에 올라가지 않게 만들어줬다.

# S3
cloud.aws.credentials.accessKey=
cloud.aws.credentials.secretKey=
cloud.aws.s3.bucket=
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto-=false

S3config 작성하기

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3 amazonS3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

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

}

따로 config 디렉토리에서 설정 값을 넣기 위해서 AmazonS3Config 설정 클래스를 만들었다. application/properties 파일에 작성한 값들을 읽어와서 AmazonS3Client 객체를 만들어 Bean으로 주입해주는 것이다. 스프링 신기하다...

S3Uploader 작성하기

package team_k.symda.Service;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor    // final 멤버변수가 있으면 생성자 항목에 포함시킴
@Component
@Service
public class S3Uploader {

    private final AmazonS3Client amazonS3Client;

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

    // MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
        return upload(uploadFile, dirName);
    }

    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);

        removeNewFile(uploadFile);  // 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)

        return uploadImageUrl;      // 업로드된 파일의 S3 URL 주소 반환
    }

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

    private void removeNewFile(File targetFile) {
        if(targetFile.delete()) {
            log.info("파일이 삭제되었습니다.");
        }else {
            log.info("파일이 삭제되지 못했습니다.");
        }
    }

    private Optional<File> convert(MultipartFile file) throws  IOException {
        File convertFile = new File(file.getOriginalFilename());
        if(convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }
        return Optional.empty();
    }

}

이제 파일받아 서버로 업로드는 하는 코드이다.

convert() 메소드에서 로컬 프로젝트에 사진 파일이 생성되지만, removeNewFile()을 통해서 바로 지워준다.

모델 생성하기

@Getter
@Setter
@ToString
@NoArgsConstructor
@Entity
public class Diary {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long diary_id;  // 일기 pk

    @Column(length = 1000)
    private String content; // 일기 내용

    @Enumerated(EnumType.STRING)
    private Weather weather;    // 날씨

    private LocalDate created_at;   // 생성 시간
    @PrePersist // DB에 해당 테이블의 insert 연산을 실행할 때 같이 실행해라
    public void created_at(){
        this.created_at = LocalDate.now();
        setMonth(created_at);
    }

    private String month;   // 연월
    public void setMonth(LocalDate created_at) {
        String year = Integer.toString(created_at.getYear());
        String month = Integer.toString(created_at.getMonthValue());
        this.month = year+month;
    }

    @Column
    private String imageUrl;

    @Enumerated(EnumType.STRING)
    private Emotion emotion;    // 감정

    @ManyToOne  // 다대일 단방향 관계, user 삭제되면 일기도 삭제
    @JoinColumn(name = "user_id")
    private User user;  // 유저 pk (FK)

    @OneToOne   // 일대일 단방향 관계
    @JoinColumn(name = "question_id")
    private Question question;  // 질문 pk (FK)

    public Diary(String content, Weather weather, LocalDate created_at, String month, Emotion emotion, User user, Question question, String imageUrl) {
        this.content = content;
        this.weather = weather;
        this.created_at = created_at;
        this.month = month;
        this.emotion = emotion;
        this.user = user;
        this.question = question;
        this.imageUrl = imageUrl;
    }
}

db에 저장할 모델을 생성해준다.

Repository 작성하기

import org.springframework.data.jpa.repository.JpaRepository;
import .Entity.Diary;

import java.util.List;

/*
 * JpaRepository 상속 -> 자동으로 빈 등록 (@Repository 안 달아도 됨)
 * */
public interface JpaDiaryRepository extends JpaRepository<Diary, Long>, DiaryRepository {   // 인터페이스 다중 상속
    @Override
    List<Diary> findByMonth(String month);  
}

db와 연결해줄 Repository도 만들어준다.

Controller 작성하기

    @ResponseBody   // Long 타입을 리턴하고 싶은 경우 붙여야 함 (Long - 객체)
    @PostMapping(value="/diary/new",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public Long saveDiary(HttpServletRequest request, @RequestParam(value="image") MultipartFile image, Diary diary) throws IOException {
        System.out.println("DiaryController.saveDiary");
        System.out.println(image);
        System.out.println(diary);
        System.out.println("------------------------------------------------------");
        Long diaryId = diaryService.keepDiary(image, diary);
        return diaryId;
    }

파일 업로드를 할 때는 MultipartFile을 사용한다. HttpServletRequest request을 사용해주면 알아서 요청으로 들어온값들이 Diary 안으로 들어가진다... 너무 신기해!

Service 작성하기

@Service
public class DiaryServiceImpl implements DiaryService{

    // 의존성 주입
    private final DiaryRepository diaryRepository;

    @Autowired
    private S3Uploader s3Uploader;

    public DiaryServiceImpl(DiaryRepository diaryRepository) {
        this.diaryRepository = diaryRepository;
    }

    @Override @Transactional
    public Long keepDiary(MultipartFile image, Diary diary) throws IOException {
        System.out.println("Diary service saveDiary");
        if(!image.isEmpty()) {
            String storedFileName = s3Uploader.upload(image,"images");
            diary.setImageUrl(storedFileName);
        }
        Diary savedDiary = diaryRepository.save(diary);
        return savedDiary.getDiary_id();
    }
}

s3Uploader.upload에서 두 번째 매개변수의 이름에 따라 S3 Bucket 내부에 해당 이름의 디렉토리가 생성이 된다.

Postman으로 테스트




테스트도 잘 되고 버킷에도 잘 올라온다

1개의 댓글

프로젝트하는 데 도움이 많이 됐습니다!
좋은 포스팅 감사합니다 ㅎㅎ

답글 달기