read

여덟번째 열혈강의


computer


Encapsulation


이전 포스팅을 한 뒤 학교 일도 있고 이것저것 하다보니 포스팅이 늦어졌다. 이전에 했던 것들을 다시 복습 한 뒤 시작하도록 하자.

오늘은 캡슐화에 대해 알아 볼 것이다.

정보은닉에 대한 내용이 나왔고, 이 것에 있어서 캡슐화는 빼놓을 수 없다. 객체지향의 클래스 설계에서 가장 기본이자 가장 중요한 원칙이기 때문이다.

이 도서에서는 실제 시중의 알약을 들어 캡슐화를 설명하고 있다. 감기 알약을 생각하면 좋다.

콘택600이라는 약이 있다고 하자. (실제로 존재했다고 한다.)

이 알약의 기능은 재채기, 콧물, 코막힘의 완화다. 그런데 만약 재채기용 캡슐, 콧물용 캡슐, 코막힘용 캡슐로 나눠져 있다면, 그래서 코감기에 걸렸을 때 총 3알의 약을 먹어야 한다면 이는 캡슐화가 성립되지 않는 상황이다.

그러나 위 알약은 코감기의 강력한 처방이라는 목적으로 둘 이상의 기능이 모여 하나의 목적을 달성하고 있다. 이 것은 캡슐화가 되어있는 상황이다.

그럼 예제로 설명을 더해 보자.

#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() {
	SinivelCap scap;
	SneezeCap zcap;
	SnuffleCap ncap;

	ColdPatient sufferer;
	sufferer.TakeSiniveLCap(scap);
	sufferer.TakeSneezeCap(zcap);
	sufferer.TakeSnuffleCap(ncap);
	return 0;
}

여기서 만약 다음 가정이 들어간다면 캡슐화는 깨지게 된다.

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

먼저 눈으로 확인 가능한 문제점 부터 보자.

복용의 절차가 너무 복잡하다. 코감기는 항상 콧물, 재채기, 코막힘을 동반하므로 항상

sufferer.TakeSiniveLCap(scap);

sufferer.TakeSneezeCap(zcap);

sufferer.TakeSnuffleCap(ncap);

세개의 과정을 거쳐야 한다. 근데 만약 콘택600처럼 하나의 캡슐로 만들었다면 (캡슐화했다면) 복용 과정이 매우 간단해진다.

만약에 가정을

약의 복용은 반드시 SinivelCap, SneezeCap, SnuffleCap 순으로 해야 한다.`

라고 한다면 이런 클래스 설계는 매우 위험해진다.

그럼 이제 약의 복용을 위해서는 3가지 클래스의 상호관계도 잘 알아야 하는 상황이 되었다.

순서가 틀어지면 부작용이 생기게 되기 때문이다.

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

그럼 예제를 보자.

#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 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() {
	CONTAC600 cap;
	ColdPatient sufferer;
	sufferer.TakeCONTAC600(cap);
	return 0;
}

실행결과는 위와 같다. 참고로, 캡슐화를 한다고 해서 굳이 하나의 클래스로만 모든 것을 구성하는 건 아니다. 이처럼 다른 클래스를 활용해도 된다. CONTAC600 클래스가 SinivelCap, SneezeCap, SnuffleCap 객체를 멤버로 둔게 보이듯이 말이다.

문제는 어떻게 구성하냐가 아니라 어떠한 내용으로 구성하냐 이다.

그럼 다음 질문을 보자.

제대로 캡슐화하려면 기침, 몸살, 두통까지 넣어야 하지 않을까?

당연히 그럴 수 있다. 그런데 이에 앞서 하나 생각해보자.

** 캡슐화가 쉬워 보이는가? 어려워 보이는가?**

사실 위의 예시를 보면 쉬워 보일지 모르겟다. 관련 있는 함수와 변수를 하나의 클래스 안에 묶는 것이기 때문이다.

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

제대로 캡슐화 하려면 첫 질문과 같이 기침, 몸살, 두통도 넣어야 하는 가에 대해서 깊이 생각해 봐야 할 것이다. 범위에 대한 문제이기 때문이다.

참고로 할 것은

정보 은닉은 쉽다. 그러나 캡슐화는 어렵다.

경험 많은 프로그래머의 구분 기중중 첫번째가 캡슐화다. 캡슐화는 일관되게 적용 할 수 있는 단순한 개념이 아니고, 구현하는 프로그램의 성격과 특성에 따라서 적용하는 범위가 달라지는, 흔히 정답이 없는 개념이라고 하기 때문이다.

그럼 첫 질문에 대해 생각해보자.

먼저 몇가지 가정을 제시한다.

감기는 코감기, 목감기, 몸살감기가 항상 함께 동반된다.

이럴 경우 CONTAC600 클래스 역시 제대로 캡슐화가 이뤄지지 않았다고 볼 수 있다.

왜냐하면 CONTAC600이외의 클래스가 위 가정을 위한 클래스를 항시 필요로 하게 되기 때문이다.

이는 처음 보였던 예시와 비슷한 상황이다.

그럼 다음 가정을 보자.

감기는 코감기, 목감기, 몸살감기가 함께 동반되기도 하고, 개별적으로 진행되기도 한다.

아마 훨씬 더 애매해 졌을 것이다.

이 경우는 답을 내리기가 힘들다. 좀더 구체적인 정보와 가정이 필요해지고, 종합감기약이 답일 수도 있고, 목감기,코감기, 몸살감기 각각의 약이 별도로 존재하는게 답일 수 있다. 그러므로 클래스의 캡슐화는 경험이 상당히 중요하다.

캡슐화에는 정보은닉이 기본적으로 포함된다.

캡슐화는 감싸는 개념이다. 그런데 감싸려면 안전하게 해야 한다. 다시 말해 이왕이면 멤버변수가 보이지 않게 은닉을 해서 감싸는 것이 좋다. 그래서 기본적으로 정보은닉을 포함하는 개념이라고 말한다. 그렇지만 일단은 이 둘을 별개로 보고 접근해서 이해하는 것이 우선이다.


Constructor and Destructor


phone

지금까지는 객체를 생성하고 객체의 멤버변수 초기화를 목적으로 InitMembers라는 이름의 함수를 정의하고 호출했다. 아마 그동안 불편했을 것이다. 다행히 Constructor(생성자)를 이용하면 생성과 동시에 초기화가 가능하다.

간단한 예시를 보자.

class SinpleClass{
private:
	int num;
public:
	SimpleClass(int n){
    	num = n;
    }
   	int GetNum() const{
    	return num;
    }
};

위 클래스에서 다음과 같은 함수가 있다.

클래스의 이름과 함수의 이름이 동일하다.

반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.

이러한 유형의 함수를 생성자(constructor)라고 하며 객체 생성시 딱 한번 호출 되는 특징이 있다.

우리는 이전에 다음과 같이 했다.

SimpleClass sc;

SimpleClass *ptr = new SimpleClass;

여기서 생성자가 쓰인다면 파라미터가 필요하다.

Simple sc(20); //생성자에 20을 전달

SimpleClass *ptr = new SimpleClass(30); //생성자에 30을 전달

사실 생성자는 어렵지 않다. 다음 두개만 추가로 알자.

생성자도 함수의 일종이니 오버로딩이 가능하다.

생성자도 함수의 일종이니 파라미터에 디폴트값을 설정할 수 있다.

그럼 다음 예제로 확인 해보자.

#include<iostream>

using namespace std;

class SimpleClass {
private:
	int num1;
	int num2;
public:
	SimpleClass() {
		num1 = 0;
		num2 = 0;
	}
	SimpleClass(int n) {
		num1 = n;
		num2 = 0;
	}
	SimpleClass(int n1, int n2) {
		num1 = n1;
		num2 = n2;
	}

	/*
	SimpleClass(int n1=0,int n2=0){
		num1 = n1;
		num2 = n2;
	}
	*/

	void ShowData() const {
		cout << num1 << " " << num2 << endl;
	}
};

int main(void) {
	SimpleClass sc1;
	sc1.ShowData();

	SimpleClass sc2(100);
	sc2.ShowData();

	SimpleClass sc3(100, 200);
	sc3.ShowData();
	return 0;
}

먼저 위 예제 그대로 컴파일 해보면 결과를 확인 할 수 있고, 주석처리를 지우고, 파라미터가 두개인 생성자를 지우면 또다시 확인이 가능하다.

물론 그냥 주석만 지우면 어떤 생성자를 호출할지 모호하다고 나온다.

이는 이전 함수 포스팅에서 배웠던 사항이기에 넘어가도록 하겠다.

다음 처럼 생성자에 파라미터가 있다면 옆에 붙여주어 생성을 해야한다.

그러나 파라미터가 없다면, 소괄호를 생략한다.

SimpleClass sc1();

위와 같은 것도 그런데 허용해야 하는게 아닐까?

허용할 수 없는 이유를 다음 예제로 살펴보자.

그러나 혹시나 스스로의 발전을 꾀한다면 예제만 보고 밑의 설명은 왜 그런지 충분히 고민 후 보자.

#include<iostream>

using namespace std;

class SimpleClass {
private:
	int num1;
	int num2;
public:
	
	SimpleClass(int n1 = 0, int n2 = 0) {
		num1 = n1;
		num2 = n2;
	}

	void ShowData() const {
		cout << num1 << " " << num2 << endl;
	}
};

int main(void) {
	SimpleClass sc1();
	SimpleClass mysc = sc1();
	mysc.ShowData();

	return 0;
}

SimpleClass sc1() {
	SimpleClass sc(20, 30);
	return sc;
}

자 실행을 해보자.

보통 함수의 원형은 전역적으로(함수 밖에) 선언하지만, 위 예제에서 보듯, 함수 내에 지역적으로 선언 가능하다. 즉, 위와 같은 상황에서 클래스 초기화와 함수의 원형 선언 두가지가 겹치게 된다. 괄호를 쓰므로써 무엇을 지칭하는지 알기 어렵다. 그래서 C++에서는 위와 같은 상황에서는 함수의 원형 하나만 지칭하는 것으로 하고 객체 생성때는 괄호를 빼는 것으로 한다.

이후의 생성자의 활용부터는 다음 포스팅에 이어서 하겠다

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview