C언어에서 스레드 우선순위와 스케줄링 관리 방법

스레드 우선순위와 스케줄링은 멀티스레드 프로그래밍에서 성능과 효율성을 최적화하는 핵심 요소입니다. C언어에서 제공하는 POSIX 스레드(pthread) 라이브러리는 다양한 스레드 관리 기능과 스케줄링 정책을 지원하여 멀티스레드 환경에서의 정교한 제어를 가능하게 합니다. 본 기사에서는 스레드 우선순위와 스케줄링의 개념, 구현 방법, 그리고 문제 해결 사례를 통해 멀티스레드 프로그래밍을 보다 효율적으로 활용하는 방법을 알아봅니다.

목차

스레드와 스케줄링의 기본 개념


스레드는 프로세스 내에서 실행되는 작은 실행 단위로, 프로세스의 자원을 공유하며 독립적으로 실행됩니다. 이를 통해 멀티태스킹과 병렬 처리가 가능해져 성능이 향상됩니다.

스레드의 구성 요소


스레드는 코드, 데이터, 스택 등의 프로세스 자원을 공유하지만, 각 스레드가 자체 스택과 레지스터를 유지하여 독립적으로 실행됩니다.

스케줄링의 역할


스케줄링은 CPU와 같은 시스템 리소스를 여러 스레드 간에 어떻게 배분할지 결정하는 프로세스입니다. 운영체제는 스케줄러를 통해 각 스레드의 실행 순서와 실행 시간을 관리하며, 이를 기반으로 효율적인 리소스 분배를 보장합니다.

스케줄링의 핵심 요소

  1. 우선순위: 스레드가 실행될 순서를 결정하는 기준으로, 높은 우선순위를 가진 스레드가 먼저 실행됩니다.
  2. 스케줄링 정책: 운영체제가 제공하는 실행 정책으로, 선입선출(FIFO), 라운드로빈(RR) 등 다양한 유형이 존재합니다.

스레드와 스케줄링은 멀티스레드 프로그램의 성능과 안정성을 크게 좌우하는 요소이며, 이를 적절히 이해하고 제어하는 것이 중요합니다.

C언어에서의 스레드 생성과 관리

C언어는 POSIX 스레드(pthread) 라이브러리를 통해 멀티스레드 프로그래밍을 지원하며, 스레드 생성과 관리를 위한 다양한 함수와 매커니즘을 제공합니다.

스레드 생성


스레드는 pthread_create 함수를 사용하여 생성됩니다. 이 함수는 다음과 같은 형식으로 호출됩니다:

#include <pthread.h>

pthread_t thread_id;
pthread_create(&thread_id, NULL, thread_function, (void *)argument);
  • 첫 번째 매개변수: 생성된 스레드의 식별자를 저장할 변수입니다.
  • 두 번째 매개변수: 스레드의 속성을 설정하는 pthread_attr_t 구조체로, 기본 설정을 사용하려면 NULL로 설정합니다.
  • 세 번째 매개변수: 스레드에서 실행할 함수의 포인터입니다.
  • 네 번째 매개변수: 실행 함수에 전달할 인수를 가리킵니다.

스레드 종료


스레드는 pthread_exit를 호출하거나 실행 함수의 반환으로 종료됩니다. 다른 스레드가 특정 스레드의 종료를 기다리려면 pthread_join 함수를 사용합니다.

pthread_join(thread_id, NULL);
  • 해당 스레드가 종료될 때까지 호출한 스레드는 대기합니다.
  • 두 번째 매개변수는 스레드의 반환값을 받을 포인터입니다.

스레드 속성 설정


pthread_attr_t를 사용하면 스레드 속성을 사용자 정의할 수 있습니다. 예를 들어, 스택 크기, 우선순위, 디태치 여부 등을 설정할 수 있습니다.

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

스레드 관리의 중요성


적절한 스레드 생성과 종료 관리는 메모리 누수와 리소스 낭비를 방지합니다. 또한, 명확한 스레드 동기화와 속성 설정을 통해 안정적이고 효율적인 프로그램 동작을 보장할 수 있습니다.

