Skip to content

Latest commit

ย 

History

History
550 lines (450 loc) ยท 18.2 KB

File metadata and controls

550 lines (450 loc) ยท 18.2 KB

CI/CD ํŒŒ์ดํ”„๋ผ์ธ ์‹œ์Šคํ…œ ์„ค๊ณ„

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

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

  1. ์ผ์ผ ๋นŒ๋“œ ์ˆ˜์™€ ๋™์‹œ ๋นŒ๋“œ ์ˆ˜๋Š” ์–ด๋А ์ •๋„์ธ๊ฐ€์š”?
  2. ๋นŒ๋“œ/ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?
  3. ์•„ํ‹ฐํŒฉํŠธ ๋ณด๊ด€ ๊ธฐ๊ฐ„๊ณผ ํฌ๊ธฐ ์ œํ•œ์ด ์žˆ๋‚˜์š”?
  4. ๋นŒ๋“œ ์‹คํŒจ ์‹œ ์•Œ๋ฆผ์ด๋‚˜ ๋กค๋ฐฑ ์ „๋žต์ด ํ•„์š”ํ•œ๊ฐ€์š”?

๋ฉด์ ‘๊ด€:

  1. ์ผ์ผ 1000ํšŒ ๋นŒ๋“œ, ์ตœ๋Œ€ 100๊ฐœ ๋™์‹œ ๋นŒ๋“œ
  2. ์™„์ „ํ•œ ์ปจํ…Œ์ด๋„ˆ ๊ฒฉ๋ฆฌ ํ•„์š”, ๋ฏผ๊ฐํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ํฌํ•จ
  3. ์•„ํ‹ฐํŒฉํŠธ๋Š” 90์ผ ๋ณด๊ด€, ์ €์žฅ์†Œ๋‹น ์ตœ๋Œ€ 500GB
  4. Slack/์ด๋ฉ”์ผ ์•Œ๋ฆผ, ์ž๋™ ๋กค๋ฐฑ ๊ธฐ๋Šฅ ํ•„์š”

1. ํŒŒ์ดํ”„๋ผ์ธ ์ฝ”์–ด ์‹œ์Šคํ…œ

@Service
public class PipelineService {
    
    private final JobExecutor jobExecutor;
    private final ArtifactStorage artifactStorage;
    private final NotificationService notificationService;

    // 1. ํŒŒ์ดํ”„๋ผ์ธ ์ •์˜
    public class Pipeline {
        private final String pipelineId;
        private final List<Stage> stages;
        private final Map<String, String> environment;
        
        @Data
        public class Stage {
            private final String stageName;
            private final List<Job> jobs;
            private final StageType type;  // BUILD, TEST, DEPLOY
            private final RetryPolicy retryPolicy;
            private final List<String> dependencies;
        }

        public PipelineExecution execute() {
            // DAG ๊ธฐ๋ฐ˜ ์Šคํ…Œ์ด์ง€ ์‹คํ–‰
            return stages.stream()
                .filter(stage -> canExecuteStage(stage))
                .map(stage -> executeStage(stage))
                .collect(Collectors.toList());
        }
    }

    // 2. ์ž‘์—… ์‹คํ–‰ ์—”์ง„
    @Service
    public class JobExecutor {
        private final KubernetesClient kubernetesClient;
        private final SecretManager secretManager;
        
        public JobResult executeJob(Job job) {
            // ์ž‘์—… ํ™˜๊ฒฝ ์ค€๋น„
            Pod pod = preparePodSpec(job);
            
            // ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ์‹œํฌ๋ฆฟ ์ฃผ์ž…
            injectSecrets(pod, job.getSecrets());
            
            // ์ž‘์—… ์‹คํ–‰
            try {
                kubernetesClient.pods().create(pod);
                return watchJobCompletion(pod.getMetadata().getName());
            } catch (Exception e) {
                handleJobFailure(job, e);
                return JobResult.failure(e);
            }
        }

        private Pod preparePodSpec(Job job) {
            return new PodBuilder()
                .withNewMetadata()
                    .withName(job.getId())
                    .withLabels(job.getLabels())
                .endMetadata()
                .withNewSpec()
                    .withContainers(createJobContainer(job))
                    .withRestartPolicy("Never")
                    .withServiceAccount("ci-runner")
                .endSpec()
                .build();
        }
    }

