Skip to content

Latest commit

ย 

History

History
562 lines (453 loc) ยท 18.3 KB

deployment.md

File metadata and controls

562 lines (453 loc) ยท 18.3 KB

์ฝ”๋“œ ๋ฐฐํฌ ์‹œ์Šคํ…œ ์„ค๊ณ„ (Spinnaker ์œ ํ˜•)

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

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

  1. ์ผ์ผ ๋ฐฐํฌ ํšŸ์ˆ˜์™€ ๋™์‹œ ๋ฐฐํฌ ์ˆ˜๋Š” ์–ด๋Š ์ •๋„์ธ๊ฐ€์š”?
  2. ๋‹ค์ค‘ ํด๋ผ์šฐ๋“œ/๋ฆฌ์ „ ์ง€์›์ด ํ•„์š”ํ•œ๊ฐ€์š”?
  3. ์ž๋™ ๋กค๋ฐฑ์˜ ๊ธฐ์ค€์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?
  4. ๋ฐฐํฌ ์Šน์ธ ํ”„๋กœ์„ธ์Šค๊ฐ€ ํ•„์š”ํ•œ๊ฐ€์š”?

๋ฉด์ ‘๊ด€:

  1. ์ผ์ผ 100ํšŒ ๋ฐฐํฌ, ์ตœ๋Œ€ 10๊ฐœ ๋™์‹œ ๋ฐฐํฌ
  2. AWS, GCP ๋ฉ€ํ‹ฐ ํด๋ผ์šฐ๋“œ ์ง€์› ํ•„์š”
  3. Error rate 2% ์ดˆ๊ณผ ๋˜๋Š” P99 ๋ ˆ์ดํ„ด์‹œ 1์ดˆ ์ดˆ๊ณผ ์‹œ ๋กค๋ฐฑ
  4. Production ํ™˜๊ฒฝ์€ ์ˆ˜๋™ ์Šน์ธ ํ•„์š”

1. ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ์‹œ์Šคํ…œ

@Service
public class DeploymentPipelineService {

    // 1. ๋ฐฐํฌ ํŒŒ์ดํ”„๋ผ์ธ ์ •์˜
    public class Pipeline {
        private final String pipelineId;
        private final ApplicationConfig application;
        private final DeploymentStrategy strategy;
        private final List<Stage> stages;
        private final Map<String, String> parameters;

        @Data
        public class Stage {
            private final String name;
            private final StageType type;
            private final Map<String, Object> config;
            private final List<String> dependencies;
            private final ApprovalConfig approval;
        }
    }

    // 2. ๋ฐฐํฌ ์ „๋žต ๊ตฌํ˜„
    @Service
    public class DeploymentStrategyExecutor {
        private final CloudProviderClient cloudClient;
        private final MetricsService metricsService;

        public DeploymentResult executeCanaryDeployment(
            DeploymentConfig config) {
            
            // ์นด๋‚˜๋ฆฌ ์ธ์Šคํ„ด์Šค ๋ฐฐํฌ
            String canaryId = deployCanaryInstance(config);
            
            // ํŠธ๋ž˜ํ”ฝ ์ ์ง„์  ์ฆ๊ฐ€
            for (int percentage : config.getTrafficSteps()) {
                routeTraffic(canaryId, percentage);
                
                // ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋‹ˆํ„ฐ๋ง
                if (!validateCanaryMetrics(canaryId)) {
                    return rollbackCanary(canaryId);
                }
                
                // ์•ˆ์ •ํ™” ๋Œ€๊ธฐ
                waitForStabilization(Duration.ofMinutes(5));
            }
            
            // ์™„์ „ ์ „ํ™˜
            return promoteCanary(canaryId);
        }

        public DeploymentResult executeBlueGreenDeployment(
            DeploymentConfig config) {
            
            // ๊ทธ๋ฆฐ ํ™˜๊ฒฝ ๋ฐฐํฌ
            String greenId = deployGreenEnvironment(config);
            
            // ํ—ฌ์Šค์ฒดํฌ
            if (!validateEnvironment(greenId)) {
                return rollbackDeployment(greenId);
            }
            
            // ํŠธ๋ž˜ํ”ฝ ์ „ํ™˜
            switchTraffic(config.getBlueId(), greenId);
            
            // ๋ธ”๋ฃจ ํ™˜๊ฒฝ ์ •๋ฆฌ
            if (config.isCleanupEnabled()) {
                scheduleCleanup(config.getBlueId());
            }
            
            return DeploymentResult.success(greenId);
        }
    }

    // 3. ๋ฉ€ํ‹ฐ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ
    @Service
    public class MultiCloudDeploymentService {
        private final Map<CloudProvider, CloudDeploymentHandler> handlers;
        
        public void executeMultiCloudDeployment(
            DeploymentConfig config) {
            
            // ํด๋ผ์šฐ๋“œ๋ณ„ ๋ฐฐํฌ ์„ค์ •
            Map<CloudProvider, DeploymentConfig> cloudConfigs = 
                prepareCloudConfigs(config);
            
            // ๋ณ‘๋ ฌ ๋ฐฐํฌ ์‹คํ–‰
            CompletableFuture<?>[] deployments = cloudConfigs.entrySet()
                .stream()
                .map(entry -> CompletableFuture.runAsync(() ->
                    handlers.get(entry.getKey())
                           .deploy(entry.getValue())))
                .toArray(CompletableFuture[]::new);
                
            try {
                CompletableFuture.allOf(deployments).join();
            } catch (Exception e) {
                // ์‹คํŒจ ์‹œ ๋ชจ๋“  ํด๋ผ์šฐ๋“œ์—์„œ ๋กค๋ฐฑ
                rollbackAllClouds(cloudConfigs.keySet());
                throw e;
            }
        }
    }
}

2. ๋ฐฐํฌ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กค๋ฐฑ ์‹œ์Šคํ…œ

@Service
public class DeploymentMonitoringService {

    // 1. ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋‹ˆํ„ฐ๋ง
    public class MetricMonitor {
        private final PrometheusClient prometheusClient;
        private final DatadogClient datadogClient;
        private final AlertManager alertManager;

        public boolean validateDeployment(String deploymentId) {
            DeploymentMetrics metrics = collectMetrics(deploymentId);
            
            // ์ฃผ์š” ๋ฉ”ํŠธ๋ฆญ ๊ฒ€์ฆ
            boolean isHealthy = Stream.of(
                validateErrorRate(metrics),
                validateLatency(metrics),
                validateResourceUsage(metrics),
                validateCustomMetrics(metrics)
            ).allMatch(result -> result);

            if (!isHealthy) {
                alertManager.sendAlert(AlertLevel.HIGH,
                    String.format("Deployment %s failed metrics validation", 
                        deploymentId));
            }

            return isHealthy;
        }

        private boolean validateErrorRate(DeploymentMetrics metrics) {
            double errorRate = metrics.getErrorRate();
            if (errorRate > 0.02) { // 2% threshold
                logViolation("Error rate too high: " + errorRate);
                return false;
            }
            return true;
        }

        private boolean validateLatency(DeploymentMetrics metrics) {
            Duration p99Latency = metrics.getP99Latency();
            if (p99Latency.toMillis() > 1000) { // 1s threshold
                logViolation("P99 latency too high: " + p99Latency);
                return false;
            }
            return true;
        }
    }

    // 2. ์ž๋™ ๋กค๋ฐฑ ์‹œ์Šคํ…œ
    @Service
    public class AutoRollbackService {
        private final DeploymentManager deploymentManager;
        private final MetricMonitor metricMonitor;
        private final NotificationService notificationService;

        public void monitorAndRollback(String deploymentId) {
            try {
                // ๋ฐฐํฌ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์ž‘
                DeploymentWatch watch = new DeploymentWatch(deploymentId);
                watch.startMonitoring(Duration.ofMinutes(30)); // 30๋ถ„ ๋ชจ๋‹ˆํ„ฐ๋ง

                while (watch.isActive()) {
                    if (!metricMonitor.validateDeployment(deploymentId)) {
                        initiateRollback(deploymentId, 
                            RollbackReason.METRIC_VIOLATION);
                        break;
                    }
                    Thread.sleep(10000); // 10์ดˆ๋งˆ๋‹ค ์ฒดํฌ
                }
            } catch (Exception e) {
                handleMonitoringFailure(deploymentId, e);
            }
        }

        private void initiateRollback(String deploymentId, 
                                    RollbackReason reason) {
            try {
                // ๋กค๋ฐฑ ์ „ ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
                DeploymentSnapshot snapshot = 
                    deploymentManager.createSnapshot(deploymentId);
                
                // ๋กค๋ฐฑ ์‹คํ–‰
                RollbackResult result = 
                    deploymentManager.rollback(deploymentId, snapshot);
                
                // ๊ฒฐ๊ณผ ํ†ต๋ณด
                notifyRollback(deploymentId, reason, result);
                
            } catch (Exception e) {
                handleRollbackFailure(deploymentId, e);
            }
        }
    }

