Skip to content

14. OOP 캡슐화(OOP Encapsulation)


1. 키워드

  • OOP 캡슐화(OOP Encapsulation)
  • 프렌드(Friend)와 friend 키워드
  • 프렌드 클래스(Friend Class)
  • 프렌드 멤버(Friend Member)
  • 순환 참조(Circular Reference)
  • 전방 선언(Forward Declaration)
  • 정적 멤버 변수(Static Member Variable)와 static 키워드
  • 정적 멤버 함수(Static member Function)와 static 키워드
  • 상수 멤버 변수(Constant Member Variable)와 const 키워드
  • 상수 멤버 함수(Constant Member Function)와 const 키워드


2. 프렌드

  • C++에서 객체의 private 멤버에는 해당 객체의 public 멤버 함수를 통해서만 접근할 수 있다.
  • 하지만 경우에 따라서는 해당 객체의 멤버 함수가 아닌 함수도 private 멤버에 접근해야만 할 경우가 발생한다.
  • 이럴 때마다 매번 private 멤버에 접근하기 위한 새로운 public 멤버 함수를 작성하는 것은 매우 비효율적이다.
  • 따라서 C++에서는 이러한 경우를 위해 프렌드라는 새로운 접근 제어 키워드를 제공한다.
  • 프렌드는 지정한 대상에 한해 해당 객체의 모든 멤버에 접근할 수 있는 권한을 부여해 준다.
  • 이러한 friend 키워드는 전역 함수, 클래스, 멤버 함수의 세 가지 형태로 사용할 수 있다.


1) 프렌드 함수 선언

  • C++에서 프렌드 함수는 friend 키워드를 사용하여 다음과 같이 선언한다.


friend 클래스이름 함수이름(매개변수목록);


  • 이렇게 선언된 프렌드 함수는 클래스 선언부에 그 원형이 포함되지만, 클래스의 멤버 함수는 아니다.
  • 이러한 프렌드 함수는 해당 클래스의 멤버 함수는 아니지만, 멤버 함수와 같은 접근 권한을 가지게 된다.
  • friend 키워드는 함수의 원형에서만 사용해야 하며, 함수의 정의에서는 사용하지 않는다.
  • 그리고 프렌드 선언은 클래스 선언부의 public, private, protected 영역 등 어디에나 위치할 수 있으며, 위치에 따른 차이는 전혀 없다.


2) 프렌드의 필요성

  • C++에서 클래스에 대해 이항 연산자를 오버로딩할 때 프렌드의 필요성이 자주 발생한다.
  • 그 이유는 바로 멤버 함수의 호출 형태에 있다.
  • 멤버 함수는 왼쪽 피연산자인 객체가 호출하는 형태이므로, 이항 연산자의 매개변수 순서라든가 타입에 민감해진다.
  • 하지만 멤버 함수가 아닌 함수를 사용하면 해당 객체의 private 멤버에 접근할 수 없게 된다.
  • 따라서 이때 사용하는 것이 바로 프렌드이다.


  • 다음의 코드는 사각형을 나타내는 Rect 클래스를 정의하면서, 크기를 조절하기 위해 *(곱셈 연산자)를 오버로딩하는 예제이다.


#include <iostream>
using namespace std;

class Rect {
   private:
    double height_;
    double width_;

   public:
    Rect(double height, double width);  // 생성자
    void DisplaySize();
    Rect operator*(double mul) const;
};

int main(void) {
    Rect origin_rect(10, 20);
    Rect changed_rect = origin_rect * 2;  // 멤버 함수는 왼쪽 피연산자인 객체가 호출함
    // Rect changed_rect = 3 * origin_rect;

    changed_rect.DisplaySize();

    return 0;
}

Rect::Rect(double height, double width) {
    height_ = height;
    width_ = width;
}

void Rect::DisplaySize() {
    cout << "이 사각형의 높이는 " << this->height_
         << "이고, 너비는 " << this->width_ << "입니다." << endl;
}

Rect Rect::operator*(double mul) const {
    return Rect(height_ * mul, width_ * mul);
}

// 이 사각형의 높이는 20이고, 너비는 40입니다.


  • 위 코드의 operator*() 함수에서 주석 처리된 구문처럼 피연산자의 순서를 바꾸어 실행하면 오류가 발생할 것이다.
  • 그 이유는 멤버 함수란 왼쪽 피연산자인 객체가 호출하는 형태가 되어야 하기 때문이다.


  • 따라서 두 구문이 모두 정상적으로 동작하기 위해서는 지금의 operator*() 함수뿐만 아니라 매개변수의 순서가 다른 또 하나의 operator*() 함수를 객체가 호출하지 않는 전역 함수로 작성해야 한다.
  • 그 전역 함수가 private 멤버인 height_width_에 접근하기 위해서는 friend 키워드를 추가해야 한다.


