[ C++ 스터디 ]

자료출처 : 열혈강의 C++ 프로그래밍
자료저자 : 윤성우

 

4. 클래스의 기본

 

★ 정보은닉 ( Information Hiding ) ★

객체를 생성하기 위해서 우리는 클래스를 디자인한다. 그렇다면 어떠한 특성을
지니는 클래스가 좋은 클래스에 해당이 될까? 기본적으로 캡슐화와 정보 은닉이라는
두 가지 특성을 충족하는 클래스를 좋은 클래스라고 할 수 있다. 그렇다면 캡슐화와
정보 은닉은 무엇을 의미하는 것일까?


< 1. 정보 은닉 ( Information Hiding )의 필요성 >

윈도우즈에 있는 그림판 프로그램을 실행하게 되면 다양한 형태의 도형들을 그릴 수
있다.


그림 4-1: 윈도우즈의 그림판

이와 같은 프로그램을 C++을 이용해서 구현한다고 가정해 보자 다양한 종류의 클래스
들이 필요 할 것이다. 그 중에서도 우리는 Point라는 이름의 클래스를 가지고 정보 은
닉에 대한 이야기를 진행해 보기로 하겠다.

Point 클래스는 마우스의 위치 정보를 저장하기 위해 정의한 클래스이다. 다음은 Point
클래스의 정의에 해당이 된다.

class Point
{

public :

int x ;
int y ;

} ;

이 클래스는 아주 간단하면서도 유용하게 사용될 것이다. 특히 사용자가 그림을 그리기
위해서 마우스를 클릭하는 경우, 그 위치 정보를 저장하는데 유용하게 사용될 수 있다.
이러한 Point 클래스는 언뜻 보기에 전혀 문제가 없어 보인다. 그렇다면 다음 예제를
보고 이야기를 계속 이어 나가자.

/* InfoHiding1 . cpp */

# include < iostream >
using std : : cout ;
using std : : endl ;
using std : : cin ;

class Point
{

public :

int x ;  // x 좌표의 범위 : 0 ~ 100 ( 1번 )
int y ;   // y 좌표의 범위 : 0 ~ 100 ( 2번 )

};

int main ( )
{

int x , y ;
cout << " 좌표 입력 : " ;
cin >> x >> y ;

Point p ;
p.x = x ;  // 문제의 원인 , 멤버 변수의 외부, 접근을 허용 ( 1번 )
p.y = y ; // 문제의 원인 ( 2번 )

cout << " 입력된 데이터를 이용해서 그림을 그림 " << endl ;
return 0 ;

}

결과

좌표 입력 : 10 , 20
입력된 데이터를 이용해서 그림을 그림



위 예제에서 1번 2번 줄에 있는 주석을 보자 x와 y값의 범위를 0 이상 100 이하로 정해
놓고 있다. 이는 프로그램을 작성하는 프로그래머가 그림을 그리는 그림판의 좌 상단의
좌표를 [ 0 , 0 ]으로 , 우 하단의 좌표를 [ 100 , 100 ]으로 정의했다는 의미가 된다.


그림 4-2: 그림판의 좌표계

이는 어디까지나 가정이지만 실제로 프로그램을 구현하다 보면 이러한 형태의 정의가
필요하기 마련이다.

실행 결과를 봐도 별 문제는 없어 보인다. 다만 마우스로부터의 좌표 입력을 대신해서
( 우리는 현재 마우스 제어가 불가능하다 ) 사용자가 키보드로부터 값을 입력하는 형
태를 취했으며 , 좌표에 해당하는 그림을 그리는 대신 간단한 메시지를 출력하는 형태
로 예제를 작성하였을 뿐이다. 그렇다면 다음과 같은 형태의 실행 결과에 대해서는
어떻게 생각하는가 ?

좌표 입력 : -20   120
입력된 데이터를 이용해서 그림을 그림

좌표 값의 범위는 0 ~ 100 사이가 되어야 유효하다. 그런데 실행 결과를 보면 범위 밖에
있는 값들이 입력되고 있다. 이러한 상황은 대부분 프로그래머의 실수에서 발생한다.
뿐만 아니라 위의 예제에서처럼 사용자의 실수에서 비롯될 수도 있다. 이러한 형태의
오류는 다양한 상황에서 발생할 수 있는 것이다.

