□ 버켓 생성
- Create Bucket 클릭
- Bucket Name 작성
- 객체 소유권 설정(ACL) → ACLs enabeld 선택
※ ACL(Access Control List) : 접근 제어 목록
ACL 을 통해 S3 버킷 및 버킷의 오브젝트에 대한 접근을 관리할 수 있다.
버킷과 개별 객체들의 권한은 독립적이다.객체는 객체가 속한 버킷으로부터 권한을 상속받지 않는다. - ACLs disabled : 내가 접속한 계정만 해당 버킷을 소유
- ACLs enabled : 다른 AWS계정에서도 소유권을 갖거나 접속제어가 가능 - Block Public Access
- 새 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
- 임의의 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
- 새 퍼블릭 버킷 또는 액세스 지점 정책을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
- 임의의 퍼블릭 버킷 또는 액세스 지점 정책을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단
- 나머지는 기본값으로 두고 생성
□ I AM 유저 생성
- Create User 클릭
- 권한 정책 부여(AmazonS3FullAccess)
- root계정에서 만든 I AM계정으로 들어가 Security credentials - Create access key 클릭
□ SpringBoot 적용
- 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
- properties설정
# AWS S3 cloud.aws.credentials.accessKey=${aws-access-key} // 엑세스 키 cloud.aws.credentials.secretKey=${aws-secret-key} // 시크릿 키 cloud.aws.region.static=ap-northeast-2 // 서울 cloud.aws.s3.bucket=${bucket-name} // 버킷 이름 spring.servlet.multipart.enabled=true // 멀티파트 파일 업로드를 지원하도록 활성화
- 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 amazonS3() { AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); return AmazonS3ClientBuilder .standard() .withCredentials(new AWSStaticCredentialsProvider(credentials)) .withRegion(region) .build(); } }
- S3Service
import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.play.hiclear.common.exception.CustomException; import com.play.hiclear.common.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.util.UUID; @Service @RequiredArgsConstructor public class AwsS3Service { @Value("${cloud.aws.s3.bucket}") private String bucket; private final AmazonS3 amazonS3; public String uploadFile(MultipartFile multipartFile){ if (multipartFile == null || multipartFile.isEmpty()) { return null; } String fileName = createFileName(multipartFile.getOriginalFilename()); ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(multipartFile.getSize()); objectMetadata.setContentType(multipartFile.getContentType()); try(InputStream inputStream = multipartFile.getInputStream()){ amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata) .withCannedAcl(CannedAccessControlList.PublicRead)); } catch (IOException e){ throw new CustomException(ErrorCode.IO_ERROR); } return fileName; } // 파일명을 난수화하기 위해 UUID 를 활용하여 난수를 돌린다. public String createFileName(String fileName){ return UUID.randomUUID().toString().concat(getFileExtension(fileName)); } // "."의 존재 유무만 판단 private String getFileExtension(String fileName){ try{ return fileName.substring(fileName.lastIndexOf(".")); } catch (StringIndexOutOfBoundsException e){ throw new CustomException(ErrorCode.IMAGE_BAD_REQUEST); } } public void deleteFile(String fileName){ amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); System.out.println(bucket); } }
- 사진을 업로드 및 삭제하는 Controller
@RestController @RequiredArgsConstructor public class AwsS3Controller { private final AwsS3Service s3Service; // 파일 업로드 @PostMapping("/images") public ResponseEntity<String> updateImage( @RequestParam("image") MultipartFile image) { return ResponseEntity.ok(s3service.uploadFile(image)); } // 파일 삭제 @DeleteMapping("/images") public ResponseEntity<String> deleteImage( @RequestParam String fileName) { return ResponseEntity.ok(s3Service.deleteFile(fileName)); } }
◎ S3Service 로직 분석
- 파일 생성
String fileName = createFileName(multipartFile.getOriginalFilename());
- 메타데이터 설정
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());
- S3에 파일 업로드
try(InputStream inputStream = multipartFile.getInputStream()){
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e){
throw new CustomException(ErrorCode.IO_ERROR);
}
- multipartFile.getInputStream()을 호출하여 파일 내용을 입력 스트림으로 가져온다, 이 입력 스트림을 사용하여 PutObjectRequest를 생성
- amazonS3.putObject() 메서드를 통해 S3에 파일을 업로드,
withCannedAcl(CannedAccessControlList.PublicRead)를 사용하여 업로드된 파일을 공개적으로 읽을 수 있도록 설정 - try-with-resources 구문을 사용하여 입력 스트림을 자동으로 닫는다.
IOException이 발생하면 커스텀 예외 CustomException을 발생
// 파일명을 난수화하기 위해 UUID 를 활용하여 난수를 돌린다.
public String createFileName(String fileName){
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// "."의 존재 유무만 판단
private String getFileExtension(String fileName){
try{
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e){
throw new CustomException(ErrorCode.IMAGE_BAD_REQUEST);
}
}