C 언어에서 파이프를 이용한 프로세스 간 통신 방법과 실전 활용

프로세스 간 통신(IPC, Inter-Process Communication)은 여러 프로세스가 데이터를 공유하거나 서로 협력할 수 있도록 하는 필수적인 기술입니다. C 언어에서는 파이프(Pipe)를 이용해 간단하면서도 효율적인 IPC를 구현할 수 있습니다. 파이프는 한 프로세스에서 데이터를 작성하고, 다른 프로세스가 이를 읽어들일 수 있도록 하는 통로를 제공합니다. 본 기사에서는 C 언어를 사용해 파이프를 구현하는 방법과 이를 실제로 활용하는 다양한 사례를 살펴봅니다.

목차

프로세스 간 통신의 개념


프로세스 간 통신(IPC, Inter-Process Communication)은 운영 체제에서 실행 중인 여러 프로세스가 데이터를 주고받거나 협력 작업을 수행할 수 있게 하는 메커니즘입니다. 프로세스는 독립적인 메모리 공간을 가지기 때문에, 다른 프로세스와 데이터를 공유하거나 전달하려면 IPC를 사용해야 합니다.

IPC의 중요성


IPC는 다음과 같은 이유로 중요합니다:

  • 자원 공유: 파일, 메모리 등과 같은 시스템 자원을 효율적으로 공유할 수 있습니다.
  • 작업 협력: 병렬로 실행되는 프로세스가 상호작용하여 복잡한 작업을 수행할 수 있습니다.
  • 데이터 전달: 데이터를 다른 프로세스로 전달하여 워크플로를 연결합니다.

IPC에서 파이프의 역할


파이프(Pipe)는 IPC를 구현하는 기본적인 도구 중 하나로, 단방향 혹은 양방향으로 데이터를 전달하는 데 사용됩니다. 프로세스는 파이프를 통해 데이터를 작성하고 읽어들임으로써 통신을 수행합니다. 이는 특히 간단한 데이터 전송 시나리오에서 유용합니다.

IPC의 다양한 방식 중 파이프는 구현이 간단하면서도 강력한 기능을 제공해 많은 응용 프로그램에서 사용됩니다.

파이프의 원리와 기본 구조

파이프(Pipe)는 운영 체제에서 제공하는 데이터 통로로, 두 프로세스 간에 데이터를 전송하는 역할을 합니다. C 언어에서는 pipe() 시스템 호출을 사용하여 파이프를 생성할 수 있으며, 생성된 파이프는 읽기 끝과 쓰기 끝 두 가지로 구성됩니다.

파이프의 작동 원리


파이프는 다음과 같은 원리로 작동합니다:

  1. 쓰기 끝: 한 프로세스가 데이터를 작성(write)합니다.
  2. 읽기 끝: 다른 프로세스가 데이터를 읽어(read)들입니다.
  3. 순차 처리: 파이프는 FIFO(First In, First Out) 방식으로 데이터를 처리합니다.

파이프 생성의 기본 구조


파이프는 다음과 같은 방식으로 생성됩니다:

#include <unistd.h>
#include <stdio.h>

int main() {
    int fd[2]; // fd[0]: 읽기 끝, fd[1]: 쓰기 끝
    if (pipe(fd) == -1) {
        perror("파이프 생성 실패");
        return 1;
    }
    printf("파이프가 성공적으로 생성되었습니다.\n");
    return 0;
}

파이프의 주요 특징

  • 단방향 데이터 흐름: 기본 파이프는 한 방향으로만 데이터를 전달합니다.
  • 부모-자식 프로세스 간 사용: 동일한 프로세스 계층에서 데이터 전송이 가능합니다.
  • 익명 파이프: 이름이 없는 파이프는 관련된 프로세스 간의 임시 통신에 사용됩니다.

이처럼 파이프는 간단하면서도 효율적으로 데이터를 전송할 수 있는 구조를 제공하여 IPC의 기본 수단으로 널리 사용됩니다.

단방향 파이프 구현

단방향 파이프는 데이터를 한 방향으로만 전송할 수 있는 통신 방법입니다. 한 프로세스는 데이터를 작성하고, 다른 프로세스는 데이터를 읽어들이는 방식으로 작동합니다. C 언어에서 pipe()를 활용해 간단한 단방향 파이프를 구현할 수 있습니다.