여기서는 오류의 발생 자체를 문제 삼자는 것이 아니다. 오류는 언제든 발생할 수 있다.
그보다 더 큰 문제는 컴퓨터가 위의 예제를 오류 없는 코드로 인신한다는 것이다.
생각해 보자 int 형 변수가 지닐 수 있는 값의 범위는 어떻게 되는가 ? 컴퓨터의 입장에
서는 -20 과 120의 입력이 전혀 이상할 것이 없다.

따라서 컴파일하는 동안에도, 프로그램을 실행하는 동안에도 에러 메세지는 고사하고,
그 흔한 경고 메시지 하나 볼 수 없다. 이것이 바로 문제다.

물론 프로그램의 실행 결과를 통해서 무엇인가 잘못되었다는 것일 인식할 수는 있을 것
이다. 그렇다면 그 다음은 어디서부터 시작을 해야 하겠는가? 상황은 이보다도 더 나쁘
게 진행될 수도 있다 ( 프로그램은 잘못되었는데 실행 결과가 그럴듯해 보인다. 그래서
오류가 발생했음 조차도 인식하지 못하는 상황 ) , 프로그램이 복잡하다면 문제는 더욱
더 심각할 것이다.

그렇다면 이러한 문제의 원인이 되는 것은 무엇인가? 그것은 객체의 외부에서 객체 내에
존재하는 멤버 변수에 직접 접근할 수 있는 권한을 주었다는데 있다. 위의 예제 1번 2번
에 해당되는 이야기이다. 따라서 해결책은 의외로 간단하다. 객체의 외부에서 객체 내에
존재하는 멤버 변수(정보)에 직접 접근하는 것을 허용하지 않으면 된다. 이것이 바로 정
보 은닉이다.


< 2. 정보 은닉의 적용 >

지금까지 정보 은닉의 필요성에 대해서 언급하였다. 그렇다면 위의 예제에서 등장하는
Point 클래스는 어떻게 정보를 은닉할 수 있겠는가? 모든 멤버 변수를 private으로 선
언하는 것이다. 단 , private으로 선언하고 아무런 접근 경로도 제공해 주지 않으면, 객
체의 멤버 변수를 이용하지 못하게 되므로, 멤버 변수에 접근할 수 있는 멤버 함수를 추
가로 제공해야 할 것이다. 다음 예제를 통해서 보다 자세히 설명하겠다.

/*

InfoHiding2.cpp

*/

#include < iostream >

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

class Point  // ( 1번 )
{

int x ;  // ( 2번 )
int y ;  // ( 3번 )

public :

int GetX ( ) { return x ; }  // ( 4번 )
int GetY ( ) { return y ; }  // ( 5번 )
void SetX ( int _x ) { x=_x }  // ( 6번 )
void SetY ( int _y ) { y=_y }  // ( 7번 )

} ;

int main ( )
{

int x , y ;
cout << " 좌표 입력 : " ;
cin >> x >> y ;

Point p ;
p.SetX ( x ) ;
p.SetY ( y ) ;

cout << " 입력된 데이터를 이용해서 그림을 그림 " << endl ;
return 0 ;

}

실행결과

좌표 입력 : -20  120
입력된 데이터를 이용해서 그림을 그림


멤버 변수의 선언이 시작되는 2번을 보면 아무런 선언도 존재하지 않는다 ( private 인지
public 인지 ) 이런 경우 기본적으로 ( Default로 ) private으로 인식을 한다. 따라서
2번 3번에 선언된 멤버들은 private 멤버가 된다. 그러나 만약에 1번에 있는 선언이 " class
Point " 가 아니라 " struct Point " 였다면 기본적으로 ( Default로 ) public 멤버가 된다
이것이 바로 C++ 에서의 class 와 struct의 유일한 차이점이다.

4 , 5 , 6 , 7번은 멤버 변수 x와 y에 접근할 수 있는 함수들을 정의하고 있다. public 멤버
이므로 외부에서 접근이 가능할 뿐만 아니라 , Point 클래스의 멤버 변수 x , y에 간접적인
접근이 가능해진다.

