๐ŸŒŒ ๋ณ„ ํ—ค๋Š” ๋ฐค: S3 ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„  ํšŒ๊ณ ๋ก

๊น€์ •๊ทœยท2025๋…„ 9์›” 19์ผ

โ˜๏ธ S3 + CloudFront ๋ถ„๋ฆฌ ๊ตฌ์กฐ ๋„์ž…๊ธฐ

์—…๋กœ๋“œ๋Š” S3 Presigned URL, ์กฐํšŒ๋Š” CloudFront Signed URL๋กœ ์„ฑ๋Šฅ๊ณผ ๋ณด์•ˆ์„ ๋™์‹œ์— ํ™•๋ณดํ•œ ๊ณผ์ •

๐ŸŽฏ ๋ฌธ์ œ ์ƒํ™ฉ

๊ธฐ์กด ๊ตฌ์กฐ์˜ ํ•œ๊ณ„

  • ๋‹จ์ผ S3 URL: ์—…๋กœ๋“œ์™€ ์กฐํšŒ ๋ชจ๋‘ ๋™์ผํ•œ S3 URL ์‚ฌ์šฉ
  • ๋ณด์•ˆ ์ทจ์•ฝ์ : ์กฐํšŒ URL์ด ์˜๊ตฌ์ ์œผ๋กœ ๋…ธ์ถœ๋˜์–ด ๋ฌด๋‹จ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ์„ฑ๋Šฅ ์ œ์•ฝ: CDN ์—†์ด S3 ์ง์ ‘ ์ ‘๊ทผ์œผ๋กœ ๊ธ€๋กœ๋ฒŒ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ €ํ•˜
  • ๋น„์šฉ ์ฆ๊ฐ€: S3 ์ง์ ‘ ์ ‘๊ทผ์œผ๋กœ ์ธํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก ๋น„์šฉ ์ƒ์Šน

์š”๊ตฌ์‚ฌํ•ญ

  • ์—…๋กœ๋“œ: S3 Presigned URL๋กœ ์ง์ ‘ ์—…๋กœ๋“œ (์„œ๋ฒ„ ๋ถ€ํ•˜ ์ตœ์†Œํ™”)
  • ์กฐํšŒ: CloudFront Signed URL๋กœ ๋ณด์•ˆ ๊ฐ•ํ™” + CDN ์„ฑ๋Šฅ
  • ๊ธฐ์กด ํ˜ธํ™˜์„ฑ: ๊ธฐ์กด API ์œ ์ง€ํ•˜๋ฉด์„œ ์ ์ง„์  ์ „ํ™˜

๐Ÿ”ง ํ•ด๊ฒฐ ๊ณผ์ •

1๋‹จ๊ณ„: ์˜์กด์„ฑ ์ถ”๊ฐ€

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'

2๋‹จ๊ณ„: Config Server ์„ค์ • ์ถ”๊ฐ€

๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ (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

3๋‹จ๊ณ„: S3Service ๋ฆฌํŒฉํ† ๋ง

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 ์—…๋กœ๋“œ์šฉ

4๋‹จ๊ณ„: API ์—”๋“œํฌ์ธํŠธ ๋ถ„๋ฆฌ

์ƒˆ๋กœ์šด 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
}

5๋‹จ๊ณ„: ํ”„๋ก ํŠธ์—”๋“œ ์ˆ˜์ •

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
};

๐Ÿ“Š ์„ฑ๊ณผ ๋ฐ ํšจ๊ณผ

๋ณด์•ˆ ๊ฐ•ํ™”

  • ์ž„์‹œ URL: CloudFront Signed URL๋กœ 1์‹œ๊ฐ„ ์ œํ•œ ์ ‘๊ทผ
  • ๋ฌด๋‹จ ์ ‘๊ทผ ์ฐจ๋‹จ: ์„œ๋ช…๋œ URL ์—†์ด๋Š” ํŒŒ์ผ ์ ‘๊ทผ ๋ถˆ๊ฐ€
  • ํ‚ค ๊ด€๋ฆฌ: AWS Key Pair ๊ธฐ๋ฐ˜ ์•ˆ์ „ํ•œ ์„œ๋ช… ์‹œ์Šคํ…œ

