비동기 작업에서는 예외 상황이 발생할 가능성이 높고, 이를 적절하게 처리하지 않으면 프로그램의 안정성을 크게 저하시킬 수 있다. 특히, 리소스 관리 측면에서 예외가 발생할 경우 메모리 누수, 파일 핸들 미반환 등의 심각한 문제로 이어질 수 있다. 이러한 상황을 방지하기 위해 다양한 리소스 관리 기법이 사용되며, 이를 엄밀하게 다루는 것이 중요하다.

RAII(Resource Acquisition Is Initialization) 기법

C++에서 자주 사용하는 RAII 기법은 리소스 관리를 자동화하여 예외가 발생해도 리소스가 적절히 해제되도록 보장한다. 객체가 생성될 때 리소스를 획득하고, 소멸될 때 리소스를 자동으로 해제하는 방식이다. 비동기 작업에서 예외가 발생할 때도 이 원칙이 적용되며, 이를 통해 누수를 방지할 수 있다.

예를 들어, 비동기 작업 중 파일을 열고 데이터를 쓰는 작업을 한다고 가정할 때, 파일 핸들은 파일 객체의 소멸자에서 자동으로 닫히도록 설계된다. 이 과정에서 예외가 발생하더라도 파일은 자동으로 닫힌다.

void asyncOperation() {
    std::ofstream file("output.txt");
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file");
    }
    // 파일에 데이터를 비동기로 씀
    file << "Async Data";
}  // 파일 객체가 스코프를 벗어날 때 파일이 자동으로 닫힘

위 코드에서, 파일 핸들이 예외 상황에서도 자동으로 해제되는 것을 볼 수 있다. 이는 비동기 작업에서 필수적으로 고려해야 할 중요한 패턴이다.

스마트 포인터를 이용한 자원 관리

비동기 작업에서 메모리 자원의 관리는 매우 중요하다. 수동으로 할당한 메모리는 예외가 발생할 경우 해제되지 않는 문제가 발생할 수 있다. 이러한 문제를 해결하기 위해 C++에서는 std::unique_ptrstd::shared_ptr 같은 스마트 포인터를 제공한다. 이러한 스마트 포인터는 객체가 더 이상 참조되지 않을 때 메모리를 자동으로 해제해준다.

특히, 비동기 작업에서는 동적으로 할당된 자원을 관리할 때 스마트 포인터를 적극 활용해야 한다. 다음은 예외 상황에서 스마트 포인터를 사용하는 예이다.

void asyncMemoryOperation() {
    std::unique_ptr<int> data(new int[100]);
    if (!data) {
        throw std::runtime_error("Memory allocation failed");
    }
    // 비동기로 메모리에 데이터를 씀
    // 예외가 발생하더라도 data는 자동으로 해제됨
}

스마트 포인터는 비동기 작업 중 예외가 발생하더라도 메모리 누수를 방지해 준다. 특히, 비동기 함수는 작업이 완료되기 전에 여러 가지 예외가 발생할 수 있기 때문에 스마트 포인터의 중요성이 더욱 부각된다.

자원의 올바른 해제를 위한 try-catch 구조

비동기 작업 중 예외 처리를 할 때, 리소스 해제를 보장하기 위해서는 try-catch 구조를 사용하는 것이 효과적이다. 이는 예외가 발생한 경우에도 자원을 해제할 수 있는 기회를 제공한다.

void asyncOperationWithCatch() {
    try {
        std::ofstream file("output.txt");
        if (!file.is_open()) {
            throw std::runtime_error("Failed to open file");
        }
        // 파일에 데이터를 비동기로 씀
        file << "Async Data";
    } catch (const std::exception& e) {
        // 예외 발생 시 리소스 해제를 위한 추가 작업
        std::cerr << "Exception: " << e.what() << std::endl;
    }
}

이 구조는 예외가 발생해도 리소스가 적절히 해제되고, 로그를 남기거나 대체 처리 로직을 수행할 수 있게 한다.

커스텀 리소스 클래스와 소멸자 활용