참고 ) 멤버 함수의 이름 중에서 Get 혹은 Set으로 시작되는 함수들을 대부분 멤버 변수의
접근을 위한 것이다. 이러한 함수들을 보통은 액세스 ( access ) 메소드 ( 함수 )라고 부른다.

위의 예제에서 정의하고 있는 Point 클래서는 정보 은닉을 적절히 수행하고 있는가 ? 외관
상으로 보면 분명히 정보 은닉이 이뤄졌다. 멤버 변수의 직접적인 접근을 허용하고 있지는
않고 있기 때문이다. 그러나 다음과 같은 예제의 실행이 여전히 허용되고 있다.

이번에도 우리는 오류가 발생했음에 대한 경고조차 받아 보지 못했다. 그러나 가능성은 보
인다. 함수를 통해서 접근하게끔 한다는 것은 인자로 전달된 값을 이용해서 경계 검사를 할
수 있다는 뜻이기 때문이다. 다음 예제는 우리가 원했던 적절한 정보 은닉이 무엇인지를 보
여준다. 즉 결론에 해당한다.

/*

InforHinding3.cpp

*/

#include < iostream >

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

class Point
{

int x ;
int y ;

public :

int GetX ( ) { return x ; }
int GetY ( ) { return y ; }

void SetX ( int _x ) ;  // void SetX ( int ) ;  ( 1번 )
void SetY ( int _y ) ;  // void SetY ( int ) ;  ( 2번 )

} ;

void Point : : SetX ( int _ x )  // ( 3번 )
{

if ( _x < 0 | | _x > 100 )  // 경계 검사
{

cout << "  x 좌표 입력 오류 , 확인 요망 " << endl ;
return ;

}
x= _ x ;

}

void Point : : SetY ( int _ y )  // ( 4번 )
{

if ( _y < 0 | | _y > 100 )  // 경계 검사
{

cout << "  y 좌표 입력 오류 , 확인 요망 " << endl ;
return ;

}
y= _ y ;

}

int main ( )
{

int x , y ;
cout << " 좌표 입력 : " ;
cin >> x >> y ;

Point p ;
p.SetX ( x ) ;
p.SetY ( y ) ;

cout << " 입력된 데이터를 이용해서 그림을 그림 " << endl ;
return 0 ;

}

실행 결과

좌표 입력 : -20  120
x 좌표 입력 오류 , 확인 요망
y 좌표 입력 오류 , 확인 요망
입력된 데이터를 이용해서 그림을 그림


3번 과 4번에는 SetX 와 SetY 함수에 대한 정의가 존재한다. 인자로 전달된 값이 정의된
범위 ( 0이상 100이하 )를 벗어하는 경우 , 오류 메시지를 출력하고 나서는 바로 리턴하고
있음을 알 수 있다. 즉 함수를 통한 간접 접근의 경우 이렇게 경계 검사에 관련된 코드를
삽입할 수 있다.

위 실행 결과를 보면 잘못된 입력에 대한 경고 메시지를 출력해 주고 있음을 볼 수 있다.
물론 값의 입력도 허용하지 않는다.

이제 정보 은닉에 대한 기본 개념을 이해하였을 것이다. 사실 정보 은닉은 이해하기에도
적용하기에도 어렵지 않은 개념이다. 다음에 이야기할 캡슐화에 비하면 말이다.

참고 ) 멤버 함수의 정의를 클래스 외부로 빼낼 경우 , 클래스 내에 존재하는 멤버 함수의
선언에서는 매개 변수의 타입과 개수에 대한 정보만 담고 있어도 된다. 즉 위 예제의
1번 2번은 다음과 같이 선언되어도 무방하다.

void SetX ( int ) ;
void SetY ( int ) ;

★ 캡슐화 ( Encapsulation ) ★