C언어에서의 스레드 관리는 멀티스레드 프로그래밍의 핵심이므로, 스레드의 생성, 속성 설정, 종료 과정을 정확히 이해하고 구현해야 합니다.

우선순위 조정의 필요성

멀티스레드 환경에서 스레드 간의 우선순위는 시스템 자원 배분에 중요한 영향을 미칩니다. 우선순위를 적절히 설정하지 않으면 성능 저하, 스레드 간 충돌, 또는 응답 속도의 문제가 발생할 수 있습니다.

우선순위 설정이 필요한 이유

  1. 중요 작업의 선행 실행
    중요한 작업을 수행하는 스레드가 다른 스레드에 의해 지연되지 않도록 보장합니다. 예를 들어, 실시간 데이터 처리 시스템에서는 데이터 입력을 담당하는 스레드의 우선순위가 높아야 합니다.
  2. 리소스 경합 해결
    여러 스레드가 CPU, 메모리, I/O 등 동일한 자원을 요구하는 경우, 우선순위를 통해 리소스의 효율적 분배를 관리할 수 있습니다.
  3. 응답성 향상
    사용자 인터페이스를 처리하는 스레드는 높은 우선순위를 부여받아, 사용자의 입력에 즉각적으로 응답할 수 있어야 합니다.

실제 사용 사례

  • 멀티미디어 애플리케이션
    동영상 재생 애플리케이션에서, 비디오 디코딩 스레드가 음성 처리 스레드보다 높은 우선순위를 가져야 끊김 없는 재생이 가능합니다.
  • 실시간 제어 시스템
    제조 공장의 제어 시스템에서는 센서 데이터를 수집하는 스레드가 제어 명령을 실행하는 스레드보다 우선적으로 실행되어야 합니다.

잘못된 우선순위 설정의 부작용

  1. 우선순위 역전
    낮은 우선순위의 스레드가 높은 우선순위 스레드의 실행을 가로막는 상황입니다. 이는 시스템 전체의 성능 저하로 이어질 수 있습니다.
  2. 스레드 굶주림
    낮은 우선순위의 스레드가 실행 기회를 얻지 못해 작업이 무한히 지연될 수 있습니다.

적절한 우선순위 조정의 효과


우선순위를 올바르게 설정하면 시스템 성능이 향상되고, 모든 스레드가 균형 잡힌 실행 환경에서 동작할 수 있습니다. 따라서 프로그램 설계 단계에서 작업의 중요도를 분석하고, 이에 따라 스레드 우선순위를 설정하는 것이 필수적입니다.

POSIX 스레드에서의 우선순위 설정

POSIX 스레드(pthread) 라이브러리는 스레드의 우선순위를 설정할 수 있는 API를 제공하며, 이를 통해 스케줄링 정책과 결합하여 스레드 실행 순서를 제어할 수 있습니다.

우선순위 설정을 위한 단계

  1. 스레드 속성 초기화
    우선순위를 설정하려면 먼저 pthread_attr_t 구조체를 초기화해야 합니다.
   pthread_attr_t attr;
   pthread_attr_init(&attr);
  1. 스케줄링 정책 지정
    스레드의 스케줄링 정책을 설정합니다. POSIX에서는 다음과 같은 주요 스케줄링 정책을 제공합니다:
  • SCHED_FIFO: 선입선출 방식으로 실행합니다.
  • SCHED_RR: 라운드로빈 방식으로 실행합니다.
  • SCHED_OTHER: 기본 정책(우선순위가 없는 방식)입니다.
   pthread_attr_setschedpolicy(&attr, SCHED_RR);
  1. 우선순위 설정
    sched_param 구조체를 사용해 우선순위를 설정합니다. 우선순위 값은 시스템에 따라 정의되며, sched_get_priority_maxsched_get_priority_min을 사용해 확인할 수 있습니다.
   struct sched_param param;
   param.sched_priority = sched_get_priority_max(SCHED_RR);
   pthread_attr_setschedparam(&attr, &param);
  1. 스레드 생성
    설정된 속성을 사용해 스레드를 생성합니다.
   pthread_t thread_id;
   pthread_create(&thread_id, &attr, thread_function, NULL);

예제 코드

아래는 높은 우선순위를 가진 스레드를 생성하는 코드 예제입니다:

#include <pthread.h>
#include <sched.h>
#include <stdio.h>

void* thread_function(void* arg) {
    printf("Thread running with high priority\n");
    return NULL;
}

int main() {
    pthread_t thread_id;
    pthread_attr_t attr;
    struct sched_param param;

    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_RR);

    param.sched_priority = sched_get_priority_max(SCHED_RR);
    pthread_attr_setschedparam(&attr, &param);

    pthread_create(&thread_id, &attr, thread_function, NULL);
    pthread_join(thread_id, NULL);

    pthread_attr_destroy(&attr);
    return 0;
}

주의사항

  • 시스템에 따라 특정 우선순위 및 스케줄링 정책이 제한될 수 있습니다.
  • 루트 권한이 없으면 일부 스케줄링 정책과 우선순위 설정이 제한될 수 있습니다.
  • 우선순위 설정 시, 우선순위 역전 문제를 방지하기 위해 동기화 메커니즘을 사용해야 합니다.

POSIX 스레드의 우선순위 설정은 멀티스레드 프로그램의 성능 최적화와 안정적인 실행을 위해 매우 유용한 도구입니다. 이를 활용하면 각 스레드의 역할에 따라 CPU 리소스를 효과적으로 분배할 수 있습니다.

스케줄링 정책의 유형

스케줄링 정책은 운영체제가 여러 스레드 간에 CPU 리소스를 분배하는 방식을 정의합니다. POSIX 스레드 라이브러리는 다양한 스케줄링 정책을 제공하며, 각 정책은 특정 응용 프로그램 요구사항에 적합합니다.

주요 스케줄링 정책

SCHED_FIFO (선입선출)

  • 특징: 먼저 준비 상태가 된 스레드가 실행되며, 실행 중인 스레드는 스스로 CPU를 포기하거나 종료하기 전까지 실행됩니다.
  • 적합한 환경: 실시간 시스템이나 긴급한 작업 처리에 적합합니다.
  • 장점: 예측 가능한 실행 순서와 안정적인 실행 시간.
  • 단점: 우선순위가 낮은 스레드가 굶주릴 위험이 있습니다.

SCHED_RR (라운드로빈)

  • 특징: 선입선출 정책과 유사하지만, 시간 할당량(타임슬라이스)이 도입되어 스레드가 CPU를 일정 시간 동안만 사용할 수 있습니다.
  • 적합한 환경: 다수의 동등한 작업을 균등하게 처리해야 하는 경우.
  • 장점: CPU 리소스가 고르게 분배됩니다.
  • 단점: 시간 할당량이 적절히 설정되지 않으면 성능이 저하될 수 있습니다.

SCHED_OTHER (기본 정책)

  • 특징: 시스템의 기본 스케줄링 정책으로, 우선순위 없이 스레드를 처리합니다.
  • 적합한 환경: 실시간 성능이 요구되지 않는 일반 애플리케이션.
  • 장점: 간단하고 안정적입니다.
  • 단점: 실시간 작업에는 부적합합니다.

스케줄링 정책 선택 기준

  1. 응용 프로그램의 요구사항
  • 실시간 처리 여부.
  • 작업의 중요도와 긴급성.
  1. 리소스 경합
  • CPU 및 메모리와 같은 리소스를 효율적으로 활용해야 하는 상황.
  1. 시스템 제약
  • 사용 중인 시스템과 운영체제가 지원하는 스케줄링 정책 및 우선순위 범위.

스케줄링 정책 설정 방법


POSIX 스레드에서 스케줄링 정책을 설정하려면 pthread_attr_setschedpolicy 함수를 사용합니다.

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

스케줄링 정책 비교

정책특징장점단점적합한 환경
SCHED_FIFO선입선출안정적이고 예측 가능함우선순위 낮은 스레드 굶주림 가능실시간 처리, 긴급 작업
SCHED_RR라운드로빈균등한 CPU 분배타임슬라이스 설정이 중요다수의 동등 작업
SCHED_OTHER기본 정책간단하고 안정적실시간 처리에 부적합일반 애플리케이션

스케줄링 정책의 적절한 선택은 멀티스레드 프로그램의 성능과 안정성을 결정짓는 중요한 요소입니다. 각 정책의 특성을 이해하고 응용 프로그램의 요구사항에 맞는 정책을 선택하는 것이 필수적입니다.

우선순위와 스케줄링 충돌 문제 해결

멀티스레드 환경에서 우선순위와 스케줄링 정책은 시스템 자원을 효율적으로 관리하는 데 필수적이지만, 잘못된 설정으로 인해 충돌 문제가 발생할 수 있습니다. 이러한 문제를 이해하고 해결하는 방법을 숙지하는 것이 중요합니다.

우선순위 역전(Priority Inversion)

  • 문제 설명: 낮은 우선순위의 스레드가 높은 우선순위의 스레드보다 먼저 리소스를 점유하게 되는 상황입니다.
  • 예시: 높은 우선순위 스레드가 낮은 우선순위 스레드가 점유 중인 리소스를 기다리는 동안, 중간 우선순위의 스레드가 계속 실행되어 높은 우선순위 스레드가 실행되지 못합니다.

해결 방법

  1. 우선순위 상속 프로토콜
    리소스를 점유한 낮은 우선순위 스레드의 우선순위를 일시적으로 높은 우선순위 스레드와 동일하게 올립니다.
   pthread_mutexattr_t mutex_attr;
   pthread_mutexattr_init(&mutex_attr);
   pthread_mutexattr_setprotocol(&mutex_attr, PTHREAD_PRIO_INHERIT);
  1. 우선순위 천장 프로토콜
    공유 리소스에 접근하는 모든 스레드가 리소스의 “우선순위 천장” 이상의 우선순위를 가지도록 강제합니다.
   pthread_mutexattr_setprotocol(&mutex_attr, PTHREAD_PRIO_PROTECT);

스레드 굶주림(Thread Starvation)

  • 문제 설명: 낮은 우선순위의 스레드가 실행되지 못해 작업이 무기한 지연되는 현상입니다.
  • 예시: 높은 우선순위의 스레드가 지속적으로 실행되어 낮은 우선순위의 스레드가 실행 기회를 얻지 못합니다.

해결 방법

  1. 스케줄링 정책 조정
    SCHED_RR와 같은 공정한 스케줄링 정책을 사용하여 모든 스레드가 실행 시간을 보장받도록 합니다.
  2. 시간 분배 전략
    시간 할당량(타임슬라이스)을 적절히 설정해 스레드 간 실행 기회를 균등하게 나눕니다.

리소스 경합 문제

  • 문제 설명: 여러 스레드가 동일한 리소스에 동시에 접근하려고 할 때 발생합니다.
  • 예시: 두 스레드가 동일한 파일에 쓰기 작업을 시도하여 데이터가 손상됩니다.

해결 방법

  1. 뮤텍스(Mutex)와 세마포어(Semaphore) 사용
  • 뮤텍스: 단일 스레드가 리소스를 점유하도록 보장.
  • 세마포어: 리소스 사용량을 제어.
   pthread_mutex_t mutex;
   pthread_mutex_init(&mutex, NULL);
   pthread_mutex_lock(&mutex);
   // Critical section
   pthread_mutex_unlock(&mutex);
  1. 리소스 잠금 시간 최소화
    필요한 작업만 잠금 상태에서 수행하고, 빠르게 잠금을 해제하여 다른 스레드가 리소스를 사용할 수 있도록 합니다.

문제 해결을 위한 팁

  • 스레드 설계 시 우선순위와 스케줄링 정책의 상호작용을 충분히 고려합니다.
  • 실시간 시스템의 경우 우선순위 역전과 스레드 굶주림을 방지하는 메커니즘을 적극적으로 도입합니다.
  • 디버깅 도구를 활용해 스레드 동작을 시각적으로 확인하고 문제를 진단합니다.

우선순위와 스케줄링 충돌 문제를 해결하면 멀티스레드 프로그램의 성능과 안정성을 대폭 향상시킬 수 있습니다. 이를 위해 적절한 설계와 메커니즘 도입이 필수적입니다.

스레드 디버깅 및 성능 튜닝

