SH1R0_HACKER

Chapter04 클래스의 완성 (작성중) 본문

Programming Language/C++

Chapter04 클래스의 완성 (작성중)

SH1R0_HACKER 2020. 10. 18. 15:29

 

Part02 객체지향의 도입

 

Chapter04 클래스의 완성

04-1 정보은닉 (Information Hiding)

- 우리는 객체의 생성을 목적으로 클래스를 디자인한다. 좋은 클래스가 되기 위한 최소한의 조건에는 '정보은닉'과 '캡슐화'가 있다. 먼저 '정보은닉'부터 알아보도록 하자.

 

제한된 방법으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와야 하고, 또 실수를 했을 때, 실수가 쉽게 발견되도록 해야 한다. 어떻게 해야 이러한 것이 가능하겠는가?

 

"멤버변수를 private으로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버 변수의 접근을 유도하는 것이 바로 '정보은닉'이며, 이는 좋은 클래스가 되기 위한 기본 조건이 된다!"

 

< 엑세스 함수 (access function) >

- 변수의 이름이 XXX일 때, 다음과 같이 함수의 이름이 GetXXX, SetXXX로 정의된 함수들을 볼 수 있다.

int GetX() const;
int GetY() const;
bool SetX(int xpos);
bool SetY(int ypos);

 

멤버변수를 private으로 선언하면서 클래스 외부에서의 멤버변수 접근을 목적으로 정의되는 함수들이다. 이들 함수는 정의되었으되 호출되지 않는 경우도 많다.

 

 

< const 함수 >

int GetX() const;
int GetY() const;
void ShowRecInfo() const;

이 const는 다음 내용을 선언하는 것이다.

 

" 이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다!"

 

매개변수도 아니고, 지역변수도 아닌, 멤버변수에 저장된 값을 변경하지 않겠다는 선언이다. 따라서 const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경하는 코드가 삽입되면, 컴파일 에러가 발생한다. 그런데 이러한 const 함수에는 또 다른 특징이 있다.

class SimpleClass
{
private:
	int num;
public:
	void InitNum(int n)
	{
		num = n;
	}
	int GetNum()	// const 선언이 추가되어야 아래의 컴파일 에러 소멸
	{
		return num;
	}
	void ShowNum() const
	{
		cout << GetNum() << endl;	// 컴파일 에러 발생
	}
};

 

위의 클래스 정의에서 ShowNum 함수는 const 함수로 선언되었다. 그리고 실제로 함수 내에서는 멤버변수 num의 값을 변경하지 않는다. 그럼에도 불구하고 GetNum 함수를 호출하는 문장에서 컴파일 에러가 발생한다. 분명 GetNum 함수도 멤버변수의 값을 변경하지 않음에도 불구하고 에러가 발생한다. 이유는 다음과 같다.

 

"const 함수 내에서는 const가 아닌 함수의 호출이 제한된다!"

 

const로 선언되지 않는 함수는 아무리 멤버변수에 저장된 값을 변경하지 않더라도, 변경할 수 있는 능력을 지닌 함수이다. 따라서 이러한 변경이 가능한 함수의 호출을 아예 허용하지 않는 것이다.

const 참조자를 이용해서는 const 함수만 호출이 가능하다. const 선언을 한번 사용하기 시작하면, 이곳 저곳에 많이 추가될 것 같은 생각이 들겠지만 그만큼 우리가 작성한 코드의 안정성은 높아질 것이다.


04-2 캡슐화(Encapsulation)

- 캡슐화가 중요한 이유를 알아보자.

#include <iostream>
using namespace std;

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

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

class SnuffleCap	// 코막힘 처치용 캡슐
{
public:
	void Take() const { cout << "코가 뻥 뚫립니다." << 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;
}

 

위 예제에 다음 내용을 가정해보리면, 캡슐화가 무너진 대표적인 사례가 된다.

 

"코감기는 항상 콧물, 재채기, 코막힘을 동반한다"

 

다음과 같은 가정을 한다면, 위의 클래스 설계는 매우 위험한 구조가 될 수 밖에 없다.

 

"약의 복용은 반드시 SinivelCap, SneezeCap, SnuffleCap 순으로 이뤄져야 한다."

 

캡슐화가 무너지면 객체의 활용이 매우 어려워진다. 뿐만 아니라, 캡슐화가 무너지면 클래스 상호관계가 복잡해지기 때문에 이는 프로그램 전체의 복잡도를 높이는 결과로 이어진다.

 

그렇다면 이제 위의 코드에 정의된 클래스에 캡슐화를 해보자.

캡슐화는 어려운 개념이다. 왜냐하면 캡슐화의 범위를 결정하는 일이 쉽지 않기 때문이다.