4 분 소요

S3에 이미지 업로드하기

1. gradle에 dependency 추가

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

2. application.yml 작성

# ==== S3 파일 업로드 용량 설정 ==== #
spring:
  servlet:
    multipart:
      max-file-size: 20MB
      max-request-size: 20MB

# ==== S3 접근 관련 내용 설정 ==== #
cloud:
  aws:
    credentials:
      access-key: IAM 사용자 엑세스 키
      secret-key: IAM 사용자 비밀 엑세스 키
    s3:
      bucket: 버킷 이름
    region:
      static: ap-northeast-2
    stack:
      auto: false

이때 IAM 사용자 엑세스 키와 비밀 엑세스키는 IAM 사용자를 생성할 때 받는 csv 파일에서 복붙하면 된다.
또한, 기본적으로 multipart로 보낼 수 있는 max-file-sizemax-request-size의 값은 1MB이며, 나는 20MB로 변경했다.

3. AmazonS3Client 객체를 bean으로 등록할 configuration 파일 생성

config/AmazonS3Config.java

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

해당 클래스에서는 @Value 애노테이션을 통해 application.yml에서 값을 읽어온다.

4. S3Uploader 클래스 생성

service/util/S3Uploader.java

@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket;  // S3 버킷 이름

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

        return upload(uploadFile, dirName, multipartFile.getOriginalFilename()); //📌 파일의 originalName을 바로 넘기도록 설정
    }

    // S3로 파일 업로드하기
    private String upload(File uploadFile, String dirName, String originalName) { //📌입력 파라미터에 originalName 추가
        String fileName = dirName + "/" + UUID.randomUUID()
            + originalName;   // S3에 저장된 파일 이름 📌random 값 + 기존의 파일명 으로 설정. 기존의 파일명은 upload 메서드 당시 multipartFile 에서 바로 getOriginalFileName으로 가져와서 입력 파라미터로 받기
        String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

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

    // 로컬에 저장된 이미지 지우기
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("File delete success");
            return;
        }
        log.info("File delete fail");
    }

    // 로컬에 파일 업로드 하기
    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(
            System.getProperty("user.dir") + "/" + UUID.randomUUID()); //📌 local에 저장할때도 randomUUID를 쓰도록 설정
        if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
            try (FileOutputStream fos = new FileOutputStream(
                convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }
}

DB에 S3 이미지 경로 저장하기

5. Image Entity 만들기

domain/post/Image.java

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Image {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    String imgurl;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

}

6. Post Entity 수정

domain/post/Post.java

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Image> img = new ArrayList<>();

Image Entity는 post에 종속되는 entity이기 때문에 Post Entity 클래스에서 CASCADE를 사용하여 OneToMany 매핑을 한다.

7. Image Repository

repository/ImageRepository.java

@Repository
public interface ImageRepository extends JpaRepository<Image,Long> {
}

Repository는 단순한 기능만 활용할 수 있도록 구현했다.

8. Image Service 구현

service/ImageService.java

@Service
public class ImageService extends S3Uploader {

    private final ImageRepository imageRepository;

    public ImageService(AmazonS3Client amazonS3Client, ImageRepository imageRepository) {
        super(amazonS3Client);
        this.imageRepository = imageRepository;
    }

    public String saveImage(MultipartFile multipartFile, String dirName, Post post) throws IOException {

        String uri = super.upload(multipartFile, dirName);

        Image img = new Image();
        img.setImgurl(uri);
        img.setPost(post);

        imageRepository.save(img);
        return uri;
    }
}

9 PostServiceImpl 클래스의 save 메서드 수정

service/PostServiceImpl.java

@Service
@RequiredArgsConstructor
@Slf4j
public class PostServiceImpl implements PostService {

    private final PostRepository postRepository;
    private final MemberRepository memberRepository;

    @Override
    @Transactional
    public Post save(PostSaveReqDto requestDto) {

        Optional<Member> oMember = memberRepository.findByUsername(SecurityUtil.getLoginUsername());
        List<Tag> tags = new ArrayList<>(requestDto.getTag());
        PostTag postTag = null;
        List<PostTag> postTags = new ArrayList<>();
        int tagListSize = 3;

        if (oMember.isPresent()) {

            if (tags.size() > tagListSize) {
                throw new CustomException(TAG_LIST_SIZE_ERROR);
            }

            Member member = oMember.get();

            for (Tag tag : tags) {
                // 포스트태그 생성
                postTag = PostTag.createPostTag(tag);
                postTags.add(postTag);
            }
            // 게시글 생성
            Post post = Post.createPost(member, requestDto.getTitle(), requestDto.getContent(), postTags);

            postRepository.save(post);

            return post;

        } else {
            throw new CustomException(MEMBER_NOT_FOUND);
        }
    }
}

기존의 리턴 값이 void였던 save 메서드의 리턴 값을 Post로 수정했다.
마찬가지로 PostService 클래스의 리턴 값도 수정하였다.

10. 컨트롤러 작성

web/BoardController.java

@RequiredArgsConstructor
@RestController
@RequestMapping
    ("/board")
@Slf4j
public class BoardController {

    private final PostService postService;
    private final ImageService imageService;

    // 이미지 포함 게시글 등록
    @PostMapping(value = "/save/image", consumes = {MediaType.APPLICATION_JSON_VALUE,
        MediaType.MULTIPART_FORM_DATA_VALUE})
    public Response savePost(@RequestPart @Valid PostSaveReqDto requestDto, @RequestPart MultipartFile image) throws
        IOException {

        Post post = postService.save(requestDto);
        imageService.saveImage(image, "test", post);

        return new Response("OK", "게시글 등록에 성공했습니다");
    }
}

파일을 업로드 할 때 api 통신을 통해 받아올 객체의 타입은 MultipartFile이다.
saveImage 메소드의 두번째 파라미터 (위의 예시에서는 test)의 이름에 따라 S3 bucket 내부에 이미지를 담을 해당 이름의 directory가 생성된다.

🌟 중요한 것은 Multipartfile은 DTO 안에 같이 필드로 들어가 있으면 안되고 따로 RequestPart 로 나눠서 api 통신시 받아와야 한다.
그리고 이렇게 하려면 @PostMapping(consumes={}) 형태로 위와 같이 각 requestpart의 content type이 무엇인지 정의를 해 주어야 한다!

포스트맨 테스트

스크린샷 2022-08-24 오후 6 06 47

KEY에는 컨트롤러의 savePost의 파라미터 이름을 작성해주고,
VALUE에는 게시글 내용(TEXT)과 이미지 파일(FILE)을 첨부한 뒤,
반드시 CONTENT TYPE을 명시해주어야 한다!
(CONTENT TYPE 필드가 안보인다면 위 사진에 표시한 부분을 클릭하여 Content Type을 선택한다.)

나중에 안드로이드에서 보낼 때도 CONTENT TYPE을 명확하게 해야할 것 같다..

Ref.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: