저자: 한동훈(
traxacun@unitel.co.kr)
벌써 하루가 지났군요. 4장에서 6장까지 읽어보라고 했는데 읽어오신 분 있나요? 예제는 해 보셨나요?
이런, 예제까지 할 시간은 없고 읽어볼 시간만 있으셨군요. 네? 클래스가 너무 어려워서 4장은 읽는 것 만으로도 벅찼다고요? 아마 그럴겁니다. C 언어만 하던 분들에게는 더더욱 어려울 것입니다. 차라리 처음부터 하시는 분들은 "클래스"라는게 있구나… 정도만 생각하셨을 테니 어려움이 없었을 테구요.
4장에는 예제가 달랑 한 개만 있습니다. 예제가 좀 길지만…음, 아마도 오타가 많아서 컴파일도 안되고, 알 수 없는 오류 메시지만 틱틱 뱉어내는 컴파일러 때문에 모니터를 한 번쯤 째려봤을 거라는 생각도 드는군요. (웃음)
1장부터 3장까지 이해가 되지 않는 분 있나요? 4장요? 아, 그건 오늘 설명할 테니, 오늘 잘 이해하세요. #if가 if랑 뭐가 다른지 모른다고요? 예, 오늘은 이 질문에 답하는 것으로 시작하도록 하죠.
자, 그러면 오늘 강의를 시작합니다.
Q&A
다행히도 질문이 있었습니다. 질문을 살펴보죠.
Q. 책을 보니 #if에 대한 것이 설명되어 있었습니다. 그런데 도대체 이걸 왜 쓰는지 이해가 잘 가지 않습니다.
A. 프로그래밍을 할 때 값에 따라서 처리를 다르게 해야 할 경우가 있습니다. 예를 들어 온라인 비디오 대여점이라면 18세 이상인 경우에는 성인용 비디오 목록도 보여주겠지만, 18세 미만에게는 성인용 비디오 목록을 보여줘서는 안되겠죠.
이런 경우에는 보통 if 문을 다음과 같은 형태로 사용합니다.
if ( customerAge > 18 )
{
// some codes here
}
else if ( customerAge < 18 )
{
// some codes here
}
이에 반해 #if는 뭔가 특별한 동작을 정의하고자 할 때 사용합니다.
위 프로그램을 실행하고 있는데 일부 사용자는 어떤 비디오 목록도 나타나지 않는다고 투덜댈 수 있습니다. 그런 경우에 Console.WriteLine(customerAge.ToString()); 이와 같은 문장을 코드 중간에 삽입해서 값을 확인해 보겠죠? (실제로 고객 나이가 18세이면 아무것도 처리되지 않으니 비디오를 고를 수가 없겠죠. -_-)
디버깅을 하기 위해 Console.WriteLine을 코드 중간 중간에 삽입하는 일은 빈번하게 일어납니다. Console.WriteLine을 사용해서 콘솔에 출력하는 것 외에 로그 파일에 쓰는 방법도 있고, 관리자에게 email로 보내는 방법도 있고, 서버의 이벤트로 전달하는 방법도 있을 겁니다. 어쨌든 다양한 디버깅 방법을 사용하기 마련이지요.
어쨌든 프로그램 개발이 모두 끝나고 나면 제품으로 판매해야 합니다. 이 경우에는 디버깅을 위해서 사용한 코드들을 모두 제거해야겠지요. 사용자에게는 의미없는 이상한 문자들이 화면에 나타나서는 안되겠죠? 따라서 위에서 디버깅을 위해 사용했던 Console.WriteLine이나 각종 디버깅 관련 코드들을 제거해야 합니다.
프로그램의 길이가 짧다면 이런 작업은 금방 할 수 있습니다. 또한 다음 버전을 개발할 때 다시 디버깅 코드를 넣을 수도 있겠죠. 하지만 코드의 길이가 10만 라인, 100만 라인... 이렇게 길어진다면 어떻게 될까요? 100만 라인 정도가 되면 디버깅을 위해 사용했던 각종 코드를 제거하는 데 한 달, 다음 버전을 개발하기 위해 다시 디버깅 코드를 집어넣는데 한 달은 걸리겠죠? 따라서 "코드를 건드리지 않고 디버깅 코드를 그대로 유지할 수 있을까"하는 고민을 해결해주는 #define과 #if를 살펴보도록 하겠습니다.
먼저, 코드의 처음에 #define을 사용합니다.
#define DEBUG
이와 같이 하고 코드 중간에 삽입한 Console.WriteLine(customerAge.ToString());을 다음과 같이 바꾸는 것이지요.
#if DEBUG
Console.WriteLine(customerAge.ToString());
#endif
이와 같이 하면 #define DEBUG로 정의되어 있기 때문에 Console.WriteLine이 수행됩니다. 그리고 제품 개발이 끝난 후 소비자에게 판매용으로 만들 때는 #define DEBUG 문장을 지워주기만 하면 됩니다. 그러면 #if DEBUG에서 DEBUG가 정의되어 있지 않기 때문에 안에 있는 코드가 실행되지 않게 됩니다.
#define DEBUG라는 문장의 유무로 모든 디버깅 코드를 추가하고 제거한 것과 같은 효과를 낼 수 있습니다. #define DEBUG는 단순히 DEBUG라는 표식을 만들어 두는 것으로 일종의 표지판과 같습니다. 자동차를 타고 도로를 가다보면 마주치게 되는 우회전, 좌회전을 지시하는 표지판처럼 컴파일러도 이러한 표지판을 보고 따라가는 것입니다.
저는 이것을 디버깅 용도로 많이 사용합니다. 그리고 Console.WriteLine() 대신 Debug.WriteLine()을 사용합니다. Debug.WriteLine은 Console.WriteLine과 사용법이 동일합니다. 차이점은 VS.NET 프로젝트에서 프로그램을 실행 중일 때 VS.NET의 "출력 창"에 결과값이 나타난다는 것입니다. Console.WriteLine()은 콘솔 응용 프로그램을 작성할 때 유용하게 사용할 수 있습니다. 따라서 윈도우용 응용 프로그램이나 컴포넌트를 프로그래밍하고 있을 때는 Debug.WriteLine 같은 것들을 더 유용하게 사용할 수 있습니다. Trace와 같은 것들도 있지만 제 경우에 Trace는 웹 응용 프로그램을 개발할 때만 사용하고 있습니다. 이것 역시 취향차이라 생각합니다.
또한 VS.NET의 프로젝트로 작업하는 경우에는 #define DEBUG 문장이 있을 때와 없을 때 모두 동일하게 동작하는 것을 알 수 있습니다. 이것은 VS.NET에서 자체적으로 #define DEBUG를 정의해 두기 때문입니다. VS.NET에 기본값으로 설정되어 있을 만큼 디버깅을 위해서 많은 개발자들이 #define DEBUG와 #if DEBUG라는 문장을 많이 쓴다는 거겠지요. #if DEBUG는 DEBUG라는 표시가 있으면 이라는 의미입니다.
아아… 이 모든 것을 외울 필요는 없습니다. 책에 보면 다 나와 있습니다. 다만, "이런 게 있었지"라고만 기억하면 됩니다. 책에 포스트-잇 하나만 떠억하니 붙여놔도 됩니다. 나중에 제가 여러분에게 이럴 때는 이런 것을 써야 하고, 저런 경우에는 저런 것을 써야 한다는 것을 강조할 겁니다. 책에 있는 C# 문법을 달달 외워봤자 어디서 뭘 써야 하고, 왜 써야 하는지 알지 못한다면 써 먹을 수 없기 때문에 외우는 것 자체는 의미가 없습니다. 뭘 써야 하고, 왜 써야 하는가 그것만 이해하시기 바랍니다.
이제, VS.NET에서 #define DEBUG가 기본으로 설정된 곳을 어떻게 찾는지 보여드리지요. 프로젝트를 만들고, 솔루션 탐색기에서 프로젝트 이름을 마우스 오른쪽 버튼으로 클릭하고 "속성"을 선택합니다.
위 그림처럼 구성 속성을 보면 오른쪽에 "조건부 컴파일 상수"라는 것이 나타납니다. 여기에 DEBUG라는 것이 있지요. 그리고 TRACE라는 것도 있습니다. 바꿔 말해 이것은 코드에서 #define DEBUG와 #define TRACE가 선언되어 있는 것과 같습니다. 여기서 DEBUG를 지웁니다. 그러면 저기에는 TRACE만 남게 되겠지요. ";"는 조건부 상수들을 구분하기 위해 사용합니다.
이제 코드에 #define이 있느냐, 없느냐에 따라서 여러분이 원하는 대로 정확하게 동작하게 됩니다. 아니면 #define DEBUG를 사용하지 말고, 프로젝트 옵션에서 설정할 수도 있겠지요. 아직은 윈도우 응용 프로그램이라든가 VS.NET을 사용하는 방법에 대해서는 크게 설명하고 있지 않기 때문에 이러한 설정을 사용할 일이 없습니다. 그러니 #define, #if를 사용하는 것부터 충분히 경험해 보기 바랍니다. 강의 중에는 제가 개발중인 프로그램에서 실제 #define DEBUG라든가, #if를 사용하는 것을 직접 보여줄 수 있었는데 여기서는 그렇게 하지 못해 아쉽습니다.
객체 지향(object orientation)
사실 객체 지향이라는 것이 너무나 말이 많은 분야인데다가 지금도 말이 많은 분야입니다. 그래서 감히 어떻다라고 얘기하기 어렵습니다. 높디 높은 곳에 있는 선인들이 "객체 지향은 이런 것이다!"라고 얘기하면 그 얘기를 사람들이 받들 뿐입니다. 사실 저 같은 무식한 사람이 얘기하면 "당신, 객체 지향에 대해서 하나도 모르는 군!"이라고 비웃을 게 뻔합니다. 그래도 어쩌겠습니까? 저도 먹고 살려니 뭔가 좀 아는 체도 하면서 객체 지향에 대해서 이러니, 저러니 하는 이야기를 해야 겠지요.
객체 지향이라는 것은 하나의 현실 세계를 모델링하는 것입니다. 하지만 현실 세계를 모델링하기란 정말 벅찹니다. 사실 현실 세계를 모델링하신 분이 있습니다. 그것도 단 7일만에 하신 분이 있지요. 하지만 저는 그런 선인이 아니라서 현실 세계는 커녕 "사람"을 모델링하는 것도 벅찹니다.
그러면 모델링이 뭐죠? 모델링은 뭘 뜻하는 거죠? 아시는 분?? 네, 뭔지는 알지만 말로 설명하기는 좀 어렵죠?
모델링은 해당하는 사물을 위한 모델을 만드는 것입니다. 하지만 모델이라는 게 현실 세계를 그대로 반영할 수는 없죠. 그러니 필요한 것들만을 가려낸 다음에 모델링하는 것입니다. 기존의 언어에서는 단순히 문제를 해결하는 방법, 즉 코드 자체에만 관심을 가졌습니다. 그리고 프로그램에 필요한 데이터는 따로 구성했습니다. 보통 이러한 데이터들은 구조체라는 것을 사용해서 표현했습니다. 사람을 모델링할 경우 사람의 이름 Name, 나이 Age에 대한 구조체를 만들고 이것들을 코드에서 다루었습니다. 이러다 보니 데이터와 코드가 분리되어 있습니다. 네, 사실은 데이터와 코드의 분리가 아니라 보다 본질적인 문제, 데이터와 데이터의 행동이 분리되어 있었습니다. 때문에 이 코드의 동작이 어떤 데이터와 연관된 것인지 파악하기가 어려웠습니다. 이러한 데이터와 데이터의 행동을 하나로 합쳐서 완전한 하나의 단위로 다루고자 도입한 것이 객체이고, 이 객체를 정의하기 위한 키워드가 바로 class입니다.
때문에 예전에는 DoSayHello()라는 메소드를 만들고 성인이 얘기할 때는 "Good Morning"이라고 출력해주고, 고등학생이 얘기할 때는 "Yo~ Bud~", 중학생이 얘기할 때는 "Hey~ Boys"라고 출력해 준다는 것을 상상할 수 없었습니다. 대상 즉, 성인, 고등학생, 중학생에 따라서 각각의 함수들 DoSayHelloAdult(), DoSayHelloHighSchool(), DoSayHelloMiddleSchool() 이런 식으로 만들어야 했을지도 모릅니다. 음… 머리가 비어있다보니 좋은 예제가 생각나질 않는군요. 헐…(사실은 언어 순화를 했습니다. 음…)
하지만, 객체 지향을 사용한다면 각 대상을 객체로 모델링할 수 있고 각각의 객체에 대한 행동을 별도로 정의할 수 있습니다. 보통 클래스 선언은 다음과 같습니다.
class Person
{
}
이것으로 클래스 선언이 끝났습니다. Person 클래스를 만들 수 있는 것이지요.
class Adult
{
public void DoSayHello()
{
Console.WriteLine("Good Morning");
}
}
class HighSchool
{
public void DoSayHello()
{
Console.WriteLine("Hey~ Bud");
}
}
이와 같이 만들 수 있습니다. 모두 같은 DoSayHello()를 갖고 있지요? 하지만 이 둘은 중복되지 않습니다. Console.WriteLine()에서 이미 사용한 것처럼 대상과 행동을 "."으로 구분합니다. 따라서 보통은 다음과 같이 사용하겠죠?
Adult person = new Adult();
person.DoSayHello();
HighSchool teens = new HighSchool();
teens.DoSayHello();
이와 같이 person이라는 대상과 teens라는 대상을 지칭한 다음에 해당하는 행동 즉 DoSayHello()를 불러내기 때문에 이들은 중복되지 않습니다.
여기에서 주의할 점이 있는데, class Adult와 같이 선언한 부분에서 아무 접근이 없다는 것이입니다. 비공개 접근을 뜻하는 private인지, 공개 접근을 뜻하는 public인지 등이 나타나 있지 않습니다. 이처럼 class만 달랑 쓰는 경우와 같이 private이나 public 등을 생략할 경우 C#에서는 이를 자동으로 private이라고 생각합니다. 이런 식의 기본 설정은 언제든지 바뀔 수 있는 것입니다만 실제로는 바뀌는 일이 없겠죠. 하지만 C#에 익숙하지 않은 사람이 보면 어떨까요? 기본이 public으로 설정되어 있는지 아니면 private으로 되어있는지 알 수 없을 겁니다.
"기본 설정은 private이다."
라는 것은 C# 프로그래머가 알고 있는, 또는 C# 프로그래머가 알아야 하는 또 하나의 "가정"에 해당합니다. 이러한 가정은 프로그램을 모호하게 만듭니다. 그리고 모호한 코드들은 다른 사람이 보면 쉽게 이해할 수 없는 코드가 되는 지름길이지요. 그러니 여러분은 항상 private이나 public 등을 지정해 주시기 바랍니다.
이제 다른 이야기를 시작해 볼까요.
객체 지향 II
지금까지 객체 지향이라는 것은 단순히 현실 세계에서 일어나는 업무를 구현하기 위해 현실 세계를 모델링하는 것이라고 얘기 했습니다. 이제 여러분이 어떤 재무관리 프로그램을 만든다고 해봅시다. 긴장하지 말고, 다음 코드를 살펴보죠.
float paid;
paid = 100f;
float bonus;
bonus = 1.2;
float salary;
salary = paid * bonus;
Console.WriteLine("Your Salary : " + salary.ToString() );
이 코드에서 월급은 만원 단위입니다. 이 사람은 월급을 얼마나 받죠? 네, 120만원을 받습니다. 저보다 많이 받는군요. 부럽군요.
이 코드가 실행이 되나요?? 실행된다구요??
실행이 안됩니다. float bonus = 1.2;에서처럼 C#은 1.2와 같은 실수를 기본으로 double로 생각하기 때문에 안됩니다. float bonus = 1.2f;라고 명시해 줘야 합니다. 이런 종류의 오류는 클래스에서 메소드를 정의할 때 가장 빈번하게 일어납니다.
public void GetSalary(float paid, float bonus)
위와 같이 선언된 메소드가 있다고 할 때 GetSalary(100, 1.2)와 같이 입력하면 double을 float로 변환할 수 없다며 무엄한 오류 메시지만 뱉어낼 뿐입니다.
자, 코드를 고칩시다. 고친 코드는 다음과 같겠죠.
float paid;
paid = 100f;
float bonus
bonus = 1.2f;
float salary;
salary = paid * bonus;
Console.WriteLine("Your Salary : " + salary.ToString() );
하지만 모든 월급 생활자가 100만원을 받고, 보너스를 20%씩 받는 것은 아니겠지요? 그렇다면 이제 월급 생활자를 한 번 클래스로 옮겨봅시다.
클래스는 뭐라고 했죠? 대상과 대상의 행동을 옮기는 것이다라고 했습니다. 다시 말해 데이터와 메소드를 옮겨서 클래스라는 것으로 묶는 것입니다. class라는 키워드는 이러이러한 것들을 하나의 객체로 묶는다는 의미입니다.
먼저 클래스를 만들어 보겠습니다.
class Person
{
public float paid;
public float bonus;
}
이렇게 해서 클래스를 만들었습니다. 그러면 앞에서 만든 코드를 고쳐보죠.
float paid;
paid = 100f;
float bonus;
bonus = 1.2;
이 코드는 다음과 같이 바꿀 수 있습니다.
Person.paid = 100f;
Person.bonus = 1.2f;
마찬가지로 계산 결과를 저장하는 부분도 다음과 같이 바뀝니다.
salary = Person.paid * Person.bonus;
완전히 수정된 코드는 다음과 같습니다.
Person.paid = 100f;
Person.bonus = 1.2f;
float salary;
salary = Person.paid * Person.bonus;
Console.WriteLine("Your Salary : " + salary.ToString() );
이제 위의 코드가 수행되나요??
역시 안되죠! class Person이라고 선언한 것은 단순히 Person이라는 붕어빵 틀을 만든 것입니다. 이 붕어빵 틀을 사용해서 붕어빵을 찍어내야 합니다. 이 붕어빵을 뭐라고 하죠? 네, 흔히 말하는 "인스턴스"입니다. 이 인스턴스 선언을 new로 합니다. 이미 앞에서 봤을 것이고, 4장을 보신 분들은 알겠지만, Person에 대한 인스턴스를 생성하기 위해 new를 사용해야 합니다. 따라서 다음과 같이 될 것입니다.
Person Cindy = new Person();
그리고 다음 코드들도
Person.paid = 100f;
Person.bonus = 1.2f;
salary = Person.paid * Person.bonus;
이와 같이 수정되어야 합니다.
Cindy.paid = 100f;
Cindy.bonus = 1.2f;
salary = Cindy.paid * Cindy.bonus;
전체 코드는 다음과 같습니다.
Person Cindy = new Person();
Cindy.paid = 100f;
Cindy.bonus = 1.2f;
float salary;
salary = Cindy.paid * Cindy.bonus;
Console.WriteLine("Your Salary : " + salary.ToString() );
이제 Person이라는 붕어빵 틀로 Cindy라는 붕어빵을 만들어 냈습니다. 흐음, Cindy가 맛있는 붕어빵이라면 시식을… (웃음)
Person Cindy = new Person();
위과 같은 문장을 반복적으로 사용해서 여러분은 Cindy도 만들 수 있고, Sunny, Luna, Kate 등도 만들어 낼 수 있습니다. 음…
다시 생각해 봅시다. 이 코드가 올바른 걸까요?
Person이라는 클래스의 정의를 생각해봅시다. 현재 Person이라는 클래스에는 무엇이 있습니까? 데이터만 있죠? 네, 그 대상을 모델링하는데 필요한 데이터만 있습니다. 그리고 행동은 빠져 있습니다. 여기서 행동이 있나요? 행동이 있습니다.
salary = Cindy.paid * Cindy.bonus;
이것이 행동입니다. 흔히들 비즈니스 규칙(business rule)이라고 얘기하죠. 어쨌거나 이제 대상의 행동을 옮기도록 하지요.
앞의 Person 클래스 정의를 옮기면 다음과 같습니다.
class Person
{
public float paid;
public float bonus;
}
여기에 Salary라는 메소드를 만들도록 하죠. Salary 메소드는 클래스 외부에서 이용할 수 있도록 공개되어야 겠죠? 그러면 private과 public 중에 어떤 것을 쓸까요? 네, public을 사용해야 합니다. 그리고 결과 값을 돌려줘야 할 테니 반환값도 필요하구요. 형식은 float로 합시다. 이제 다음과 같이 만들어 냅니다.
class Person
{
public float paid;
public float bonus;
public float Salary()
{
return this.paid * this.bonus;
}
}
값을 돌려줘야하니까 return 문장을 사용합니다. 여기서 this 라는 것이 사용되었는데 이것은 현재 사용하고 있는 클래스를 가리킵니다. 정확히는 현재 사용하고 있는 인스턴스를 가리키지만 크게 구분하지 않아도 됩니다. 지금 수준에서 정확하게 구분하려고 하면 오히려 혼동만 가중되니까요. 앞으로도 this라든가 클래스라든가 하는 것들은 많이 접하고 많이 사용하게 될 겁니다. 백문이 불여일타. 라는 말이 있듯이 예제를 하다보면 자연히 머리 속에 있는 지식이 이해되기 시작하고 개념이 정립되기 시작합니다. 아무리 책을 많이 읽어서 이해한 것 같아도 막상 코드를 입력하면 할 수 없는 경우가 많기 때문에 예제는 꼭 한 번 해보는 것이 좋습니다.
이제, 클래스에 데이터와 행동을 모두 옮겨왔습니다. 그럼 코드를 수정해봅시다. 수정한 코드는 다음과 같습니다.
Person Cindy = new Person();
Cindy.paid = 100f;
Cindy.bonus = 1.2f;
float salary;
salary = Cindy.Salary();
Console.WriteLine("Your Salary : " + salary.ToString() );
이전 보다 단순해 진 것 같습니다. 그런데 이 코드를 다음과 같이 더 간단하게 줄일 수도 있습니다.
Person Cindy = new Person();
Cindy.paid = 100f;
Cindy.bonus = 1.2f;
Console.WriteLine("Your Salary : " + Cindy.Salary().ToString() );
그러나 이런 식의 코드는 한 줄에 너무 많은 내용을 담고 있기 때문에 이해하기 어렵습니다. 네? 이해하기 쉽다구요? 제가 이해력이 좀 부족한 편이라서 그렇습니다. 누가 불러도 못 들을 때도 많고, 다른 사람이 말하는 것을 쉽게 이해하지 못하는 경우가 너무 많습니다. 그래서 제게는 저런 코드가 이해하기 버겁습니다. 그러니 저처럼 이해력이 부족한 분들은 전자와 같은 방법을 쓰십시오. 코드는 조금 더 많이 입력해야 하지만 이해하기 쉽다는 장점이 있습니다.
어떤가요? 처음에 사용했던 절차지향 코드를 객체지향 코드로 바꿔봤습니다. 여기서 뭔가 이점이 있다는 생각이 들던가요? 네? 이점이 있으니까 설명한 거 아니냐고요? 아뇨~ 여기에서는 얻을 수 있는 이점이 전혀 없습니다. 라인 수를 세어보죠. (실은 절차지향 코드와 객체 지향 코드가 모두 칠판에 써 있었습니다) 절차지향은 30줄인데, 절차 지향 코드는 28줄이군요. 별로 얻을 수 있는 이점이 없군요. 코드도 훨씬 복잡해 보이는데 단 2줄의 이득을 얻다니… -_-
자… 그런데 이러한 급여를 계산하는 코드를 한 곳에서만 쓰지는 않겠죠? 프로그램을 살펴보면 다양한 곳에서 사용되고 있다는 것을 알 수 있을 겁니다. 급여를 정산하는 곳에도 쓰일 것이고, 통계를 뽑아내는 데도 쓰일 것이고, 세금 계산을 하는 곳에도 쓰일 겁니다. 이와 같이 3곳 모두에 코드가 쓰인다면 어떨까요? 절차지향일 경우 30 * 3이면 되니까 90줄이죠? 객체지향은 클래스가 28줄이고, 3곳에는 인스턴스를 생성하고 메소드를 호출하는 코드만 필요하니까 6 * 3이면 되죠? 그러면 28 + 18 = 46 줄이군요. 90줄과 46줄이라고 해도, 객체 지향을 써야할 이유로는 납득이 되질 않는군요.
왜 쓰죠? 모르겠나요?
절차지향에서는 코드가 3곳으로 나뉘어져 있죠? 따라서 보너스 율이 1.2에서 1.3으로 변경된다면 3곳의 코드를 모두 변경해야 합니다. 만약 두 곳만 변경하고 한 곳을 잊어버리면 안되죠. -_-
하지만 객체 지향에서는 클래스를 사용해서 데이터와 메소드를 한 곳에서 관리하기 때문에 클래스에서 한 번 바꾸는 것 만으로 안전하게 변경된 내용을 적용할 수 있습니다. 그리고 코드의 길이는 별 차이가 없었지만 실제로 아주 긴 코드들, 10만 라인 이상의 코드들을 상대하게 된다면 코드의 크기를 기하급수적으로 줄여줄 수 있고 논리를 간단하게 해줄 수 있습니다.
급여를 계산하는 복잡한 계산식이나 소스 코드를 알 필요없이 Person.Salary()가 급여를 계산해 주기만 하면 된다는 것을 알면 되니까요. 이처럼 세세한 것을 알 필요가 없게 만들어주는 것을 캡슐화(encapsulation) 또는 은닉이라고 합니다. 어쨌거나 용어에 얽매이지는 맙시다. 내부 구현을 숨겨주고, Salary()라는 것만 알아도 계산된 급여를 얻을 수 있다는 사실만으로 개발자는 행복하니까요. 굳이 여러분이 급여 계산 공식을 알려고 안달할 필요는 없습니다. ^^;
그렇다면 객체지향은 어느 정도의 코드에서 장점이 있나요? 글쎄요. 제 개인적으로는 10만 라인 이상일 때가 적절하다고 생각하지만, 일단 여러분이 객체 지향 언어를 쓰고 있고, 1만 라인 이상의 코드를 작성하고 있다면 객체지향을 쓰는 것이 적절하다고 생각합니다. 뭐, 책을 팔아먹으려고 여러분을 현혹시키려고 무조건 객체 지향을 써야 한다고 말하고 싶지는 않습니다. 실제로 저는 절차지향 언어로도 만족스럽게 50만 라인 이상의 코드들을 다년간 분석하고 코딩한 경험이 있습니다. 이러한 것들은 처음부터 디렉토리 구조, 파일 명명 규칙 등을 부여해야 합니다. 그리고 철저한 사전 계획이 필요하죠. 객체지향에서는 철저한 사전 계획이 필요없다는 것은 아닙니다. 실제로 리눅스 커널은 200만 라인을 훌쩍 뛰어넘었지만 여전히 C 언어와 같은 객체지향 언어로 모듈식의 개념으로 잘 구성되어 있습니다.
그렇지만 큰 코드들에서는 대상을 객체로 모델링하는 것이 코드를 보다 쉽게 이해할 수 있게 해줍니다. 종종 모델링을 잘못해서 절차지향보다 더 어려운 코드를 만들어 내기는 하지만 실제로 객체지향은 여러분을 괴롭히자는 것이 아니라 여러분을 도와서 프로그래밍을 쉽게 하자는 개념입니다. 객체 지향은 말 그대로 현실을 모델링하는 것이라 여러분의 은유를 계속 포함시켜서 확장시키는 경향이 있습니다. 하지만 이 이야기는 지금하기에 적절하지 않은 것 같군요. 어쨌거나, 여러분과 함께 코딩해 나가고 여러분의 코드를 살펴보면서 저는 절차지향이 아닌 객체지향적인 코딩에 대해서 설명하고 코드를 잡아드릴 것입니다.
객체 지향 III, 상속
자… 지금까지 붕어빵 틀을 만들어서 붕어빵 찍어내는 방법을 살펴봤습니다. Person을 만들고 다양한 사람들을 만들어내는 방법은 알아봤습니다. 하지만 모든 사람들이 같은 월급을 받을 까요?
사원, 계약직, 일용직은 모두 받는 월급이 다를 겁니다. 예를 들어 사원의 급여 항목이 40여가지 정도 있다면, 계약직은 급여 항목이 2-3가지 정도일 것이고, 일용직은 급여 항목이 1-2가지 정도일 겁니다. 게다가 계약직이나 일용직이라면 보너스 같은 것들이 없겠죠. 마찬가지로 사원들은 급여 항목도 많은 만큼 세금이라는 무시무시한 항목도 많습니다. 이에 반해 계약직이나 일용직은 그러한 세금 항목도 그만큼 적겠지요. 심지어 세금도 안 낸다니까요.
그렇다면 지금 종이를 꺼내서 그림을 그려보시기 바랍니다. (강의할 때는 화이트 보드를 사용하지만 독자들은 직접 그림을 그려봐야지요.) 고용인 이라는 박스를 하나 그리고 이 박스 밑에 나란히 사원, 계약직, 일용직이라는 그림을 그려 봅니다.
위와 같은 형태가 되겠지요. 여기에서 알 수 있는 것처럼 공통된 내용은 고용인이 정의하고, 몇 가지 세부적인 내용들을 사원, 계약직, 일용직에서 구현할 수 있도록 할 수 있을 겁니다. 앞에서 만든 Person에서는 paid나 bonus와 같은 내용들은 모두 일관되게 적용할 수 있지만, 급여항목이나 세금 항목이 각각 다르기 때문에 Salary 메소드의 내부 구현은 달라야 할 겁니다. 따라서 고용인으로 모델링한 것들을 사원, 계약직, 일용직에서 그대로 사용할 수 있으면 얼마나 좋을까 하는 생각에서 출발한 것이 상속입니다.
이와 같은 클래스와 비슷하게 현실 세계를 계층 적인 구조로 모델링한 것에 뭐가 있을까요? 예로 설명할 수 있는 분 있나요? 아마 중학교 때나 고등학교 때 생물 시간에 다음과 같은 것들을 배웠을 겁니다. 과학시간 이었던가요?
종 < 속 < 과 < 목 < 강 < 문 < 계
지겹게 외우지 않았나요? 종은 가장 기본 단위로 교배라는 행동을 종이라는 것으로 모델링하고, 서로간에 친척관계에 있는 것들을 속으로 모델링하고…이와 같은 상속 관계를 자연계에서 이미 여러분은 배웠습니다. 동물도 포유류, 조류, 양서류, 파충류 등으로 분류했었고요. 아차, 지금 제가 여러분에게 큰 실수를 범하고 말았군요. 동물은 원생 동물, 절지 동물, 연체 동물, 척추 동물과 같이 분류하고, 척추 동물을 다시 포유류, 조류, 양서류… 이런 식으로 분류했었지요.
여기서 저는 계속해서 생물학 이야기를 했습니다. 사실 얼마 안했지만…뭔가 느껴지는게 있습니까? 네, 계층 구조(상속 구조)에서 알 수 있는 것처럼 각 계층은 고유의 특징만을 갖는 것이고, 하위 계층은 이러한 특징들을 물려 받는 다는 것입니다. 상위 계층과 하위 계층은 완전히 독립되어야하고, 모델링한 관점에 따라서 그 기준이 일관되어야 합니다. 하지만 많은 분들이 작성한 클래스를 살펴보면 그렇지 않다는 것을 알 수 있습니다.
객체 지향 IV, 상속 2
이 만큼 떠들었으니 코드에서 Person을 상속하는 것을 살펴보도록 합시다. 보드에 쓰는 손도 아프니 사원만 상속시켜 보도록 하죠.
class Employee : Person
{
}
Employee가 Person을 상속받는 다는 것을 표현하기 위해 클래스 정의에 :을 사용합니다. 사실, 여러분은 이것으로 모든 작업이 끝났습니다. ^^; 이제 Employee는 다음과 같이 사용할 수 있습니다.
Employee Cecil = new Employee();
Cecil.paid = 150f;
Cecil.bonus = 0.3f;
float salary;
salary = Cecil.Salary();
Console.WriteLine("Your salary : " + salary.ToString() );
Employee 클래스에 아무것도 구현하지 않아도 이와 같이 사용할 수 있습니다. 여기에 새로운 함수를 추가하면 그 함수는 Employee에서만 사용할 수 있게 되는 것이지요. 새로운 함수를 추가하는 것은 Person에서 함수를 정의한 것과 같습니다. 이제, Salary() 함수를 Person 클래스와 Employee 클래스에서 다르게 동작하게 하고 싶다면 어떻게 할까요?
이런 경우에 virtual과 override를 사용합니다. 먼저 Person 클래스를 다음과 같이 수정합니다.
class Person
{
public float paid;
public float bonus;
public virtual float Salary()
{
return this.paid * this.bonus;
}
}
Employee 클래스 정의는 다음과 같습니다.
class Employee : Person
{
}
다시 다음 코드를 실행합니다.
Employee Cecil = new Employee();
Cecil.paid = 150f;
Cecil.bonus = 0.3f;
float salary;
salary = Cecil.Salary();
Console.WriteLine("Your salary : " + salary.ToString() );
여전히 잘 동작하는 것을 알 수 있습니다. 클래스 메소드 선언에 virtual을 붙이는 것쯤은 동작에 아무런 영향을 미치지 않습니다. 이제 Employee 클래스에서 세금을 10만원 빼는 계산을 추가한 Salary() 메소드를 새로 만들어 보도록 하지요. Employee 클래스는 다음과 같습니다.
class Employee : Person
{
public override float Salary()
{
return ( base.paid * base.bonus ) ? 10f;
}
}
이제 같은 코드를 실행해보면 120 대신에 110이라는 값이 출력되는 것을 알 수 있을 것입니다. 이는 Employee에서 재정의한 Salary()가 사용되는 것을 뜻합니다. 이처럼 메소드의 동작을 클래스에서 변경하기 위해서 부모 클래스는 virtual을 자식 클래스에는 override를 사용합니다. Employee에서 재정의한 Salary() 메소드를 보면 base.paid와 base.bonus를 사용하는 것을 알 수 있습니다. 변수 paid와 bonus는 실제로 부모 클래스 Person에서 정의된 것입니다. 이처럼 부모 클래스의 변수를 사용한다는 것을 나타내기 위해 base.paid와 base.bonus와 같이 base를 접두어로 사용합니다. 실제로 이러한 의미는 컴파일러가 파악해 주기 때문에 this나 base와 같은 접두어를 붙이지 않아도 잘 동작합니다. 하지만 이러한 것들은 클래스를 작성하는 프로그래머의 가정이므로 다른 사람들은 이러한 가정을 알기 어렵습니다. 따라서 this나 base와 같은 키워드를 명시적으로 사용해 주는 것이 좋습니다.
지금까지 살펴본 것처럼 virtual이니 override니 하는 것은 부모 클래스와 자식 클래스 사이의 메소드들의 관계를 나타내기 위한 것입니다. 그러니 개념이 중요한 것이지 이러한 키워드가 중요한 것은 아닙니다. 개념을 이해하고 이런 경우에 어떤 키워드를 사용하는 가만 알면 된다고 생각합니다.
클래스에 대한 설명에서 여러분이 어려워하는 것들이 public, private, protected, internal, sealed 등을 구분하는 것이 아닌가 싶습니다. 하지만 여러분이 이것들을 실제로 모두 사용하는 경우는 그리 많지 않습니다. 대부분의 경우에 public, private, protected 정도를 사용하게 될 것이고, 아주 가끔 internal과 sealed 정도를 사용하게 되겠지요. 이러한 의미들은 지금 모두 잊어버리셔도 됩니다. 중요하지도 않고, 나중에 책이나 MSDN에서 키보드 몇 번 두드리면 다 나옵니다. 중요한 건 자꾸 사용하다보면 자연스러워 지고 이건 이럴 때, 저건 저럴 때 써야 한다는 것들도 자연스럽게 체득될 거라는 것이죠. 그러니 책에 나오는 예제들은 가급적 열심히 합시다…
객체 지향 V, 서브 클래싱(Sub Classing)과 서브 타이핑(Sub Typing)
서브 클래싱이라는 것은 지금까지 설명했던 상속을 얘기하는 것입니다. Person에 정의된 데이터와 구조를 Employee에서 물려 받았으며 이것들을 사용할 수 있었습니다. Employee라는 껍데기만을 선언해도 모든 데이터와 메소드를 사용할 수 있다는 것은 앞의 예제에서 이미 살펴보았지요. 이처럼 부모 클래스의 데이터와 메소드를 모두 물려받는 것을 서브 클래싱이라 합니다. 클래스를 몽땅 물려 받아서 하위 클래스를 만든다는 정도로 이해하면 됩니다.
이에 반해서 서브 타이핑은 형식만을 정의하는 것입니다. 데이터도 정의하지 않고, 오직 형식만을 정의하는 것입니다. 예를 들어 앞에서 구현한 Person 클래스에 대해서 각각의 Employee, Contract, PartTime가 상속을 받고, Salary() 메소드를 모두 재정의한다면 Person.Salary()에 정의된 코드는 실제로 쓸데없는 코드, 한 번도 사용되지 않는 코드를 뜻합니다. 이런 경우에는 부모 클래스에 메소드에 대한 껍데기만을 남겨두는 것입니다. 그리고 자식 클래스에서 껍데기에 대한 실제 구현을 정의하도록 하는 것이지요.
C#에서는 서브 클래싱과 서브 타이핑을 구분하기 위해 인터페이스, interface 라는 것을 도입했습니다. interface에 대한 설명은 9장에 있으니 interface에 대한 다양한 이야기는 나중에 하겠습니다. 그러나 여기서는 서브 타이핑을 위해서 interface를 사용한다는 것만 이해하시기 바랍니다.
인터페이스는 어떤 경우에 사용하는 것이 유리한가?
만약 Employee, Contract, PartTime 클래스를 각각의 개발자가 구현한다고 가정합시다. 이 경우에 월급을 계산하는 메소드 이름은 Salary(), GetSalary(), Pay(), GetPay() 등의 다양한 메소드를 사용할 수 있을 것입니다. 이런 경우, 모든 클래스들이 지켜야 할 공통의 약속을 정의하기 위해 인터페이스를 사용할 수 있습니다. 인터페이스에서 월급을 계산하는 메소드는 Salary()라고 정의하고, 각각의 클래스는 이 인터페이스를 상속하고, 인터페이스에 정의된 메소드를 각 클래스에서 구현하는 것입니다. 이렇게 하면 다른 개발자가 개발하더라도 공통의 인터페이스를 쓰기 때문에 같은 메소드를 사용해서 해당 기능들에 접근할 수 있습니다. 사실 이 외에도 인터페이스를 사용해서 얻을 수 있는 이점은 많지만 그에 대해서는 나중에 설명하겠습니다. 사실 인터페이스의 유용성에 대해서는 클래스보다 더 많은 코드를 사용해보고, 이해한 다음에 설명해야 제대로 납득할 수 있는 부분이 많습니다.
C++와 같은 언어는 서브 클래싱과 서브 타이핑을 엄격히 구분하지 않습니다. 때문에 서브 클래싱과 서브 타이핑이 class라는 정의 안에 혼재되어 있습니다. C#에서는 class와 interface 라는 두 키워드로 분리시킨 것에 비하면 다르다고 할 수 있습니다. 히지만 C++와 C# 모두 메소드의 껍데기를 만들기 위해 abstract 키워드는 사용할 수 있습니다. C#에서 abstract를 사용해서 메소드의 껍데기를 만드는 것과 interface를 사용해서 메소드의 껍데기를 만드는 것에는 어떤 차이가 있습니까?
『C# 프로그래밍』에서는 이 둘을 거의 같은 것으로 얘기하고 있지만 실제로 abstract로 되어 있는 것들은 반드시 클래스를 상속시켜야하고, 상속 받은 클래스에서 해당 메소드를 구현해야 합니다. 반면 인터페이스는 인터페이스를 상속한 클래스에서 해당 인터페이스에 정의된 메소드를 구현해야 한다는 차이점이 있습니다. 즉, 반드시 클래스에서 클래스로 상속해서 구현해야 하는 것이 abstract입니다. 그러나 대부분의 경우에 저는 abstract 보다는 interface를 사용할 것을 권합니다. 마찬가지로 C++에서는 다중 상속을 지원하지만, C#에서는 단일 상속만 지원합니다. 이에 대해서는 내일 다시 설명을 하도록 하지요.