    // 3. ์•„ํ‹ฐํŒฉํŠธ ๊ด€๋ฆฌ
    @Service
    public class ArtifactManager {
        private final S3Client s3Client;
        private final ArtifactMetadataRepository metadataRepo;
        
        public void storeArtifact(String pipelineId, 
                                 MultipartFile artifact) {
            String artifactPath = generateArtifactPath(pipelineId);
            
            // S3์— ์•„ํ‹ฐํŒฉํŠธ ์ €์žฅ
            s3Client.putObject(PutObjectRequest.builder()
                .bucket(ARTIFACT_BUCKET)
                .key(artifactPath)
                .build(), 
                RequestBody.fromInputStream(
                    artifact.getInputStream(), 
                    artifact.getSize()
                ));
                
            // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
            ArtifactMetadata metadata = ArtifactMetadata.builder()
                .pipelineId(pipelineId)
                .path(artifactPath)
                .size(artifact.getSize())
                .createdAt(Instant.now())
                .build();
                
            metadataRepo.save(metadata);
        }
        
        // ์•„ํ‹ฐํŒฉํŠธ ๋ณด๊ด€ ์ •์ฑ… ์ ์šฉ
        @Scheduled(cron = "0 0 0 * * *")  // ๋งค์ผ ์ž์ •
        public void applyRetentionPolicy() {
            LocalDate cutoffDate = 
                LocalDate.now().minusDays(90);
                
            List<ArtifactMetadata> expiredArtifacts = 
                metadataRepo.findByCreatedAtBefore(cutoffDate);
                
            expiredArtifacts.forEach(artifact -> {
                s3Client.deleteObject(DeleteObjectRequest.builder()
                    .bucket(ARTIFACT_BUCKET)
                    .key(artifact.getPath())
                    .build());
                    
                metadataRepo.delete(artifact);
            });
        }
    }
}

2. ํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ

@Service
public class PipelineExecutionService {

    // 1. ๋ณ‘๋ ฌ ์‹คํ–‰ ๊ด€๋ฆฌ
    public class ParallelExecutionManager {
        private final ExecutorService executorService;
        private final SemaphoreManager semaphoreManager;

        public ParallelExecutionManager() {
            this.executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors() * 2
            );
            this.semaphoreManager = new SemaphoreManager(100); // ์ตœ๋Œ€ 100๊ฐœ ๋™์‹œ ์‹คํ–‰
        }

        public CompletableFuture<StageResult> executeStageInParallel(Stage stage) {
            return CompletableFuture.supplyAsync(() -> {
                String resourceType = stage.getResourceRequirements().getType();
                try {
                    semaphoreManager.acquire(resourceType);
                    return executeStage(stage);
                } finally {
                    semaphoreManager.release(resourceType);
                }
            }, executorService);
        }
    }

    // 2. ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง
    @Service
    public class PipelineMonitor {
        private final MetricsRegistry metricsRegistry;
        private final AlertService alertService;

        public void monitorPipeline(String pipelineId) {
            Pipeline pipeline = getPipeline(pipelineId);
            
            // ๋นŒ๋“œ ๋งคํŠธ๋ฆญ์Šค ์ˆ˜์ง‘
            metricsRegistry.gauge(
                "pipeline.duration",
                pipeline.getDuration().toSeconds()
            );
            
            metricsRegistry.counter(
                "pipeline.stages.total",
                pipeline.getStages().size()
            );
            
            // ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
            if (pipeline.getDuration().toMinutes() > 30) {
                alertService.sendAlert(
                    AlertLevel.WARNING,
                    "Pipeline duration exceeds 30 minutes: " + pipelineId
                );
            }

            // ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ ๋ชจ๋‹ˆํ„ฐ๋ง
            monitorResourceUsage(pipeline);
        }

        private void monitorResourceUsage(Pipeline pipeline) {
            ResourceUsage usage = pipeline.getResourceUsage();
            
            if (usage.getCpuUsage() > 80) {
                alertService.sendAlert(
                    AlertLevel.CRITICAL,
                    "High CPU usage in pipeline: " + pipeline.getId()
                );
            }
        }
    }

    // 3. ๋กœ๊ทธ ์ง‘๊ณ„ ์‹œ์Šคํ…œ
    @Service
    public class LogAggregator {
        private final ElasticsearchClient esClient;
        private final KafkaTemplate<String, LogEvent> kafkaTemplate;

        @KafkaListener(topics = "pipeline-logs")
        public void handleLogs(LogEvent logEvent) {
            // ๋กœ๊ทธ ์ธ๋ฑ์‹ฑ
            IndexRequest<LogEvent> request = IndexRequest.of(r -> r
                .index("pipeline-logs-" + 
                    LocalDate.now().format(DateTimeFormatter.ISO_DATE))
                .document(logEvent)
            );

            esClient.index(request);

            // ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ์ŠคํŠธ๋ฆฌ๋ฐ
            if (logEvent.getLevel().equals(LogLevel.ERROR)) {
                notifyError(logEvent);
            }
        }

        public List<LogEvent> queryLogs(String pipelineId, 
                                      TimeRange range) {
            // ๋กœ๊ทธ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ
            SearchRequest request = SearchRequest.of(r -> r
                .index("pipeline-logs-*")
                .query(q -> q
                    .bool(b -> b
                        .must(m -> m
                            .term(t -> t
                                .field("pipelineId")
                                .value(pipelineId)))
                        .must(m -> m
                            .range(ra -> ra
                                .field("timestamp")
                                .gte(JsonData.of(range.getStart()))
                                .lte(JsonData.of(range.getEnd()))))))
            );

            return esClient.search(request, LogEvent.class)
                .hits()
                .hits()
                .stream()
                .map(hit -> hit.source())
                .collect(Collectors.toList());
        }
    }

    // 4. ์‹คํŒจ ๋ถ„์„ ๋ฐ ์ž๋™ ๋ณต๊ตฌ
    @Service
    public class FailureAnalyzer {
        
        public void analyzePipelineFailure(Pipeline pipeline) {
            FailureReport report = FailureReport.builder()
                .pipelineId(pipeline.getId())
                .failureStage(pipeline.getFailedStage())
                .logs(extractRelevantLogs(pipeline))
                .resourceUsage(pipeline.getResourceUsage())
                .build();

            // ์‹คํŒจ ํŒจํ„ด ๋ถ„์„
            FailurePattern pattern = detectFailurePattern(report);

            // ์ž๋™ ๋ณต๊ตฌ ์‹œ๋„
            if (pattern.isAutoRecoverable()) {
                attemptRecovery(pipeline, pattern);
            } else {
                // ์ˆ˜๋™ ๊ฐœ์ž… ํ•„์š”
                notifyTeam(report);
            }
        }

        private void attemptRecovery(Pipeline pipeline, 
                                   FailurePattern pattern) {
            switch (pattern.getType()) {
                case RESOURCE_EXHAUSTION:
                    scaleResources(pipeline);
                    break;
                case TRANSIENT_FAILURE:
                    retryPipeline(pipeline);
                    break;
                case DEPENDENCY_FAILURE:
                    updateDependencies(pipeline);
                    break;
            }
        }
    }
}

์ด๋Ÿฌํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์‹คํ–‰ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด:

  1. ๋ณ‘๋ ฌ ์‹คํ–‰ ๊ด€๋ฆฌ

    • ๋ฆฌ์†Œ์Šค ๊ธฐ๋ฐ˜ ์ œํ•œ
    • ์„ธ๋งˆํฌ์–ด๋ฅผ ํ†ตํ•œ ๋™์‹œ์„ฑ ์ œ์–ด
    • ํšจ์œจ์ ์ธ ์Šค์ผ€์ค„๋ง
  2. ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง

    • ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘
    • ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์ 
    • ์•Œ๋ฆผ ์‹œ์Šคํ…œ
  3. ๋กœ๊ทธ ๊ด€๋ฆฌ

    • ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ์ˆ˜์ง‘
    • ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋กœ๊ทธ ์ €์žฅ
    • ์—๋Ÿฌ ๊ฐ์ง€ ๋ฐ ์•Œ๋ฆผ
  4. ์‹คํŒจ ๋ถ„์„

    • ํŒจํ„ด ๊ธฐ๋ฐ˜ ๋ถ„์„
    • ์ž๋™ ๋ณต๊ตฌ ์‹œ๋„
    • ํŒ€ ์•Œ๋ฆผ

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

๋ฉด์ ‘๊ด€: ๋Œ€๊ทœ๋ชจ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์˜ CI/CD๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐ์ •ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

3. ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค CI/CD ์กฐ์ • ์‹œ์Šคํ…œ

@Service
public class MicroserviceCICoordinator {

    // 1. ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ๊ด€๋ฆฌ
    public class DependencyGraphManager {
        private final Map<String, ServiceNode> serviceGraph = new ConcurrentHashMap<>();

        @Data
        public class ServiceNode {
            private final String serviceId;
            private final Set<String> dependencies;
            private final Set<String> dependents;
            private ServiceVersion currentVersion;
            private DeploymentStatus status;
        }

        // ์˜์กด์„ฑ ๊ธฐ๋ฐ˜ ๋นŒ๋“œ ์ˆœ์„œ ๊ฒฐ์ •
        public List<String> determineBuildOrder() {
            // ์œ„์ƒ ์ •๋ ฌ ๊ตฌํ˜„
            Set<String> visited = new HashSet<>();
            List<String> buildOrder = new ArrayList<>();
            
            serviceGraph.keySet().forEach(serviceId -> {
                if (!visited.contains(serviceId)) {
                    topologicalSort(serviceId, visited, buildOrder);
                }
            });
            
            return buildOrder;
        }
        
        // ์˜ํ–ฅ๋„ ๋ถ„์„
        public Set<String> analyzeImpact(String changedService) {
            Set<String> impactedServices = new HashSet<>();
            Queue<String> queue = new LinkedList<>();
            queue.add(changedService);
            
            while (!queue.isEmpty()) {
                String service = queue.poll();
                ServiceNode node = serviceGraph.get(service);
                
                node.getDependents().forEach(dependent -> {
                    if (impactedServices.add(dependent)) {
                        queue.add(dependent);
                    }
                });
            }
            
            return impactedServices;
        }
    }

    // 2. ๋ฒ„์ „ ๊ด€๋ฆฌ ๋ฐ ํ˜ธํ™˜์„ฑ ์ฒดํฌ
    @Service
    public class VersionCompatibilityManager {
        private final ContractTestRunner contractTestRunner;

        public boolean checkCompatibility(ServiceVersion newVersion, 
                                        Set<ServiceVersion> dependencyVersions) {
            // API ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ ์‹คํ–‰
            ContractTestResult result = contractTestRunner
                .runTests(newVersion, dependencyVersions);

            if (!result.isCompatible()) {
                handleCompatibilityFailure(result);
                return false;
            }

            // ์„ฑ๋Šฅ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ
            PerformanceTestResult perfResult = 
                runPerformanceTests(newVersion);
                
            return perfResult.meetsThreshold();
        }

        private void handleCompatibilityFailure(ContractTestResult result) {
            // ์‹คํŒจํ•œ ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ ๋ถ„์„
            List<FailedContract> failedContracts = result.getFailedContracts();
            
            // ์˜ํ–ฅ๋ฐ›๋Š” ์„œ๋น„์Šค ํŒ€์— ์•Œ๋ฆผ
            notifyTeams(failedContracts);
            
            // ๋ณ€๊ฒฝ ๊ฐ€์ด๋“œ ์ƒ์„ฑ
            generateChangeGuide(failedContracts);
        }
    }

    // 3. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
    @Service
    public class IntegrationTestOrchestrator {
        private final KubernetesClient kubernetesClient;
        private final TestEnvironmentManager testEnvManager;

        public IntegrationTestResult runIntegrationTests(
            Set<MicroserviceDeployment> deployments) {
            
            // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ํ”„๋กœ๋น„์ €๋‹
            TestEnvironment env = testEnvManager
                .provisionEnvironment(deployments);

            try {
                // ์„œ๋น„์Šค ๋ฐฐํฌ
                deployServices(env, deployments);

                // ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰
                TestSuite integrationTests = 
                    TestSuiteBuilder.forDeployments(deployments)
                        .withEnvironment(env)
                        .build();

                return testRunner.runTests(integrationTests);

            } finally {
                // ํ™˜๊ฒฝ ์ •๋ฆฌ
                testEnvManager.cleanupEnvironment(env);
            }
        }

