티스토리 뷰

주의 사항!

  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

클래스는 객체의 생성을 목적으로 디자인합니다. 그렇다면 정말 잘 디자인된 클래스가 있을 것이고, 또 그렇지 않고 안 좋게 디자인된 클래스도 있을 것입니다. 그렇다면 좋은 클래스가 되기 위한 조건은 어떤 것들이 있을까요?

 

좋은 클래스가 되기 위한 최소한의 조건으로 '정보은닉' '캡슐화'가 있습니다.

 

정보은닉

먼저, 정보은닉에 대해 배워보겠습니다. 클래스를 선언할 때 멤버 변수를 private로 선언하면 정보은닉이 된 것입니다. public으로 선언하면 그렇지 않습니다. 그럼 왜 private을 사용한 정보은닉이 좋은 클래스의 조건이 되는 것일까요?

 

예를 들어서 설명해 보겠습니다. 2차원 평면상의 한 점을 표현하는 프로그램을 만들고 싶습니다. 단, 해당 점이 표현될 수 있는 평면의 크기는 좌하단(0, 0)에서부터 우상단(100, 100)까지의 정사각형 범위에만 해당됩니다. 따라서 (-5, 56)이나 (12, 120)과 같은 점은 표현이 불가능합니다.

 

여기서 점의 좌표 정보를 가지는 클래스 Point를 선언하는 예제를 살펴보겠습니다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

class Point
{
public:
	int x;
	int y;

	void ShowPointInfo(void)
	{
		cout << "position : (" << x << ", " << y << ")" << endl;
	}
};

int main(void)
{
	Point pos;

	pos.x = -50;
	pos.y = 150;

	pos.ShowPointInfo();

	return 0;
}

/*
실행결과

position : (-50, 150)

*/

 

위 예제를 보면 멤버 변수 x와 y가 public으로 선언되어 은닉되어있지 않습니다. 그래서 main 함수에서 x와 y에 직접적으로 접근할 수 있고, x를 -50, y를 150으로 초기화했습니다. 그런데 해당 점이 표현될 수 있는 공간은 좌하단(0, 0), 우하단(100, 100)인 정사각형 범위입니다. 해당 좌표는 이 공간을 벗어났으므로 존재할 수 없습니다. 즉, 프로그램은 정상적으로 컴파일되고, 실행도 되었지만, 존재할 수 없는 점을 표현했으므로 분명한 오류를 범하고 있습니다.

 

이런 유형의 오류는 주로 개발자의 실수로 인해서 발생합니다. 개발자는 분명 해당 점이 존재할 수 있는 공간의 범위를 알고 있었겠지만 누구든 언제든지 실수를 할 수 있으니 해당 오류를 범하게 될 수도 있습니다.

 

개발자의 실수가 곧 코드의 부정적 평가 요인이 되진 않지만, 해당 실수를 눈치조차 챌 수 없게 작성된 코드는 분명 잘못 작성된 코드입니다. 개발자가 범할 수 있는 실수를 프로그램 실행 전에 알 수 있도록 방지하는 대책이 필요합니다.

 

이번에는 Point 클래스의 멤버 변수를 private으로 선언하여 정보를 은닉시키도록 수정한 예제입니다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

class Point
{
private:
	int x;
	int y;

public:
	void ShowPointInfo(void) const;
	bool SetX(int xpos);
	bool SetY(int ypos);
	int GetX(void) const;
	int GetY(void) const;
};

void Point::ShowPointInfo(void) const
{
	cout << "position : (" << x << ", " << y << ")" << endl;
}

bool Point::SetX(int xpos)
{
	if (xpos < 0 || xpos > 100)
	{
		cout << "범위를 벗어난 좌표값입니다.\n";
		return false;
	}

	x = xpos;
	return true;
}

bool Point::SetY(int ypos)
{
	if (ypos < 0 || ypos > 100)
	{
		cout << "범위를 벗어난 좌표값입니다.\n";
		return false;
	}

	y = ypos;
	return true;
}

int Point::GetX(void) const
{
	return x;
}

int Point::GetY(void) const
{
	return y;
}

int main(void)
{
	Point pos;

	pos.SetX(120);
	pos.SetY(32);

	pos.ShowPointInfo();

	return 0;
}

/*
실행결과

범위를 벗어난 좌표값입니다.
position : (-858993460, 32)

*/

 