    // 3. ๋ฐฐํฌ ์ƒํƒœ ์ถ”์ 
    @Service
    public class DeploymentStateTracker {
        private final RedisTemplate<String, DeploymentState> redisTemplate;
        
        public void trackDeploymentProgress(String deploymentId) {
            DeploymentState state = new DeploymentState();
            
            // ๋‹จ๊ณ„๋ณ„ ์ง„ํ–‰์ƒํ™ฉ ์ถ”์ 
            state.addStage("PREPARATION", StageStatus.COMPLETED);
            state.addStage("VALIDATION", StageStatus.IN_PROGRESS);
            
            // Redis์— ์ƒํƒœ ์ €์žฅ
            redisTemplate.opsForValue().set(
                "deployment:" + deploymentId, 
                state,
                Duration.ofHours(24)
            );
        }

        // ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ์กฐํšŒ
        public DeploymentState getDeploymentState(String deploymentId) {
            return redisTemplate.opsForValue()
                .get("deployment:" + deploymentId);
        }

        // ๋ฐฐํฌ ์ด๋ ฅ ๊ด€๋ฆฌ
        @Scheduled(fixedRate = 86400000) // ๋งค์ผ ์‹คํ–‰
        public void archiveDeploymentHistory() {
            List<DeploymentState> completedDeployments = 
                findCompletedDeployments();
            
            for (DeploymentState deployment : completedDeployments) {
                // S3์— ์ด๋ ฅ ์ €์žฅ
                archiveDeployment(deployment);
                // Redis์—์„œ ์ œ๊ฑฐ
                redisTemplate.delete("deployment:" + 
                    deployment.getDeploymentId());
            }
        }
    }

    // 4. ์•Œ๋ฆผ ๋ฐ ๋ฆฌํฌํŒ…
    @Service
    public class DeploymentNotificationService {
        private final SlackClient slackClient;
        private final EmailService emailService;
        private final MetricsRegistry metricsRegistry;

        public void sendDeploymentNotification(
            DeploymentEvent event) {
            
            // Slack ์•Œ๋ฆผ
            if (event.isHighPriority()) {
                slackClient.sendMessage(
                    "#deployments-critical",
                    createSlackMessage(event)
                );
            }

            // ์ด๋ฉ”์ผ ์•Œ๋ฆผ
            if (event.requiresApproval()) {
                emailService.sendApprovalRequest(
                    event.getApprovers(),
                    createApprovalEmail(event)
                );
            }

            // ๋ฉ”ํŠธ๋ฆญ ๊ธฐ๋ก
            metricsRegistry.counter(
                "deployments.total",
                "status", event.getStatus().toString()
            ).increment();
        }

        public DeploymentReport generateDeploymentReport(
            String deploymentId) {
            
            DeploymentState state = stateTracker
                .getDeploymentState(deploymentId);
            
            return DeploymentReport.builder()
                .deploymentId(deploymentId)
                .duration(state.getDuration())
                .stages(state.getStages())
                .metrics(metricMonitor.getMetrics(deploymentId))
                .incidents(getIncidents(deploymentId))
                .build();
        }
    }
}

์ด๋Ÿฌํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กค๋ฐฑ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด:

  1. ์‹ค์‹œ๊ฐ„ ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋‹ˆํ„ฐ๋ง

    • ์—๋Ÿฌ์œจ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ๋ ˆ์ดํ„ด์‹œ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ ์ง€์›
  2. ์ž๋™ ๋กค๋ฐฑ

    • ๋ฉ”ํŠธ๋ฆญ ๊ธฐ๋ฐ˜ ๋กค๋ฐฑ
    • ์Šค๋ƒ…์ƒท ๊ธฐ๋ฐ˜ ๋ณต๊ตฌ
    • ๋กค๋ฐฑ ์‹คํŒจ ์ฒ˜๋ฆฌ
  3. ์ƒํƒœ ์ถ”์ 

    • ์‹ค์‹œ๊ฐ„ ์ง„ํ–‰์ƒํ™ฉ ์ถ”์ 
    • ์ด๋ ฅ ๊ด€๋ฆฌ
    • ์•„์นด์ด๋น™
  4. ์•Œ๋ฆผ ๋ฐ ๋ณด๊ณ 

    • ๋‹ค์ค‘ ์ฑ„๋„ ์•Œ๋ฆผ
    • ์Šน์ธ ์š”์ฒญ ๊ด€๋ฆฌ
    • ์ƒ์„ธ ๋ฆฌํฌํŠธ ์ƒ์„ฑ

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

๋ฉด์ ‘๊ด€: ๋Œ€๊ทœ๋ชจ ๋ฐฐํฌ ์‹œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌ์Šคํฌ๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