단방향 파이프의 기본 예제


아래 코드는 부모 프로세스에서 데이터를 작성하고, 자식 프로세스가 해당 데이터를 읽어들이는 단방향 파이프 구현 예제입니다.

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd[2]; // 파이프 파일 디스크립터
    pid_t pid;
    char write_msg[] = "안녕하세요, 자식 프로세스!";
    char read_msg[100];

    // 파이프 생성
    if (pipe(fd) == -1) {
        perror("파이프 생성 실패");
        return 1;
    }

    pid = fork(); // 프로세스 생성
    if (pid < 0) {
        perror("프로세스 생성 실패");
        return 1;
    }

    if (pid > 0) { // 부모 프로세스
        close(fd[0]); // 읽기 끝 닫기
        write(fd[1], write_msg, strlen(write_msg) + 1); // 메시지 작성
        close(fd[1]); // 쓰기 끝 닫기
    } else { // 자식 프로세스
        close(fd[1]); // 쓰기 끝 닫기
        read(fd[0], read_msg, sizeof(read_msg)); // 메시지 읽기
        printf("자식 프로세스에서 읽은 메시지: %s\n", read_msg);
        close(fd[0]); // 읽기 끝 닫기
    }

    return 0;
}

코드 설명

  1. pipe(fd): 파이프를 생성하며, fd[0]은 읽기 끝, fd[1]은 쓰기 끝을 나타냅니다.
  2. fork(): 부모와 자식 프로세스를 생성합니다.
  3. 부모 프로세스: 파이프의 쓰기 끝(fd[1])을 통해 데이터를 작성합니다.
  4. 자식 프로세스: 파이프의 읽기 끝(fd[0])을 통해 데이터를 읽어들입니다.

단방향 파이프의 특징

  • 데이터를 한 방향으로만 전송할 수 있습니다.
  • 부모-자식 프로세스 간 통신에 주로 사용됩니다.
  • 작성한 데이터는 FIFO(First In, First Out) 방식으로 처리됩니다.

단방향 파이프는 간단한 구조와 효율성을 제공하며, 프로세스 간 데이터 전달의 첫걸음으로 사용됩니다.

양방향 파이프 구현

양방향 파이프는 데이터를 양쪽 방향으로 전송할 수 있는 통신 방법을 제공합니다. C 언어에서는 기본 파이프(pipe())가 단방향으로 설계되어 있어, 양방향 통신을 구현하려면 두 개의 파이프를 사용해야 합니다. 이를 통해 각 파이프가 하나의 데이터 흐름을 처리하도록 구성합니다.

양방향 파이프의 기본 예제


아래 코드는 부모와 자식 프로세스 간에 데이터를 양방향으로 교환하는 예제입니다.

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipe1[2], pipe2[2]; // 두 개의 파이프 생성
    pid_t pid;
    char parent_msg[] = "안녕하세요, 자식 프로세스!";
    char child_msg[] = "안녕하세요, 부모 프로세스!";
    char buffer[100];

    // 파이프 생성
    if (pipe(pipe1) == -1 || pipe(pipe2) == -1) {
        perror("파이프 생성 실패");
        return 1;
    }

    pid = fork(); // 프로세스 생성
    if (pid < 0) {
        perror("프로세스 생성 실패");
        return 1;
    }

    if (pid > 0) { // 부모 프로세스
        close(pipe1[0]); // 읽기 끝 닫기 (pipe1)
        close(pipe2[1]); // 쓰기 끝 닫기 (pipe2)

        write(pipe1[1], parent_msg, strlen(parent_msg) + 1); // 자식에게 메시지 보내기
        close(pipe1[1]); // 쓰기 끝 닫기

        read(pipe2[0], buffer, sizeof(buffer)); // 자식으로부터 메시지 받기
        printf("부모가 받은 메시지: %s\n", buffer);
        close(pipe2[0]); // 읽기 끝 닫기
    } else { // 자식 프로세스
        close(pipe1[1]); // 쓰기 끝 닫기 (pipe1)
        close(pipe2[0]); // 읽기 끝 닫기 (pipe2)

        read(pipe1[0], buffer, sizeof(buffer)); // 부모로부터 메시지 받기
        printf("자식이 받은 메시지: %s\n", buffer);
        close(pipe1[0]); // 읽기 끝 닫기

        write(pipe2[1], child_msg, strlen(child_msg) + 1); // 부모에게 메시지 보내기
        close(pipe2[1]); // 쓰기 끝 닫기
    }

    return 0;
}