์„ฑ๋Šฅ ํ–ฅ์ƒ

  • CDN ํ™œ์šฉ: ์ „ ์„ธ๊ณ„ ์—ฃ์ง€ ๋กœ์ผ€์ด์…˜์—์„œ ๋น ๋ฅธ ํŒŒ์ผ ์ œ๊ณต
  • ์บ์‹œ ํšจ์œจ: CloudFront ์บ์‹ฑ์œผ๋กœ S3 ์ง์ ‘ ์ ‘๊ทผ ๊ฐ์†Œ
  • ๋Œ€์—ญํญ ์ ˆ์•ฝ: ์••์ถ• ๋ฐ ์ตœ์ ํ™”๋œ ์ „์†ก

๋น„์šฉ ์ตœ์ ํ™”

  • S3 ์š”์ฒญ ๊ฐ์†Œ: CloudFront ์บ์‹œ ํžˆํŠธ๋กœ S3 GET ์š”์ฒญ ๊ฐ์†Œ
  • ๋ฐ์ดํ„ฐ ์ „์†ก ๋น„์šฉ: CloudFront ์š”๊ธˆ์ด S3 ์ง์ ‘ ์ „์†ก๋ณด๋‹ค ์ €๋ ด
  • ๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค: ์ง€์—ญ๋ณ„ ์ตœ์ ํ™”๋œ ๋น„์šฉ ๊ตฌ์กฐ

์•„ํ‚คํ…์ฒ˜ ๊ฐœ์„ 

  • ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ: ์—…๋กœ๋“œ(S3) vs ์กฐํšŒ(CloudFront) ๋ช…ํ™•ํ•œ ์—ญํ•  ๋ถ„๋‹ด
  • ํ™•์žฅ์„ฑ: CDN ๊ธฐ๋ฐ˜์œผ๋กœ ํŠธ๋ž˜ํ”ฝ ์ฆ๊ฐ€์— ์œ ์—ฐํ•œ ๋Œ€์‘
  • ๋ชจ๋‹ˆํ„ฐ๋ง: CloudFront ๋ฉ”ํŠธ๋ฆญ์œผ๋กœ ์ƒ์„ธํ•œ ์‚ฌ์šฉ๋Ÿ‰ ๋ถ„์„

๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์šด์˜

CloudFront ๋ฐฐํฌ ์„ค์ •

# 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/

๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

  • CloudWatch ๋ฉ”ํŠธ๋ฆญ: ์š”์ฒญ ์ˆ˜, ์บ์‹œ ํžˆํŠธ์œจ, ์˜ค๋ฅ˜์œจ
  • ๋กœ๊ทธ ๋ถ„์„: CloudFront ์•ก์„ธ์Šค ๋กœ๊ทธ๋กœ ์‚ฌ์šฉ ํŒจํ„ด ๋ถ„์„
  • ์•Œ๋ฆผ ์„ค์ •: ์˜ค๋ฅ˜์œจ ์ž„๊ณ„๊ฐ’ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์•Œ๋ฆผ

์žฅ์•  ๋Œ€์‘

  • Fallback ๋ฉ”์ปค๋‹ˆ์ฆ˜: CloudFront ์‹คํŒจ ์‹œ S3 ์ง์ ‘ URL ์ œ๊ณต
  • ํ‚ค ๋กœํ…Œ์ด์…˜: ์ •๊ธฐ์ ์ธ Key Pair ๊ต์ฒด ํ”„๋กœ์„ธ์Šค
  • ์บ์‹œ ๋ฌดํšจํ™”: ๊ธด๊ธ‰ ์‹œ CloudFront ์บ์‹œ ์ฆ‰์‹œ ๋ฌดํšจํ™”

๐Ÿ” ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ

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"]
    }
  }
}

๐Ÿ“ˆ ํ–ฅํ›„ ๊ฐœ์„  ๊ณ„ํš

๋‹จ๊ธฐ ๊ณ„ํš (1-2๊ฐœ์›”)

  • ์ด๋ฏธ์ง€ ์ตœ์ ํ™”: CloudFront์—์„œ WebP ๋ณ€ํ™˜ ๋ฐ ๋ฆฌ์‚ฌ์ด์ง•
  • ๋ณด์•ˆ ๊ฐ•ํ™”: WAF ์—ฐ๋™์œผ๋กœ DDoS ๋ฐ ์•…์„ฑ ์š”์ฒญ ์ฐจ๋‹จ
  • ๋ชจ๋‹ˆํ„ฐ๋ง ๊ณ ๋„ํ™”: ์‹ค์‹œ๊ฐ„ ๋Œ€์‹œ๋ณด๋“œ ๋ฐ ์•Œ๋ฆผ ์‹œ์Šคํ…œ