RAII 기법을 활용한 커스텀 리소스 관리 클래스를 설계하면, 특정 리소스에 대해 더 정교하게 관리할 수 있다. 이러한 클래스는 비동기 작업에서 자주 사용되는 네트워크 소켓, 파일 핸들, 메모리 등 다양한 자원에 대한 관리를 자동화해 준다.

다음은 소켓 리소스를 관리하는 커스텀 클래스를 설계하는 예시이다. 이 클래스는 소켓 리소스가 스코프를 벗어날 때 자동으로 닫힌다.

class SocketManager {
public:
    SocketManager(int socket_fd) : socket_fd(socket_fd) {}
    ~SocketManager() {
        if (socket_fd != -1) {
            close(socket_fd);
        }
    }
    int getSocket() const { return socket_fd; }
private:
    int socket_fd;
};

위 클래스는 비동기 작업에서 소켓이 할당된 이후 예외가 발생하더라도 소켓을 자동으로 닫아준다. 이 방식으로 리소스가 안전하게 관리되며, 명시적으로 소켓을 닫지 않아도 된다.

이 커스텀 클래스를 비동기 작업에서 사용하는 예는 다음과 같다.

void asyncSocketOperation(int socket_fd) {
    SocketManager socketManager(socket_fd);
    // 비동기로 소켓에 데이터를 전송
    // 예외가 발생하더라도 소켓은 자동으로 닫힘
}

이처럼, 소멸자에서 자원을 해제하도록 설계된 커스텀 클래스는 비동기 작업에서 예외가 발생하더라도 리소스를 안전하게 관리하는 데 큰 도움이 된다.

비동기 작업에서 std::futurestd::promise의 예외 관리

비동기 작업에서 std::futurestd::promise는 주로 작업 결과나 예외를 전달하는 데 사용된다. 특히, 예외가 발생했을 때 해당 예외를 std::future를 통해 호출자에게 전달하는 방식은 비동기 예외 처리에서 매우 유용하다.

std::promise는 예외가 발생한 경우 이를 설정할 수 있으며, std::future는 설정된 예외를 받을 수 있다. 다음 예시는 비동기 작업에서 예외를 std::promisestd::future를 통해 전달하는 방법을 보여준다.