코드 설명

  1. 파이프 생성: pipe1은 부모에서 자식으로 데이터를 전송하고, pipe2는 자식에서 부모로 데이터를 전송합니다.
  2. fork(): 프로세스를 두 개로 나눕니다.
  3. 부모 프로세스:
  • pipe1[1]로 데이터를 전송합니다.
  • pipe2[0]에서 데이터를 읽어들입니다.
  1. 자식 프로세스:
  • pipe1[0]에서 데이터를 읽습니다.
  • pipe2[1]로 데이터를 전송합니다.

양방향 파이프의 특징

  • 두 개의 파이프를 사용하여 데이터의 양방향 흐름을 처리합니다.
  • 부모-자식 간 효율적인 데이터 교환이 가능합니다.
  • 통신 방향을 명확히 구분하여 동기화 및 데이터 관리가 용이합니다.

이 방법은 프로세스 간 양방향 데이터 전송이 필요한 경우 매우 유용하게 활용됩니다.

익명 파이프와 명명된 파이프

파이프는 익명 파이프(Anonymous Pipe)와 명명된 파이프(Named Pipe, FIFO)로 나눌 수 있습니다. 두 유형은 사용 목적과 통신 방식에서 차이가 있으며, IPC 시나리오에 따라 적합한 방식을 선택해야 합니다.

익명 파이프


익명 파이프는 이름이 없는 일시적인 데이터 통로로, 관련된 프로세스 간(일반적으로 부모-자식 프로세스) 통신에 사용됩니다.

특징:

  • 이름이 없고, pipe() 시스템 호출로 생성됩니다.
  • 생성된 프로세스 계층 내부에서만 사용 가능하며, 다른 프로세스와의 통신에는 적합하지 않습니다.
  • 단방향 혹은 양방향 통신에 사용할 수 있습니다(양방향 통신 시 두 개의 파이프 필요).

예제:
앞서 다룬 단방향 및 양방향 파이프 예제가 익명 파이프를 사용한 사례입니다.

명명된 파이프 (FIFO)


명명된 파이프는 파일 시스템에 이름이 등록된 영속적인 데이터 통로로, 서로 독립적인 프로세스 간 통신에 사용됩니다.

특징:

  • 파일 시스템에 존재하며, 이름을 통해 여러 프로세스가 접근할 수 있습니다.
  • mkfifo() 시스템 호출로 생성됩니다.
  • 부모-자식 관계 없이 독립적인 프로세스 간 통신이 가능합니다.

명명된 파이프 생성 및 사용 예제:

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#define FIFO_NAME "mypipe"

int main() {
    char buffer[100];
    pid_t pid;

    // 명명된 파이프 생성
    if (mkfifo(FIFO_NAME, 0666) == -1) {
        perror("명명된 파이프 생성 실패");
        return 1;
    }

    pid = fork(); // 프로세스 생성
    if (pid < 0) {
        perror("프로세스 생성 실패");
        return 1;
    }

    if (pid > 0) { // 부모 프로세스
        int write_fd = open(FIFO_NAME, O_WRONLY); // 쓰기 모드로 파이프 열기
        write(write_fd, "안녕하세요, 자식 프로세스!", 30); // 데이터 전송
        close(write_fd);
    } else { // 자식 프로세스
        int read_fd = open(FIFO_NAME, O_RDONLY); // 읽기 모드로 파이프 열기
        read(read_fd, buffer, sizeof(buffer)); // 데이터 읽기
        printf("자식이 받은 메시지: %s\n", buffer);
        close(read_fd);
    }

    return 0;
}

익명 파이프와 명명된 파이프의 비교

특징익명 파이프명명된 파이프
이름없음파일 시스템에 이름 존재
사용 범위부모-자식 프로세스 간 사용독립적인 프로세스 간 사용
생성 방식pipe() 호출mkfifo() 호출
영속성프로세스 종료 시 제거파일 시스템에 지속적으로 유지