์ค‘๊ธฐ ๊ณ„ํš (3-6๊ฐœ์›”)

  • ๋ฉ€ํ‹ฐ CDN: CloudFront + ๋‹ค๋ฅธ CDN ์กฐํ•ฉ์œผ๋กœ ๊ฐ€์šฉ์„ฑ ํ–ฅ์ƒ
  • ์—ฃ์ง€ ์ปดํ“จํŒ…: Lambda@Edge๋กœ ๋™์  ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
  • ๋น„์šฉ ์ตœ์ ํ™”: ์‚ฌ์šฉ ํŒจํ„ด ๋ถ„์„ ๊ธฐ๋ฐ˜ ์บ์‹œ ์ •์ฑ… ์ž๋™ ์กฐ์ •

์žฅ๊ธฐ ๊ณ„ํš (6๊ฐœ์›”+)

  • ๊ธ€๋กœ๋ฒŒ ํ™•์žฅ: ์ง€์—ญ๋ณ„ ์ตœ์ ํ™”๋œ CDN ์ „๋žต
  • AI ๊ธฐ๋ฐ˜ ์ตœ์ ํ™”: ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ๋ฐ˜ ์บ์‹œ ์˜ˆ์ธก ๋ฐ ํ”„๋ฆฌ๋กœ๋”ฉ
  • ์™„์ „ ์ž๋™ํ™”: ์ธํ”„๋ผ ์ฝ”๋“œํ™” ๋ฐ ์ž๋™ ์Šค์ผ€์ผ๋ง

๐Ÿ’ก ๊ตํ›ˆ ๋ฐ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

ํ•ต์‹ฌ ๊ตํ›ˆ

  1. ์ ์ง„์  ์ „ํ™˜: ๊ธฐ์กด API ์œ ์ง€ํ•˜๋ฉด์„œ ์ƒˆ๋กœ์šด ๊ตฌ์กฐ ๋„์ž…
  2. Fallback ํ•„์ˆ˜: ํ•ญ์ƒ ๋Œ€์•ˆ ๊ฒฝ๋กœ ์ค€๋น„
  3. ๋ชจ๋‹ˆํ„ฐ๋ง ์šฐ์„ : ๋ณ€๊ฒฝ ์‚ฌํ•ญ์˜ ์˜ํ–ฅ๋„ ์‹ค์‹œ๊ฐ„ ์ถ”์ 
  4. ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ: ๋‘ ๋งˆ๋ฆฌ ํ† ๋ผ๋ฅผ ๋ชจ๋‘ ์žก์„ ์ˆ˜ ์žˆ๋Š” ์„ค๊ณ„

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

  • ์„ค์ • ์•”ํ˜ธํ™”: ๋ฏผ๊ฐํ•œ CloudFront ์„ค์ •์€ ๋ฐ˜๋“œ์‹œ ์•”ํ˜ธํ™”
  • ํ‚ค ๊ด€๋ฆฌ: Private Key ๋ณด์•ˆ ์ €์žฅ ๋ฐ ์ •๊ธฐ ๋กœํ…Œ์ด์…˜
  • ์บ์‹œ ์ „๋žต: ํŒŒ์ผ ํƒ€์ž…๋ณ„ ์ฐจ๋ณ„ํ™”๋œ ์บ์‹œ ์ •์ฑ…
  • ๋น„์šฉ ๋ชจ๋‹ˆํ„ฐ๋ง: CloudFront ์‚ฌ์šฉ๋Ÿ‰ ์ •๊ธฐ ๊ฒ€ํ†  ๋ฐ ์ตœ์ ํ™”

๐ŸŽฏ ๊ฒฐ๋ก : S3 + CloudFront ๋ถ„๋ฆฌ ๊ตฌ์กฐ๋กœ ๋ณด์•ˆ ๊ฐ•ํ™” + ์„ฑ๋Šฅ ํ–ฅ์ƒ + ๋น„์šฉ ์ตœ์ ํ™”๋ฅผ ๋™์‹œ์— ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

profile
๊ธฐํš๊ณผ ์„ค๊ณ„ ๊ทธ๋ฆฌ๊ณ  ๊ตฌํ˜„๊นŒ์ง€ ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค

0๊ฐœ์˜ ๋Œ“๊ธ€