#include <iostream>
using namespace std;

class Rect {
   private:
    double height_;
    double width_;

   public:
    Rect(double height, double width);  // 생성자
    void DisplaySize();
    Rect operator*(double mul) const;
    friend Rect operator*(double mul, const Rect &origin);  // 프렌드 함수
};

int main(void) {
    Rect origin_rect(10, 20);
    Rect rect01 = origin_rect * 2;
    Rect rect02 = 3 * origin_rect;

    rect01.DisplaySize();
    rect02.DisplaySize();

    return 0;
}

Rect::Rect(double height, double width) {
    height_ = height;
    width_ = width;
}

void Rect::DisplaySize() {
    cout << "이 사각형의 높이는 " << this->height_
         << "이고, 너비는 " << this->width_ << "입니다." << endl;
}

Rect Rect::operator*(double mul) const {
    return Rect(height_ * mul, width_ * mul);
}

Rect operator*(double mul, const Rect &origin) {
    return origin * mul;
}

// 이 사각형의 높이는 20이고, 너비는 40입니다.
// 이 사각형의 높이는 30이고, 너비는 60입니다.


  • 멤버 함수 원형의 맨 마지막에 const 키워드를 추가하면, 멤버 함수를 상수 멤버 함수로 정의할 수 있다.
  • 상수 멤버 함수란 자신이 호출하는 객체를 수정하지 않는 읽기 전용 함수를 의미한다.


3. 다양한 프렌드

1) 프렌드 클래스

  • C++에서 프렌드는 전역 함수, 클래스, 멤버 함수의 세 가지 형태로 사용할 수 있다.
  • 만약 두 클래스가 기능적으로 서로 밀접한 관계에 있고, 상대방의 private 멤버에 접근해야만 한다면 클래스 자체를 프렌드로 선언하는 것이 좋다.
  • 프렌드 클래스란 해당 클래스의 모든 멤버 함수가 특정 클래스의 프렌드인 클래스르 의미한다.


  • C++에서 프렌드 클래스는 다음과 같이 선언한다.


friend class 클래스이름;


  • 만약 Rect 클래스의 선언에 다음과 같은 프렌드 클래스의 선언이 존재한다고 가정해 보자.


friend class Display;


  • 이때 Display 클래스의 모든 멤버 함수는 Rect 클래스에 대한 프렌드 접근 권한을 부여 받게 된다.
  • 즉, Display 클래스의 모든 멤버 함수는 Rect 클래스의 모든 멤버에 접근할 수 있다.


  • 다음의 코드는 Display 클래스의 모든 멤버 함수가 Rect 클래스의 모든 멤버에 접근할 수 있도록 선언한 것이다.


#include <cmath>
#include <iostream>
using namespace std;

class Rect {
   private:
    double height_;
    double width_;

   public:
    Rect(double height, double width);  // 생성자
    void height() const;
    void width() const;
    friend class Display;  // 프렌드 클래스 선언
};

class Display {
   public:
    void ShowSize(const Rect &target);
    void ShowDiagonal(const Rect &target);
};

int main(void) {
    Rect rect01 = Rect(10, 20);
    Display rect_display = Display();

    rect_display.ShowSize(rect01);
    rect_display.ShowDiagonal(rect01);

    return 0;
}

Rect::Rect(double height, double width) {
    height_ = height;
    width_ = width;
}

void Rect::height() const {
    cout << "이 사각형의 높이는 " << this->height_ << "입니다." << endl;
}

void Rect::width() const {
    cout << "이 사각형의 너비는 " << this->width_ << "입니다." << endl;
}

void Display::ShowSize(const Rect &target) {
    target.height();
    target.width();
}

void Display::ShowDiagonal(const Rect &target) {
    double diagonal;

    diagonal = sqrt(pow(target.height_, 2) + pow(target.width_, 2));

    cout << "이 사각형의 대각선 길이는 " << diagonal << "입니다." << endl;
}

// 이 사각형의 높이는 10입니다.
// 이 사각형의 너비는 20입니다.
// 이 사각형의 대각선 길이는 22.3607입니다.


2) 프렌드 멤버 함수

  • 프렌드 클래스란 해당 클래스의 모든 멤버 함수가 특정 클래스의 프렌드가 된다.
  • 하지만 멤버 함수에 따라 특정 클래스의 프렌드 설정이 필요없는 멤버 함수도 있다.
  • 앞선 코드에서 ShowDiagonal() 함수는 Rect 클래스의 private 멤버에 직접 접근하도록 구현되어 있다.
  • 하지만 ShowSize() 함수는 Rect 클래스의 public 인터페이스만으로 구현되어 있다.
  • 따라서 Display 클래스에서 Rect 클래스에 대해 프렌드 설정이 필요한 함수는 ShowDiagonal() 함수뿐이다.


  • 이처럼 프렌드 멤버 함수란 해당 클래스의 특정 멤버 함수만을 프렌드로 지정하는 것을 의미한다.
  • 이것은 프렌드 설정이 꼭 필요한 함수에 대해서만 접근을 허락하므로, 정보 은닉 및 캡슐화 개념에 더욱 가깝게 구현할 수 있게 된다.


  • 다음의 코드는 Display 클래스의 ShowDiagonal() 함수만이 Rect 클래스의 모든 멤버에 접근할 수 있도록 선언한 것이다.