익명 파이프는 단순한 부모-자식 프로세스 간 통신에 적합하며, 명명된 파이프는 독립 프로세스 간 데이터 교환이 필요한 복잡한 시나리오에 유용합니다.

파이프와 멀티프로세싱

파이프는 멀티프로세싱 환경에서 프로세스 간 데이터를 교환하는 데 널리 사용됩니다. 특히, 부모 프로세스가 여러 자식 프로세스를 생성해 병렬 처리를 수행하고 결과를 수집하거나 분배할 때 효과적인 도구입니다.

멀티프로세싱에서 파이프의 역할


멀티프로세싱 환경에서 파이프는 다음과 같은 역할을 수행합니다:

  1. 작업 분배: 부모 프로세스가 데이터를 여러 자식 프로세스로 분산 전송합니다.
  2. 결과 수집: 자식 프로세스가 처리한 결과를 부모 프로세스에 다시 전달합니다.
  3. 프로세스 간 동기화: 파이프를 통해 데이터를 주고받음으로써 프로세스 간 실행 흐름을 제어합니다.

멀티프로세싱 파이프 예제


아래 코드는 부모 프로세스가 작업 데이터를 분배하고, 각 자식 프로세스가 결과를 반환하는 구조를 보여줍니다.

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

#define NUM_CHILD 3

int main() {
    int pipes[NUM_CHILD][2];
    pid_t pids[NUM_CHILD];
    int i;

    // 여러 파이프 생성
    for (i = 0; i < NUM_CHILD; i++) {
        if (pipe(pipes[i]) == -1) {
            perror("파이프 생성 실패");
            return 1;
        }
    }

    // 자식 프로세스 생성
    for (i = 0; i < NUM_CHILD; i++) {
        pids[i] = fork();
        if (pids[i] < 0) {
            perror("프로세스 생성 실패");
            return 1;
        }

        if (pids[i] == 0) { // 자식 프로세스
            close(pipes[i][1]); // 쓰기 끝 닫기
            int data;
            read(pipes[i][0], &data, sizeof(data)); // 데이터 읽기
            printf("자식 %d: 받은 데이터 %d, 처리 중...\n", i, data);
            data *= 2; // 데이터 처리
            close(pipes[i][0]); // 읽기 끝 닫기
            return data; // 처리된 데이터를 부모에 반환
        }
    }

    // 부모 프로세스
    for (i = 0; i < NUM_CHILD; i++) {
        close(pipes[i][0]); // 읽기 끝 닫기
        int task_data = i + 1; // 작업 데이터
        write(pipes[i][1], &task_data, sizeof(task_data)); // 데이터 쓰기
        close(pipes[i][1]); // 쓰기 끝 닫기
    }

    // 자식 프로세스 결과 수집
    for (i = 0; i < NUM_CHILD; i++) {
        int status;
        waitpid(pids[i], &status, 0); // 자식 프로세스 종료 대기
        if (WIFEXITED(status)) {
            printf("자식 %d: 처리 결과 %d\n", i, WEXITSTATUS(status));
        }
    }

    return 0;
}

코드 설명

  1. 파이프 생성: 각 자식 프로세스와 부모 간 통신을 위해 다중 파이프를 생성합니다.
  2. 데이터 송신: 부모 프로세스가 각 파이프의 쓰기 끝을 통해 작업 데이터를 자식에게 보냅니다.
  3. 데이터 처리: 자식 프로세스는 데이터를 읽고 처리한 후, 처리 결과를 반환합니다.
  4. 결과 수집: 부모 프로세스는 자식 프로세스의 반환 값을 수집합니다.

멀티프로세싱 파이프의 특징

  • 병렬 처리: 여러 자식 프로세스를 활용하여 작업을 병렬로 처리할 수 있습니다.
  • 개별 통신: 각 파이프가 독립적인 데이터 흐름을 보장합니다.
  • 효율성 향상: 파이프를 활용한 데이터 전송은 간단하고 효율적입니다.

이 구조는 병렬 계산, 데이터 처리, 분산 작업 등 다양한 응용 프로그램에 활용될 수 있습니다.

파이프 사용 시 발생 가능한 문제