캡슐화와 정보 은닉을 거의 유사한 개념으로 받아들이는 경우가 많다 . 캡슐화를
설명하면서 정보은닉이 캡슐화의 한 형태라고 설명하는 경우가 많기 때문에 어쩌면
이는 당연한 것일지도 모른다. 그러나 엄밀히 따지면 캐슐화와 정보 은닉은 서로 다른
개념이다. 그렇다면 캡슐화란 무엇을 의미 하는 것일까 ?

< 1. 캡슐화의 기본 개념 >

일단 캡슐화에 대한 정의부터 내려보자 캡슐화란 ? " 관련 있는 데이터와 함수를
하나의 단위로 묶는 것이다 " 라고 정의할 수 있다. 다시 풀어서 이야기하면 , " 관련
있는 데이터와 함수를 클래스라는 하나의 캡슐 내에 모두 정의하는 것 " 이라고 이야기
할 수 있다. 이렇듯 기본 개념은 오히려 정보 은닉보다도 간단하다. 다음 예제를 보자.
그리고 캡슐화가 적절히 잘 이뤄져 있는지 함께 판단해 보기로 하자.

/*

Encapsulation1.cpp

*/

#include < iostream >

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

class Point
{

int x ;
int y ;

public :

int GetX ( ) { return x ; }
int GetY ( ) { return y ; }

void SetX ( int _x ) ;
void SetY ( int _y ) ; 

} ;

void Point : : SetX ( int _ x ) 
{

if ( _x < 0 | | _x > 100 )
{

cout << "  x 좌표 입력 오류 , 확인 요망 " << endl ;
return ;

}
x= _ x ;

}

void Point : : SetY ( int _ y )
{

if ( _y < 0 | | _y > 100 )
{

cout << "  y 좌표 입력 오류 , 확인 요망 " << endl ;
return ;

}
y= _ y ;

}

class PointShow  // ( 1번 )
{

public :

void ShowData ( Point p )  // void ShowData ( Point & p ) 2번
{

cout << " x 좌표 : " << p.GetX ( ) << endl ;
cout << " y 좌표 : " << p.GetY ( ) << endl ;

}

} ;

int main ( )
{

int x , y ;
cout << " 좌표 입력 : " ;
cin >> x >> y ;

Point p ;  ( 3번 )
p.SetX ( x ) ;
p.SetY ( y ) ;

PointShow show ;  // ( 4번 )
show . ShowData ( p ) ;  // ( 5번 )

return 0 ;

}

실행 결과

좌표 입력 : 20  70
x 좌표 : 20
y 좌표 : 70


1번을 보자 Point 클래스 내에 멤버 변수 x , y를 출력할 함수가 존재하지 않는 관계로 이
러한 역할을 대신하기 위한 클래스 PointShow를 등장시키고 있다.

PointShow 클래스는 ShowData라는 이름의 멤버 함수 하나만 지니고 있다 ( 2번 ) 이 함
수는 인자로 Point 객체를 전달받는다. 그리고 인자로 전달된 객체가 지니고 있는 멤버 변
수의 값을 참조해서 적절한 출력을 보여 준다.

4번에서 PointShow 객체를 생성한 다음 멤버 함수 ShowData를 호출하면서 ( 5번쨰 줄)
3번에서 생성한 Point 객체를 인자로 전달하고 있다. 이 과정을 그림을 설명하면 다음과
같다.


그림 4-3 : 객체의 인자 전달

C 언어를 공부하면서 구조체 변수를 함수의 인자로 전달했던 것을 기억할 것이다.
그것과 차이가 없음을 알 수 있다. 참고로 만약에 전달 인자를 레퍼런스로 받을 경우
위의 그림이 어떻게 달라질지에 대해서 생각해 보기 바란다.

위 예제는 적절히 캡슐화되었다고 말할 수 있을까 ? 실행해 보면 잘 돌아가고 대충 코드도
분석해 보면 문제 없어 보인다. 그러나 적절한 형태로 캡슐화되어 있지는 않다. 캡슐화란
관련 있는 데이터와 함수를 하나의 클래스로 정의하는 것이라고 하였다. 그런데 위의
예제에서 보면 x , y 좌표에 관련된 데이터와 함수가 두 개의 클래스로 양분되어 있다.
따라서 캡슐화에 실패한 예제라고 볼 수 있다.