Point 클래스의 멤버 변수를 private로 선언하게 되면서, 해당 변수에 접근할 수 있는 멤버 함수들이 선언되었습니다. GetX 함수는 x값을 가져오는 기능을 가지며, SetX 함수는 x값을 설정하는 기능을 가지고 있습니다. GetY 함수와 SetY 함수도 같은 역할을 수행합니다. 이렇게 변수의 이름이 XXX일 때, GetXXX, SetXXX라는 이름으로 정의된 함수들을 '액세스 함수'라고 부릅니다.

 

x가 private으로 정보 은닉되어 있으므로 이 값을 설정하기 위해서는 SetX함수를 호출해야만 합니다. main 함수를 보면 SetX함수를 호출하고 x의 값으로 120을 주고 있습니다. 하지만 이 값은 점이 존재할 수 있는 범위를 넘어선 값입니다. 실행결과를 보면 SetX는 "범위를 벗어난 좌표값입니다." 경고 메시지를 출력했고, 해당 값을 x에 저장하지 않았습니다. 그래서 점의 좌표를 출력할 때 x변수에 남아있던 쓰레기 값이 출력되었습니다.

 

이처럼 멤버 변수를 private로 선언하여 정보를 은닉하고, 액세스 함수를 통해 접근하도록 하면, 개발자의 실수를 미연에 방지할 수 있습니다.

 

const 함수

또 위의 예제를 보면 GetX 함수 옆에 const가 붙어 있습니다. 이런 함수를 const 함수라고 부릅니다. const 함수에서 const는 "이 함수 내에서는 멤버 변수에 저장된 값을 변경하지 않겠다!" 하는 선언과 같습니다. 매개변수도, 지역변수도 아닌, 오로지 멤버 변수에 대해서만 저장된 값을 변경하지 않습니다. const 선언이 추가된 함수 내에서 멤버 변수의 값을 변경하는 코드가 삽입되면 컴파일 과정에서 에러가 발생합니다. 따라서 const 선언이 개발자의 실수를 방지해 줄 수 있습니다. const 선언도 '정보은닉'과 같은 맥락에서 좋은 클래스의 조건이 될 수 있을 것 같습니다.

 

const 함수에는 또 다른 특징이 있습니다. 다음의 예를 통해서 확인해보겠습니다.

class SimpleClass
{
private:
	int num;

public:
	void InitNum(int n)
	{
		num = n;
	}

	int GetNum()
	{
		return num;
	}

	void ShowNum() const
	{
		cout << GetNum() << endl;    //여기서 오류 발생
	}
};

 

위 예에서 왜 오류가 발생하는지 살펴보겠습니다. 오류가 발생한 시점은 ShowNum 안에서 GetNum 함수를 호출할 때입니다.

 

ShowNum 함수는 const로 선언되어 있습니다. 따라서 이 함수 내에서는 멤버 변수에 저장된 값을 절대 변경할 수 없습니다. 그런데 이 함수 안에서 호출되는 GetNum 함수는 const로 선언되지 않았습니다. 즉, GetNum 함수 내에서는 멤버 변수의 값이 변경될 수 있습니다.

 

만약 ShowNum 함수 안에서 호출된 GetNum 함수 안에서 멤버 변수 num의 값을 변경하려는 코드를 삽입한다면, 프로그램은 멤버 변수 num의 값을 변경해야 할까요? 하지 말아야 할까요??

 

이제 문제를 이해했을 것입니다. 실제로 GetNum 함수 안에서 멤버 변수의 값이 변경되지 않는다고 해도, GetNum 함수는 멤버 변수의 값을 변경시킬 가능성을 가지고 있는 함수이기 때문에 컴파일러는 오류를 발생하게 됩니다. 해당 오류를 잡기 위해서는 GetNum 함수가 ShowNum 함수에 대해 확실한 아군임을 증명하기 위해 GetNum 함수에도 const를 선언해 주어야 합니다.

 

캡슐화

캡슐화라는 말을 들으면 무언가'들'을 감싸는 것을 쉽게 떠올릴 수 있습니다. 뭔가 관련이 있는 여럿을 하나로 묶어 캡슐로 만드는 것을 생각할 수 있습니다. 캡슐화의 이해를 돕기 위해 감기와 약을 예로 들어서 설명해 보겠습니다.

 