파이프는 간단하고 효율적인 IPC 도구이지만, 사용 중 몇 가지 문제를 마주칠 수 있습니다. 이러한 문제를 이해하고 해결 방법을 알고 있으면 파이프 기반 통신의 안정성과 신뢰성을 높일 수 있습니다.

1. 데드락 (Deadlock)


문제:

  • 파이프의 읽기 끝과 쓰기 끝이 올바르게 닫히지 않으면 데드락이 발생할 수 있습니다.
  • 쓰기 끝이 닫히지 않은 상태에서 읽기를 기다리거나, 읽기 끝이 닫히지 않은 상태에서 쓰기를 시도할 때 무한 대기 상태가 발생합니다.

해결 방법:

  • 필요하지 않은 파이프 끝을 즉시 닫습니다.
  • 데이터를 전송한 후 쓰기 끝을 닫아 데드락을 방지합니다.

2. 파이프 오버플로 (Buffer Overflow)


문제:

  • 파이프는 유한한 크기의 버퍼를 가지고 있습니다. 쓰기 끝에서 버퍼가 가득 찰 때까지 데이터를 계속 작성하면 쓰기 작업이 블로킹됩니다.

해결 방법:

  • 데이터 쓰기 전에 버퍼가 비었는지 확인합니다.
  • 적절한 크기의 데이터 청크를 사용해 버퍼를 관리합니다.

3. EOF (End of File) 처리 문제


문제:

  • 읽기 끝에서 더 이상 읽을 데이터가 없으면 EOF가 발생합니다.
  • EOF를 처리하지 못하면 데이터 소실 또는 무한 대기 상태가 발생할 수 있습니다.

해결 방법:

  • 읽기 작업 후 반환값을 확인하고 EOF 조건을 적절히 처리합니다.

4. 경쟁 상태 (Race Condition)


문제:

  • 여러 프로세스가 동일한 파이프에 접근할 때, 데이터 손실이나 충돌이 발생할 수 있습니다.

해결 방법:

  • 파이프 사용 시 상호 배제를 구현해 경쟁 상태를 방지합니다.
  • 데이터 전송 순서를 명확히 정의합니다.

5. 데이터 손실


문제:

  • 파이프의 데이터를 제대로 읽지 못하거나, 잘못된 순서로 읽으면 데이터 손실이 발생합니다.

해결 방법:

  • 데이터 패키지 구조를 정의하고, 전송된 데이터의 순서와 완전성을 확인합니다.

6. 명명된 파이프의 접근 문제


문제:

  • 명명된 파이프(FIFO)에 잘못된 권한 설정이 있을 경우, 접근이 차단될 수 있습니다.

해결 방법:

  • mkfifo() 호출 시 적절한 권한(예: 0666)을 설정합니다.
  • 파일 시스템 내 파이프 경로가 올바른지 확인합니다.

7. 디버깅 어려움


문제:

  • 비동기적으로 작동하는 파이프의 데이터를 디버깅하는 것은 어려울 수 있습니다.

해결 방법:

  • 로그 파일 또는 디버깅 출력을 추가하여 파이프 데이터를 추적합니다.
  • 디버깅 도구(예: gdb)를 활용하여 실행 흐름을 점검합니다.

결론


파이프는 간단하면서도 강력한 IPC 메커니즘을 제공하지만, 올바르게 관리하지 않으면 문제를 야기할 수 있습니다. 위에서 언급한 문제와 해결책을 참고하면 파이프를 안정적이고 효율적으로 사용할 수 있습니다.

파이프 활용 응용 예시

파이프는 다양한 상황에서 유용하게 활용될 수 있습니다. 파일 처리, 네트워크 통신, 병렬 데이터 처리 등의 응용 사례를 통해 파이프의 실전 사용 방법을 알아봅니다.

1. 파일 처리에서 파이프 활용


시나리오:
프로세스 A가 파일에서 데이터를 읽고, 프로세스 B가 해당 데이터를 처리한 후 결과를 다른 파일에 저장합니다.

