메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

Programming C# - 4부

한빛미디어

|

2002-08-14

|

by HANBIT

5,390

저자: 한동훈(traxacun@unitel.co.kr)

지난 시간에 제가 숙제를 내드렸습니다.
해보신 분들 있나요? 예, 다들 해보셨군요. 그럼 숙제를 해보신 분들 중에 몇 분의 소스를 직접 살펴보면서 얘기하도록 하지요.

첫번째는 상속을 구현하는 예제였습니다. 한번 살펴볼까요?
using System;

public class Parents
{
   public virtual void Say()
   {

      Console.WriteLine("Parents");

   } //End of virtual Say()


} // End of class parents

public class Child : Parents
{
   static void Main()
   {
      Child say = new Child();
      say.Do();

   } // End of Main()

   private void Do()
   {

      Parents sayp = new Parents();
      sayp.Say();

      Child sayc = new Child();
      sayc.Say();

   } // End of Do()

   public override void Say()
   {

      Console.WriteLine ("Child");

   } //End of override Say()


} // End of class Child
제가 원하는 주석 스타일대로 잘 해주셨네요. 괄호의 끝에 // end of 와 같이 주석을 사용하는 것이 좋습니다. 코드가 길어졌을 때 어디가 클래스의 끝인지, 어디가 메소드의 끝인지 쉽게 알 수 있기 때문입니다. 이것은 개인적인 취향의 차이입니다. 쓰고 싶지 않은 분들은 쓰지 않아도 됩니다. 하지만 C# 처럼 괄호를 많이 쓰는 언어에서는 혼동되지 않게 할 수 있는 좋은 방법이기도 합니다.

Child 클래스에 프로그램을 테스트하는 Do 메소드와 응용 프로그램을 실행하는 Main 메소드가 함께 포함되어 있기 때문에 이와 같이 하지 않도록 합시다. 여러분이 만들어내는 클래스라는 것은 현실 세계를 모델링해서 만드는 것입니다. Child 클래스와 Do 메소드는 아무 관련이 없습니다. 마찬가지로 Main 메소드는 응용프로그램을 실행하는 부분이므로 Child 클래스와는 아무 관련이 없는 부분입니다. 따라서 다음과 같이 바꾸도록 하세요.
public class Child : Parents
{
   public override void Say()
   {
      Console.WriteLine ("Child");
   } //End of override Say()

} // End of class Child

class AppMain
{
static void Main()
   {
      Child say = new Child();
      say.Do();

   } // End of Main()

   private void Do()
   {

      Parents sayp = new Parents();
      sayp.Say();

      Child sayc = new Child();
      sayc.Say();

   } // End of Do()
}
그리고 또 Main에서 Child 클래스를 생성해서 Do 메소드를 호출하고 있고, Do 메소드는 또 Parent와 Child 클래스를 모두 생성하고 있습니다. 이와 같은 형식말고 다음과 같이 Main을 정의해보도록 합시다.
    public static void Main()
   {
      AppMain ap = new AppMain();
      ap.Do();
   } // End of Main()
