- 본 문서는 Naver Cloud 에서 서비스하는 Object Storage API 사용법에 대해 기술합니다.
- 사용 분야는 영상 파일, json파일, 썸네일파일 업로드 입니다.
- Naver Cloud를 사용하지 않아도 되며 AWS의 S3와 호환됩니다.
- Naver Cloud의 Java용 AWS SDK 가이드 를 참조하였습니다.
- Last Edit : 2023.09.30
- 왜 사용하여야 하는지?
- 아키텍쳐
- Access-key and Secret-key 발급
- Object Storage bucket 생성
- gradle을 통한 aws-java-sdk-s3 의존성 주입
- application.yml 기입
- S3Client 설정파일 만들기
- API 사용법
- Test 코드를 통한 S3 Client 시작하기
Object Storage를 사용한 이유는 다음과 같습니다:
- 효율적인 데이터 관리: 저희가 만든 오픈소스는 Client와 AI Server 모두와 데이터를 주고 받습니다. Client에서 BE 서버로 전송한 파일들을 Object Storage에 저장하면, AI Server 및 Client 측에서 이를 사용할 수 있어 BE에서 받는 API Request 수가 줄어듭니다.
- 레이턴시 감소: 파일들이 URL 형태로 접근 가능하므로, File 전송을 통한 레이턴시가 적어집니다.
- 서버 용량 및 관리 효율성: 서버 내부에 파일을 저장할 경우 서버 용량 증가 문제와 서버 증설 시 파일 관리의 어려움이 발생합니다. 이러한 문제들은 Object Storage를 통해 해결할 수 있습니다.
- 일관된 파일 관리: Redis의 Global Cache처럼, 모든 파일들을 한 곳에서 관리함으로써 일관된 데이터 처리가 가능합니다.
- NaverCloud에 로그인 -> 마이페이지 -> 인증키 관리 -> 신규 API 인증키 생성
- 콘솔 -> Services -> Object Storage -> Bucket Management - > 버킷 생성
- 새 폴더 생성 (앞으로 사용하게 될 폴더들을 생성합니다)
- 본 오픈소스에서는 video, json, thumbnail 이라는 폴더 3개를 생성 하였습니다.
implementation 'com.amazonaws:aws-java-sdk-s3:1.11.238'
- 발급받은 키 정보와 region, bucket 이름 등을 spring boot 의 application.yml 에 기입합니다.
- 민감한 정보들이니 별도의 설정 파일을 만들어 main yml에서 불러들이는 방식을 사용합니다.
cloud:
aws:
credentials:
access-key: "your access-key"
secret-key: "your secret-key"
stack:
auto: false
region:
static: ap-northeast-2 // your location -> if you in korea, use ap-northeast-2
s3:
endpoint: https://kr.object.ncloudstorage.com
bucket: "your bucket name"
- @Configuration을 통해 설정파일을 만들어줍니다. (싱글톤으로 관리하기 위함)
- Amazon의 S3 SDK를 사용하므로 AmazonS3와 연결하기위한 객체를 만들어서 반환 받습니다.
- @Value를 이용해 yml로 작성한 설정파일에서 정보를 불러들입니다.
@Configuration
public class S3Client {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.s3.endpoint}")
private String endPoint;
@Value("${cloud.aws.region.static}")
private String regionName;
public AmazonS3 getAmazonS3() {
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endPoint, regionName))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.build();
}
}
- Path와 URL의 차이점
- Path는 Object Storage에서 파일들이 실제 저장되는 경로이며, 삭제 할 경우 이를 이용하여 삭제합니다.
- URL은 사용자가 실제로 Object를 얻고자 할 때 사용되는 URL입니다. 이는 S3 SDK에서 제공하지 않지만 패턴이 일정하므로 직접 만들어 저장합니다.
@Value("${cloud.aws.s3.bucket}")
private String bucketName; // your bucketName
private final S3Client s3Client; // connect to S3
private final VideoRepository videoRepository;
- createVideo 메소드 부분에서 MultipartFile로 업로드 할 파일들을 받습니다.
- uploadFileToS3 메소드를 만들어 video 파일 및 json 파일 들을 매개변수로 전달합니다.
@Override
public String createVideo(VideoRequestDTO videoRequestDTO) {
MultipartFile[] files = videoRequestDTO.getFiles();
MultipartFile videoFile = files[0];
MultipartFile jsonFile = files[1];
UploadFileDTO uploadFileDTO = uploadFileToS3(videoFile, jsonFile);
... 다음 코드들은 8번에서 설명합니다.
}
- DI받은 s3Client의 getAmazonS3를 사용하여 싱글톤 객체를 생성합니다.
AmazonS3 s3 = s3Client.getAmazonS3();
- MultipartFile을 File 객체로 변환해주는 메소드를 통해 File 객체로 변환합니다.
uploadVideoFile = convertMultipartFileToFile(videoFile, videoFileName);
uploadJsonFile = convertMultipartFileToFile(jsonFile, jsonFileName);
...
@Override
public File convertMultipartFileToFile(MultipartFile multipartFile, String fileName) {
File convertedFile = new File(fileName);
try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
fos.write(multipartFile.getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
return convertedFile;
}
- s3.putObject 메소드를 이용해 원하는 bucket에 Path를 지정해주어 파일을 업로드합니다.
s3.putObject(bucketName, VideoObjectPath, uploadVideoFile);
s3.putObject(bucketName, jsonObjectPath, uploadJsonFile);
s3.putObject(bucketName, thumbnailObjectPath, uploadThumbnailFile);
- 업로드 한 파일들은 첫 생성 시 사용자로 하여금 접근을 불가능하게 하여 권한을 설정해주어야 합니다.
- setACL 메소드를 구현하여 방금 생성한 파일들을 접근 가능하도록 설정합니다.
// All User can access Object
setAcl(s3, VideoObjectPath);
setAcl(s3, jsonObjectPath);
setAcl(s3, thumbnailObjectPath);
@Override
public void setAcl(AmazonS3 s3, String objectPath) {
AccessControlList objectAcl = s3.getObjectAcl(bucketName, objectPath);
objectAcl.grantPermission(GroupGrantee.AllUsers, Permission.Read);
s3.setObjectAcl(bucketName, objectPath, objectAcl);
}
- URL, Path등을 만들고 Video Entity를 저장합니다.
@Override
public String createVideo(VideoRequestDTO videoRequestDTO) {
MultipartFile[] files = videoRequestDTO.getFiles();
MultipartFile videoFile = files[0];
MultipartFile jsonFile = files[1];
UploadFileDTO uploadFileDTO = uploadFileToS3(videoFile, jsonFile);
Video video = videoRequestDTO.toVideo(uploadFileDTO);
videoRepository.save(video);
return "success";
}
- Video의 PK를 전달 받습니다.
- PK를 이용해 Video 객체를 얻고 파일들의 Path를 꺼냅니다.
@Override
public String deleteVideo(Long vno) {
Optional<Video> byId = videoRepository.findById(vno);
if (byId.isEmpty()) return "Video not found for Id : " + vno;
Video video = byId.get();
String videoPath = video.getVideoPath();
String jsonPath = video.getJsonPath();
String thumbnailPath = video.getThumbnailPath();
deleteFileFromS3(videoPath, jsonPath, thumbnailPath);
... 4번에서 설명합니다.
}
- deleteFileFromS3 메소드를 만들어 Path를 이용해 Object Storage에서 삭제합니다.
@Override
public void deleteFileFromS3(String videoPath, String jsonPath, String thumbnailPath) {
AmazonS3 s3 = s3Client.getAmazonS3();
try {
s3.deleteObject(bucketName, videoPath);
s3.deleteObject(bucketName, jsonPath);
log.info("Delete Object successfully");
} catch(SdkClientException e) {
e.printStackTrace();
log.info("Error deleteFileFromS3");
}
}
- Video 객체도 삭제합니다.
@Override
public String deleteVideo(Long vno) {
Optional<Video> byId = videoRepository.findById(vno);
if (byId.isEmpty()) return "Video not found for Id : " + vno;
Video video = byId.get();
String videoPath = video.getVideoPath();
String jsonPath = video.getJsonPath();
String thumbnailPath = video.getThumbnailPath();
deleteFileFromS3(videoPath, jsonPath, thumbnailPath);
videoRepository.delete(video); // --> delete from DB
return "success";
}
- S3Client 의존성 주입
- bucketName 선언
@Autowired
S3Client s3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucketName; // your bucketName
- s3Client에 설정한 계정에 대한 bucket 목록을 나열합니다.
@Test
public void bucketList() {
AmazonS3 s3 = s3Client.getAmazonS3();
try {
List<Bucket> buckets = s3.listBuckets();
System.out.println("Bucket List: ");
for (Bucket bucket : buckets) {
System.out.println(" name=" + bucket.getName() + ", creation_date=" + bucket.getCreationDate() + ", owner=" + bucket.getOwner().getId());
}
} catch (AmazonS3Exception e) {
e.printStackTrace();
} catch(SdkClientException e) {
e.printStackTrace();
}
}
- local에 있는 sample 파일을 업로드합니다.
- 서비스 로직과는 달리 filePath를 이용해 File 객체를 바로 만들 수 있습니다.
@Test
public void uploadFile() {
AmazonS3 s3 = s3Client.getAmazonS3();
String bucketName = "your bucketName";
// upload local file
String objectPath = "video/video1";
String filePath = "src/main/resources/sample.mp4";
try {
s3.putObject(bucketName, objectPath, new File(filePath));
System.out.format("Object %s has been created.\n", objectPath);
} catch (AmazonS3Exception e) {
e.printStackTrace();
} catch(SdkClientException e) {
e.printStackTrace();
}
}
- objectPath를 이용해서 bucket에서 해당 파일을 삭제합니다.
@Test
public void deleteFile() {
AmazonS3 s3 = s3Client.getAmazonS3();
String bucketName = "your bucketName";
String objectPath = "video/video1";
// delete object
try {
s3.deleteObject(bucketName, objectPath);
System.out.format("Object %s has been deleted.\n", objectPath);
} catch (AmazonS3Exception e) {
e.printStackTrace();
} catch(SdkClientException e) {
e.printStackTrace();
}
}