구현 예제:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd[2];
    char buffer[100];
    pipe(fd); // 파이프 생성

    pid_t pid = fork();
    if (pid == 0) { // 자식 프로세스
        close(fd[1]); // 쓰기 끝 닫기
        read(fd[0], buffer, sizeof(buffer)); // 부모로부터 데이터 읽기
        close(fd[0]); // 읽기 끝 닫기
        printf("자식 프로세스에서 처리: %s\n", buffer);
    } else { // 부모 프로세스
        close(fd[0]); // 읽기 끝 닫기
        int file = open("input.txt", O_RDONLY); // 파일 읽기
        read(file, buffer, sizeof(buffer)); // 파일에서 데이터 읽기
        write(fd[1], buffer, sizeof(buffer)); // 데이터를 파이프에 쓰기
        close(fd[1]); // 쓰기 끝 닫기
        close(file);  // 파일 닫기
    }
    return 0;
}


이 코드는 input.txt의 내용을 부모 프로세스에서 읽고, 자식 프로세스에서 처리하는 데 사용됩니다.

2. 네트워크 통신과 파이프


시나리오:
클라이언트가 서버에 요청을 보내고, 서버가 응답을 파이프를 통해 전달합니다.

구현 개념:

  • 클라이언트-서버 구조에서, 파이프를 사용해 메시지를 전달하고 결과를 수신합니다.
  • 실제 네트워크 소켓 대신, 테스트 환경에서 파이프를 활용해 통신을 시뮬레이션합니다.

3. 병렬 데이터 처리


시나리오:
대량의 데이터가 여러 프로세스를 통해 병렬로 처리된 후, 결과가 부모 프로세스로 수집됩니다.

구현 예제:

#include <stdio.h>
#include <unistd.h>

#define NUM_CHILD 3

int main() {
    int pipes[NUM_CHILD][2];
    pid_t pids[NUM_CHILD];
    char data[NUM_CHILD][20] = {"데이터1", "데이터2", "데이터3"};
    char buffer[100];

    // 파이프와 자식 프로세스 생성
    for (int i = 0; i < NUM_CHILD; i++) {
        pipe(pipes[i]);
        pids[i] = fork();
        if (pids[i] == 0) { // 자식 프로세스
            close(pipes[i][1]); // 쓰기 끝 닫기
            read(pipes[i][0], buffer, sizeof(buffer)); // 부모로부터 데이터 읽기
            close(pipes[i][0]); // 읽기 끝 닫기
            printf("자식 %d: 받은 데이터 %s, 처리 완료\n", i, buffer);
            return 0;
        }
    }

    // 부모 프로세스: 데이터 전송
    for (int i = 0; i < NUM_CHILD; i++) {
        close(pipes[i][0]); // 읽기 끝 닫기
        write(pipes[i][1], data[i], sizeof(data[i])); // 데이터 전송
        close(pipes[i][1]); // 쓰기 끝 닫기
    }

    return 0;
}

4. 명명된 파이프를 활용한 로그 시스템


시나리오:
애플리케이션 프로세스가 로그 데이터를 작성하면, 로그 관리 프로세스가 명명된 파이프를 통해 데이터를 수집하고 저장합니다.

구현 개념:

  • mkfifo()를 사용해 명명된 파이프를 생성합니다.
  • 애플리케이션 프로세스는 로그 메시지를 파이프에 작성합니다.
  • 로그 관리 프로세스는 파이프에서 메시지를 읽어 파일에 저장하거나 콘솔에 출력합니다.

결론


파이프는 파일 처리, 병렬 데이터 처리, 네트워크 통신 등 다양한 응용 시나리오에서 사용할 수 있는 강력한 도구입니다. 이러한 사례들은 실제 프로젝트에서 파이프를 어떻게 활용할 수 있는지에 대한 유용한 가이드를 제공합니다.

요약


C 언어에서 파이프는 프로세스 간 데이터를 교환하기 위한 간단하고 효율적인 도구로, 단방향 및 양방향 통신, 익명 및 명명된 파이프를 통해 다양한 IPC 시나리오를 지원합니다. 본 기사에서는 파이프의 원리와 구조, 구현 방법, 멀티프로세싱 활용, 문제 해결 방안, 그리고 실전 응용 사례를 다뤘습니다. 파이프를 적절히 활용하면 효율적인 데이터 교환과 병렬 처리가 가능해져 소프트웨어의 성능과 확장성이 크게 향상됩니다.

목차