void asyncWorkWithPromise(std::promise<int>& prom) {
    try {
        // 비동기 작업 수행
        throw std::runtime_error("Async error occurred");
        prom.set_value(42);  // 정상적으로 완료된 경우 결과 설정
    } catch (...) {
        prom.set_exception(std::current_exception());  // 예외 발생 시 전달
    }
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    std::thread(asyncWorkWithPromise, std::ref(prom)).detach();

    try {
        int result = fut.get();  // 비동기 작업의 결과 또는 예외 수신
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
}

이 구조는 비동기 작업 중 예외가 발생하면 std::future로 이를 받아 처리할 수 있게 한다. 예외가 발생하더라도 자원 관리를 안정적으로 수행하고, 적절한 오류 처리를 할 수 있다.

멀티스레드 환경에서의 자원 관리

비동기 작업은 종종 멀티스레드 환경에서 실행된다. 이때 자원 관리가 더욱 복잡해질 수 있으며, 여러 스레드에서 동일한 자원에 접근할 때 충돌이 발생할 수 있다. 이러한 문제를 해결하기 위해서는 뮤텍스과 같은 동기화 도구를 사용하여 자원을 안전하게 관리해야 한다.

다음 예시는 여러 스레드에서 공통 리소스에 접근할 때 std::mutex를 사용하여 자원을 보호하는 방법이다.

std::mutex mtx;

void asyncWorkWithMutex() {
    std::lock_guard<std::mutex> lock(mtx);
    // 보호된 리소스에 접근
    std::cout << "Accessing shared resource" << std::endl;
}

int main() {
    std::thread t1(asyncWorkWithMutex);
    std::thread t2(asyncWorkWithMutex);
    t1.join();
    t2.join();
}

std::lock_guard를 사용하면 예외가 발생하더라도 락이 자동으로 해제되어 교착 상태를 방지할 수 있다. 이를 통해 비동기 작업에서 발생할 수 있는 리소스 충돌을 효과적으로 해결할 수 있다.

멀티스레드 환경에서의 RAII와 리소스 동기화

멀티스레드 환경에서 비동기 작업을 처리할 때, 여러 스레드가 동일한 리소스를 공유하는 상황이 발생할 수 있다. 이때, 스레드 간 동기화를 적절히 하지 않으면 데이터 경합이나 교착 상태 등의 문제가 발생할 수 있다. 이를 방지하기 위해, RAII 기법과 함께 뮤텍스나 락을 결합하여 자원을 보호하는 것이 중요하다.

다음은 뮤텍스와 RAII를 결합한 예이다. 여기서는 std::lock_guard를 사용하여 스레드 간 자원 접근을 안전하게 관리한다.

class ResourceManager {
public:
    ResourceManager() {
        // 리소스 초기화
    }

    ~ResourceManager() {
        // 리소스 해제
    }

    void accessResource() {
        std::lock_guard<std::mutex> lock(mtx);
        // 보호된 리소스 접근
        std::cout << "Resource accessed by thread " << std::this_thread::get_id() << std::endl;
    }

private:
    std::mutex mtx;  // 스레드 간 동기화를 위한 뮤텍스
};

ResourceManager 클래스는 비동기 작업에서 자원을 안전하게 관리할 수 있도록 설계되었다. 리소스에 접근할 때마다 뮤텍스를 통해 동기화되며, RAII 원칙에 따라 리소스 해제가 자동으로 이루어진다. 이를 사용하는 예는 다음과 같다.

void asyncWorkWithResource(ResourceManager& manager) {
    manager.accessResource();  // 멀티스레드 환경에서 안전하게 리소스 접근
}

int main() {
    ResourceManager manager;
    std::thread t1(asyncWorkWithResource, std::ref(manager));
    std::thread t2(asyncWorkWithResource, std::ref(manager));
    t1.join();
    t2.join();
}

위 예제에서는 두 개의 스레드가 동일한 자원에 접근하지만, 뮤텍스와 RAII를 사용하여 리소스 경합을 방지하고 자원이 안전하게 관리된다.

std::scoped_lock을 이용한 다중 리소스 동기화

비동기 작업에서 여러 개의 리소스를 동시에 관리해야 할 때, 다중 리소스에 대한 동기화를 효과적으로 처리해야 한다. 이를 위해 C++17에서는 std::scoped_lock을 도입하여 여러 뮤텍스를 동시에 락할 수 있도록 지원한다.

이 기법은 여러 개의 리소스가 각각 다른 뮤텍스로 보호되어 있을 때 유용하며, 교착 상태를 방지하기 위해 락을 순차적으로 해제한다. 다음은 std::scoped_lock을 사용하는 예이다.

std::mutex mtx1, mtx2;

void asyncWorkWithMultipleMutexes() {
    std::scoped_lock lock(mtx1, mtx2);  // 두 개의 뮤텍스를 동시에 락
    // 보호된 리소스 접근
    std::cout << "Accessing multiple resources safely" << std::endl;
}

int main() {
    std::thread t1(asyncWorkWithMultipleMutexes);
    std::thread t2(asyncWorkWithMultipleMutexes);
    t1.join();
    t2.join();
}

std::scoped_lock은 다중 뮤텍스를 사용하여 리소스를 보호할 때 예외가 발생하더라도 락을 안전하게 해제해 준다. 이를 통해 멀티스레드 환경에서 다중 리소스를 보다 안전하게 관리할 수 있다.

자원 관리와 비동기 작업에서의 std::shared_mutex

일부 비동기 작업에서는 여러 스레드가 읽기 작업을 수행할 수 있지만, 쓰기 작업이 동시에 이루어지면 안 되는 경우가 있다. 이때, std::shared_mutex를 사용하여 다중 스레드가 리소스를 읽을 수 있도록 허용하면서도, 쓰기 작업이 진행 중일 때는 다른 스레드가 읽기 작업을 수행하지 못하게 할 수 있다.

std::shared_mutex는 다중 스레드가 동시에 읽을 수 있도록 허용하는 공유 락(shared lock)과, 오직 한 스레드만 리소스를 변경할 수 있도록 하는 배타 락(exclusive lock)을 지원한다. 다음은 이 개념을 적용한 예이다.

std::shared_mutex sharedMtx;

void readOperation() {
    std::shared_lock<std::shared_mutex> lock(sharedMtx);
    // 공유 리소스를 읽음
    std::cout << "Reading shared resource" << std::endl;
}

void writeOperation() {
    std::unique_lock<std::shared_mutex> lock(sharedMtx);
    // 공유 리소스를 씀
    std::cout << "Writing to shared resource" << std::endl;
}

int main() {
    std::thread reader1(readOperation);
    std::thread reader2(readOperation);
    std::thread writer(writeOperation);

    reader1.join();
    reader2.join();
    writer.join();
}

이 방식은 비동기 작업에서 읽기와 쓰기 작업을 동시에 관리할 때 매우 유용하다. 여러 스레드가 동시에 읽을 수 있지만, 쓰기 작업이 이루어질 때는 읽기 작업이 차단되도록 한다. 이를 통해 리소스의 일관성을 유지할 수 있다.

비동기 작업에서의 리소스 해제와 std::async 활용

std::async는 비동기 작업을 손쉽게 수행할 수 있도록 C++ 표준 라이브러리에서 제공하는 함수이다. 이 함수는 비동기적으로 실행된 작업의 결과를 std::future 객체로 반환하며, 리소스 해제를 자동으로 처리하는 구조를 제공한다.

비동기 작업 중 예외가 발생하더라도, std::async는 작업의 결과나 예외를 std::future를 통해 전달하며, 이를 호출자에서 처리할 수 있다. 이때, 리소스는 std::future의 소멸자에서 자동으로 해제된다.

int asyncTask() {
    // 비동기 작업 수행
    if (true) {  // 임의의 예외 조건
        throw std::runtime_error("Error in async task");
    }
    return 42;  // 작업이 성공적으로 완료된 경우
}

int main() {
    std::future<int> fut = std::async(std::launch::async, asyncTask);

    try {
        int result = fut.get();  // 비동기 작업 결과 받기
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
}

이 예에서는 std::async를 사용하여 비동기 작업을 실행하고, 그 결과나 예외를 std::future를 통해 전달받는다. 예외가 발생하면 호출자가 이를 처리할 수 있으며, 예외가 발생하더라도 작업이 종료되면서 모든 자원은 자동으로 해제된다.

커스텀 리소스 클래스에서의 예외 안전성 보장

비동기 작업에서 자원을 안전하게 관리하려면, 커스텀 리소스 클래스에서도 예외 안전성을 확보해야 한다. 이는 자원이 반드시 적절히 해제될 수 있도록 해야 한다는 의미이다. 커스텀 클래스는 소멸자에서 리소스를 안전하게 해제하고, 예외 발생 시 불완전한 상태에 빠지지 않도록 설계되어야 한다.

다음은 예외 안전성을 보장하는 커스텀 리소스 클래스의 예이다.

class Resource {
public:
    Resource() {
        resource_ptr = new int[100];  // 자원 할당
    }

    ~Resource() {
        delete[] resource_ptr;  // 자원 해제
    }

    void performTask() {
        if (!resource_ptr) {
            throw std::runtime_error("Resource not allocated");
        }
        // 자원을 사용하여 작업 수행
    }

private:
    int* resource_ptr;
};

위 클래스는 자원을 동적으로 할당하고, 소멸자에서 자원을 해제하여 예외가 발생하더라도 메모리 누수를 방지한다. performTask 함수에서는 자원이 제대로 할당되었는지 확인하고, 그렇지 않은 경우 예외를 던진다.

이 클래스는 비동기 작업에서도 동일하게 사용할 수 있으며, 예외가 발생해도 자원은 안전하게 해제된다.

void asyncOperation() {
    Resource res;
    res.performTask();  // 비동기 작업에서 자원 사용
}

int main() {
    try {
        std::thread t(asyncOperation);
        t.join();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
}

이 예에서는 비동기 작업에서 커스텀 리소스를 안전하게 관리하고, 예외 발생 시 자원이 자동으로 해제되는 구조를 보여준다.

비동기 작업에서 리소스 해제와 지연 초기화

비동기 작업에서는 자원이 필요한 시점에만 초기화하는 지연 초기화(lazy initialization) 기법을 사용할 수 있다. 이는 자원의 사용 빈도가 낮거나, 초기화 비용이 높은 경우에 유용하다. 지연 초기화를 통해 비동기 작업에서 불필요한 자원 할당을 줄일 수 있다.

다음은 지연 초기화 기법을 사용하는 예이다.

class LazyResource {
public:
    LazyResource() : resource_ptr(nullptr) {}

    ~LazyResource() {
        if (resource_ptr) {
            delete[] resource_ptr;  // 자원 해제
        }
    }

    void initialize() {
        if (!resource_ptr) {
            resource_ptr = new int[100];  // 자원을 필요한 시점에 초기화
        }
    }

    void performTask() {
        if (!resource_ptr) {
            throw std::runtime_error("Resource not initialized");
        }
        // 자원을 사용하여 작업 수행
    }

private:
    int* resource_ptr;
};

이 클래스에서는 자원을 초기화하지 않은 상태에서 작업을 시도할 경우 예외를 던지며, 자원이 필요한 시점에만 할당하는 구조를 갖는다.

void asyncTaskWithLazyInit() {
    LazyResource res;
    res.initialize();  // 자원을 지연 초기화
    res.performTask();
}

int main() {
    std::thread t(asyncTaskWithLazyInit);
    t.join();
}

지연 초기화를 활용하면 자원의 낭비를 줄이고, 비동기 작업의 성능을 향상시킬 수 있다. 또한, 예외가 발생해도 자원이 자동으로 해제되도록 보장된다.

비동기 작업에서의 리소스의 이동 의미론 적용

비동기 작업에서는 자원을 안전하게 관리하는 방법으로 이동 의미론(move semantics)을 활용할 수 있다. 이동 의미론은 자원의 복사 대신 소유권을 이전하여 성능을 최적화하고, 불필요한 복사로 인한 자원 낭비를 방지하는 중요한 C++ 기법이다. 이를 비동기 작업에서 사용할 때, 특히 대용량 데이터를 다루는 경우 성능 상의 이점을 크게 얻을 수 있다.

이동 의미론의 기초 개념

C++에서 이동 의미론을 활용하기 위해서는 rvalue 참조std::move를 이해해야 한다. 이동 의미론을 사용하면 복사 연산자 대신 이동 연산자를 구현하여 리소스의 소유권을 이전할 수 있다. 이는 비동기 작업에서 리소스를 다른 스레드로 안전하게 전달하는 데 매우 유용하다.

다음은 이동 의미론을 활용한 예이다.

class MovableResource {
public:
    MovableResource(int size) : data(new int[size]), size(size) {}

    // 이동 생성자
    MovableResource(MovableResource&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // 이동 대입 연산자
    MovableResource& operator=(MovableResource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    ~MovableResource() {
        delete[] data;
    }

private:
    int* data;
    int size;
};

위 클래스는 이동 생성자와 이동 대입 연산자를 통해 이동 의미론을 구현한 예이다. 리소스의 소유권을 이동할 때, 기존의 복사와는 달리 자원의 할당 및 해제가 효율적으로 처리된다. 이동된 객체는 소유권을 잃고, 이후 삭제될 때 자원이 해제되지 않도록 처리한다.

이동 의미론을 활용한 비동기 작업

이동 의미론을 비동기 작업에 적용할 수 있다. std::move를 사용하여 객체의 소유권을 새로운 스레드로 안전하게 전달할 수 있으며, 이를 통해 불필요한 복사 비용을 줄일 수 있다.

void asyncTaskWithMove(MovableResource res) {
    // 비동기 작업에서 이동된 리소스를 사용
}

int main() {
    MovableResource resource(1000);
    std::thread t(asyncTaskWithMove, std::move(resource));  // 리소스 이동
    t.join();
}

위 예에서는 std::move를 사용하여 resource 객체의 소유권을 비동기 작업으로 이동시킨다. 이동된 객체는 더 이상 원래 스레드에서 사용할 수 없으며, 비동기 작업 내에서 안전하게 사용될 수 있다.

비동기 작업에서의 std::move와 스마트 포인터 결합

스마트 포인터와 이동 의미론을 결합하면, 동적으로 할당된 자원을 더욱 안전하게 비동기 작업으로 전달할 수 있다. 특히 std::unique_ptr은 이동 가능한 스마트 포인터로, 자원의 소유권을 한 스레드에서 다른 스레드로 안전하게 이동시킬 수 있다.

다음은 std::unique_ptr을 이동 의미론과 함께 사용하는 예이다.

void asyncTaskWithUniquePtr(std::unique_ptr<int[]> data) {
    // 비동기 작업에서 data 사용
    std::cout << "Processing data in async task" << std::endl;
}

int main() {
    std::unique_ptr<int[]> data(new int[1000]);
    std::thread t(asyncTaskWithUniquePtr, std::move(data));  // 소유권 이동
    t.join();

    // 여기서 data는 더 이상 사용 불가능
}

위 코드에서 std::unique_ptr의 소유권이 std::move를 통해 비동기 작업으로 이동된다. 이는 자원이 안전하게 관리되며, 예외가 발생하더라도 std::unique_ptr이 자원을 해제해 주기 때문에 메모리 누수를 방지할 수 있다.

예외 안전성과 이동 의미론의 결합

비동기 작업에서 이동 의미론을 사용하는 경우에도 예외 안전성은 중요한 요소이다. 특히, 이동된 객체가 예외 상황에서 소유권을 잃게 될 경우 자원의 안전한 해제가 이루어져야 한다. 이를 위해 이동 연산자는 예외를 던지지 않도록 noexcept로 선언하여, 예외 발생 시에도 자원이 일관되게 관리되도록 해야 한다.

예를 들어, 다음과 같이 이동 연산자를 예외 안전하게 작성할 수 있다.

class SafeMovableResource {
public:
    SafeMovableResource(int size) : data(new int[size]), size(size) {}

    // 이동 생성자 (noexcept 보장)
    SafeMovableResource(SafeMovableResource&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    // 이동 대입 연산자 (noexcept 보장)
    SafeMovableResource& operator=(SafeMovableResource&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

    ~SafeMovableResource() {
        delete[] data;
    }

private:
    int* data;
    int size;
};

이 클래스는 예외 안전성을 보장하기 위해 이동 연산자에 noexcept를 사용하였다. 이를 통해 비동기 작업에서 자원이 이동될 때 예외가 발생하더라도 프로그램의 안정성을 유지할 수 있다.

비동기 작업에서의 리소스 우선순위와 성능 최적화

비동기 작업에서 리소스 관리와 함께 성능 최적화도 중요한 요소이다. 리소스가 자주 사용되지 않거나, 비용이 큰 작업이 예상될 때는 자원의 우선순위를 설정하여 비동기 작업을 효율적으로 분배하는 것이 필요하다. 이를 위해 리소스를 관리하는 큐(queue)나 작업 스케줄러를 사용하여 자원의 활용을 최적화할 수 있다.

자원의 우선순위를 결정하고 비동기 작업 간에 자원을 동적으로 할당하는 전략은, 특히 대규모 시스템에서 리소스 병목 현상을 줄이는 데 도움이 된다. 이는 다음과 같이 간단한 작업 스케줄러로 구현될 수 있다.

std::queue<std::function<void()>> taskQueue;
std::mutex queueMutex;

void scheduleTask(std::function<void()> task) {
    std::lock_guard<std::mutex> lock(queueMutex);
    taskQueue.push(task);
}

void processTasks() {
    while (!taskQueue.empty()) {
        std::lock_guard<std::mutex> lock(queueMutex);
        auto task = taskQueue.front();
        taskQueue.pop();
        task();  // 작업 실행
    }
}

이 작업 스케줄러는 자원의 우선순위를 고려하여 비동기 작업을 효율적으로 관리할 수 있으며, 각 작업이 적절한 시점에 리소스를 사용할 수 있도록 보장한다.