이와 같이 한 후, Do 메소드에서 Parent 클래스와 Child 클래스를 각각 테스트합니다. Main을 보면 저는 public 이라고 했습니다. 처음에는 static void Main()과 같이 정의되어 있었습니다. 이것이 의미하는 것은 무엇일까요? private이 기본이라는 것을 나타내지만 이것은 개발자가 하고 있는 하나의 가정(C#에서는 기본 형식이 private이라는 것을 알고 있어야 한다는 가정)입니다. 이러한 가정은 다른 언어를 사용하는 프로그래머들은 모르기 때문에 다른 프로그래머의 작업을 어렵게 할 겁니다. ^^;
따라서 이러한 가정은 철저하게 배제하는 것이 좋습니다. Main은 보통 응용프로그램의 진입점을 지칭하기 때문에 public 보다는 private으로 많이 사용합니다. 그렇지만 저는 public을 사용하는 것을 좋아하지요(이것은 개인적인 취향의 차이라고 생각해주세요. ^^;) 그러니 Main을 정의할 때 private을 사용하거나 public을 사용하거나 별 차이는 없습니다.

지금 이 코드를 보면 Parent와 Child 클래스를 분리시켰고, 응용 프로그램 부분도 분리시켰습니다. 실제로 절차지향에서는 모든 것을 하나로 작성했을 테지만 객체지향적인 접근 방법에서는 그렇지 않습니다. 모든 것들을 모델, 즉 대상이라는 형태로 잘게 나누고, 그 대상은 다시 행동, 즉 메소드라는 단위로 나누는 것입니다. 따라서 단순한 것을 작성한다 하더라도 미리 이러한 습관을 들여놓지 않으면 객체 지향의 세계로 나아가기 어려울 것입니다.

지금 위 프로그램은 단순히 Parent와 Child만 출력하기 때문에 진짜 어떤 클래스에서 실행되고 있는지 알 수 없습니다. 바로 이런 경우에 생성자를 사용합니다. 생성자는 클래스 이름과 같은 이름을 사용하는 메소드입니다. Parent의 생성자를 한번 작성해 보도록 하지요.
public class Parents
{
   public Parents()
{
  Console.WriteLine("Parent Constructor");
}

   public virtual void Say()
   {
      Console.WriteLine ("Parent");
   } //End of virtual Say()

} // End of class Parents
이와 같이 하고 다시 컴파일해서 실행해 봅니다. 이번에는 실행 결과가 Parent Constructor, Parent, Child가 차례대로 출력되는 것을 알 수 있습니다. Parent Constructor는 어디서 출력될까요?
Main을 살펴보면 이런 부분이 있습니다
  Parents sayp = new Parents();
위 문장에서 첫번째 Parents는 클래스 이름을 지칭합니다. sayp는 클래스의 인스턴스에서 사용할 이름을 지정하는 것입니다. new는 새로운 클래스를 만들라는 의미입니다(붕어빵 기계에서 붕어빵을 찍어내라는 이야기). Parents()는 뭘까요? 클래스 이름이 아니라 생성자를 지칭하는 것입니다. 여기서는 Parents()라는 메소드를 사용해서 클래스에 대한 인스턴스를 생성하라는 의미입니다.

여러분이 흔히 잘못 알고 있는 것들, 또는 책에서 잘못 생각하고 있는 것들이 바로 이 부분입니다. Parents sayp = new Parents()에서 첫번째 Parents와 Parents()가 모두 같은 클래스라고 설명하는 것은 잘못된 것입니다. 첫번째 Parents는 클래스를 생성하는 것입니다. 두 번째 Parents()는 "이러한 종류의 생성자를 사용하라"는 의미입니다.
이제 두 번째 생성자를 만들어보지요.
public class Parents
{
   public Parents()
{
  Console.WriteLine("Parent Constructor");
}

public Parents(string str)
{
  Console.WriteLine("Parent Constructor: " + str );
}

   public virtual void Say()
   {
      Console.WriteLine ("Parent");
   } //End of virtual Say()

} // End of class Parents

AppMain 클래스의 Do 메소드를 다음과 같이 변경합니다.

   private void Do()
   {
      Parents sayp = new Parents("Vernon");
      sayp.Say();

      Child sayc = new Child();
      sayc.Say();

   } // End of Do()
소스 코드를 다시 컴파일하고 실행하면 결과가 달라집니다. 두 번째 생성자가 호출됩니다. 컴파일러는 Parents sayp = new Parents("Vernon");을 만나는 순간에 Parents 클래스의 생성자들 중에 어떤 것을 사용해야 할지 알 수 있습니다. 왜 알 수 있죠? 제가 처음에 설명했습니다. 메소드 이름과 메소드에 전달되는 인자들을 모두 합쳐서 메소드 서명이라고 한다고 했습니다. 이 메소드 서명을 통해서 컴파일러는 어떤 것을 사용하려고 하는지 알아낼 수 있습니다. 쉽게 말해서 뭐죠? 서명이 일치하는 것을 찾아내는 겁니다.
Parents 클래스와 비교하는 것이지요.
여기서는 Parents sayp = new Parents("Vernon")이면 Parents 클래스를 사용하고, 인스턴스는 sayp로 사용하고, new는 붕어빵을 찍어라, Parents("Vernon")는 뭐죠? Parents( string ) 형식으로 된 생성자를 사용하라는 의미입니다. 마찬가지로 자식 클래스에도 생성자를 생성합니다. 생성자는 다음과 같습니다.
public class Child : Parents
{
   public Child()
{
  Console.WriteLine("Child Constructor");
}

   public override void Say()
   {
      Console.WriteLine ("Child");
   } //End of override Say()

} // End of class Child
예제를 다시 컴파일하고 실행하면 조금 의외의 결과가 나타나는 것을 볼 수 있습니다. Child 클래스에 대한 인스턴스를 생성하는 순간 Parents 클래스의 생성자로 함께 실행되는 것을 알 수 있습니다. 왜 그렇죠? Child 클래스가 Parents 클래스를 상속받기 때문에 그렇습니다. 지금까지 우리는 생성자를 만들어쓰지 않았습니다. 그런데 어떻게 Class aClass = new Class(); 와 같이 생성자를 지정할 수 있었을까요? 이런 경우에 C# 컴파일러가 생성자에 대한 껍데기를 자동으로 만들기 때문에 그렇습니다.

질문, 생성자는 언제 쓰는 게 좋을까요?
네, 생성자는 클래스 사용시 초기화할 필요가 있는 것들에 대해서 사용합니다. 지금부터 조급해 할 필요는 없습니다. 제가 설명해 나가다가 이런 경우에 생성자를 쓰는 게 좋다고 알려드릴 테니까요. 제가 지금 여러분에게 제 아무리 생성자는 클래스를 초기화할 때 사용하는 것이 좋다고 말한들 그렇게 할 수 없을 거라는 것을 알고 있습니다. 여러분이 모른다거나, 잘 못하기 때문이 아닙니다. 저 역시 그러한 단계를 밟아왔습니다. 그렇게 때문에 여러분도 지금 당장 그런 의미를 깨닫는 데에는 무리가 있습니다. 단계를 밟아 천천히 꾸준히 나아가도록 하지요.

두번째 숙제는 int와 int를 더하고, float와 float를 더하는 데 사용하는 Add를 만들어 보자는 것이었습니다. Add라는 의미가 int끼리도 더할 수 있고, float끼리도 더할 수 있게 만들어 보자는 것이었습니다. 이것의 의미는 무엇일까요?

오버라이딩 인가요? 오버로딩 인가요?

오버라이딩을 뭐라고 했죠? "하나의 의미를 다른 것으로 대체하는 것"이라고 했습니다. 오버로딩은 뭐라고 했죠?? 네? 좀 크게 얘기해 보세요. 안들리네요. 틀려도 되요. 부끄러워 할 필요는 없어요. 배우는 사람이 부끄러워하면 배워나갈 게 없습니다. 학생일 때 부끄러워하지 않으면 어떻게 하나요. 학생은 틀리면 그걸로 끝이지만 회사에서는 틀리면 틀린 걸로 끝나지 않잖아요. 그런 의미에서 오버로딩이 뭐라구요? 네!
그렇죠… "하나가 여러 개의 의미를 갖는 것"이라고 했습니다. 제가 대표적인 걸로 뭘 설명했었죠? + 기호를 설명했었죠. 숫자와 숫자를 더하는 것 뿐만 아니라 문자열과 문자열을 더하기 위해서 + 기호를 사용한다고 했었죠. + 기호라는 것은 실제로 숫자를 더하기 위한 연산기호일뿐, 문자열을 더할 수 있는 기호는 아니지요. 제가 여러분들에게 내드린 두 번째 숙제는 오버로딩에 대한 것이었습니다. 이번에도 다른 분의 소스를 보면서 제가 다시 올바른 코딩에 대해서 지적해 드리도록 하겠습니다.
using System;

public class Additem
{

   /*******************************************
    *
    * int 형과 int 형을 더하는 메소드
    *
    *******************************************/

   public void Add(int val1,int val2)
   {

      int sum = val1 + val2;
      Console.WriteLine("Result =" + sum.ToString() );

   } // End of Add(int)


   /********************************************
    *
    * float형과 float형을 더하는 메소드
    *
    ********************************************/

   public void Add(float fval1, float fval2)
   {
      float sum = fval1 + fval2;
      Console.WriteLine("Result =" + sum.ToString());

   } // End of Add(float)


} // End of Additem


public class AppMain
{

   static void Main()
   {
      AppMain ap = new AppMain();
      ap.AddData();

   } // End of Main()

   private void AddData()
   {
      Additem a1 = new Additem();
      a1.Add(val1,val2);

      Additem a2 = new Additem();
      a2.Add(fval1,fval2);

   } // End of AddData


   private int val1 = 1;
   private int val2 = 2;
   private float fval1 = 3f;
   private float fval2 = 4f;

} // End of class AppMain
와우! 이 분의 코드는 잘 실행됩니다. 주석도 꽤 공들여서 했습니다. 제가 늘 하는 말이 있습니다. 아름다운 코드가 보기에도 좋다! 다른 말이 또 있죠. 보기 좋은 떡이 먹기도 좋다! 뭐, 또 아리따운 아가씨도 있으면 좋겠지만, 그런 경우에는 남자들한테 흔히 하는 말이 있죠. 견물생심, 그림에 떡이라고…(웃음) 먼저 오버로딩이 어디서 어떻게 되어 있는지 살펴보도록 하지요.
  public void Add(int val1,int val2)
  public void Add(float fval1, float fval2)
위 부분이 오버로딩을 구현한 부분입니다. 네, 이처럼 메소드 서명이 다른 경우에 오버로딩이 일어납니다. 그리고 Add(1, 3)과 Add(1.1f, 4.5f)를 만나게 될 때 컴파일러는 서명이 일치하는 것을 찾아서 실행하게 되는 것이지요.
여기서 제가 왜 Add(1.1f, 4.5f)를 사용했죠? 네, C#에서는 기본 실수형은 double입니다. 따라서 1.1과 4.5만 사용하면 오류를 만나게 됩니다. 하지만 실제로는 위와 같은 메소드를 사용하지 않습니다. 이분은 Add 메소드 안에서 출력을 했지만, 대부분의 경우에 값을 반환하는 형태가 됩니다. 즉, return result;와 같이 적절한 결과를 반환하는 것이 필요하겠지요. 따라서 첫번째 Add 메소드의 void는 int로 변경하고, 두 번째 Add 메소드는 float로 변경하는 것이 필요합니다.
AppMain 클래스의 AddData() 메소드를 한 번 살펴볼까요.
   private void AddData()
   {
      Additem a1 = new Additem();
      a1.Add(val1,val2);

      Additem a2 = new Additem();
      a2.Add(fval1,fval2);

   } // End of AddData
잘 작성되어 있습니다. 하지만 꼭 두 개의 인스턴스를 생성할 필요는 없습니다. 오버로딩의 의미가 다른 Add를 갖는 각각의 인스턴스를 자동으로 만들어 준다든가 하는 것은 아니니까요. 그저 a1.Add(val1, val2);와 a1.Add(fval1, fval2); 와 같이 사용하면 됩니다. 또한 a1.Add(val1,val2);와 같이 몽땅 붙여서 쓰지 마세요. 제가 쓴 것처럼 모두 띄어 쓰세요. 왜 이렇게 할까요?
보기 좋은 떡이 먹기도 좋다!
모니터에서 조금 멀찍이 떨어져서 위 코드를 살펴보세요. 아마 띄어쓰기를 한 코드가 더 눈에 잘 들어오고, 쉽게 이해할 수 있을 겁니다. 그러니 조금 귀찮고 번거롭더라도 스페이스 바 몇 번 더 눌러서 코드를 이쁘게 작성하기 바랍니다. 그리고 a1.Add(val1, val2)와 같이 작성하지 말고 a1.Add(this.val1, this.val2)와 같이 작성하시기 바랍니다. this라는 것은 이 클래스의 것을 사용한다라는 의미입니다. val1이 어떤 것인지 보다 명확하게 해 주는 것이지요.
이제 다른 것들을 살펴볼까요.
   private int val1 = 1;
   private int val2 = 2;
   private float fval1 = 3f;
   private float fval2 = 4f;
AddData() 메소드 밑에 위와 같이 내부 데이터가 있습니다. 이 경우에 AddData() 메소드에서 테스트하는 데이터가 별도로 떨어져 있습니다. 만약 AddData() 메소드와 테스트에 사용할 데이터들 사이에 코드가 너무 많이 있어서 서로 떨어져 있다면 이러한 값들이 어디서 오는지 알기 어렵게 됩니다. 그런 경우도 있지 않나요? 다른 사람이 작성한 코드를 보다 보면 ii, jj, kk라는 변수에 도대체 어떤 값이 들어가고, 어떤 역할을 하는지 몰라서…
"저게 버그의 원흉인 것 같은데!!"
하면서 의심의 눈초리를 보내본 적이 없나요? 불행히도 저는 아주 많습니다. -_-

이러한 것들은 뭐죠? 프로그램을 작성한 사람만이 알고있는 "가정"입니다. 프로그래밍 코드에서는 이러한 가정을 철저하게 배제해야 합니다. 이러한 가정은 프로그램을 작성한 사람도 한 달만 지나면 "자신이 어떠한 가정을 했는지 잊어버린다"는 중요한 맹점이 있습니다. 그러니 이와 같은 private으로 선언된 변수들은 모두 AddData() 안으로 깔끔하게 옮겨야 합니다.
   private void AddData()
   {

      int val1 = 1;
      int val2 = 2;
      float fval1 = 3f;
      float fval2 = 4f;

      Additem a1 = new Additem();
      a1.Add(val1, val2);
a1.Add(fval1, fval2);

   } // End of AddData
이것으로 제대로 된 코드가 완성된 것 같습니다. 여기서는 this.val1과 같이 사용하지 않습니다. 클래스의 변수가 아니라 메소드 내부에 선언한 변수를 사용하는 것이니까요. ^^; 여러분이 작성한 코드가 제대로 동작하지 않는다는 의미가 아닙니다. 보다 객체 지향에 맞게, 또는 보다 유지 보수하기 쉽고, 프로그래밍하기 쉬운 코드가 되었다는 의미입니다.

오늘도 계속해서 농담따먹기나 하려합니다. 휴… 벌써 시간이 많이 흘렀군요. 오늘은 수업에 늦은 사람을 위해서 객체 지향을 다시 설명해 봅시다. 클래스나 이런 것들을 다시 설명하지는 않습니다. 오버로딩과 오버라이딩에 대해서 다시 한 번 설명해 보겠습니다. 햐~ 저도 참 골치 아파하던 사실을 이제는 이렇게 당연하게 얘기하고 있다니… 제 자신도 놀랍게 생각하고 있습니다. 하하… 저도 참 많이 컸군요.

먼저 여기서는 예외처리에 대해서 얘기하도록 하지요. 사실, 첫날에도 예외처리에 대해서 설명했었는데, 글을 쓸 때는 도통 기억이 나질 않아서 예외 처리에 대한 이야기를 쏙 빼먹었습니다. 그리고 객체 지향에서 상속을 설명할 때도 예외처리를 설명했는데, 글에서는 쏙 빼먹었지요. 기억력이 나날이 감퇴되고 있다는…

예외 처리하기

첫날에 얘기했던 것이 무엇이죠? 프로그래밍 언어는 크게 3가지로 되어 있다는 것입니다. 메모리에 값을 집어넣는 할당, 메모리에 있는 값을 가져와서 계산하는 연산, 할당과 연산을 무한히 반복해는 루프 즉, 반복문이지요. 그리고 연산에 대해서 이야기하면서 다음과 같은 예를 들었습니다.
int a = 5;
int b = 7;
Console.WriteLine( (a + b).ToString() );
이와 같이 얘기했습니다. 여기서 +는 숫자와 숫자를 더하는 연산 기호지요. 연산 기호에는 4가지가 있다고 했습니다. 뭐가 있다고 했지요? +, -, *, /이 있다고 했습니다. 그리고 이중에서 +만 가장 많이 사용한다고 했습니다. 나머지는 자주 사용하지는 않습니다. 어쨌거나 만약에 a * b를 계산한다고 하지요. 여기서는 값을 미리 정했지만 계산기 프로그램을 작성한다면 사용자가 어떤 값을 넣는지 알 수 없지요?
네, 어떤 사용자가 계산기에 100000000000을 입력하고 *를 누르고, 다시 1000000000을 입력할 수 있습니다. 이 경우에 int 형으로는 저렇게 큰 값의 계산 결과를 저장할 수 없고, 프로그램은 예상되지 못한 동작을 하기 때문에 오류가 나면서 종료하게 됩니다. 이와 같은 경우를 막기 위해 어떻게 하지요?

보통 C 언어에서는 이런 방법을 사용했습니다. 입력되는 값이 int 형이나 long에서 사용할 수 있는 크기보다 작은지 검사하는 방법을 사용합니다. 주로 if 문을 써서 검사하겠지요. 때문에 예외 처리를 하느라 여기저기 if 문을 사용해야 했습니다. C#에서도 if 문을 써서 값을 검사하는 방법을 쓸 수 있지만, 조금 세련된 방법을 사용합니다. 보통은 다음과 같이 사용합니다.
try
{
  // 에러가 발생할 지도 모르는 코드
}
이와 같이 에러가 발생할 수 있는 코드를 try로 감쌉니다. 그리고 에러가 발생하면 발생한 에러를 잡기 위해 catch 문을 추가합니다. catch문을 추가하면 다음과 같습니다.
try
{
  // 에러가 발생할 지도 모르는 코드
}

catch( Exception e )
{
  Console.WriteLine(e.Message.ToString());
}
그리고 예외가 발생하는 것과 상관없이 항상 실행해야 할 코드들을 지정하기 위해 finally 블록을 사용합니다. 예를 들어 게임 프로그래밍을 하고 있는데 알 수 없는 에러가 발생했다고 "검은 화면"만 보여주고 프로그램이 죽어버려서는 안되겠지요. 어떻게든 게임 프로그램을 끝내고 원래 윈도우 화면으로 돌아갈 수 있게 해주어야 합니다. 이럴때 finally가 위력을 발휘합니다.
try
{
  // 에러가 발생할 지도 모르는 코드
}

catch( Exception e )
{
  Console.WriteLine(e.Message.ToString());
}

finally
{
  // 반드시 실행해야 할 코드
}
이와 같이 하여 보다 안전하게 예외를 잡을 수 있습니다. 예외에는 이 외에도 많은 유형들이 있습니다. 그중에 대표적인 것으로는 OverflowExceptionsk IndexOutOfRangeException 등이 있습니다. 이름에서 알 수 있는 것처럼 int 형이 담을 수 있는 데이터 크기를 넘어버리면 OverflowException이 발생합니다. 위에 있는 Exception은 모든 예외를 잡아버릴 수 있는 "만병통치약"입니다. ㅎㅎ…

하지만, 여러분도 잘 알고 있다시피, "날이면 날마다 오는 게 아닙니다!" 라고 광고하면서, 북치고, 원숭이가 묘기도 부리면서 박카스병 비스무리한 것에 담긴 정체를 알 수 없는 그 약이 "만병통치약"이라고 불리지만, 아무도 정체를 모를 뿐더러 먹고 나면 오히려 병만 더 생기지 않았나요? 아… 여러분은 똑똑해서 그럴 일이 없다고요? 전 오늘도 지하철을 타다가 그런 만병통치약을 봤습니다. 특정 제품을 홍보할 수는 없겠지만, "오늘 밤도 거뜬하게~ 파워 XX"라든가, "아내가 먼저 알아요~ XX 타임"이런 광고들이 지하철에 버젓이 붙어있죠. (웃음) 뭐… 건강보조 식품으로 허가된 것들이 마치 약처럼 선전되고 있는 것을 보면 "만병통치약"과 별다를 게 없다고 생각합니다.
네, 병이 있다면 이놈의 "만병통치약"을 먹을 게 아니라 증상에 따른 적절한 처방을 받는 것이 좋습니다. 그래야 위장약이라고 무좀약을 먹는 실수는 안하겠죠. 그래서 증상에 맞는 처방을 하기 위해서 Exception보다는 OverflowException이라든가 IndexOutOfRangeException 등을 사용할 수 있습니다. 이러한 것들을 처리하는 방법은 다음과 같습니다.
try
{
  // 에러가 발생할 지도 모르는 코드
}

catch( OverflowException oe )
{
  Console.WriteLine(e.Message.ToString());
}

catch( IndexOutOfRangeException rangeException )
{
  Console.WriteLine(rangeException.Message.ToString());
}

catch( Exception e )
{
  Console.WriteLine(e.Message.ToString());
}

finally
{
  // 반드시 실행해야 할 코드
}
이처럼 catch는 여러 개를 사용할 수 있습니다. 그리고 이러한 "약"들이 모두 듣지 않을 경우, 가장 마지막으로 정체가 뭔지 모를 "만병통치약" Exception을 사용하는 겁니다.

상속

상속의 다른 이야기를 해보지요. 지금까지 예외에 대해서 한참을 신나게 설명했습니다. 자세한 계층구조는 무시하겠지만 Exception은 보통 다음과 같은 구조로 되어 있습니다.
System.Exception
-	System.OverflowException
    -   System.SystemException
- System.IndexOutOfRangeException
- System.OutOfMemoryException
여기서 알 수 있는 것처럼 Exception 클래스도 사실은 복잡한 계층 구조로 되어 있는 것을 알 수 있습니다. SystemException은 Exception 클래스를 상속하고, IndexOutOfRangeException이나 OutOfMemoryException은 System.Exception 클래스를 상속하고 있는 것을 알 수 있습니다.
즉, OutOfMemoryException이 발생했을 때 catch 구문에서 OutOfMemoryException을 잡아내는 곳이 없으면 SystemException으로 올라갑니다. 다시 catch에서 SystemException을 잡아내는 곳이 없으면 다시 Exception으로 올라갑니다. 이와 같이 계층 구조가 올라가는 것을 알 수 있습니다.

실제로 Exception 클래스들은 Exception 클래스와 동일한 기능을 제공합니다. 실제로 다른 게 하나도 없습니다. 99%의 Exception 클래스들은 모두 Exception 클래스와 동일합니다. 그러면 왜 저렇게 긴 상속을 만들어 냈는가? 네, Exception 클래스는 다소 예외긴 하지만 만병 통치약을 쓰는 대신에 "증상에 따른 약 처방"을 하자라는 목표아래 위와 같이 많은 계층 구조를 만들어 낸 것입니다. 이러한 배려 때문에 프로그래머는 발생하는 예외에 대해서 적절한 처리를 해 줄 수 있습니다.

휴… 오버로딩과 오버라이딩에 대해서는 또 계속해서 설명할 기회가 있을 테니 그때마다 다시 설명해 드리겠습니다. 그리고 try…catch…finally 구조는 여러분들이 자주 사용하지 않을거라 생각합니다. 그도 그럴 것이 처음보는 녀석인데 친하다면 이상하지 않나요? 가끔, 동성끼리도 첫눈에 "feel~"이 통해서 친해지는 경우도 있다고 하지만, 음… 제 경우엔 좀처럼 그렇게 되진 않는군요. 도대체 처음 만나서 친해지다니… -_-
여자도 마찬가지 아닌가요. 우연을 가장한 만남을 계속 가져서, 조금씩 친해지는 게 아닌가 싶습니다. 그러니 try … catch … finally를 처음 본 순간부터 느낌이 통해서 친숙하게 사용할 수는 없을 겁니다. 걱정하지 마세요. 제가 이것들을 매우 친숙하게 사용하게 만들어 드릴 겁니다. 어떻게 그렇게 할 수 있을거냐구요?
ㅎㅎ… 여기서 저의 악마적인 본성이 드러나는군요. 네! 저는 앞으로도 여러분에게 계속해서 "프로그램을 작성해 봅시다"로 시작할 겁니다. 그리고 여러분이 만들어낸 프로그램을 계속해서 망가뜨릴 겁니다. 그리고 이렇게 망가지는 것을 예방할 수 있는 방법은 try … catch … finally라는 것을 알려드릴 겁니다. 걱정하지 마시길…

모든 아기 엄마가 아가의 예방 접종일을 알아 두고 엄마가 된 것이 아니듯, 여러분도 계속해서 코드가 망가지지 않는 것에 관심을 가지게 되면 try…catcg…finally를 사용하고, 적절한 Exception 클래스를 사용하는 것에 익숙해지게 될 겁니다.

음… 오늘의 주제는 이거군요. 최후의 순간이 아니면 정체를 알 수 없는 "만병통치약"은 사용하지 맙시다. 증상은 의사에게, 처방은 약사에게 받으세요. 헐…

날씨가 무더워지는군요. 수고하시기 바랍니다.
ps. 질문 사항이나 여러분이 작성한 코드를 위에서처럼 친절하게 검토받고 싶다면 한빛미디어 편집진(webmaster@hanbitbook.co.kr)에게 보내세요. 아차, Programming C#을 구입하시고 등록하신 분만 보내세요. 다른 분들까지 해드리기에는 제 손이 미치질 않습니다. 어디까지나 이 강의는 Programming C#을 교재로 진행되고 있음을 알려드립니다. 음, 저도 먹고 살려니 어쩔 수 없는 것 아니겠어요. (웃음)
TAG :
댓글 입력
자료실