감기와 관련한 약은 여러 가지가 있습니다. 이 중 다음 세 가지 약을 먼저 생각해보겠습니다.

  • 막힌 코를 뻥 뚫어주는 코막힘약
  • 콧물이 나오는 증상을 완화해주는 콧물약
  • 재채기를 멎게 해주는 재채기 약

 

만약 감기가 걸렸고, 다른 증상은 없이 콧물만 계속 나온다면 콧물약 하나만 복용하면 됩니다. 그러지 않고 콧물이 나오면서 재채기까지 동반된다면 콧물 약과 재채기 약 두 개를 복용하면 됩니다. 만약 코가 막히기까지 한다면 위 약 세 개 모두 복용하면 됩니다.

 

그런데 감기에 걸리면 보통은 위 세 가지 증상이 모두 나타난다고 볼 수 있을 것 같습니다. 그리고 그런 상황에서 세 번에 거쳐 위 약들을 복용하는 것이 불편하다고 생각됩니다. 그래서 위 세 가지 증상을 모두 완화해줄 수 있는 약 하나를 찾고 있습니다.

 

소비자의 이러한 니즈가 반영되면 제약회사는 코막힘 약의 성분, 콧물약의 성분, 재채기 약의 성분을 하나의 캡슐에 담아 새로운 약을 제조할 수 있을 것입니다.

 

클래스의 캡슐화는 바로 이런 개념입니다. 앞으로 프로그래밍을 하면서 여러 클래스를 다루게 될 것입니다. 그러다 보면 어느 클래스의 멤버 함수를 호출할 때, 그와 연관해서 다른 클래스들의 멤버 함수를 연달아 호출해야 하는 경우도 생기게 마련입니다. 감기가 걸리면 통상적으로 코가 막히고, 콧물이 나오며, 재채기도 동반되기 때문에 코막힘 약을 먹고, 콧물약을 먹고, 재채기 약을 먹어야 하는 경우가 이런 경우입니다.

 

'코막힘 약' 클래스의 '약 복용' 함수를 호출하고, '콧물약' 클래스의 '약 복용' 함수를 호출하고, '재채기 약' 클래스의 '약 복용' 함수를 호출하는 과정이 복잡하고 불편하기 때문에 '일반 감기약' 클래스의 '약 복용' 함수에 위 함수들을 호출하는 기능을 넣는 것입니다. 그렇게 되면 약 세 개를 복용하지 않고 '일반 감기약' 클래스의 '약 복용' 함수만 호출하면 됩니다.

 

예제를 통해 확인해보겠습니다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

class SnuffleCap    //코막힘약
{
public:
	void Take(void) const
	{
		cout << "코가 뻥 뚫립니다." << endl;
	}
};

class SinivelCap    //콧물약
{
public:
	void Take(void) const
	{
		cout << "콧물이 싹~ 납니다." << endl;
	}
};

class SneezeCap    //재채기약
{
public:
	void Take(void) const
	{
		cout << "재채기가 멎습니다." << endl;
	}
};


class ColdPatient    //감기환자
{
public:
	void TakeSnuffleCap(const SnuffleCap& cap) const
	{
		cap.Take();
	}

	void TakeSinivelCap(const SinivelCap& cap) const
	{
		cap.Take();
	}

	void TakeSneezeCap(const SneezeCap& cap) const
	{
		cap.Take();
	}
};

int main(void)
{
	SnuffleCap ncap;
	SinivelCap scap;
	SneezeCap zcap;
	ColdPatient sufferer;

	sufferer.TakeSnuffleCap(ncap);
	sufferer.TakeSinivelCap(scap);
	sufferer.TakeSneezeCap(zcap);

	return 0;
}

/*
실행결과

코가 뻥 뚫립니다.
콧물이 싹~ 납니다.
재채기가 멎습니다.

*/

 

위 예제의 main함수를 보면 약 세 개를 한 번씩 총 3번에 걸쳐서 복용하는 것을 볼 수 있습니다. 다음은 캡슐화가 이루어진 다음 예제입니다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

class SnuffleCap    //코막힘약
{
public:
	void Take(void) const
	{
		cout << "코가 뻥 뚫립니다." << endl;
	}
};

class SinivelCap    //콧물약
{
public:
	void Take(void) const
	{
		cout << "콧물이 싹~ 납니다." << endl;
	}
};