3. ๋ฐฐํฌ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ

@Service
public class DeploymentRiskManagementService {

    // 1. ๋ฆฌ์Šคํฌ ํ‰๊ฐ€ ์‹œ์Šคํ…œ
    @Service
    public class RiskAssessor {
        private final DeploymentHistory deploymentHistory;
        private final ServiceDependencyGraph dependencyGraph;

        public RiskAssessment assessDeploymentRisk(DeploymentRequest request) {
            RiskScore score = RiskScore.builder()
                .changeScope(evaluateChangeScope(request))
                .timeRisk(evaluateTimeRisk(request))
                .dependencyRisk(evaluateDependencyRisk(request))
                .historicalRisk(evaluateHistoricalRisk(request))
                .build();

            // ๋ฆฌ์Šคํฌ ๋ ˆ๋ฒจ ๊ฒฐ์ •
            RiskLevel riskLevel = determineRiskLevel(score);
            
            return RiskAssessment.builder()
                .level(riskLevel)
                .score(score)
                .mitigationStrategies(suggestMitigationStrategies(riskLevel))
                .build();
        }

        private double evaluateDependencyRisk(DeploymentRequest request) {
            Set<String> impactedServices = 
                dependencyGraph.getImpactedServices(request.getServiceId());
            
            return impactedServices.stream()
                .mapToDouble(serviceId -> 
                    calculateServiceCriticality(serviceId))
                .sum();
        }

        private double evaluateHistoricalRisk(DeploymentRequest request) {
            // ๊ณผ๊ฑฐ ๋ฐฐํฌ ์„ฑ๊ณต๋ฅ  ๋ถ„์„
            DeploymentStats stats = deploymentHistory
                .getStats(request.getServiceId());
            
            return calculateRiskScore(
                stats.getFailureRate(),
                stats.getAverageRecoveryTime(),
                stats.getRollbackFrequency()
            );
        }
    }

    // 2. ๋ฐฐํฌ ์ œ์•ฝ ์กฐ๊ฑด ๊ด€๋ฆฌ
    @Service
    public class DeploymentConstraintManager {
        private final BusinessCalendarService calendarService;
        private final TrafficAnalyzer trafficAnalyzer;

        public boolean validateDeploymentConstraints(
            DeploymentRequest request) {
            
            return Stream.of(
                validateTimeWindow(request),
                validateConcurrentDeployments(),
                validateTrafficConditions(),
                validateDependencyFreeze()
            ).allMatch(result -> result);
        }

        private boolean validateTimeWindow(DeploymentRequest request) {
            // ๋ฐฐํฌ ๊ธˆ์ง€ ์‹œ๊ฐ„๋Œ€ ์ฒดํฌ
            if (isInBlackoutPeriod()) {
                throw new DeploymentConstraintException(
                    "Deployment not allowed during blackout period");
            }

            // ๋น„์ฆˆ๋‹ˆ์Šค ํฌ๋ฆฌํ‹ฐ์ปฌ ์‹œ๊ฐ„๋Œ€ ์ฒดํฌ
            if (isBusinessCriticalPeriod() && 
                !request.hasEmergencyOverride()) {
                throw new DeploymentConstraintException(
                    "Deployment not allowed during business critical hours");
            }

            return true;
        }

        private boolean validateTrafficConditions() {
            TrafficStats currentTraffic = 
                trafficAnalyzer.getCurrentTraffic();
            
            return currentTraffic.getLoad() < 
                   ThresholdConfig.MAX_DEPLOYMENT_TRAFFIC_LOAD;
        }
    }

    // 3. ์ ์ง„์  ๋กค์•„์›ƒ ์ œ์–ด
    @Service
    public class GradualRolloutController {
        private final MetricAnalyzer metricAnalyzer;
        private final AlertManager alertManager;

        public void controlRollout(DeploymentConfig config) {
            List<RolloutStage> stages = planRolloutStages(config);
            
            for (RolloutStage stage : stages) {
                try {
                    executeRolloutStage(stage);
                    
                    // ์•ˆ์ •ํ™” ๊ธฐ๊ฐ„ ๋™์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง
                    if (!monitorStageHealth(stage)) {
                        initiateStageRollback(stage);
                        throw new RolloutException(
                            "Stage health check failed");
                    }
                    
                } catch (Exception e) {
                    handleRolloutFailure(stage, e);
                    break;
                }
            }
        }

