우리는 객체의 생성을 목적으로 클래스를 디자인한다. 그렇다면 좋은 클래스가 되기 위한 조건으로는 어떤 것들이 있을까? 여기에는 정보은닉과 캡슐화가 있다. 이는 좋은 클래스가 되기 위한 최소한의 조건이다.
왼쪽 위의 좌표가 (0, 0)이고 오른쪽 아래의 좌표가 (100, 100)인 그림판 위의 한 점에 대한 정보를 담고 있는 클래스와 사각형에 대한 정보를 담고 있는 클래스를 만들어보자.
#include <iostream>
using namespace std;
class Point
{
public:
int x;
int y;
};
Class Rectangle
{
public:
point upLeft;
point lowRight;
public:
void showRecInfo()
{
cout << "왼쪽 위: (" << upLeft.x << ", " << upLeft.y << ")" << std::endl;
cout << "오른쪽 아래: (" << lowRight.x << ", " << lowRight.y << ")" << std::endl;
}
};
int main(void)
{
Point pos1 = {-2, 4};
Point pos2 = {5, 9};
Rectangle rec = {pos2, pos1}; // 멤버 변수 초기화
rec.showRecInfo();
return (0);
}
위 코드에는 두 가지 문제가 있다.
pos1
의 좌표가 음수위에 정의된 클래스는 이러한 실수에 대한 대비가 전혀 되어있지 않다! 문법 상으로는 문제가 없기에 프로그램을 실행할 때까지 실수를 발견할 수가 없다.
때문에 제한된 방법으로의 접근만 허용해서 잘못된 값이 저장되지 않도록 도와야 하고, 실수를 했을 때 실수가 쉽게 발견되도록 해야 한다.
위의 목적을 달성할 수 있도록 Point 클래스를 수정해보자.
#ifndef POINT_H
# define POINT_H
class Point
{
private: //멤버 변수를 private으로 선언해서 아무데서나 접근할 수 없도록 수정
int x;
int y;
public: // 메소드 정의는 Point.cpp에 작성하자.
bool initMembers(int xpos, int ypos);
int getX() const;
int getY() const;
bool setX(int xpos);
bool setY(int ypos);
}
#endif
[Point.hpp]
#include <iostream>
#include "Point.hpp"
using namespace std;
bool Point::initMembers(int xpos, int ypos)
{
if (xpos < 0 || ypos < 0)
{
cout << "범위 밖의 값 전달" << endl;
return false;
}
x = xpos;
y = ypos;
return true;
}
int Point::getX() const // const 함수에 대해서는 잠시 후에 설명
{
return x;
}
int Point::getY() const
{
return y;
}
bool Point::setX(int xpos)
{
if (xpos < 0 || xpos > 100)
{
cout << "범위 밖의 값 전달" << endl;
return false;
}
x = xpos;
return true;
}
bool Point::setY(int ypos)
{
if (ypos < 0 || ypos > 100)
{
cout << "범위 밖의 값 전달" << endl;
return false;
}
y = ypos;
return true;
}
[Point.cpp]
먼저, 멤버변수에 값을 저장하는 함수(initMembers, setX, setY)에서는 범위에서 벗어난 값을 전달하면, 에러메시지를 출력하며 값을 저장하지 못하는 형태로 정의했다. 따라서 잘못된 값이 저장되지 않을 뿐더러, 값이 잘못 전달되는 경우 출력된 메시지를 통해 문제가 있음을 확인할 수 있다.
멤버 변수를 private으로 선언하고, 해당 병수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버 변수의 접근을 유도하는 것이 바로 정보은닉이며, 이는 좋은 클래스가 되기 위한 기본 조건이 된다.
위 코드에는 아래의 함수들이 있다.
int getX() const;
bool setX(int xpos);
int getY() const;
bool setY(int ypos);
이들을 가리켜 '액세스 함수(access function)'라고 하는데, 이들은 멤버변수를 private으로 선언하면서 클래스 외부에서의 멤버변수 접근을 목적으로 정의되는 함수들이다. 이들 함수는 정의되었지만 호출하지 않는 경우도 있다.
클래스를 정의할 때 호출될 함수를 위주로 멤버함수를 구성하는 것이 맞지만, 필요할 수 있다고 판단되는 함수들도 더불어 멤버에 포함시키기도 한다.
위에서 Point.h 내에
int getX() const;
int getY() const;
와 같이 const 선언이 추가되어 있다. 이는
이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다!"
를 의미한다. 매개변수도 아니고, 지역변수도 아닌, 멤버변수에 저장된 값을 변경하지 않겠다는 선언이다. const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경하는 코드가 삽입되면, 컴파일 에러가 발생한다. 이렇게 함수를 const로 선언하면, 실수로 자신의 의도와 다르게 멤버변수의 값을 변경했을 때, 컴파일 에러를 통해서 이를 확인할 수 있다.
또한, const 함수 내에서는 const가 아닌 함수의 호출이 제한된다. const 함수 내에서 호출된 함수가 멤버 변수의 값을 수정할 위험이 있기 때문이다.
이와 유사한 기능이 하나 더 있다. 다음 코드를 살펴보자.
class ExampleFirst
{
private:
int num;
public:
void initNum(int n)
{
num = n;
}
int getNum()
{
return (num);
}
}
class ExampleSecond
{
private:
int num;
public:
void initNum(const ExampleFirst &ref)
{
num = ref.getNum(); // 컴파일 에러 발생
}
}
ExampleSecond
클래스에서 initNum
의 매개변수 ref는 'const 참조자'이다. 그런데 이를 대상으로 getNum을 호출하면 컴파일 에러가 발생한다. 이는 getNum이 const함수가 아니기 때문이다.
C++에서는 const참조자를 대상으로 값의 변경 능력을 가진 함수의 호출을 허용하지 않는다(const가 아닌 함수의 호출을 허용하지 않음!!). 그러므로 getNum
함수 함수에 const를 추가해야 에러가 해결된다.
관련 있는 함수와 변수를 하나의 클래스 안에 묶는 것
캡슐화는 감싸는 개념이다. 감싸려면 안전하게 감싸야 한다. 다시 말해 이왕이면 멤버변수가 보이지 않게 정보를 은닉해서 감싸는 것이 좋다. 그래서 캡슐화는 기본적으로 정보 은닉을 포함하는 개념이라고도 이야기 한다.
캡슐화는 일관되게 적용할 수 있는 단순한 개념이 아니고, 구현하는 프로그램의 성격과 특성에 따라 적용하는 범위가 달랒리는, 흔히 하는 말로 정답이란 것이 딱히 없는 개념이다.