[Spring] Spring Boot + gradle + S3 + React.js 이미지 업로드 구현하기 - 1 (백엔드 구현)

안녕하세요. 오늘은 프로필 사진을 업로드 할 수 있는 기능을 구현해볼거예요

환경은

Spring Boot 2.5.x
gradle
react

입니다. 이번 포스팅에서는 React.js는 다루지 않을거고요,
스프링 부트에서 S3로 연동하는 방법과 로직을 작성할겁니다.


기본 의존성 설정


	//spring-cloud-starter-aws
	compileOnly 'org.springframework.cloud:spring-cloud-starter-aws:2.0.2.RELEASE'
	implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.228')
	implementation 'com.amazonaws:aws-java-sdk-s3'
	// https://mvnrepository.com/artifact/commons-io/commons-io
	implementation 'commons-io:commons-io:2.6'
//commons-io 는 File을 처리하기 위함

S3를 연동하기 이전에, AWS에서 버킷 생성과 IAM설정을 해줘야한다.

해당글에서는 저부분은 다루지 않으니 다른 블로그를 참고하길 바란다.
https://velog.io/@jinseoit/AWS-S3-bucket
저는 이분꺼를 보고했습니다.

S3에서 성공적으로 사용자 설정과 버킷 생성을 하고 나면,
Spring boot에서 사용해야할 정보를 입력해야한다.

application-credentials.yml
credentials이라는 yml을 파일을 따로 만들어 정보를 넣어줬다.

application-aws.yml
aws이라는 yml파일에는 aws정보를 써줬다.

cloud:
  aws:
    s3:
      bucket: [버킷이름써주세요]
#      folder:
#        [VARIABLE]: [VALUE]
    region:
      static: ap-northeast-1
    stack:
      auto: false

application.yml파일에는 위에서 작성한 application-aws.yml파일과 application-credentials.yml 파일을 포함할 수 있도록 설정을 해주어야한다.

application.yml 파일

spring:
  profiles:
    include:
      - aws
      - credentials
  
 servlet:
   multipart:
     enabled: true
     max-file-size: 20MB
     max-request-size: 20MB

이렇게 작성하면 aws, credentials 파일이 포함된다.
저 aws, credentials 파일은 꼭 gitignore 설정을 해줘야함!!

밑에는 S3사진의 크기를 제한하기 위한 설정이다.

이제 코드를 작성하러 가자.

AWSConfiguration


package com.record.backend.aws;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@Configuration
public class AWSConfiguration {

	@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 AmazonS3Client amazonS3Client() {
		BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
		return (AmazonS3Client)AmazonS3ClientBuilder.standard()
			.withRegion(region)
			.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
			.build();
	}
}

amazonS3Client()를 통해 내 아마존의 아이디와 시크릿 키를 주입하고 레젼을 설정해준다.
이렇게 하면 S3에 넣기 빼기 권한 수행이 가능하다.

S3Uploader


일단 S3 upload를 위한 Service단이라고 생각하면 된다.

이 클래스는 다른 분들의 블로그를 많이 참고했는데,
나는 React와 연동을 할 것이기 때문에, 파일을 업로드했을때 넘겨주는 값을 ResponseEntity로 넘겨주기 위해 Dto클래스를 반환했다.

package com.record.backend.aws;

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

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.record.backend.domain.user.User;
import com.record.backend.repository.UserRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {

	private final AmazonS3Client amazonS3Client;
	private final UserRepository userRepository;

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

	public FileUploadResponse upload(Long userId, MultipartFile multipartFile, String dirName) throws IOException {


		File uploadFile = convert(multipartFile)
			.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File로 전환이 실패했습니다."));

		return upload(userId, uploadFile, dirName);
	}


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

//사용자의 프로필을 등록하는 것이기때문에, User 도메인에 setProfile을 해주는 코드.
//이 부분은 그냥 업로드만 필요하다면 필요없는 부분이다.
		User user = userRepository.findById(userId).get();
		user.setProfilePhoto(uploadImageUrl);

//FileUploadResponse DTO로 반환해준다.
		return new FileUploadResponse(fileName, uploadImageUrl);
		//return uploadImageUrl;
	}

	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("파일이 삭제되었습니다.");
		} 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();
	}


}

코드의 순서는

  • MultipartFile을 전달 받고
  • S3에 전달할 수 있도록 MultiPartFile을 File로 전환
    • S3는 Multipartfile 타입은 전송이 안되기때문
  • 전환된 File을 S3에 public 권한으로 put
    • 외부에서 정적 파일을 읽을 수 있도록 하기 위함
  • 로컬에서 생성된 File 삭제
    • Multipartfile -> File로 전환되면서 로컬에 파일 생성된 것을 삭제합니다.
  • 업로드 된 파일의 FileName과 S3 URL 주소를 dto로 반환합니다.

위의 코드에서 dirName은 S3에 생성된 디렉토리를 말하는것인데,
나는 profile이라는 폴더를 사용한다.

FileUploadResponse (DTO)


앞단에 ResponseEntity를 사용하여 DTO로 반환할 것이기 때문에,
DTO클래스를 넘겨줬다.

@Getter @Setter
public class FileUploadResponse {

	String fileName;
	String url;

	public FileUploadResponse(String fileName, String url) {
		this.fileName = fileName;
		this.url = url;
	}
}

Controller


유저 객체에 사진넣는거까지 있는 내 코드

@RestController
@RequiredArgsConstructor
public class UserApiController {
//저는 사용자 프로필을 업로드하기위해사용했으므로 이름이 UserApiController입니다.

	private final S3Uploader s3Uploader;
	
    //유저 프로필 업로드
	@PostMapping("/users/profilePhoto")
	public ResponseEntity<?> uploadProfilePhoto(@PathVariable("user_id") Long userId, @RequestParam("profilePhoto") MultipartFile multipartFile) throws IOException {
		//S3 Bucket 내부에 "/profile"

		FileUploadResponse profile = s3Uploader.upload(userId, multipartFile, "profile");
		return ResponseEntity.ok(profile);
	}
    
}

나는 해당 유저의 프로필을 업로드하는 컨트롤러를 작성하였기 때문에
저렇게 user_id를 받아오는데, 그래서 service단 코드를 보면 upload메서드에

User user = userRepository.findById(userId).get();
		user.setProfilePhoto(uploadImageUrl);

이 부분이 있는 것이다.

저 유저설정하는 부분없이 그냥 업로드 기능만 있는 코드는

유저고 뭐고 그냥 업로드만 하는 코드

@RestController
@RequiredArgsConstructor
public class UserApiController {
//저는 사용자 프로필을 업로드하기위해사용했으므로 이름이 UserApiController입니다.

	private final S3Uploader s3Uploader;
	
    //유저 프로필 업로드
	@PostMapping("/users/profilePhoto")
	public String uploadProfilePhoto(@RequestParam("profilePhoto") MultipartFile multipartFile) throws IOException {
		//S3 Bucket 내부에 "/profile" 폴더

		return s3Uploader.upload(multipartFile, "profile");
        //이렇게바꾸면 된다. 대신 서비스단에서 파라미터 주의 ㅋㅋ 바까주셈
	}   
}

여기까지 작성완이다.

postman으로 확인

작성한 api대로 http://localhost:8080/users/profilePhoto 로 post요청을 보내면,

리턴으로 FileUploadResponse
fileName, url을 받아온다.

다음글에서는 이렇게 업로드하고 받아오는걸 작성해보겟읍니다~!~!~

좋은 웹페이지 즐겨찾기