        private boolean monitorStageHealth(RolloutStage stage) {
            Duration monitoringWindow = stage.getMonitoringWindow();
            Instant deadline = Instant.now().plus(monitoringWindow);
            
            while (Instant.now().isBefore(deadline)) {
                StageMetrics metrics = 
                    metricAnalyzer.analyzeStage(stage);
                
                if (metrics.hasAnomalies()) {
                    alertManager.sendAlert(
                        createAnomalyAlert(stage, metrics));
                    return false;
                }
                
                Thread.sleep(stage.getCheckInterval().toMillis());
            }
            
            return true;
        }
    }

    // 4. ๊ธด๊ธ‰ ๋Œ€์‘ ์‹œ์Šคํ…œ
    @Service
    public class EmergencyResponseSystem {
        private final IncidentManager incidentManager;
        private final ServiceRegistry serviceRegistry;

        public void handleEmergencyDeployment(
            EmergencyDeploymentRequest request) {
            
            // ์ธ์‹œ๋˜ํŠธ ์ƒ์„ฑ
            Incident incident = incidentManager
                .createIncident(request.getReason());
            
            try {
                // ๊ธด๊ธ‰ ๋ฐฐํฌ ์‹คํ–‰
                executeEmergencyDeployment(request);
                
                // ์˜ํ–ฅ ๋ถ„์„
                analyzeImpact(request.getServiceId());
                
            } catch (Exception e) {
                // ์ž๋™ ๋กค๋ฐฑ ๋ฐ ์•Œ๋ฆผ
                handleEmergencyFailure(incident, e);
            }
        }

        public void initiateEmergencyRollback(String deploymentId) {
            // ๋กค๋ฐฑ ์ „ ์ƒํƒœ ์Šค๋ƒ…์ƒท
            DeploymentSnapshot snapshot = 
                createPreRollbackSnapshot(deploymentId);
            
            // ์šฐ์„ ์ˆœ์œ„ ๊ฒฉ์ƒ
            prioritizeRollback(deploymentId);
            
            // ๊ธด๊ธ‰ ๋กค๋ฐฑ ์‹คํ–‰
            executeEmergencyRollback(deploymentId, snapshot);
        }
    }
}

์ด๋Ÿฌํ•œ ๋ฆฌ์Šคํฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด:

  1. ์‚ฌ์ „ ๋ฆฌ์Šคํฌ ํ‰๊ฐ€

    • ๋ณ€๊ฒฝ ๋ฒ”์œ„ ํ‰๊ฐ€
    • ์‹œ๊ฐ„๋Œ€ ๋ฆฌ์Šคํฌ
    • ์˜์กด์„ฑ ๋ฆฌ์Šคํฌ
    • ํžˆ์Šคํ† ๋ฆฌ ๊ธฐ๋ฐ˜ ๋ฆฌ์Šคํฌ
  2. ๋ฐฐํฌ ์ œ์•ฝ ๊ด€๋ฆฌ

    • ์‹œ๊ฐ„ ์œˆ๋„์šฐ ์ œํ•œ
    • ๋™์‹œ ๋ฐฐํฌ ์ œํ•œ
    • ํŠธ๋ž˜ํ”ฝ ๊ธฐ๋ฐ˜ ์ œํ•œ
    • ์˜์กด์„ฑ ํ”„๋ฆฌ์ฆˆ
  3. ์ ์ง„์  ๋กค์•„์›ƒ

    • ๋‹จ๊ณ„๋ณ„ ๋ฐฐํฌ
    • ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ์ž๋™ ๋กค๋ฐฑ
    • ์•ˆ์ •ํ™” ๊ธฐ๊ฐ„ ๊ด€๋ฆฌ
  4. ๊ธด๊ธ‰ ์ƒํ™ฉ ๋Œ€์‘

    • ๊ธด๊ธ‰ ๋ฐฐํฌ ํ”„๋กœ์„ธ์Šค
    • ์‹ ์†ํ•œ ๋กค๋ฐฑ
    • ์˜ํ–ฅ ๋ถ„์„
    • ์ธ์‹œ๋˜ํŠธ ๊ด€๋ฆฌ

์„ ๊ตฌํ˜„ํ•˜์—ฌ ๋Œ€๊ทœ๋ชจ ๋ฐฐํฌ์˜ ๋ฆฌ์Šคํฌ๋ฅผ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ์ค‘์š”ํ•œ ์ ์€:

  • ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์˜์‚ฌ๊ฒฐ์ •
  • ์ž๋™ํ™”๋œ ์•ˆ์ „์žฅ์น˜
  • ์ ์ง„์  ์ ‘๊ทผ
  • ๊ธด๊ธ‰ ์ƒํ™ฉ ๋Œ€๋น„

์ž…๋‹ˆ๋‹ค.