๐ฉ์ต์ข ํ๋ก์ ํธ - S3๋ฅผ ์ด์ฉํ ํ์ผ(์ด๋ฏธ์ง) ์ ๋ก๋
AWS S3๋ฅผ ์ด์ฉํ ํ์ผ(์ด๋ฏธ์ง) ์ ๋ก๋
S3 : Simple Storage Service ์ ์ฝ์๋ก ์ฃผ๋ก ํ์ผ ์๋ฒ๋ก ์ฌ์ฉ๋จ.
๊ตฌ๊ธ ๋๋ผ์ด๋ธ์ฒ๋ผ ํ์ผ ์ ์ฅ ์๋น์ค์ด๋ฉฐ, ๋ฐ์ดํฐ๋ฅผ ์จ๋ผ์ธ ์ค๋ธ์ ํธ ํํ๋ก ์ ์ฅํ๋ ์๋น์ค๋ผ๊ณ ๋ณด๋ฉด ๋จ!
์ S3๋ฅผ ์ฌ์ฉํด์ ์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ ํ๋๊ฐ?
- S3 ์์ฒด๋ก ์ ์ ์น ์๋น์ค ๊ฐ๋ฅ
- S3๋ ์ ์ฅ ์ฉ๋์ด ๋ฌดํ๋์ด๊ณ ํ์ผ ์ ์ฅ์ ์ต์ ํ๋์ด ์๋ค. ์ฉ๋์ ์ถ๊ฐํ๊ฑฐ๋ ์ฑ๋ฅ์ ๋์ด๋ ์์ ์ด ํ์์์.
ํ์ฅ์ฑ : ํ์ผ ์๋ฒ๋ ํธ๋ํฝ์ด ์ฆ๊ฐํจ์ ๋ฐ๋ผ ์๋ฒ ์ธํ๋ผ ๋ฐ ์ฉ๋ ๊ณํ์ ๋ณ๊ฒฝํด์ผ ๋๋๋ฐ S3๊ฐ ํ์ฅ ๋ฐ ์ฑ๋ฅ ๋ถ๋ถ์ ๋์ ์ฒ๋ฆฌํด์ค.
๋ด๊ตฌ์ฑ : ์ฌ๋ฌ ์์ญ์ ์ฌ๋ฌ ๋ฐ์ดํฐ ๋ณต์ฌ๋ณธ์ ์ ์ฅํ๋ฏ๋ก ํ ์์ญ์ด ๋ค์ด๋๋๋ผ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์๊ณ ๋ณต๊ตฌ๊ฐ ๊ฐ๋ฅํจ.
๊ฒฐ๋ก : S3๋ ์ ์ฅํ๋ ๋ฐ์ดํฐ ์์ ๋ํ ๋น์ฉ๋ ์ ๋ ดํ๊ณ ์ ์ฅํ ์ ์๋ ๋ฐ์ดํฐ ์์ด ๋ฌดํ์ ๊ฐ๊น์. ๋ํ โelasticโ ํ ์ฑ์ง ๋๋ฌธ์ ๋ณ๋์ ์คํ ๋ฆฌ์ง ํ์ฅ, ์ถ์์ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋จ. FTP ์๋ฒ์ฒ๋ผ ๋จ์ํ ํ์ผ ์ ์ฅ ์์ญ์ผ๋ก ์ฌ์ฉํ ์๋ ์์ผ๋ฉฐ, ๋ค์ํ AWS ์๋น์ค์ ์ฌ์ฉ ๋ก๊ทธ ์ ์ฅ, ์ ์ ์น ์ฌ์ดํธ ํธ์คํ , EBS ์ค๋ ์ท์ ์ ์ฅ ์์ญ ๊ธฐ๋ฅ ๋ฑ์ ๊ฐ์ง๊ณ ์์.
- ํ์ง๋ง S3๋ ํ์ผ ์ ๋ก๋, ์ญ์ , ์ ๋ฐ์ดํธ๋ง ๊ฐ๋ฅํ๊ณ ํ๋ก๊ทธ๋จ์ ์ค์นํด์ ์ ์ฅํ๋ ๊ธฐ๋ฅ์ ์์.
S3 ๊ด๋ จ ์ฉ์ด
๊ฐ์ฒด(Object) : ํ์ผ๊ณผ ํ์ผ ์ ๋ณด๋ก ๊ตฌ์ฑ๋ ์ ์ฅ ๋จ์๋ก ํ์ผ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋จ!
S3์ ์ ์ฅ๋๋ ๋ฐ์ดํฐ๋ ๋ชจ๋ ๊ฐ์ฒด๋ผ๊ณ ๋ถ๋ฆ
๊ฐ ๊ฐ์ฒด๋ ๋ฐ์ดํฐ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ง๋๋๋ฐ S3 ๋ฒํท์ ์ฌ๋ฆฌ๋ ๊ฐ์ฒด๊ฐ ๋ฐ๋ก ๋ฐ์ดํฐ์ด๊ณ ์ต์ข ์์ ์ผ, ํ์ผ ํ์ ๋ฑ์ ๋ฐ์ดํฐ๋ฅผ ๋ฉํ๋ฐ์ดํฐ๋ผ๊ณ ํจ. (๋ฉํ๋ฐ์ดํฐ๋ ๋ค์-๋ฒจ๋ฅ ์์ผ๋ก ์ด์ฐ๋ฌ์ ธ ์๋ค.)
๊ฐ์ฒด๋ ํค๋ฅผ ํตํด์ ๋ฒํท์์ ์ ์ผํ ๊ฒ์ผ๋ก ์๋ณ๋ ์ ์์ผ๋ฉฐ, ๋ฒํท์ ์กด์ฌํ๋ ๋ชจ๋ ๊ฐ์ฒด๋ ๋จ ํ๋์ ํค๋ฅผ ์ง๋๋ค.
= ๋ฐ๋ผ์ S3 ๋ด์์ ๋ฒํท, ํค, ๋ฒ์ ID๋ฅผ ํตํด ํน์ ๊ฐ์ฒด๋ฅผ ํ์ ํ ์ ์๋ค.
๋ฒํท(Bucket) : ๋ค์์ ๊ฐ์ฒด๋ฅผ ๊ด๋ฆฌํ๋ ์ปจํ ์ด๋๋ก ํ์ผ ์์คํ ์ด๋ผ๊ณ ๋ณด๋ฉด ๋จ!
๋ง์ฝ Nayoung(๋ฒํท ์ด๋ฆ)์ด๋ผ๋ ์ด๋ฆ์ ๋ฒํท์ test.png ๊ฐ์ฒด ํ์ผ์ ์ ์ฅํ๋ฉด http://Nayoung.s3.amazonaws.com/test.png ๋ผ๋ URL์ด ์์ฑ๋๊ฒ ๋จ.
S3์ ๊ตฌ์ฑํ ๋, ๋ฒํท(Bucket)์ด๋ผ๋ ์ปจํ ์ด๋๋ฅผ ๋์ ๋ฆฌ์ ์ ์ ํํ๊ณ , ํด๋น ์ปจํ ์ด๋ ๋ด๋ถ์ ๊ฐ์ฒด(Object)๋ผ๋ ํํ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ํํ๋ก ์คํ ๋ฆฌ์ง๋ฅผ ๊ตฌ์ถํจ.
ํ ๊ณ์ ๋น Bucket์ ์ต๋ 100๊ฐ๊น์ง ์ฌ์ฉ์ด ๊ฐ๋ฅํ๊ณ , ๋ฒํท ๋จ์๋ก ์ ๊ทผ ์ ํ์ ์ค์ ํ ์๋ ์๋ค.
๋จ, Bucket์ ์์ ๊ถ์ ์ด์ ํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฃผ์ํด์ผ ํ๋ค.
S3 ์ฌ์ฉ ์์
ํด๋ผ์ฐ๋ ์ ์ฅ์ - ๊ตฌ๊ธ ๋๋ผ์ด๋ธ์ฒ๋ผ ์ฌ์ฉ ๊ฐ๋ฅ
- ์๋น์ค์ ๋์ฉ๋ ํ์ผ ์ ์ฅ์ - ์ด๋ฏธ์ง, ๋์์, ๋น ๋ฐ์ดํฐ
- ์๋น์ค ๋ก๊ทธ ์ ์ฅ ๋ฐ ๋ถ์
- ์๋น์ค ์ฌ์ฉ์์ ๋ฐ์ดํฐ ์ ๋ก๋ ์๋ฒ
- ์ค์ํ ํ์ผ์ EC2์ SSD์ ์ ์ฅํ์ง ๋ง๊ณ S3์ ์ ์ฅ
AWS S3 ์์ฑํ๊ธฐ
1. AWS ๊ฐ์ ํ๊ธฐ
2. AWS Console > S3 > ๋ฒํท > ๋ฒํท ๋ง๋ค๊ธฐ
- ๊ฒ์ ์ฐฝ์ S3๋ผ๊ณ ์ ๋ ฅํ๋ฉด ๋จ
- ํด๋น ๋ฒํท ๋ง๋ค๊ธฐ ๋ฒํผ ํด๋ฆญ
AWS ๋ฆฌ์ ์ด โ์์์ ํํ์(์์ธ) ap-northeast-2์ธ์ง ํ์ธ
๋ฒํท ์ด๋ฆ ์ ๋ ฅํ๊ณ ์ก์ธ์ค ์ฐจ๋จ ์ค์ ์ ํด์
- ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ผ ์กฐ์ ๊ถํ์ ๊ฐ๊ฒ ํ๊ณ ๊ถํ ๋ณ๊ฒฝ ํ์ ์์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฒญ์ ๋ณด๋ด๋ ํด๋ผ์ด์ธํธ์ ํ์ผ ์กฐ์ ๊ถํ์ ์คํ๋ง ์ํ๋ฆฌํฐ ๊ถํ ์ค์ ์ผ๋ก ํ๋ฉด ๋จ.
- ์๋ ACL์ ๋นํ์ฑํ ํ์๋๋ฐ ๋ค์๊ณผ ๊ฐ์ ์ค๋ฅ๋ก ์ธํด ํ์ฑํ๋ก ๋ณ๊ฒฝํจ
1
2
3
4
: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed:
com.amazonaws.services.s3.model.AmazonS3Exception: The bucket does not allow ACLs (Service: Amazon S3; Status Code: 400; Error Code:
AccessControlListNotSupported; Request ID: CQP6Y0Z9A93YXA1W; S3 Extended Request ID:
2NqX1W81AanB67RTBGMPwbJxsEDAIl9FnsYuVOv9qLQFieLv7sNYubR9WzP5zVmC+/bbGBPD+q8XNr9Tn385cQ==; Proxy: null), S3 Extended Request ID: 2NqX1W81AanB67RTBGMPwbJxsEDAIl9FnsYuVOv9qLQFieLv7sNYubR9WzP5zVmC+/bbGBPD+q8XNr9Tn385cQ==] with root cause
์ด ์ค๋ฅ๋ Amazon S3 ๋ฒํท์ด ์ก์ธ์ค ์ ์ด ๋ชฉ๋ก(ACLs)์ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ๋ ๋ฌธ์ . ACL ํ์ฑํ ํ์ง๋ง ์กฐ๊ธ ๋ ์ฐพ์๋ด์ผ ๋ ๋ฏ!
ํ์ง๋ง S3 ๊ณต๊ฐ์ค์ ์ S3์ ์ ๋ก๋ ๋ ํ์ผ์ ํ์ผ ์์ ์๊ฐ ์๋ ๋ค๋ฅธ ์ฌ๋์ด ๋ณผ ์ ์์ด, ๋ฐ์ดํฐ ์ ์ถ์ฌ๊ณ ๊ฐ๋ฅ์ฑ์ด ๋์์ง.
โถ ๋ณด์์ฌ๊ณ ์ฌ๋ก1: https://www.asiatime.co.kr/article/20220707500294
โถ ๋ณด์์ฌ๊ณ ์ฌ๋ก2: https://m.boannews.com/html/detail.html?mtype=1&idx=108196
AWS๋ S3 ๊ณต๊ฐ์ค์ ์ผ๋ก ๋ณด์ ์ฌ๊ณ ๋ฅผ ์ค์ด๊ธฐ ์ํด. 23๋ 4์๋ถํฐ ์์ฑํ S3๋ ๊ณต๊ฐ์ค์ ์ ๋นํ์ฉํ๊ณ ACL๋ฅผ ๋นํ์ฑํํ์.
- ๊ฒฐ๋ก : ๋นํ์ฑํํด์ ์ค๋ฅ๋ฅผ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ์์ ํด์ผํจ.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
multipartFiles.forEach(file -> {
String originalFilename = file.getOriginalFilename();
String fileName = createFileName(originalFilename);
String extension = getFileExtension(originalFilename);
ObjectMetadata objectMetadata = new ObjectMetadata();
validateFileSize(file.getSize(), extension);
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new CustomException(ErrorCode.PUT_OBJECT_EXCEPTION);
}
String fileUrl = amazonS3.getUrl(bucket, fileName).toString();
// ํ์ผ ์ ๋ณด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
File fileEntity = new File(fileName, fileUrl, file.getContentType(), file.getSize());
fileRepository.save(fileEntity);
// ํด๋น ์ํฐํฐ๋ฅผ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๊ณ ๋ฐํ
fileEntities.add(fileEntity);
});
return fileEntities;
ํด๋น ์ฝ๋์์
.withCannedAcl(CannedAccessControlList.PublicRead))
ํด๋น ๋ถ๋ถ ์ ๊ฑฐํ์ฌ ์ค๋ฅ ํด๊ฒฐํด๊ฒฐํ ๋ฐฉ๋ฒ : ์ด ์ค๋ฅ๋ ํ์ฌ S3 ๋ฒํท์ด ACL์ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ๋ ๊ฒ์ผ๋ก ํด๊ฒฐ ๋ฐฉ๋ฒ์ ์ฝ๋๋ฅผ ์์ ํ์ฌ
CannedAccessControlList
๋ฅผ ์ ๊ฑฐํจ์ผ๋ก์จ S3 ๋ฒํท์ ACL ๋นํ์ฑํ ์ค์ ๊ณผ ์ผ์นํ๊ฒ ๋์๊ณ S3 ๋ฒํท์ด ACL์ ์ฌ์ฉํ์ง ์๋๋ก ์ค์ ๋์ด ์๊ฑฐ๋, ๋ฒํท ์ ์ฑ ์ด ์ ์ ํ ์ค์ ๋ ๊ฒฝ์ฐ ACL ์์ด๋ ํ์ผ ์ ๋ก๋๊ฐ ์ ์์ ์ผ๋ก ์งํ๋จ!
3. ์ฌ์ฉ์ ์์ฑ
- AWS console > IAM > ์ก์ธ์ค ๊ด๋ฆฌ > ์ฌ์ฉ์ > ์ฌ์ฉ์ ์ถ๊ฐ ํด๋ฆญ
- ์ฌ์ฉ์ ์ด๋ฆ์ ์ ๋ ฅํ๊ณ ๋ค์์ ํด๋ฆญ
- ์ง์ ์ ์ฑ ์ฐ๊ฒฐ์ ์ ํํ๊ณ AmazonS3FullAccess๋ฅผ ์ ํ ํ ๋ค์์ ํด๋ฆญํ๊ณ ์ฌ์ฉ์ ์์ฑ ๋ฒํผ์ ๋๋ฅด๋ฉด ์ฌ์ฉ์๊ฐ ์์ฑ๋จ!
4. ์ก์ธ์ค ํค ์์ฑ
์ธ๋ถ์์ ์ ์ํ ์ ์๋๋ก ์ฌ์ฉ์์ ์ก์ธ์ค ํค๋ฅผ ๋ง๋ค์ด ์ฃผ์ด์ผ ํจ.
AWS console > IAM > ์ก์ธ์ค ๊ด๋ฆฌ์ > ์ฌ์ฉ์ > ์์ฑํ ์ฌ์ฉ์ ์ด๋ฆ ํด๋ฆญ > ๋ณด์ ์๊ฒฉ ์ฆ๋ช > ์ก์ธ์ค ํค ๋ง๋ค๊ธฐ ํด๋ฆญ!
์๋ฌด๊ฑฐ๋ ํด๋ฆญํ๊ณ ๋ค์์ ํด๋ฆญ ( ํด๋ฆญํ๋ฉด ์ก์ธ์ค ํค ์ฌ์ฉ ์ฌ๋ก์ ๋์์ ํ๋จ์ ๋์์ฃผ๋ ๊ธฐ๋ฅ๋ง ํ๊ธฐ ๋๋ฌธ์ ๋ฌด์์ ๊ณจ๋ผ๋ ์๊ด์์)
ํ๊ทธ๋ฅผ ์ ๋ ฅํ๊ณ ์ก์ธ์ค ํค ๋ง๋ค๊ธฐ ํด๋ฆญ!
์ก์ธ์ค ํค ์์ฑ ์๋ฃ ํ๋ฉด์์ ์์ฑ๋ ๊ณต๊ฐํค์ ๋น๋ฐํค๋ฅผ ํ์ธํ ์ ์์ (ํด๋น ํ๋ฉด์ด ์๋๋ฉด ๋น๋ฐ ์ก์ธ์ค ํค๋ฅผ ๋ณผ ์ ์๊ธฐ ๋๋ฌธ์ .csv ํ์ผ๋ก ๋ฐ์๋๋ ๊ฒ์ด ์ข์)
5. ์คํ๋ง ์ฐ๋
1
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
- build.gradle์ ์์กด์ฑ ์ถ๊ฐ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cloud:
aws:
credentials:
accessKey: ${AWS_ACCESS_KEY}
secretKey: ${AWS_SECRET_KEY}
region:
static: ${AWS_REGION}
auto: false
stack:
auto: false
s3:
bucket: ${AWS_BUCKET_NAME}
.env ํ์ผ์ ๋ง๋ค์ด์ ํ๊ฒฝ๋ณ์์ ์ถ๊ฐํจ
stack.auto : false
: EC2์์ Spring Cloud ํ๋ก์ ํธ๋ฅผ ์คํ์ํค๋ฉด ๊ธฐ๋ณธ์ผ๋ก CloudFormation ๊ตฌ์ฑ์ ์์ํ๊ธฐ ๋๋ฌธ์ ์ค์ ํ CloudFormation์ด ์์ผ๋ฉด ํ๋ก์ ํธ ์คํ์ด ๋์ง ์๊ธฐ ๋๋ฌธ์ ํด๋น ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ง ์๋๋ก false๋ก ์ค์ .S3Config.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
6. Postman API ํ ์คํธ
- muitipart ๋ฐ์ดํฐ๋ฅผ ์ ์กํด์ผ ํ๋ฏ๋ก Body ์ ํ์ form-data๋ก ์ ํํ๊ณ KEY ์์ฑ์ File์ ์ ํํจ.
- s3์์ ์ ์์ ์ผ๋ก ์ด๋ฏธ์ง๊ฐ ์ ์ฅ๋ ๊ฒ์ ํ์ธํ ์ ์์.
์ฝ๋ ๋ถ์
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "files")
public class File extends TimeStamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String fileName;
private String fileUrl;
private String fileType;
private long fileSize;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "marker_id")
private Marker marker;
public File(String fileName, String fileUrl, String fileType, long fileSize) {
this.fileName = fileName;
this.fileUrl = fileUrl;
this.fileType = fileType;
this.fileSize = fileSize;
}
}
- ํ์ผ ์ํฐํฐ๋ id, fileName, fileUrl, fileType, fileSize๋ฅผ ํ๋๋ก ์ ์ธํ๊ณ markerId์ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋งบ์. fileId๊ฐ ์๋์ผ๋ก ์์ฑ๋๊ณ ๋ช ๋ฒ์ marker์ ์ฐ๊ฒฐ๋์ด์๋์ง ํ์ธํ๊ธฐ ์ํจ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class FileService {
private static final long MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB
private static final long MAX_VIDEO_SIZE = 200 * 1024 * 1024; // 200MB
@Value("${cloud.aws.s3.bucket}")
private String bucket; // ํ์ผ์ด ์ ์ฅ๋ S3 ๋ฒํท์ ์ด๋ฆ. ์ ํ๋ฆฌ์ผ์ด์
ํ๋กํผํฐ์์ ์ฃผ์
๋จ
private final AmazonS3 amazonS3; //S3์ ์ํธ์์ฉํ๋ AWS S3 ํด๋ผ์ด์ธํธ
private final FileRepository fileRepository; //ํ์ผ ๋ฉํ๋ฐ์ดํฐ์ ์ํธ์์ฉํ๋ JPA ๋ ํฌ
// ์ฌ๋ฌ ํ์ผ์ S3์ ์
๋ก๋ํ๊ณ ํ์ผ ๋ฉํ๋ฐ์ดํฐ๋ฅผ DB์ ์ ์ฅํ๋ ๋ฉ์๋
public List<File> uploadFile(List<MultipartFile> multipartFiles) {
List<File> fileEntities = new ArrayList<>();
multipartFiles.forEach(file -> {
String originalFilename = file.getOriginalFilename();
String fileName = createFileName(originalFilename);
String extension = getFileExtension(originalFilename);
ObjectMetadata objectMetadata = new ObjectMetadata();
validateFileSize(file.getSize(), extension);
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
// amazonS3.putObject()๋ฅผ ํตํด ํ์ผ์ S3์ ์
๋ก๋
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata));
} catch (IOException e) {
throw new CustomException(ErrorCode.PUT_OBJECT_EXCEPTION);
}
String fileUrl = amazonS3.getUrl(bucket, fileName).toString();
// ํ์ผ ์ ๋ณด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ
File fileEntity = new File(fileName, fileUrl, file.getContentType(), file.getSize());
fileRepository.save(fileEntity);
// ํด๋น ์ํฐํฐ๋ฅผ ๋ฆฌ์คํธ์ ์ถ๊ฐํ๊ณ ๋ฐํ
fileEntities.add(fileEntity);
});
return fileEntities;
}
// ๊ณ ์ ํ ํ์ผ ์ด๋ฆ ์์ฑํ๋ ๋ฉ์๋
private String createFileName(String fileName) {
// fileName์ด ์๋ ํ์ผ ์ด๋ฆ
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
// ์๋ ํ์ผ ์ด๋ฆ์์ ํ์ผ ํ์ฅ์๋ฅผ ์ถ์ถ
private String getFileExtension(String fileName) {
try {
validateImageFileExtension(fileName);
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new CustomException(ErrorCode.FILE_NAME_INVALID);
}
}
//ํ์ผ ํ์ฅ์๊ฐ ํ์ฉ๋ ๋ชฉ๋ก์ ์๋์ง ๊ฒ์ฆํจ.
private void validateImageFileExtension(String filename) {
int lastDotIndex = filename.lastIndexOf(".");
if (lastDotIndex == -1) {
throw new CustomException(ErrorCode.EXTENSION_IS_EMPTY);
}
String extension = filename.substring(lastDotIndex + 1).toLowerCase();
List<String> allowedExtensionList = Arrays.asList("jpg", "jpeg", "png", "avi", "mp4", "gif");
if (!allowedExtensionList.contains(extension)) {
throw new CustomException(ErrorCode.EXTENSION_INVALID);
}
}
// ํ์ผ ํฌ๊ธฐ ๊ฒ์ฆํ๋ ๋ฉ์๋
private void validateFileSize(long size, String extension) {
if (isImageFile(extension) && size > MAX_IMAGE_SIZE) {
throw new IllegalArgumentException("์ด๋ฏธ์ง ํ์ผ ํฌ๊ธฐ๋ 10MB๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค.");
} else if (isVideoFile(extension) && size > MAX_VIDEO_SIZE) {
throw new IllegalArgumentException("๋น๋์ค ํ์ผ ํฌ๊ธฐ๋ 200MB๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค.");
}
}
// ํ์ผ ํ์ฅ์๊ฐ ์ด๋ฏธ์ง ํ์ผ์ธ์ง ํ์ธ
private boolean isImageFile(String extension) {
return extension.equals("jpeg") || extension.equals("jpg") || extension.equals("png");
}
// ํ์ผ ํ์ฅ์๊ฐ ๋น๋์ค ํ์ผ์ธ์ง ํ์ธ
private boolean isVideoFile(String extension) {
return extension.equals("mp4") || extension.equals("avi") || extension.equals("gif");
}
public void deleteFile(String fileUrl) {
// ํ์ผ URL๋ก S3์์ ์ญ์
String key = getKeyFromFileAddress(fileUrl);
try {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, key));
// ํ์ผ URL๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ญ์
fileRepository.deleteByFileUrl(fileUrl);
} catch (Exception e) {
log.error("Error occurred while deleting the file", e);
throw new CustomException(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
// ํ์ผ URL์์ S3ํค๋ฅผ ์ถ์ถ
private String getKeyFromFileAddress(String fileAddress) {
try {
// url์ URL ๊ฐ์ฒด๋ก ๋ณํ
URL url = new URL(fileAddress);
String decodingKey = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
return decodingKey.substring(1); // ๋งจ ์์ '/' ์ ๊ฑฐํ์ฌ S3ํค๋ฅผ ์ป์
} catch (MalformedURLException e) {
e.printStackTrace();
throw new CustomException(ErrorCode.IO_EXCEPTION_ON_IMAGE_DELETE);
}
}
}
์๋ FileService์์ ์๊ธฐ์ ๋ด์ฉ์ ์ฒ๋ฆฌํ๊ณ MarkerController์ MarkerService์์ ํ๊ธฐ์ ๋ด์ฉ์ ์ฒ๋ฆฌํ๋ ค๊ณ ํจ.
์ถ๊ฐ๋ก ํ์ผ ์ ์ฅ์ MarkerResponseDto๋ก ๋ฐํํ๋ ค๊ณ ํจ. ์๋ํ๋ฉด ์๋, ๊ฒฝ๋, ๊ธ ๋ด์ฉ ๋ฑ์ ๋ด์ฉ๋ ํ๋ก ํธ์ ๊ฐ์ด ๋ณด๋ด๊ธฐ ์ํจ.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ํ์ผ ์ ์ฅ
@PostMapping("/markers/files")
public ResponseEntity<CommonResponseDto<List<MarkerResponseDto>>> uploadFile(@RequestPart(value = "file", required = false) List<MultipartFile> multipartFiles) {
List<MarkerResponseDto> responseDto = markerFileService.uploadFiles(multipartFiles);
return new ResponseEntity<>(new CommonResponseDto<>(201, "ํ์ผ ์
๋ก๋์ ์ฑ๊ณตํ์์ต๋๋ค. ๐", responseDto), HttpStatus.CREATED);
}
// ํ์ผ ์ญ์
@DeleteMapping("/markers/{markerId}/files")
public ResponseEntity<CommonResponseDto<String>> deleteFile( @PathVariable Long markerId,
@RequestParam String fileUrl){
markerFileService.deleteFile(markerId, fileUrl);
return ResponseEntity.ok(new CommonResponseDto<>(200, "ํ์ผ ์ญ์ ์ ์ฑ๊ณตํ์์ต๋๋ค. ๐", null));
}
ํด๋น ์ค๋ฅ ๋ฐ์ : ์ํ ์ฐธ์กฐ(circular reference) ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด MarkerService์ FileService ๊ฐ์ ์์กด์ฑ์ ๋์ด์ผ ํจ. ์ํ ์ฐธ์กฐ๋ ๋ ๊ฐ ์ด์์ ๋น์ด ์๋ก๋ฅผ ์ฐธ์กฐํ ๋ ๋ฐ์.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด MarkerSevice์์ ํ์ผ ๊ด๋ จ ์์ ์ ์ํํ๋ ๋ก์ง์ ๋ณ๋์ ์๋น์ค๋ก ๋ถ๋ฆฌํจ >
MarkerFileService
MarkerFileService
์์ uploadFile ์ฒ๋ฆฌ๋ ํด๋น ๋ฆฌ์คํธ๋ฅผ MarkerResponseDto๋ก ๋ฐํ๋๊ฒ ์์ ํด์ ํด๊ฒฐํจ.
S3 ๊ถํ ์ ์ฑ ์์
- AmazonS3 > ๋ฒํท > [๋ฒํทname] > ์ ๋ค์ด์์ ์์ฑ ๊ฐ์ฒด URL์ ์ ์ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ธ
1
2
3
4
5
6
7
8
9
10
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<Error>
<script/>
<script/>
<script/>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>9F6RK80ZW51YE3F2</RequestId>
<HostId>0L7lglmFWQNleQGjCw5NF9itdEkjhzS+itwhMeNxNpyNHKshc1GmwUthSaU/w3IyCFGrDJMlzDU=</HostId>
</Error>
- AmazonS3 > ๋ฒํท > ๊ถํ์์ ๋ฒํท ์ ์ฑ ์์
1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::[๋ฒํทname]/*"
}
]
}
- ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ฉด ๊ฐ์ฒด URL์ ์ ์ํ ๋ ์ ์์ ์ผ๋ก ์ด๋ฏธ์ง๊ฐ ๋ธ!