[Spring] Spring Boot + AWS S3를 이용한 이미지 업로드 구현

Paek·2024년 2월 16일
3

Spring프로젝트

목록 보기
9/9
post-thumbnail

오늘은 프로젝트를 진행하며 이미지를 저장하고 업로드 하기 위해 AWS S3 버킷과 스프링 프로젝트를 연동하여 사용하는 방법을 정리해보겠습니다.

AWS S3란?

AWS S3란 Simple Storage Service의 약자로, 주로 파일 서버에 이용됩니다.

  • 모든 종류의 데이터를 원하는 형식으로 저장
  • 저장할 수 있는 데이터의 전체 볼륨과 객체 수에는 제한이 없음
  • Amazon S3는 간단한 key 기반의 객체 스토리지이며, 데이터를 저장 및 검색하는데 사용할 수 있는 고유한 객체 키를 할당.
  • Amazon S3는 간편한 표준 기반 REST 웹 서비스 인터페이스를 제공

AWS S3를 사용하는 이유

여러가지가 있겠지만, 아래 두가지 이유가 가장 큽니다.

  • 확장성(Scalability) : 파일 서버는 트래픽이 증가함에 따라 서버 인프라 및 용량 계획을 변경해야 되는데, S3가 확장 및 성능 부분을 대신 처리해준다.

  • 내구성(Durability) : 여러 영역에 여러 데이터 복사본을 저장하므로 한 영역이 다운되더라도 데이터를 사용할 수 있고, 복구가 가능하다.

Bucket (버킷)

버킷은 다수의 객체를 관리하는 컨테이너로, 파일 시스템이라고 생각하면 됩니다.

Object (객체)

파일과 파일 정보로 구성된 저장단위로, 파일 이라고 생각하면 됩니다.


AWS S3 설정

이제 본격적으로 AWS S3를 사용해보도록 하겠습니다.

1. 버킷 생성

버킷 생성하는 과정은 생략하도록 하겠습니다. 인터넷에서 잘 나와있기에 혹시라도 어려우신 분들은 여기를 참고하여 진행하시면 될 것 같습니다.

2. 스프링 환경 설정

먼저 의존성을 아래와 같이 추가해줍니다.

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

이어서 appplication-aws.yml 파일을 생성하여 원래 존재하던 yml에 설정 정보를 추가하고, 새로운 파일에는 필요한 설정을 해주겠습니다.

#기존 application.yml에 코드 추가
spring:
  profiles:
    include: jwt, aws #jwt.yml 불러오기

이어서 appplication-aws.yml는 아래와 같이 설정해줍니다.

cloud:
  aws:
    s3:
      bucket: <버킷 이름>
    stack.auto: false
    region.static: ap-northeast-2
    credentials:
      accessKey: <발급 받은 accessKey>
      secretKey: <발급 받은 secretKey>

여기서 매우 중요한 점은, 새로 만든 application-aws.yml은 꼭!!!! .gitginore에 넣어 외부에 공개되지 않도록 해주어야 합니다.

안그러면 가끔 뉴스에 나오듯, 해킹 당해 엄청난 요금 폭탄을 맞을 수 있습니다.

  • cloud.aws.stack.auto=false : EC2에서 Spring Cloud 프로젝트를 실행시키면 기본으로 CloudFormation 구성을 시작하기 때문에 설정한 CloudFormation이 없으면 프로젝트 실행이 되지 않는다. 해당 기능을 사용하지 않도록 false로 설정 합니다.

  • cloud.aws.region.static:ap-northeast-2 : 리전을 한국으로 고정해둡니다.

3. 스프링 설정 추가

S3Config.java 파일을 만들어 설정을 진행해주도록 하겠습니다.

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

@Value 애노테이션을 통해 yml 파일에 있는 값을 받아옵니다. 이 값들을 이용하여 AmamzonS3Client 빈을 생성하여 리턴해줍니다.