멀티스레드 환경에서는 디버깅과 성능 최적화가 어려울 수 있습니다. 스레드 관련 문제를 효과적으로 진단하고 성능을 향상시키기 위해 특정 도구와 기법을 활용할 필요가 있습니다.

스레드 디버깅

1. 디버깅 도구 사용


다양한 디버깅 도구는 스레드의 동작을 시각화하고 문제를 진단하는 데 도움을 줍니다.

  • GDB (GNU Debugger)
    스레드별 상태를 확인하고 실행 흐름을 분석할 수 있습니다.
  gdb ./program
  (gdb) thread apply all bt  # 모든 스레드의 백트레이스를 출력
  • Valgrind
    메모리 누수 및 스레드 동기화 문제를 확인할 수 있습니다.
  valgrind --tool=helgrind ./program
  • Visualizer Tools
    Intel VTune Profiler, Perf 등의 도구를 사용해 스레드의 성능 병목을 분석합니다.

2. 로그를 활용한 문제 분석


스레드의 상태를 기록하는 로그를 사용하면 병렬 실행 중 발생하는 문제를 추적할 수 있습니다.

  • 로그에 스레드 ID, 시간, 실행 상태 등을 포함하여 디버깅을 용이하게 합니다.
  #include <pthread.h>
  #include <stdio.h>

  void log_thread_status(const char* status) {
      printf("[Thread %ld]: %s\n", pthread_self(), status);
  }

3. 교착 상태(Deadlock) 진단


교착 상태를 진단하려면 스레드가 공유 리소스에 접근하는 순서와 잠금 해제 상태를 분석합니다.

  • 탐지 방법: 모든 스레드의 상태를 기록하여 교착 상태의 원인을 파악.
  • 해결 방법: 리소스 잠금을 항상 같은 순서로 처리하고 타임아웃 메커니즘을 도입합니다.

스레드 성능 튜닝

1. 우선순위 조정


작업의 중요도에 따라 스레드 우선순위를 조정하여 성능 병목을 해결합니다.

  • pthread_attr_setschedparam으로 스케줄링 정책과 우선순위를 최적화합니다.

2. 작업 분할 및 부하 분산


스레드 간 작업을 고르게 분할하여 CPU 활용도를 극대화합니다.

  • 작업 분할 시 데이터 의존성을 최소화하여 스레드 간 충돌을 줄입니다.

3. 동기화 비용 최소화


동기화 메커니즘은 성능에 영향을 줄 수 있으므로 필요 최소한으로 사용합니다.

  • 뮤텍스 대신 락프리(lock-free) 데이터 구조를 사용하는 것을 고려합니다.
  • 비차단(non-blocking) 알고리즘으로 전환하여 성능 향상 가능.

4. 스케줄링 정책 최적화


스레드의 작업 특성에 맞는 스케줄링 정책을 선택합니다.

  • 실시간 처리가 필요한 경우 SCHED_FIFOSCHED_RR 사용.
  • 일반적인 작업은 SCHED_OTHER로 처리.

5. 성능 측정 및 분석


프로파일링 도구를 활용하여 병목 구간을 식별하고 개선합니다.

  • Perf: 스레드 실행 시간 및 컨텍스트 전환 분석.
  perf record ./program
  perf report

최적화 팁

  • 작업 특성에 따라 적절한 스레드 수를 설정합니다.
  • CPU 집약적 작업: CPU 코어 수와 동일하게 설정.
  • I/O 집약적 작업: CPU 코어 수보다 많은 스레드 생성.
  • 캐시 효율성을 높이기 위해 데이터의 지역성을 유지합니다.
  • 코드의 병렬성을 높이고 불필요한 연산을 줄입니다.

스레드 디버깅과 성능 튜닝은 멀티스레드 프로그램의 안정성과 효율성을 높이는 데 필수적인 과정입니다. 디버깅 도구와 성능 최적화 기법을 적절히 활용하면 복잡한 멀티스레드 프로그램도 안정적으로 실행할 수 있습니다.

실무 예제: 멀티스레드 파일 처리

멀티스레드 환경에서 스레드 우선순위와 스케줄링을 활용하여 대량의 파일을 병렬로 처리하는 프로그램을 구현해 보겠습니다. 이 예제는 스레드 간 작업을 효율적으로 분배하고 CPU 활용도를 극대화하는 방법을 보여줍니다.