#include <cmath>
#include <iostream>
using namespace std;

class Rect;

class Display {
   public:
    void ShowSize(const Rect &target);
    void ShowDiagonal(const Rect &target);
};

class Rect {
   private:
    double height_;
    double width_;

   public:
    Rect(double height, double width);  // 생성자
    void height() const;
    void width() const;
    friend void Display::ShowDiagonal(const Rect &target);  // 프렌드 멤버 함수 선언
};

int main(void) {
    Rect rect01 = Rect(10, 20);
    Display rect_display = Display();

    rect_display.ShowSize(rect01);
    rect_display.ShowDiagonal(rect01);

    return 0;
}

Rect::Rect(double height, double width) {
    height_ = height;
    width_ = width;
}

void Rect::height() const {
    cout << "이 사각형의 높이는 " << this->height_ << "입니다." << endl;
}

void Rect::width() const {
    cout << "이 사각형의 너비는 " << this->width_ << "입니다." << endl;
}

void Display::ShowSize(const Rect &target) {
    target.height();
    target.width();
}

void Display::ShowDiagonal(const Rect &target) {
    double diagonal;

    diagonal = sqrt(pow(target.height_, 2) + pow(target.width_, 2));

    cout << "이 사각형의 대각선 길이는 " << diagonal << "입니다." << endl;
}

// 이 사각형의 높이는 10입니다.
// 이 사각형의 너비는 20입니다.
// 이 사각형의 대각선 길이는 22.3607입니다.


3) 전방 선언

  • 앞선 코드에서 Rect 클래스는 Display 클래스를 참조하고, Display 클래스는 Rect 클래스를 참조하고 있다.
  • 이처럼 두 클래스의 선언 내에서 서로를 참조하고 있는 상황을 순환 참조라고 한다.
  • 이러한 순환 참조를 피하기 위해서는 한 클래스를 다른 클래스의 앞에 미리 선언하는 전방 선언을 사용해야 한다.


  • C++에서 전방 선언은 다음과 같이 선언한다.


class 클래스이름;


  • 위의 코드에서는 순환 참조를 피하고자 Rect 클래스를 전방 선언했다.


  • 또한, 프렌드 멤버 함수를 선언할 때에는 각 클래스의 선언 위치도 신경 써야 한다.
  • 앞선 코드에서는 Rect 클래스와 Display 클래스를 다음과 같은 순서로 선언했다.


class Rect;             // 전방 선언

class Display { ... };  // Display 클래스 선언

class Rect { ... };     // Rect 클래스 선언


  • 만약 다음과 같이 순서를 바꾸면 컴파일러는 오류를 발생시킬 것이다.


class Rect;             // 전방 선언

class Rect { ... };     // Rect 클래스 선언

class Display { ... };  // Display 클래스 선언


  • Rect 클래스 내에서 ShowDiagonal() 함수가 프렌드로 선언된 것을 처리하기 전에, 컴파일러는 이미 ShowDiagonal() 함수의 선언을 알고 있어야만 한다.
  • 따라서 순서를 바꾸게 되면 컴파일러는 프렌드로 선언된 ShowDiagonal() 함수를 알지 못하므로, 오류를 발생시킨다.


4. 정적 멤버와 상수 멤버

1) 정적 멤버 변수

  • C++에서 정적 멤버란 클래스에는 속하지만, 객체 별로 할당되지 않고 클래스의 모든 객체가 공유하는 멤버를 의미한다.
  • 멤버 변수가 정적(Static)으로 선언되면, 해당 클래스의 모든 객체에 대해 하나의 데이터만이 유지 관리된다.
  • 정적 멤버 변수는 클래스 영역에서 선언되지만, 정의는 파일 영역에서 수행된다.
  • 이러한 정적 멤버 변수는 외부 연결(External Linkage)을 가지므로, 여러 파일에서 접근할 수 있다.
  • 정적 멤버 변수에도 클래스 멤버의 접근 제한 규칙이 적용되므로, 클래스의 멤버 함수나 프렌드만이 접근할 수 있다.
  • 하지만 정적 멤버 변수를 외부에서도 접근할 수 있게 하고 싶으면, 정적 멤버 변수를 public 영역에 선언하면 된다.


  • 다음의 코드는 모든 Person 객체가 같이 공유하는 person_count_라는 정적 멤버 변수를 선언하는 것이다.


