객체지향의 중요 요소인 정보은닉과 캡슐화에 대해 이야기하겠습니다.
정보은닉
우리는 객체의 생성을 목적으로 클래스를 디자인합니다. 좋은 클래스가 되기 위한 조건으로는 어떤 것이 있을까요? 여기에는 정보은닉과 캡슐화가 있습니다.
#pragma warning(disable:4996) #include<iostream> using namespace std; class Point { public: int x; int y; }; class Rectangle { public: Point upLeft; Point lowRight; public: void ShowRecInfo() { cout << "Left upper: " << '[' << upLeft.x << ", "; cout << upLeft.y << ']' << endl; cout << "Right lower: " << '[' << lowRight.x << ", "; cout << lowRight.y << ']' << endl << endl; } }; int main(void) { Point pos1 = { -2, 4 }; Point pos2 = { 5, 9 }; Rectangle rec = { pos2, pos1 }; rec.ShowRecInfo(); return 0; } |
점을 표현한 Point 클래스의 멤버변수 x, y는 public 으로 선언되어서 어디서든 접근이 가능합니다. Rectangle 클래스는 직사각형을 표현한 것입니다. 직사각형은 두 개의 점으로 표현이 가능하므로, 두 개의 Point 객체를 멤버로 두었습니다. 클래스의 멤버로 객체를 둘 수 있습니다. 직사각형을 표현할 두 개의 Point 객체를 생성하였습니다. 이렇듯 멤버 변수가 public 으로 선언되면, 구조체 변수를 초기화하듯이 초기화가 가능해집니다. 위의 생성한 Point 객체를 이용하여 Rectangle 객체를 생성 및 초기화하였습니다. 객체를 생성하고 초기화하는 문장입니다. 즉, 미리 생성해 놓은 두 개의 Point 객체에 저장된 값이 Rectangle 객체의 멤버에 대입이 됩니다. 점의 좌표는 0이상 100이하가 되어야 하는데, 그렇지 못한 Point 객체가 있습니다. 직사각형을 의미하는 Rectangle 객체의 좌 상단 좌표 값이 우 하단 값보다 큽니다. 제한된 방버으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와야 하고, 또 실수를 했을 경우, 실수가 쉽게 발견되도록 해야 합니다.
멤버변수 x와 y를 private 으로 선언해서 임으로 값이 저장되는 것을 막았습니다. 즉, x와 y라는 정보를 은닉한 상황입니다. 대신에 값의 저장 및참조를 위한 함수를 추가로 정의하였습니다. 따라서 이 함수내에서 멤버변수에 저장되는 값을 제한할 수 있게 되었습니다.
#ifndef __POINT_H_ #define __POINT_H_ class Point { private: int x; int y; public: bool InitMembers(int xpos, int ypos); int GetX() const; int GetY() const; bool SetX(int xpos); bool SetY(int ypos); }; #endif |
#include <iostream> #include "Point.h" using namespace std; bool Point::InitMembers(int xpos, int ypos) { if (xpos < 0 || ypos < 0) { cout << "error." << endl; return false; } x = xpos; y = ypos; return true; } int Point::GetX() const { return x; } int Point::GetY() const { return y; } bool Point::SetX(int xpos) { if (0 > xpos || xpos > 100) { cout << "error" << endl; return false; } x = xpos; return true; } bool Point::SetY(int ypos) { if (0 > ypos || ypos > 100) { cout << "error" << endl; return false; } y = ypos; return true; } |
멤버변수에 값을 저장하는 함수는 지정된 범위 내에 값이 전달되지 않으면, 에러 메시지를 출려가면서 값의 저장을 허용하지 않는 형태로 정의되어 있습니다. 따라서 잘못된 값이 저장되지 않을뿐더라, 값이 잘못 전달되는 경우 출련된 메시지를 통해서 문제가 있음을 확인 할 수 있습니다. 멤버변수 private 으로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버 변수의 접근을 유도하는 것이 바로 정보 으닉이며, 이는 좋은 클래스가 되기 위한 기본조건이 됩니다.
액세스 함수라 하는데, 이들은 멤버 변수를 private 으로 선언하면서 클래스 외부에서의 멤버 변수 접근을 목적으로 정의되는 함수들입니다. 이들 함수는 정의되었으되 호출되지 않는 경우도 많습니다. 클래스를 정의할 때 호출될 함수들 위주로 멤버함수를 구성하는 것은 분명 맞습니다. 그러나 클래스의 정의과정에서 지금 당장은 필요하지 않지만, 필요할 수 있다고 판단되는 함수들도 더불어 멤버에 포함시키는 경우도 많습니다. 대표적인 예가 위에서 보인 엑세스 함수들입니다. 그러니 이후에 필자가 제시하는 클래스들 중에서 호출되지 않지만 삽입된 함수가 있다면, 이러한 맥락에서 이해하면 됩니다.
#ifndef __RECTANGLE_H_ #define __RECTANGLE_H_ #include "Point.h" class Rectangle { private: Point upLeft; Point lowRight; public: bool InitMembers(const Point &ul, const Point &lr); void ShowRecInfo() const; }; #endif
|
#include<iostream> #include"Rectangle.h" using namespace std; bool Rectangle::InitMembers(const Point &ul, const Point &lr) { if (ul.GetX() > lr.GetX() || ul.GetY() > lr.GetY()) { cout << "error" << endl; return false; } upLeft = ul; lowRight = lr; return true; } void Rectangle::ShowRecInfo() const { cout << "Left upper: " << '[' << upLeft.GetX() << ", "; cout << upLeft.GetY() << ']' << endl; cout << "Right lower: " << '[' << lowRight.GetX() << ", "; cout << lowRight.GetY() << ']' << endl << endl; } |
#include<iostream> #include "Point.h" #include "Rectangle.h" using namespace std; int main(void) { Point pos1; if (!pos1.InitMembers(-2, 4)) cout << "error" << endl; if (!pos1.InitMembers(2, 4)) cout << "error" << endl;
Point pos2; if (!pos2.InitMembers(5, 9)) cout << "error" << endl; Rectangle rec; if (!rec.InitMembers(pos2, pos1)) cout << "error" << endl; if (!rec.InitMembers(pos1, pos2)) cout << "error" << endl; rec.ShowRecInfo(); return 0; } |
모든 초기화 함수들이 초기화의 실패 여부에 따라서 true 또는 false 를 반환하도록 정의했기 때문에, 함수를 호출한 영역에서 성공여부를 확인하고 그에 따른 조치를 취할 수 있습니다. 비록 위에서는 초기화의 실패를 알리는 목적의 문자열을 출력하는 정도로 예제를 작성했지만, 좌표정보를 프로그램 사용자로부터 입력 받는 예제였다면, 정상적인 좌표의 입력을 재 요청할 수도 있는 상황입니다.
const 함수
이 함수 내에서는 멤버 변수에 저장된 값을 변경하지 않겠습니다. 라는 뜻입니다. 매개 변수도 아니고, 지역변수도 아닌, 멤버변수에 저장된 값을 변경하지 않겠다는 선언입니다. 따라서 const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경하는 코드가 삽입되면, 컴파일 에러가 발생합니다. 이렇게 함수를 const 로 선언하면, 실수로 자신의 의도와 다르게 멤버변수의 값을 변경했을 때, 컴파일 에러를 통해서 이를 확인할 수 있습니다. 이는 프로그래머의 실수를 최소화하기 위한 매우 의미 있는 선언입니다. 실제로 함수 내에서는 메버변수 num 의 값을 변경하지 않습니다. const 함수 내에서는 const 가 아닌 함수의 호출이 제한됩니다. const로 선언되지 않은 함수는 아무리 멤버변수에 저장된 값을 변경하지 않더라도, 변경할 수 있는 능력을 지닌 함수이다. 따라서 이러한 변경이 가능한 함수의 호출을 아예 허용하지 않는 것입니다. C++ 에서는 const 참조자를 대상으로 값의 변경 능력을 가진 함수의 호출을 허용하지 않습니다.
캡슐화
#include <iostream> using namespace std; class SinivelCap { public: void Take() const { cout << "nose." << endl; } }; class SneezeCap { public: void Take() const { cout << "jaechaeggi" << endl; } }; class SnuffleCap { public: void Take() const { cout << "Snuffle" << endl; } }; class ColdPatient { public: void TakeSinivelcap(const SinivelCap &cap) const { cap.Take(); } void TakeSneezecap(const SneezeCap &cap) const { cap.Take(); } void TakeSnuffleCap(const SnuffleCap &cap) const { cap.Take(); } }; int main(void) { SinivelCap scap; SneezeCap zcap; SnuffleCap ncap; ColdPatient sufferer; sufferer.TakeSinivelcap(scap); sufferer.TakeSneezecap(zcap); sufferer.TakeSnuffleCap(ncap); return 0; } |
약의 복용을 위해서는 클래스들의 상호관계도 매우 잘 알아야 하는 상황에 놓인다. 만약에 순서가 틀어지면 원치않는 결과를 초래하기 때문입니다. 정리하면, 캡슐화가 무너지면 객체의 활용이 매우 어려워집니다. 뿐만 아니라, 캡슐화가 무너지면 클래스 상호관계가 복잡해지기 때문에 이는 프로그램 전체의 복잡도를 높이는 결과로 이어집니다.
#include <iostream> using namespace std; class SinivelCap { public: void Take() const { cout << "Sinivel." << endl; } }; class SneezeCap { public: void Take() const { cout << "Sneeze." << endl; } }; class SnuffleCap { public: void Take() const { cout << "Snuffle." << endl; } }; class CONTAC600 { private: SinivelCap sin; SneezeCap sne; SnuffleCap snu; public: void Take() const { sin.Take(); sne.Take(); snu.Take(); } }; class ColdPatient { public: void TakeCONTAC600(const CONTAC600 &cap) const { cap.Take(); } }; int main(void) { CONTAC600 cap; ColdPatient sufferer; sufferer.TakeCONTAC600(cap); return 0; } |
캡슐화가 되어있는 코 감기약 CONTAC600 클래스가 정의되어 있습니다. 이전 예제와 비교해서 ColdPatient 클래스가 매우 간결해졌음을 알 수 있습니다. 이는 클래스와 관련 있는 CONTAC600 클래스의 적절한 캡슐화의 결과입니다. 약의 복용 역시 매우 간결해졌음을 알 수 있습니다. 약의 복용 순서를 고민하지 않아도 됩니다. 문제는 어떻게 구성을 하느냐 아니고, 어떠한 내용으로 구성을 하느냐에 있습니다. 관련 있는 함수와 변수를 하나의 클래스 안에 묶는 것이 캡슐화이므로. 캡슐화의 범이를 결정하는 일이 쉽지 않기 때문입니다. 경험 많은 객체지향 프로그래머를 구분하는 첫 번째 기준은 캡슐화입니다. 캡슐화는 일관되게 적용할 수 있는 단순한 개념이 아니고, 구현하는 프로그램의 성격과 특성에 따라서 적용하는 범위가 달라지는, 정달이란 것이 딱히 없는 개념이기 때문입니다. 캡슐하는 감싸는 개념입니다. 감싸려면 안전하게 감싸야 합니다. 이왕이면 멤버 변수가 보이지 않게 정보를 은닉해서 감싸는 것이 좋습니다. 그래서 캡슐화는 기본적으로 정보 은닉을 포함하는 개념이라고도 이야기 합니다.