프로그램 개요

  • 목적: 여러 파일의 데이터를 읽고 처리한 결과를 별도의 출력 파일에 저장.
  • 기능: 스레드 우선순위와 스케줄링 정책을 사용하여 파일 처리 속도 최적화.
  • 구조:
  1. 각 스레드가 파일 하나를 처리.
  2. 중요한 파일(예: 크리티컬 데이터를 포함한 파일)은 높은 우선순위를 가진 스레드가 처리.
  3. FIFO 스케줄링 정책을 적용하여 처리 순서를 보장.

구현 코드

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>

#define NUM_FILES 5
const char* input_files[NUM_FILES] = {
    "file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"
};
const char* output_files[NUM_FILES] = {
    "output1.txt", "output2.txt", "output3.txt", "output4.txt", "output5.txt"
};

void* process_file(void* arg) {
    int index = *(int*)arg;
    free(arg);

    printf("Thread %ld processing %s\n", pthread_self(), input_files[index]);

    // Simulate file processing
    sleep(1); // Replace with actual file processing logic

    FILE* infile = fopen(input_files[index], "r");
    FILE* outfile = fopen(output_files[index], "w");
    if (infile && outfile) {
        char buffer[1024];
        while (fgets(buffer, sizeof(buffer), infile)) {
            fprintf(outfile, "Processed: %s", buffer);
        }
        fclose(infile);
        fclose(outfile);
    }

    printf("Thread %ld finished %s\n", pthread_self(), input_files[index]);
    return NULL;
}

int main() {
    pthread_t threads[NUM_FILES];
    pthread_attr_t attr;
    struct sched_param param;

    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

    for (int i = 0; i < NUM_FILES; i++) {
        int* arg = malloc(sizeof(int));
        *arg = i;

        // Set priority: higher for critical files
        param.sched_priority = (i == 0) ? sched_get_priority_max(SCHED_FIFO) : sched_get_priority_min(SCHED_FIFO);
        pthread_attr_setschedparam(&attr, &param);

        if (pthread_create(&threads[i], &attr, process_file, arg) != 0) {
            perror("Failed to create thread");
        }
    }

    for (int i = 0; i < NUM_FILES; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_attr_destroy(&attr);
    printf("All files processed.\n");
    return 0;
}

코드 설명

  1. 스레드 생성 및 속성 설정
  • pthread_attr_t를 사용해 FIFO 스케줄링 정책과 우선순위를 설정합니다.
  • 중요한 파일(file1.txt)은 높은 우선순위를 부여합니다.
  1. 파일 처리 함수
  • 각 스레드는 하나의 파일을 읽고 처리한 결과를 출력 파일에 저장합니다.
  1. 스레드 동기화
  • pthread_join으로 모든 스레드가 완료될 때까지 대기합니다.

실행 결과

Thread 139789101090560 processing file1.txt
Thread 139789092697856 processing file2.txt
...
Thread 139789101090560 finished file1.txt
...
All files processed.

효율성을 높이기 위한 팁

  • 파일의 크기에 따라 작업을 나누어 각 스레드의 부하를 균등하게 분배.
  • I/O 병목을 줄이기 위해 비차단 I/O 사용.
  • 우선순위와 스케줄링 정책을 실시간으로 동적으로 변경 가능하도록 설계.

이 예제는 멀티스레드 프로그램에서 우선순위와 스케줄링을 활용하여 작업 효율성을 극대화하는 방법을 잘 보여줍니다. 이를 응용하면 다양한 병렬 작업에 적용할 수 있습니다.

요약


C언어에서 스레드 우선순위와 스케줄링은 멀티스레드 환경에서 성능과 효율성을 최적화하는 데 중요한 역할을 합니다. 본 기사에서는 스레드 생성과 관리, 우선순위 설정, 스케줄링 정책의 선택, 충돌 문제 해결, 디버깅과 성능 튜닝 방법, 그리고 실무 예제를 통해 이를 효과적으로 구현하는 방법을 소개했습니다. 적절한 스레드 설계와 관리로 시스템의 안정성과 성능을 극대화할 수 있습니다.

목차