ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] 객체 간 관계
    C++/이것이 C++이다 2021. 10. 19. 00:09

    C++는 OOP Language 입니다.

    OOP적 관점에서 C++의 객체는 다른 객체와 특정한 관계가 성립합니다.

    이번 글에서는 이하의 내용을 다뤄 볼 것입니다.

    • 객체가 다른 객체와 어떤 관계가 될 수 있는지
    • 객체 간 관계 형성과 관련된 문법이 무엇인지

    기초와 관련된 만큼, 많은 내용이 포함되어 있지는 않습니다.

    다만, 설계와 관련된 내용이 조금 나오는 관계로 짚고 넘어가면 좋을 것 같습니다.

     


     

    1. friend 키워드

     

    더보기

    friend 키워드는 클래스 내부에서, 함수나 클래스 선언 앞에 사용되는 키워드 입니다.

    다음과 같이 사용합니다.

    friend class [Class Name];
    friend [Function Prototype];

    이렇게 선언된 클래스와 함수는 접근 제어 지시자의 영향을 받지 않습니다.

    키워드의 사전적 의미 그대로, 선언한 클래스와 함수를 친구처럼 대우하는 것 입니다.

     

    예제를 살펴보겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyClass {
    public:
        MyClass(int param) : my_data(param) { }
        int get_data() { return my_data; }
    
        friend void function(MyClass&); // Line 9
    
    private:
        int my_data;
    };
    
    void function(MyClass& data) {
        data.my_data += 10; // Line 16
    }
    
    int main() {
        MyClass a(10);
        cout << a.get_data() << "\n";
        function(a);
        cout << a.get_data() << "\n";
    
        return 0;
    }

    Line 9에서 function 함수를 friend로 선언했고, 이후 전역함수 function에서 MyClass의 private 멤버 변수에 접근한 것을 볼 수 있습니다.

    이와 같이, friend로 클래스, 함수는 접근 제어 지시자의 영향을 받지 않습니다.

    그렇다면 이런 의문이 들 수 있습니다.

    정보은닉, 캡슐화 등 여러 이유로 접근을 차단했는데, 너무 쉽게 풀어주는것이 아닌가?

    지난 글(다중 상속) 에서 언급된 내용과 어느정도 궤를 같이 하는 질문이라고 생각합니다.

    C++은 개발자의 자유도를 최대한 보장해주는 언어입니다.

    상황에 따라서는 설정한 접근 제어 지시자를 풀어야 하는 상황이 발생할 수 있는 것 입니다.

    또 다른 이유로는, 객체 간 응집성을 보장해주기 위함입니다.

    응집성을 보장해준다는 것은, 서로 다른 객체라도 관련이 깊다면 긴밀하게 엮일 수 있도록 묶어주는 것 입니다.

     

    2. 응집성 

     

    더보기

    위 문단에 언급된 응집성에 대해 이야기를 이어나가보겠습니다.

    응집성은 객체 간의 관계를 긴밀하게 묶어주는 것 입니다. 하나의 클래스로 묶어두기에는 애매한 관계지만, 서로 관련이 없다고 하기에는 애매한 객체들을 friend 키워드로 묶어줄 수 있습니다.

     

    예제를 살펴보겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyNode {
        friend class MyList;
    private:
        MyNode(const int param) : data(param) {}
        int data;
        MyNode* next_node = nullptr;
    };
    
    class MyList {
    public:
        MyList() : head_node(new MyNode(0)){}
        void add_node(const int data) {
            MyNode* node = new MyNode(data);
            MyNode* temp_node = head_node;
            while (temp_node->next_node != nullptr) {
                temp_node = temp_node->next_node;
            }
            temp_node->next_node = node;
        }
        ~MyList() {
            MyNode* node = head_node->next_node;
            MyNode* d_node = nullptr;
    
            while (node) {
                d_node = node;
                node = node->next_node;
    
                cout << d_node->data << "\n";
                delete d_node;
            }
            head_node->next_node = nullptr;
        }
    
    private:
        MyNode* head_node;
    };
    
    int main() {
        MyList list;
        list.add_node(1);
        list.add_node(2);
        list.add_node(3);
    
        return 0;
    }

    위 예제는 간단하게 삽입 연산만 구현된 단일 연결 리스트 입니다.

    주목해야 할 점은 MyNode의 구성입니다. friend로 MyList를 선언했고, 생성자를 private로 설정했습니다.

    이런 구성을 통해 MyNode의 생성은 MyList 내부에서만 할 수 있게 되었습니다.

    두 클래스는 노드와 리스트라는 서로 다른 객체이지만, friend 키워드를 통해 한 객체에 준하는 관계가 되었습니다.

     

    3. 객체 간 집합 관계

     

    더보기

    지금까지 살펴본 객체 간 관계의 대표적인 예시는 상속 관계 입니다.

    이번 문단에서는 위 문단에서 살펴본 응집성과 관련된, 객체 간 집합 관계에 대해 살펴보겠습니다.

     

    집합 관계에도 두 종류가 있습니다.

    첫 번째로는 위 문단의 연결 리스트와 같은 Composition 관계입니다. MyList를 구성하는 MyNode가 존재하듯이, 큰 객체 아래에 하위 객체가 존재하는 관계입니다.

     

    예제를 살펴보겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyNode {
        friend class MyList;
    private:
        MyNode(const int param) : data(param) {}
        int data;
        MyNode* next_node = nullptr;
    };
    
    class MyList {
    public:
        MyList() : head_node(new MyNode(0)){}
        void add_node(const int data) {
            MyNode* node = new MyNode(data);
            MyNode* temp_node = head_node;
            while (temp_node->next_node != nullptr) {
                temp_node = temp_node->next_node;
            }
            temp_node->next_node = node;
        }
        void print() {
            MyNode* node = head_node->next_node;
            while (node) {
                cout << node->data << "\n";
                node = node->next_node;
            }
        }
        ~MyList() {
            MyNode* node = head_node->next_node;
            MyNode* d_node = nullptr;
    
            while (node) {
                d_node = node;
                node = node->next_node;
    
                cout << d_node->data << "\n";
                delete d_node;
            }
            head_node->next_node = nullptr;
        }
    
    private:
        MyNode* head_node;
    };
    
    class MyUI {
    public:
        int print_menu() {
            cout << "[1] Add [2] Print [0] Exit \nInput : ";
            cout.flush();
            int input = -1;
            cin >> input;
    
            return input;
        }
    
        void run() {
            int data;
            int input;
    
            while ((input = print_menu()) != 0) {
                switch (input) {
                case 1:
                    cout << "Data : ";
                    cout.flush();
                    cin >> data;
                    list.add_node(data);
                    continue;
                case 2:
                    list.print();
                    continue;
                case 0:
                    break;
                default:
                    continue;
                }
            }
        }
    private:
        MyList list;
    };
    
    int main() {
        MyUI ui;
        ui.run();
    
        return 0;
    }
    [1] Add [2] Print [0] Exit
    Input : 1
    Data : 123
    [1] Add [2] Print [0] Exit
    Input : 1
    Data : 425
    [1] Add [2] Print [0] Exit
    Input : 2
    123
    425
    [1] Add [2] Print [0] Exit
    Input : 0
    123
    425

    위 문단의 연결 리스트에서 몇 가지가 추가되었습니다.

    MyList는 print함수가, 그 외에 사용자의 입력을 받을 수 있는 UI 클래스가 추가되었습니다.

    주목해야 할 점은, MyUI클래스가 MyList클래스의 인스턴스를 멤버로 가지고 있는 점 입니다.

    따라서, MyUI의 소멸시점에 MyList도 함께 소멸합니다.

    위와 같이 여러 객체 사이에서 주도적인 객체가 존재하고, 그 객체의 일부를 이루는 다른 객체들이 존재하는 것이 Composition 관계 입니다.

     

    다음은 Aggregation관계 입니다. 

    Aggregation관계는 독립적인 여러 객체가 모여 하나를 이루는 것 입니다.

     

    예제를 살펴보겠습니다.

    #include <iostream>
    using namespace std;
    
    class MyNode {
        friend class MyList;
    private:
        MyNode(const int param) : data(param) {}
        int data;
        MyNode* next_node = nullptr;
    };
    
    class MyList {
    public:
        MyList() : head_node(new MyNode(0)){}
        void add_node(const int data) {
            MyNode* node = new MyNode(data);
            MyNode* temp_node = head_node;
            while (temp_node->next_node != nullptr) {
                temp_node = temp_node->next_node;
            }
            temp_node->next_node = node;
        }
        void print() {
            MyNode* node = head_node->next_node;
            while (node) {
                cout << node->data << "\n";
                node = node->next_node;
            }
        }
        ~MyList() {
            MyNode* node = head_node->next_node;
            MyNode* d_node = nullptr;
    
            while (node) {
                d_node = node;
                node = node->next_node;
    
                cout << d_node->data << "\n";
                delete d_node;
            }
            head_node->next_node = nullptr;
        }
    
    private:
        MyNode* head_node;
    };
    
    class MyUI {
    public:
        MyUI(MyList& r_list) : list(r_list){}
        int print_menu() {
            cout << "[1] Add [2] Print [0] Exit \nInput : ";
            cout.flush();
            int input = -1;
            cin >> input;
    
            return input;
        }
    
        void run() {
            int data;
            int input;
    
            while ((input = print_menu()) != 0) {
                switch (input) {
                case 1:
                    cout << "Data : ";
                    cout.flush();
                    cin >> data;
                    list.add_node(data);
                    continue;
                case 2:
                    list.print();
                    continue;
                case 0:
                    break;
                default:
                    continue;
                }
            }
        }
    private:
        MyList& list;
    };
    
    int main() {
        MyList list;
        MyUI ui(list);
        ui.run();
        ui.~MyUI();
    
        cout << "------------\n";
        MyUI ui_two(list);
        ui_two.run();
    
        return 0;
    }
    [1] Add [2] Print [0] Exit
    Input : 1
    Data : 123
    [1] Add [2] Print [0] Exit
    Input : 1
    Data : 321
    [1] Add [2] Print [0] Exit
    Input : 2
    123
    321
    [1] Add [2] Print [0] Exit
    Input : 0
    ------------
    [1] Add [2] Print [0] Exit
    Input : 2
    123
    321
    [1] Add [2] Print [0] Exit
    Input : 0
    123
    321

    Composition 예제에서 일부 코드가 수정되었습니다.

    MyUI의 멤버 변수 MyList가 참조 형식이 되었고, 그에 따라 생성자에서 초기화를 할 수 있도록 변경했습니다.

    또한 main함수에서 차이점을 알 수 있도록, UI를 두개 생성했습니다.

    Aggregation 관계에서는 첫 번째 MyUI가 소멸되어도 List는 그대로 남아, 두 번째 MyUI에서 정보가 그대로 남아있는 것을 볼 수 있습니다.

    이와 같이 서로 모여서 작동하지만, 따로 떨어질 수 있는 관계가 Aggregation 관계 입니다.

     

    두 관계를 설명할 때 거의 동일한 객체들을 사용한 이유는 객체의 성질에 따라 Composition 관계인지, Aggregation 관계인지 정해지지 않는다는 이야기를 하기 위함이었습니다.

    위 예제는 자료구조와, 사용자 인터페이스와 관련된 객체입니다.

    위와 같은 구조에서는 Aggregation 관계가 되는 것이 좋은 설계가 될 수 있습니다.

    이와 관련된 내용은 차후 디자인 패턴과 관련된 글에서 다룰 수 있도록 하겠습니다.

     


     

    클래스 설계와, 설계된 클래스 간의 올바른 관계 형성은 개발 과정이나, 이후 유지보수 단계에서 많은 도움이 됩니다.

    이번 글에서 다룬 C++의 문법은 friend 키워드 하나뿐이지만, 설계에 관련된 내용이 포함되어 있는 만큼 중요도는 낮지 않다고 생각됩니다.

     

    다음 글은 C++의 생산성과 관련된 내용, 템플릿입니다.

    읽어주셔서 감사합니다.

    'C++ > 이것이 C++이다' 카테고리의 다른 글

    [C++] 스마트 포인터  (0) 2021.11.03
    [C++] 템플릿  (0) 2021.10.26
    [C++] 다중 상속  (0) 2021.09.24
    [C++] 형 변환 연산자  (0) 2021.09.19
    [C++] 가상 함수  (0) 2021.09.05

    댓글

Designed by Tistory.