        private void deployServices(TestEnvironment env, 
                                  Set<MicroserviceDeployment> deployments) {
            // ๋ณ‘๋ ฌ ๋ฐฐํฌ ์‹คํ–‰
            CompletableFuture<?>[] deployFutures = deployments.stream()
                .map(deployment -> CompletableFuture.runAsync(() ->
                    deployService(env, deployment)))
                .toArray(CompletableFuture[]::new);

            CompletableFuture.allOf(deployFutures).join();
        }
    }

    // 4. ๋กค์•„์›ƒ ์กฐ์ •
    @Service
    public class RolloutCoordinator {
        private final DeploymentManager deploymentManager;
        private final HealthCheckService healthCheckService;

        public void coordinateRollout(
            Set<MicroserviceDeployment> deployments) {
            
            // ๋ฐฐํฌ ๊ทธ๋ฃน ์ƒ์„ฑ
            List<DeploymentGroup> deploymentGroups = 
                createDeploymentGroups(deployments);

            for (DeploymentGroup group : deploymentGroups) {
                // ๊ทธ๋ฃน๋ณ„ ๋กค์•„์›ƒ
                RolloutResult result = rolloutGroup(group);
                
                if (!result.isSuccessful()) {
                    // ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
                    handleRolloutFailure(group, result);
                    break;
                }
                
                // ์•ˆ์ •ํ™” ๋Œ€๊ธฐ
                waitForStabilization(group);
            }
        }

        private void waitForStabilization(DeploymentGroup group) {
            // ํ—ฌ์Šค์ฒดํฌ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋‹ˆํ„ฐ๋ง
            Duration stabilizationPeriod = Duration.ofMinutes(15);
            Instant deadline = Instant.now().plus(stabilizationPeriod);
            
            while (Instant.now().isBefore(deadline)) {
                if (!isGroupHealthy(group)) {
                    throw new StabilizationException("Group failed to stabilize");
                }
                Thread.sleep(Duration.ofSeconds(30).toMillis());
            }
        }

        private boolean isGroupHealthy(DeploymentGroup group) {
            return group.getDeployments().stream()
                .allMatch(deployment -> 
                    healthCheckService.isHealthy(deployment) &&
                    metricsService.isWithinThresholds(deployment));
        }
    }
}

์ด๋Ÿฌํ•œ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค CI/CD ์กฐ์ • ์‹œ์Šคํ…œ์„ ํ†ตํ•ด:

  1. ์˜์กด์„ฑ ๊ด€๋ฆฌ

    • ์„œ๋น„์Šค ๊ฐ„ ์˜์กด์„ฑ ์ถ”์ 
    • ๋นŒ๋“œ ์ˆœ์„œ ์ตœ์ ํ™”
    • ์˜ํ–ฅ๋„ ๋ถ„์„
  2. ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ

    • API ๊ณ„์•ฝ ํ…Œ์ŠคํŠธ
    • ์„ฑ๋Šฅ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ
    • ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ
  3. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    • ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ
    • ๋ณ‘๋ ฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰
    • ํ™˜๊ฒฝ ๊ด€๋ฆฌ ์ž๋™ํ™”
  4. ์กฐ์ •๋œ ๋กค์•„์›ƒ

    • ๊ทธ๋ฃน ๊ธฐ๋ฐ˜ ๋ฐฐํฌ
    • ํ—ฌ์Šค์ฒดํฌ ๋ชจ๋‹ˆํ„ฐ๋ง
    • ์ž๋™ ๋กค๋ฐฑ

์„ ๊ตฌํ˜„ํ•˜์—ฌ ๋Œ€๊ทœ๋ชจ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ์˜ CI/CD๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

  • ์„œ๋น„์Šค ๊ฐ„ ์˜์กด์„ฑ ๊ด€๋ฆฌ
  • ์ž๋™ํ™”๋œ ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ
  • ์•ˆ์ •์ ์ธ ๋กค์•„์›ƒ ์ „๋žต
  • ํšจ์œจ์ ์ธ ํ…Œ์ŠคํŠธ ์‹คํ–‰

์ž…๋‹ˆ๋‹ค.