#include <iostream>
#include <string>
using namespace std;

class Person {
   private:
    string name_;
    int age_;

   public:
    static int person_count_;             // 정적 멤버 변수의 선언
    Person(const string &name, int age);  // 생성자
    ~Person() { person_count_--; }        // 소멸자
    void ShowPersonInfo();
};

int Person::person_count_ = 0;  // 정적 멤버 변수의 정의 및 초기화

int main(void) {
    Person hong = Person("길동", 29);
    hong.ShowPersonInfo();

    Person lee = Person("순신", 35);
    lee.ShowPersonInfo();

    return 0;
}

Person::Person(const string &name, int age) {
    name_ = name;
    age_ = age;
    cout << ++person_count_ << "번째 사람이 생성되었습니다." << endl;
}

void Person::ShowPersonInfo() {
    cout << "이 사람의 이름은 " << name_
         << "이고, 나이는 " << age_ << "살입니다." << endl;
}

// 1번째 사람이 생성되었습니다.
// 이 사람의 이름은 길동이고, 나이는 29살입니다.
// 2번째 사람이 생성되었습니다.
// 이 사람의 이름은 순신이고, 나이는 35살입니다.


  • 이 정적 멤버는 Person 객체가 생성될 때마다 1씩 증가하여, 현재까지 총 몇 개의 Person 객체가 생성되었는지를 알려준다.


2) 정적 멤버 함수

  • C++에서는 클래스의 멤버 함수도 정적으로 선언할 수 있다.
  • 이렇게 선언된 정적 멤버 함수는 해당 클래스의 객체를 생성하지 않고도, 클래스 이름만으로 호출할 수 있다.


1. 객체이름.멤버함수이름();    // 일반 멤버 함수의 호출
2. 클래스이름.멤버함수이름();  // 정적 멤버 함수의 호출


  • 정적 멤버 함수는 정적 멤버 변수를 선언하는 방법과 같이 static 키워드를 사용하여 선언한다.
  • 이러한 정적 멤버 함수는 다음과 같은 특징을 가진다.


1] 객체를 생성하지 않고 클래스 이름만으로 호출할 수 있다.

2] 객체를 생성하지 않으므로, this 포인터를 가지지 않는다.

3] 특정 객체와 결합하지 않으므로, 정적 멤버 변수밖에 사용할 수 없다.


#include <iostream>
#include <string>
using namespace std;

class Person {
   private:
    string name_;
    int age_;

   public:
    static int person_count_;             // 정적 멤버 변수의 선언
    static int person_count();            // 정적 멤버 함수의 선언
    Person(const string &name, int age);  // 생성자
    ~Person() { person_count_--; }        // 소멸자
    void ShowPersonInfo();
};

int Person::person_count_ = 0;  // 정적 멤버 변수의 정의 및 초기화

int main(void) {
    Person hong = Person("길동", 29);
    Person lee = Person("순신", 35);

    cout << "현재까지 생성된 총 인원 수는 "
         << Person::person_count() << "명입니다." << endl;

    return 0;
}

Person::Person(const string &name, int age) {
    name_ = name;
    age_ = age;
    cout << ++person_count_ << "번째 사람이 생성되었습니다." << endl;
}

void Person::ShowPersonInfo() {
    cout << "이 사람의 이름은 " << name_
         << "이고, 나이는 " << age_ << "살입니다." << endl;
}

int Person::person_count() {  // 정적 멤버 함수의 정의
    return person_count_;
}

// 1번째 사람이 생성되었습니다.
// 2번째 사람이 생성되었습니다.
// 현재까지 생성된 총 인원 수는 2명입니다.


  • 위의 코드에서는 정적 멤버 변수인 person_count_의 값을 출력하기 위해서 정적 멤버 함수 person_count()를 선언한다.


3) 상수 멤버 변수

  • 상수 멤버 변수란 한 번 초기화하면, 그 값을 변경할 수 없는 멤버 변수를 의미한다.
  • 이러한 상수 멤버 변수는 변수의 타입 앞에 const 키워드를 사용하여 선언한다.


const 타입 멤버변수이름;


  • 클래스 전체에 걸쳐 사용되는 중요한 상수는 상수 멤버 변수로 정의하여 사용하는 것이 좋다.


4) 상수 멤버 함수

  • 상수 멤버 함수란 호출한 객체의 데이터를 변경할 수 없는 멤버 함수를 의미한다.
  • 이러한 상수 멤버 함수는 함수의 원형 마지막에 const 키워드를 사용하여 선언한다.


함수의원형 const;


  • 호출한 객체의 데이터를 단순히 읽기만 하는 멤버 함수는 상수 멤버 함수로 정의하는 것이 정보 보호 측면에서도 좋다.

References