Skip to content

Latest commit

ย 

History

History
486 lines (393 loc) ยท 16.3 KB

video-streaming.md

File metadata and controls

486 lines (393 loc) ยท 16.3 KB

๋™์˜์ƒ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”Œ๋žซํผ ์„ค๊ณ„ (Netflix/YouTube ์œ ํ˜•)

๋ฉด์ ‘๊ด€: "๋Œ€๊ทœ๋ชจ ๋™์˜์ƒ ์ŠคํŠธ๋ฆฌ๋ฐ ํ”Œ๋žซํผ์„ ์„ค๊ณ„ํ•ด์ฃผ์„ธ์š”. ๋™์˜์ƒ ์—…๋กœ๋“œ, ํŠธ๋žœ์Šค์ฝ”๋”ฉ, ์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."

์ง€์›์ž: ๋„ค, ๋ช‡ ๊ฐ€์ง€ ์š”๊ตฌ์‚ฌํ•ญ์„ ํ™•์ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

  1. ์˜ˆ์ƒ ์‚ฌ์šฉ์ž ์ˆ˜์™€ ๋™์‹œ ์‹œ์ฒญ์ž ์ˆ˜๋Š” ์–ด๋Š ์ •๋„์ธ๊ฐ€์š”?
  2. ํ‰๊ท  ๋™์˜์ƒ ํฌ๊ธฐ์™€ ๊ธธ์ด๋Š” ์–ด๋Š ์ •๋„์ธ๊ฐ€์š”?
  3. ์ง€์›ํ•ด์•ผ ํ•˜๋Š” ํ™”์งˆ๊ณผ ๋””๋ฐ”์ด์Šค ์ข…๋ฅ˜๋Š” ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?
  4. ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ๋„ ์ง€์›ํ•ด์•ผ ํ•˜๋‚˜์š”?

๋ฉด์ ‘๊ด€:

  1. DAU 100๋งŒ, ์ตœ๋Œ€ ๋™์‹œ ์‹œ์ฒญ์ž 50๋งŒ ๋ช…
  2. ํ‰๊ท  10๋ถ„ ๊ธธ์ด, 500MB ํฌ๊ธฐ
  3. 240p๋ถ€ํ„ฐ 4K๊นŒ์ง€, ๋ชจ๋ฐ”์ผ/ํƒœ๋ธ”๋ฆฟ/์›น/์Šค๋งˆํŠธTV ์ง€์›
  4. ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ์€ ํ˜„์žฌ ๋‹จ๊ณ„์—์„œ๋Š” ๋ถˆํ•„์š”

1. ๋™์˜์ƒ ์—…๋กœ๋“œ ๋ฐ ์ €์žฅ

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

2. ํŠธ๋žœ์Šค์ฝ”๋”ฉ ์„œ๋น„์Šค

@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());
    }
}

3. ์ŠคํŠธ๋ฆฌ๋ฐ ์„œ๋น„์Šค

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

4. ์บ์‹ฑ ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™”

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

์ด๋Ÿฌํ•œ ์„ค๊ณ„๋ฅผ ํ†ตํ•ด:

  1. ํšจ์œจ์ ์ธ ๋™์˜์ƒ ์—…๋กœ๋“œ ๋ฐ ์ €์žฅ
  2. ๋‹ค์–‘ํ•œ ํ™”์งˆ์˜ ํŠธ๋žœ์Šค์ฝ”๋”ฉ ์ง€์›
  3. ์ ์‘ํ˜• ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ์ตœ์ ์˜ ์‹œ์ฒญ ๊ฒฝํ—˜
  4. ํšจ์œจ์ ์ธ CDN ํ™œ์šฉ
  5. ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉด์ ‘๊ด€: ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ์ƒํ™ฉ์—์„œ ๋น„์šฉ ์ตœ์ ํ™”๋Š” ์–ด๋–ป๊ฒŒ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

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

@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();
            }
        }
    }
}

์ด๋Ÿฌํ•œ ๋น„์šฉ ์ตœ์ ํ™” ์ „๋žต์„ ํ†ตํ•ด:

  1. ์Šคํ† ๋ฆฌ์ง€ ๋น„์šฉ ์ตœ์ ํ™”

    • ์ ‘๊ทผ ๋นˆ๋„์— ๋”ฐ๋ฅธ ์Šคํ† ๋ฆฌ์ง€ ๊ณ„์ธตํ™”
    • ์˜ค๋ž˜๋œ ์ปจํ…์ธ ์˜ ์ž๋™ ์•„์นด์ด๋น™
    • ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์••์ถ•
  2. CDN ๋น„์šฉ ์ตœ์ ํ™”

    • ์ง€์—ญ๋ณ„ ํŠธ๋ž˜ํ”ฝ ๋ถ„์„ ๋ฐ ์ตœ์ ํ™”
    • ์บ์‹ฑ ์ •์ฑ… ์ตœ์ ํ™”
    • ํ’ˆ์งˆ ์„ค์ • ์ž๋™ ์กฐ์ •
  3. ํŠธ๋žœ์Šค์ฝ”๋”ฉ ๋น„์šฉ ์ตœ์ ํ™”

    • ์ปจํ…์ธ  ํƒ€์ž…๋ณ„ ์ตœ์  ์„ค์ •
    • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ ๋น„์šฉ ์ ˆ๊ฐ
    • ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ์Šค์ผ€์ค„๋ง
  4. ์‹ค์‹œ๊ฐ„ ๋น„์šฉ ๋ชจ๋‹ˆํ„ฐ๋ง

    • ๋น„์šฉ ์ดˆ๊ณผ ์‹œ ์ž๋™ ์•Œ๋ฆผ
    • ์ž๋™ํ™”๋œ ๋น„์šฉ ์ ˆ๊ฐ ์กฐ์น˜
    • ์ƒ์„ธํ•œ ๋น„์šฉ ๋ถ„์„ ๋ฐ ๋ฆฌํฌํŒ…

์ด๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ํ’ˆ์งˆ์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ํšจ์œจ์ ์ธ ๋น„์šฉ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.