์ ๋ก๋๋ S3 Presigned URL, ์กฐํ๋ CloudFront Signed URL๋ก ์ฑ๋ฅ๊ณผ ๋ณด์์ ๋์์ ํ๋ณดํ ๊ณผ์
build.gradle ์์
// AWS S3 & CloudFront
implementation 'software.amazon.awssdk:s3'
implementation 'software.amazon.awssdk:cloudfront' // ์ถ๊ฐ
implementation 'software.amazon.awssdk:auth'
๋ถํ์ํ ์์กด์ฑ ์ ๊ฑฐ
// ์ ๊ฑฐ๋ ์์กด์ฑ
// testImplementation 'org.testcontainers:junit-jupiter'
๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ (byeolnight-local.yml)
cloud:
aws:
s3:
bucket: byeolnight-bucket
cloudfront:
distribution-domain: 'dummy-cloudfront-domain.cloudfront.net'
key-pair-id: 'APKAI23HVI2C4JEXAMPLE'
private-key-path: '/path/to/cloudfront-private-key.pem'
signed-url-expiration: 3600
์ด์ ํ๊ฒฝ (byeolnight-prod.yml)
cloud:
aws:
cloudfront:
distribution-domain: '{cipher}d1234567890abcdef.cloudfront.net'
key-pair-id: '{cipher}APKAI23HVI2C4JEXAMPLE'
private-key-path: '/app/cloudfront-private-key.pem'
signed-url-expiration: 3600
CloudFront ๊ธฐ๋ฅ ์ถ๊ฐ
@Value("${cloud.aws.cloudfront.distribution-domain}")
private String cloudFrontDomain;
@Value("${cloud.aws.cloudfront.key-pair-id}")
private String keyPairId;
@Value("${cloud.aws.cloudfront.private-key-path}")
private String privateKeyPath;
@Value("${cloud.aws.cloudfront.signed-url-expiration:3600}")
private int signedUrlExpiration;
/**
* CloudFront Signed URL ์์ฑ (์กฐํ/๋ค์ด๋ก๋์ฉ)
*/
public String generateCloudFrontSignedUrl(String s3Key) {
try {
String resourceUrl = String.format("https://%s/%s", cloudFrontDomain, s3Key);
Instant expiration = Instant.now().plusSeconds(signedUrlExpiration);
Path privateKeyFile = Paths.get(privateKeyPath);
CannedSignerRequest signerRequest = CannedSignerRequest.builder()
.resourceUrl(resourceUrl)
.privateKey(privateKeyFile)
.keyPairId(keyPairId)
.expirationDate(expiration)
.build();
CloudFrontUtilities utilities = CloudFrontUtilities.create();
SignedUrl signedUrl = utilities.getSignedUrlWithCannedPolicy(signerRequest);
return signedUrl.url();
} catch (Exception e) {
log.error("CloudFront Signed URL ์์ฑ ์คํจ: {}", s3Key, e);
// Fallback to direct S3 URL
return String.format("%s/%s", baseUrl, s3Key);
}
}
๊ธฐ์กด ๋ฉ์๋ ์์
// ์
๋ก๋ ์ CloudFront URL ๋ฐํ
String viewUrl = generateCloudFrontSignedUrl(s3Key);
result.put("url", viewUrl); // CloudFront ์กฐํ์ฉ
result.put("uploadUrl", presignedUrl); // S3 ์
๋ก๋์ฉ
์๋ก์ด API ์ถ๊ฐ
@PostMapping("/upload-url")
public ResponseEntity<CommonResponse<Map<String, String>>> getUploadUrl(
@RequestParam("filename") String filename,
@RequestParam(value = "contentType", required = false) String contentType) {
// S3 ์
๋ก๋ ์ ์ฉ API
}
@GetMapping("/view-url")
public ResponseEntity<CommonResponse<Map<String, String>>> getViewUrl(
@RequestParam("s3Key") String s3Key) {
// CloudFront ์กฐํ ์ ์ฉ API
}
@PostMapping("/presigned-url")
@Deprecated
public ResponseEntity<CommonResponse<Map<String, String>>> getPresignedUrl() {
// ๊ธฐ์กด ํธํ์ฑ์ ์ํ ๋ ๊ฑฐ์ API
}
s3Upload.ts ์ ๋ฐ์ดํธ
// 1. ์
๋ก๋ URL ์์ฒญ (์๋ก์ด ์๋ํฌ์ธํธ)
response = await axios.post('/files/upload-url', null, {
params: { filename: file.name, contentType: file.type }
});
// 2. S3 ์ง์ ์
๋ก๋
await fetch(presignedData.uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': presignedData.contentType }
});
// 3. CloudFront ์กฐํ URL ์์ฑ
const viewUrlResponse = await axios.get('/files/view-url', {
params: { s3Key: presignedData.s3Key }
});
const viewUrl = viewUrlResponse.data.data.viewUrl;
// 4. CloudFront URL ๋ฐํ
return {
url: viewUrl, // CloudFront URL
s3Key: presignedData.s3Key,
originalName: presignedData.originalName,
contentType: presignedData.contentType
};
# 1. CloudFront Distribution ์์ฑ
aws cloudfront create-distribution --distribution-config file://cloudfront-config.json
# 2. Key Pair ์์ฑ ๋ฐ ๋ฑ๋ก
aws cloudfront create-public-key --public-key-config file://public-key-config.json
# 3. Private Key ์๋ฒ ๋ฐฐํฌ
scp cloudfront-private-key.pem server:/app/
1. CloudFront Signed URL ์์ฑ ์คํจ
์์ธ: Private Key ํ์ผ ๊ฒฝ๋ก ์ค๋ฅ ๋๋ ๊ถํ ๋ฌธ์
ํด๊ฒฐ: ํ์ผ ๊ฒฝ๋ก ํ์ธ ๋ฐ ์ฝ๊ธฐ ๊ถํ ๋ถ์ฌ
2. Key Pair ID ๋ถ์ผ์น
์์ธ: CloudFront์ ๋ฑ๋ก๋ Key Pair ID์ ์ค์ ๊ฐ ๋ถ์ผ์น
ํด๊ฒฐ: AWS ์ฝ์์์ Key Pair ID ์ฌํ์ธ ํ ์ค์ ์
๋ฐ์ดํธ
3. ์บ์ ๋ฌธ์
์์ธ: CloudFront ์บ์๋ก ์ธํ ์ด์ ๋ฒ์ ํ์ผ ์ ๊ณต
ํด๊ฒฐ: ํ์ผ ์
๋ฐ์ดํธ ์ ์บ์ ๋ฌดํจํ ๋๋ ๋ฒ์ ๊ด๋ฆฌ
1. ์บ์ ์ ์ฑ ์ต์ ํ
{
"CachePolicyId": "custom-policy",
"TTL": {
"DefaultTTL": 86400,
"MaxTTL": 31536000
}
}
2. ์์ถ ํ์ฑํ
{
"Compress": true,
"ViewerProtocolPolicy": "redirect-to-https"
}
3. ์ง์ญ๋ณ ์ต์ ํ
{
"PriceClass": "PriceClass_100",
"Restrictions": {
"GeoRestriction": {
"RestrictionType": "whitelist",
"Locations": ["KR", "JP", "US"]
}
}
}
๐ฏ ๊ฒฐ๋ก : S3 + CloudFront ๋ถ๋ฆฌ ๊ตฌ์กฐ๋ก ๋ณด์ ๊ฐํ + ์ฑ๋ฅ ํฅ์ + ๋น์ฉ ์ต์ ํ๋ฅผ ๋์์ ๋ฌ์ฑํ์ต๋๋ค.