class SneezeCap    //재채기약
{
public:
	void Take(void) const
	{
		cout << "재채기가 멎습니다." << endl;
	}
};

class TotalCap
{
private:
	SnuffleCap snu;
	SinivelCap sin;
	SneezeCap sne;

public:
	void Take(void) const
	{
		snu.Take();
		sin.Take();
		sne.Take();
	}
};


class ColdPatient    //감기환자
{
public:
	void TakeSnuffleCap(const SnuffleCap& cap) const
	{
		cap.Take();
	}

	void TakeSinivelCap(const SinivelCap& cap) const
	{
		cap.Take();
	}

	void TakeSneezeCap(const SneezeCap& cap) const
	{
		cap.Take();
	}

	void TakeTotalCap(const TotalCap& cap) const
	{
		cap.Take();
	}
};

int main(void)
{
	TotalCap total;
	ColdPatient sufferer;

	sufferer.TakeTotalCap(total);

	return 0;
}

/*
실행결과

코가 뻥 뚫립니다.
콧물이 싹~ 납니다.
재채기가 멎습니다.

*/

 

위 예제를 보면 TotalCap 클래스가 추가로 선언되었습니다. 그리고 해당 클래스의 멤버 함수를 보면 다른 나머지 세 개 클래스의 멤버 함수들을 호출하고 있습니다. 이로써 TotalCap 클래스의 Take 함수만 호출하면 약 세 개를 한 번에 복용할 수 있게 되었습니다. 그리고 ColdPatient 클래스를 보면 네 번째에 TakeTotalCap 함수가 추가로 선언되었습니다.

 

main 함수를 보면 이전 예제와는 확연하게 간단해진 모습을 확인할 수 있습니다. 약 세 개를 각각 한 번씩 복용할 필요 없이 TotalCap만 한 번 복용하면 되기 때문에 TotalCap 클래스 외 나머지 클래스의 선언은 필요하지 않습니다. 그리고 약의 복용도 TakeTotalCap 함수를 한 번 호출하는 것으로 마칩니다.

 

캡슐화는 간단해 보이지만 생각보다 어려운 개념입니다. 캡슐화가 어려운 이유는 어떤 클래스의 멤버 함수까지를 하나의 캡슐로 묶어야 하는지 프로그램의 기능과, 주어진 환경 등등의 영향을 받아 정해진 답이 없기 때문입니다.

 

감기약 복용만 하더라도 위 예에서는 코막힘 약, 콧물약, 재채기 약을 하나의 캡슐로 묶었지만 만약 감기의 통상적인 증상으로 코막힘이 없다고 한다면 해당 캡슐화는 제대로 되었다고 보기 힘듭니다. 또, 감기의 통상적인 증상으로 두통, 발열 등등의 증상들도 나타난다고 한다면 이 또한 캡슐화가 제대로 이루어졌다고 볼 수 없습니다. 그리고 약의 복용 순서도 코막힘 약을 반드시 마지막에 복용해야 한다면 위 캡슐화는 정말 최악으로 수행된 것입니다.

 

그래도 이 정도는 약과입니다. 비록 캡슐화가 잘못되었다고 해도 나름 정답은 있으니 코막힘 약 순서를 맨 마지막으로 옮기고, 두통과 발열까지도 캡슐화를 시키는 등 쉽게 수정할 수는 있습니다. 그런데 다음과 같은 경우에는 캡슐화를 진행하기가 매우 까다로울 것입니다.

  • 보통 감기에 걸리면 매우 높은 확률로 재채기를 하고 콧물이 나온다
  • 콧물이 나오는 환자는 코가 같이 막힐 확률이 높다
  • 매우 높은 확률은 아니지만 그래도 두통을 호소하고 발열 증상을 보이는 경우도 흔하다
  • 낮은 확률로 몸살 증상도 발생할 수 있다
  • 몸살이 발생하면 반드시 발열과 두통이 뒤따른다 이때는 콧물이 나오지 않을 확률이 높다
  • 두통약을 먹은 사람은 코막힘 약을 먹으면 안 된다.

 

이렇듯 캡슐화에는 정해진 답이 없습니다. 클래스를 캡슐화시키는 능력은 오랜 시간을 두고 다양한 사례를 접하면서 길러져야 합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함