4. AwsFileService 생성

이제 실제 Aws S3에 파일을 저장하는 역할을 수행하는 Service 클래스를 생성해보도록 하겠습니다.

@Service
@RequiredArgsConstructor
@Transactional
@Slf4j
public class AwsFileService implements FileService{

	private final AmazonS3Client amazonS3Client;

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

	private String PROFILE_IMG_DIR = "profile/";
	private String MEMBER_IMG_DIR = "member/";
	@Override
	public String savePhoto(MultipartFile multipartFile, Long memberId) throws IOException {
		File uploadFile = convert(multipartFile)  // 파일 변환할 수 없으면 에러
				.orElseThrow(() -> new IllegalArgumentException("error: MultipartFile -> File convert fail"));
		return upload(uploadFile, MEMBER_IMG_DIR, memberId);
	}

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

	// S3로 파일 업로드하기
	private String upload(File uploadFile, String dirName, Long memberId) {
		String fileName = dirName + memberId  + "/" + UUID.randomUUID() + uploadFile.getName();   // S3에 저장된 파일 이름
		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.home") + "/" + file.getOriginalFilename());
		if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
			try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
				fos.write(file.getBytes());
			}
			return Optional.of(convertFile);
		}
		return Optional.empty();
	}

	public void createDir(String bucketName, String folderName) {
		amazonS3Client.putObject(bucketName, folderName + "/", new ByteArrayInputStream(new byte[0]), new ObjectMetadata());
	}
}

주석에 설명을 최대한 많이 달아놓았습니다. 대략적으로 멤버의 프로필 사진은 /profile/{memberId} 하위에 저장되도록, 그냥 일반 사진은 /member/{memberId} 하위에 저장되도록 구현하였습니다.

여기서 하나 알아봐야 할 점은, Local 파일로 변환한 후 저장하는 로직입니다. 굳이 로컬로 저장 후 업로드 하게 한 이유는 아래와 같습니다.

1. 네트워크 안정성

파일을 로컬에 저장하면 업로드 프로세스를 어느 정도 제어할 수 있습니다. 업로드 중에 네트워크 중단이 발생하는 경우 전체 콘텐츠를 다시 가져오지 않고도 로컬 파일에서 프로세스를 재개할 수 있습니다.

2. 오류 처리 및 검증

로컬 저장소를 사용하면 실제 업로드 전에 로컬 유효성 검사 및 오류 검사가 가능합니다. 파일 내용, 형식 또는 크기를 확인하고 Amazon S3로 전송하기 전에 문제를 처리할 수 있습니다.

3. 보안 및 액세스 제어

로컬 저장소를 사용하면 파일이 클라우드로 전송되기 전에 파일 권한 및 액세스 제어를 제어할 수 있습니다. 파일이 Amazon S3에 도달하기 전에 로컬 보안 조치와 정책을 적용할 수 있습니다.

4. 최적화

로컬 저장소를 사용하면 요구 사항에 따라 파일을 압축하거나 다른 변환을 적용하는 등 업로드할 파일을 최적화할 수 있습니다. 이렇게 하면 네트워크를 통해 전송되는 데이터의 양을 줄이는 데 도움이 됩니다.

5. 오프라인 처리

일부 시나리오에서는 파일을 업로드하기 전에 로컬에서 처리해야 할 수도 있습니다. 여기에는 로컬에서 가장 잘 수행되는 암호화, 데이터 유효성 검사 또는 형식 변환과 같은 작업이 포함될 수 있습니다.


이렇게 구현을 할 수도 있지만, 꼭 이렇게 해야한다 라거나 이렇게 하면 다른 방법에 비해서 엄청 좋다는 아닌 것 같습니다. 최대한 쉽게 구현한, 여러 방법 중 하나라고 생각하고 봐주시면 감사하겠습니다.

profile
티스토리로 이전했습니다. https://100cblog.tistory.com/

0개의 댓글