๋ฉด์ ๊ด: "๋๊ท๋ชจ ๋์์ ์คํธ๋ฆฌ๋ฐ ํ๋ซํผ์ ์ค๊ณํด์ฃผ์ธ์. ๋์์ ์ ๋ก๋, ํธ๋์ค์ฝ๋ฉ, ์คํธ๋ฆฌ๋ฐ ๊ธฐ๋ฅ์ด ํ์ํฉ๋๋ค."
์ง์์: ๋ค, ๋ช ๊ฐ์ง ์๊ตฌ์ฌํญ์ ํ์ธํ๊ณ ์ถ์ต๋๋ค.
- ์์ ์ฌ์ฉ์ ์์ ๋์ ์์ฒญ์ ์๋ ์ด๋ ์ ๋์ธ๊ฐ์?
- ํ๊ท ๋์์ ํฌ๊ธฐ์ ๊ธธ์ด๋ ์ด๋ ์ ๋์ธ๊ฐ์?
- ์ง์ํด์ผ ํ๋ ํ์ง๊ณผ ๋๋ฐ์ด์ค ์ข ๋ฅ๋ ์ด๋ป๊ฒ ๋๋์?
- ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ๋ ์ง์ํด์ผ ํ๋์?
๋ฉด์ ๊ด:
- DAU 100๋ง, ์ต๋ ๋์ ์์ฒญ์ 50๋ง ๋ช
- ํ๊ท 10๋ถ ๊ธธ์ด, 500MB ํฌ๊ธฐ
- 240p๋ถํฐ 4K๊น์ง, ๋ชจ๋ฐ์ผ/ํ๋ธ๋ฆฟ/์น/์ค๋งํธTV ์ง์
- ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ์ ํ์ฌ ๋จ๊ณ์์๋ ๋ถํ์
@Service
public class VideoUploadService {
private final S3Client s3Client;
private final CloudFrontClient cloudFrontClient;
private final TranscodingService transcodingService;
// 1. ์ฒญํฌ ๊ธฐ๋ฐ ์
๋ก๋
public UploadResponse handleChunkUpload(MultipartFile chunk,
String uploadId,
int partNumber) {
try {
// ์ฒญํฌ ์์ ์ ์ฅ
String chunkKey = String.format("temp/%s/part-%d", uploadId, partNumber);
s3Client.putObject(PutObjectRequest.builder()
.bucket(UPLOAD_BUCKET)
.key(chunkKey)
.build(),
RequestBody.fromInputStream(chunk.getInputStream(),
chunk.getSize()));
// ์
๋ก๋ ์งํ์ํฉ ์
๋ฐ์ดํธ
updateUploadProgress(uploadId, partNumber);
return new UploadResponse(uploadId, partNumber, "SUCCESS");
} catch (Exception e) {
log.error("Upload failed for chunk: " + partNumber, e);
return new UploadResponse(uploadId, partNumber, "FAILED");
}
}
// 2. ์ฒญํฌ ๋ณํฉ ๋ฐ ํธ๋์ค์ฝ๋ฉ ์์
public void completeUpload(String uploadId) {
// ์ฒญํฌ ๋ณํฉ
List<CompletedPart> completedParts = getCompletedParts(uploadId);
String finalKey = "raw/" + generateVideoId() + ".mp4";
s3Client.completeMultipartUpload(CompleteMultipartUploadRequest.builder()
.bucket(UPLOAD_BUCKET)
.key(finalKey)
.uploadId(uploadId)
.multipartUpload(CompletedMultipartUpload.builder()
.parts(completedParts)
.build())
.build());
// ํธ๋์ค์ฝ๋ฉ ์์
์์
transcodingService.startTranscoding(finalKey);
}
}
@Service
public class TranscodingService {
private final MediaConvertClient mediaConvert;
private final JobRepository jobRepository;
private final NotificationService notificationService;
// 1. ํธ๋์ค์ฝ๋ฉ ์์
์์ฑ
public void startTranscoding(String videoKey) {
// ํ์ง๋ณ ์ถ๋ ฅ ์ค์
List<OutputGroup> outputs = Arrays.asList(
createOutput("240p", 426, 240),
createOutput("480p", 854, 480),
createOutput("720p", 1280, 720),
createOutput("1080p", 1920, 1080),
createOutput("4K", 3840, 2160)
);
// ํธ๋์ค์ฝ๋ฉ ์์
์์
Job job = Job.builder()
.input(Input.builder()
.fileInput("s3://" + UPLOAD_BUCKET + "/" + videoKey)
.build())
.outputGroups(outputs)
.build();
StartJobResponse response = mediaConvert.startJob(job);
// ์์
์ํ ์ถ์
jobRepository.save(new TranscodingJob(
response.jobId(),
videoKey,
JobStatus.PROCESSING
));
}
// 2. ํธ๋์ค์ฝ๋ฉ ์๋ฃ ์ฒ๋ฆฌ
@EventListener
public void handleTranscodingComplete(TranscodingCompleteEvent event) {
// CDN ์บ์ ๋ฌดํจํ
invalidateCDNCache(event.getVideoId());
// ๋ฉํ๋ฐ์ดํฐ ์
๋ฐ์ดํธ
updateVideoMetadata(event.getVideoId(), event.getOutputFiles());
// ์๋ฆผ ์ ์ก
notificationService.notifyTranscodingComplete(event.getVideoId());
}
}
@Service
public class StreamingService {
private final CDNService cdnService;
private final VideoMetadataService metadataService;
private final BandwidthMonitor bandwidthMonitor;
// 1. ์ ์ํ ์คํธ๋ฆฌ๋ฐ ๋งค๋ํ์คํธ ์์ฑ
public String generateManifest(String videoId, String deviceType) {
VideoMetadata metadata = metadataService.getMetadata(videoId);
// HLS ๋งค๋ํ์คํธ ์์ฑ
M3U8Manifest manifest = M3U8Manifest.builder()
.version(3)
.targetDuration(10)
.streams(generateStreamInfo(metadata, deviceType))
.build();
return manifest.toString();
}
// 2. ์คํธ๋ฆผ ํ์ง ์ ํ ๋ก์ง
private List<StreamInfo> generateStreamInfo(VideoMetadata metadata,
String deviceType) {
List<StreamInfo> streams = new ArrayList<>();
// ๋๋ฐ์ด์ค๋ณ ์ต์ ํ์ง ์ค์
int maxQuality = determineMaxQuality(deviceType);
metadata.getAvailableQualities()
.stream()
.filter(quality -> quality.getHeight() <= maxQuality)
.forEach(quality -> {
streams.add(StreamInfo.builder()
.bandwidth(quality.getBitrate())
.resolution(quality.getWidth(), quality.getHeight())
.codecs("avc1.64001f,mp4a.40.2")
.url(generateStreamUrl(metadata.getId(), quality))
.build());
});
return streams;
}
// 3. CDN ์คํธ๋ฆฌ๋ฐ URL ์์ฑ
private String generateStreamUrl(String videoId, Quality quality) {
String baseUrl = cdnService.getBaseUrl();
String token = generateSecureToken(videoId, quality);
return String.format("%s/videos/%s/%s/index.m3u8?token=%s",
baseUrl, videoId, quality.getName(), token);
}
}
@Service
public class AdaptiveBitrateService {
// 4. ํด๋ผ์ด์ธํธ ๋์ญํญ ๋ชจ๋ํฐ๋ง
public void trackClientBandwidth(String sessionId,
BandwidthSample sample) {
bandwidthMonitor.recordSample(sessionId, sample);
// ํ์ง ์ ํ ํ์์ฑ ์ฒดํฌ
if (shouldSwitchQuality(sessionId)) {
Quality newQuality = determineOptimalQuality(
sessionId,
bandwidthMonitor.getAverageBandwidth(sessionId)
);
notifyQualityChange(sessionId, newQuality);
}
}
// 5. ๋ฒํผ๋ง ๋ฐฉ์ง๋ฅผ ์ํ ์ ์ ์ ํ์ง ์กฐ์
@Scheduled(fixedRate = 1000)
public void monitorBuffering() {
activeSessions.forEach((sessionId, session) -> {
BufferingMetrics metrics = session.getBufferingMetrics();
if (metrics.getBufferHealth() < BUFFER_THRESHOLD) {
// ํ์ง ๋ค์ด๊ทธ๋ ์ด๋
downgradeQuality(sessionId);
}
});
}
}
@Service
public class VideoDeliveryService {
// 6. ์ง์ญ ๊ธฐ๋ฐ CDN ๋ผ์ฐํ
public String getOptimalCDNEndpoint(String clientIp) {
GeoLocation location = geoService.getLocation(clientIp);
List<CDNEndpoint> endpoints = cdnService.getAvailableEndpoints();
return endpoints.stream()
.min(Comparator.comparingDouble(endpoint ->
calculateLatency(location, endpoint.getLocation())))
.map(CDNEndpoint::getUrl)
.orElse(defaultEndpoint);
}
// 7. ๋์ ์์ฒญ์ ๊ด๋ฆฌ
public void manageViewerLoad(String videoId) {
int currentViewers = getActiveViewers(videoId);
if (currentViewers > VIEWER_THRESHOLD) {
// ์ถ๊ฐ CDN ์ฉ๋ ํ๋ณด
cdnService.scaleUpCapacity(videoId);
// ๋ถํ ๋ถ์ฐ
redistributeViewers(videoId);
}
}
}
@Configuration
public class CachingConfig {
// 1. ๋ค์ธต ์บ์ฑ ์ ๋ต
@Bean
public CacheManager videoCacheManager() {
return new LayeredCacheManager(
new EdgeCache(1000), // Edge ์บ์
new RegionalCache(), // ์ง์ญ ์บ์
new OriginCache() // ์๋ณธ ์บ์
);
}
// 2. ์ธ๊ธฐ ์ปจํ
์ธ ํ๋ฆฌ๋ก๋ฉ
@Scheduled(fixedRate = 3600000) // 1์๊ฐ๋ง๋ค
public void preloadPopularContent() {
List<String> popularVideos =
analyticsService.getTopVideos(100);
popularVideos.forEach(videoId -> {
List<Quality> qualities =
metadataService.getAvailableQualities(videoId);
// ์ฃผ์ ํ์ง์ ์์ ์ธ๊ทธ๋จผํธ๋ฅผ ํ๋ฆฌ๋ก๋
qualities.forEach(quality ->
cacheManager.preload(videoId, quality));
});
}
}
@Service
public class PerformanceOptimizer {
// 3. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ๋ฐ ์ต์ ํ
@Scheduled(fixedRate = 5000)
public void optimizePerformance() {
// CDN ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
Map<String, PerformanceMetrics> cdnMetrics =
monitorCDNPerformance();
// ๋ฌธ์ ์๋ ์ฃ์ง ๋
ธ๋ ์๋ณ
List<String> problematicNodes =
identifyProblematicNodes(cdnMetrics);
// ํธ๋ํฝ ์ฌ๋ผ์ฐํ
problematicNodes.forEach(node ->
rerouteTraffic(node, findHealthyNode()));
// ์ฑ๋ฅ ์งํ ๋ก๊น
logPerformanceMetrics(cdnMetrics);
}
}
์ด๋ฌํ ์ค๊ณ๋ฅผ ํตํด:
- ํจ์จ์ ์ธ ๋์์ ์ ๋ก๋ ๋ฐ ์ ์ฅ
- ๋ค์ํ ํ์ง์ ํธ๋์ค์ฝ๋ฉ ์ง์
- ์ ์ํ ์คํธ๋ฆฌ๋ฐ์ผ๋ก ์ต์ ์ ์์ฒญ ๊ฒฝํ
- ํจ์จ์ ์ธ CDN ํ์ฉ
- ์ฑ๋ฅ ์ต์ ํ ๋ฐ ๋ชจ๋ํฐ๋ง
์ ๊ตฌํํ ์ ์์ต๋๋ค.
๋ฉด์ ๊ด: ๋๊ท๋ชจ ํธ๋ํฝ ์ํฉ์์ ๋น์ฉ ์ต์ ํ๋ ์ด๋ป๊ฒ ํ์๊ฒ ์ต๋๊น?
@Service
public class CostOptimizationService {
// 1. ์คํ ๋ฆฌ์ง ๊ณ์ธตํ
private class StorageTierManager {
private final S3Client s3Client;
public void optimizeStorageTiers() {
// ์ ๊ทผ ํจํด ๋ถ์
Map<String, AccessPattern> accessPatterns =
analyzeVideoAccessPatterns();
accessPatterns.forEach((videoId, pattern) -> {
if (pattern.getLastAccessedDays() > 30) {
// ๋ฎ์ ๋น๋ ์ ๊ทผ ์์ -> Glacier๋ก ์ด๋
moveToGlacier(videoId);
} else if (pattern.getLastAccessedDays() > 7) {
// ์ค๊ฐ ๋น๋ ์ ๊ทผ ์์ -> S3 IA๋ก ์ด๋
moveToInfrequentAccess(videoId);
}
});
}
private void moveToGlacier(String videoId) {
// ๋ฉํ๋ฐ์ดํฐ ์ ์ง, ์ค์ ์ปจํ
์ธ ๋ง ์ด๋
s3Client.copyObject(CopyObjectRequest.builder()
.sourceBucket(CONTENT_BUCKET)
.sourceKey(videoId)
.destinationBucket(GLACIER_BUCKET)
.storageClass(StorageClass.GLACIER)
.build());
}
}
// 2. CDN ๋น์ฉ ์ต์ ํ
public class CDNCostOptimizer {
@Scheduled(cron = "0 0 * * * *") // ๋งค์๊ฐ
public void optimizeCDNUsage() {
// ์ง์ญ๋ณ ํธ๋ํฝ ๋ถ์
Map<Region, TrafficStats> trafficStats =
analyzeRegionalTraffic();
trafficStats.forEach((region, stats) -> {
if (stats.getCost() > stats.getRevenue()) {
// ๋น์ฉ์ด ์์ต์ ์ด๊ณผํ๋ ์ง์ญ์ ์บ์ ์ ์ฑ
์กฐ์
adjustCachingPolicy(region, CachePolicy.AGGRESSIVE);
// ์ ํ์ง ์คํธ๋ฆผ์ผ๋ก ๊ธฐ๋ณธ ์ค์ ๋ณ๊ฒฝ
adjustDefaultQuality(region, Quality.MEDIUM);
}
});
}
private void adjustCachingPolicy(Region region, CachePolicy policy) {
CloudFrontDistribution distribution =
getDistributionForRegion(region);
// TTL ๋ฐ ์บ์ ๋์ ์กฐ์
distribution.updateCacheBehavior(behavior ->
behavior.withTTL(policy.getTtl())
.withCompression(true)
.withQueryStringForwarding(false));
}
}
// 3. ํธ๋์ค์ฝ๋ฉ ์ต์ ํ
public class TranscodingOptimizer {
public void optimizeTranscodingSettings(String videoId,
VideoMetadata metadata) {
// ์ปจํ
์ธ ํ์
๋ณ ์ต์ ์ค์
if (metadata.getType() == ContentType.ANIMATION) {
// ์ ๋๋ฉ์ด์
์ฉ ์ต์ ํ ์ค์
return TranscodingSettings.builder()
.keyframeInterval(120)
.bitrateLadder(BitrateOptimizer.forAnimation())
.build();
} else if (metadata.getType() == ContentType.LECTURE) {
// ๊ฐ์์ฉ ์ต์ ํ ์ค์
return TranscodingSettings.builder()
.keyframeInterval(240)
.bitrateLadder(BitrateOptimizer.forLecture())
.build();
}
}
// ๋ณ๋ ฌ ํธ๋์ค์ฝ๋ฉ ์ต์ ํ
public void scheduleTranscoding(List<String> videoIds) {
// ์ฐ์ ์์ ๊ธฐ๋ฐ ์ค์ผ์ค๋ง
PriorityQueue<TranscodingJob> queue = new PriorityQueue<>(
Comparator.comparingInt(this::calculatePriority));
queue.addAll(createJobs(videoIds));
// ๋ฐฐ์น ์ฒ๋ฆฌ๋ก ๋น์ฉ ์ต์ ํ
while (!queue.isEmpty()) {
List<TranscodingJob> batch =
getBatchWithinCostLimit(queue);
executeTranscodingBatch(batch);
}
}
}
// 4. ๋น์ฉ ๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ
@Service
public class CostMonitoringService {
private final AlertService alertService;
@Scheduled(cron = "0 0 * * * *")
public void monitorCosts() {
CostBreakdown costs = calculateCurrentCosts();
// ์์ฐ ์ด๊ณผ ํ์ธ
if (costs.isOverBudget()) {
// ์๋ ๋น์ฉ ์ ๊ฐ ์กฐ์น
applyCostReductionMeasures(costs);
// ๊ด๋ฆฌ์ ์๋ฆผ
alertService.sendAlert(AlertLevel.HIGH,
"Budget exceeded: " + costs.getDetails());
}
}
private void applyCostReductionMeasures(CostBreakdown costs) {
if (costs.getCdnCosts() > THRESHOLD) {
// CDN ๋น์ฉ ์ ๊ฐ
reduceCDNCosts();
}
if (costs.getStorageCosts() > THRESHOLD) {
// ์คํ ๋ฆฌ์ง ๋น์ฉ ์ ๊ฐ
reduceStorageCosts();
}
if (costs.getTranscodingCosts() > THRESHOLD) {
// ํธ๋์ค์ฝ๋ฉ ๋น์ฉ ์ ๊ฐ
reduceTranscodingCosts();
}
}
}
}
์ด๋ฌํ ๋น์ฉ ์ต์ ํ ์ ๋ต์ ํตํด:
-
์คํ ๋ฆฌ์ง ๋น์ฉ ์ต์ ํ
- ์ ๊ทผ ๋น๋์ ๋ฐ๋ฅธ ์คํ ๋ฆฌ์ง ๊ณ์ธตํ
- ์ค๋๋ ์ปจํ ์ธ ์ ์๋ ์์นด์ด๋น
- ์ค๋ณต ์ ๊ฑฐ ๋ฐ ์์ถ
-
CDN ๋น์ฉ ์ต์ ํ
- ์ง์ญ๋ณ ํธ๋ํฝ ๋ถ์ ๋ฐ ์ต์ ํ
- ์บ์ฑ ์ ์ฑ ์ต์ ํ
- ํ์ง ์ค์ ์๋ ์กฐ์
-
ํธ๋์ค์ฝ๋ฉ ๋น์ฉ ์ต์ ํ
- ์ปจํ ์ธ ํ์ ๋ณ ์ต์ ์ค์
- ๋ฐฐ์น ์ฒ๋ฆฌ๋ฅผ ํตํ ๋น์ฉ ์ ๊ฐ
- ์ฐ์ ์์ ๊ธฐ๋ฐ ์ค์ผ์ค๋ง
-
์ค์๊ฐ ๋น์ฉ ๋ชจ๋ํฐ๋ง
- ๋น์ฉ ์ด๊ณผ ์ ์๋ ์๋ฆผ
- ์๋ํ๋ ๋น์ฉ ์ ๊ฐ ์กฐ์น
- ์์ธํ ๋น์ฉ ๋ถ์ ๋ฐ ๋ฆฌํฌํ
์ด๋ฅผ ํตํด ์๋น์ค ํ์ง์ ์ ์งํ๋ฉด์๋ ํจ์จ์ ์ธ ๋น์ฉ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.