Add(int a, int b)위와 같은 함수를 정의했다고 합시다. 이때 실수와 실수끼리 더하는 경우에는 사용할 수 없으므로 다른 함수를 만들어야 할 겁니다.
AddFloat(float a, float b)네… 지금 저는 실제 코드가 아니라 단순한 의사 코드만을 적고 있습니다. 이점은 양해를 구하지요. 마찬가지로 double을 더하는 함수를 만든다고 한다면 다음과 같은 함수를 만들어야겠지요.
AddDouble(double a, double b)나머지 형식들에 대해서도 비슷한 함수를 만들어야 할 겁니다.
Add(int a, int b) AddFloat(float a, float b) AddDouble(doube a, doube b) AddLong(long a, long b)다른 경우로 만약에 정수와 실수를 더한다면 다음과 같은 함수도 만들어야 겠죠.
AddIntFloat(int a, float b)하아… 4가지 데이터 형에 대해서 덧셈을 하는 함수를 만든다고 하면 4*4니까 16개의 함수를 만들어야겠군요. 그리고 이 클래스를 사용하는 프로그래머가 외워야 할 함수의 개수도 16개가 되는군요. -_- 인간의 머리도 한계가 있는지라 이처럼 복잡한 것은 외우기 어렵습니다. 차라리 이러한 것들을 모두 Add(a, b)라는 형태로 사용할 수 있다면 좋지 않을까요? 이것을 가능하게 하기 위해 도입한 것이 바로 오버로딩입니다. 오버로딩을 해준다면 다음과 같이 써주면 됩니다.
int Add(int a, int b) float Add(float a, float b) double Add(double a, double b)여기서도 마찬가지로 저는 의사 코드를 사용하고 있습니다. 실제 오버로딩에 대한 예는 책에 나와있으니 완전한 형식은 책을 참고하세요.
SomeClass some = new SomeClass(); Console.WriteLine( some.Add(3, 5) ); Console.WriteLine( some.Add(1.3f, 4.5f).ToString() );두 번째 줄을 보면 Add(int, int) 형식을 사용하고 있습니다. 컴파일러는 이것을 알아보고 자동으로 int Add(int a, int b)로 선언된 부분의 코드를 실행합니다. 마찬가지로 Add(float, float) 형식을 알아보고, float Add(float a, float b)로 선언된 부분의 코드를 실행합니다. 이것은 컴파일러가 똑똑하다든가, 찍기를 잘 한다든가 하는 게 아닙니다. 위에서 메소드를 선언한 것처럼 public int Add(int a, int b)와 같은 전체를 메소드 서명이라고 합니다. 사람 개개인에 따라 고유의 서명을 갖고 있듯이 각 메소드를 고유하게 구분하기 위해서는 이러한 것들을 하나의 "서명"으로 취급합니다. 그러니 컴파일러는 사용된 코드에서 볼 수 있는 서명 중에 적절한 것을 찾아서 저렇게 쓸 뿐이지요. ^^; (기특한 것…)
Console.WriteLine("MyAge: {0}, MyWeight: {1}", age, weight);이렇게 쓰는 것은 번거로우니 다음과 같이 쓰자고 말이지요.
Console.WriteLine("MyAge: " + age + ", MyWeight: " + weight);여기서 + 라는 것이 어떤 의미죠? 네, 문자열을 합친다는 의미로 사용했습니다. 원래 +는 숫자와 숫자를 더한다는 의미입니다. 따라서 문자열과 문자열을 합치는 데는 사용할 수 없습니다. C 언어를 하셨다면 아마 문자열을 합치기 위해서 strcat 함수를 사용했던 것이 기억이 날 겁니다. STRing CATenate(스트링 캐터네잇이라고 읽죠)를 합친 이름이죠. 어쨌거나 문자열을 합친다는 게 별로 개념적으로 와닿지 않습니다. 사실, 저도 문자열을 합치기 위해서 다음과 같은 코드를 사용하는 것은 좋아하지 않습니다.
char string[80]; strcpy(string, "Hello"); strcat(string, " Cruel"); strcat(string, " World"); strcat(string, " !");정말이지 이런 코드는 싫습니다. 다음과 같이 쓰는 것은 어떨까요?
string str; str = "Hello" + " Cruel" + " World " + " !";얼마나 멋져요…!
int a;라고 쓰는 것은
System.Int32 a;라고 쓰는 것과 같고, VB.NET에서
Dim a As Integer라고 쓰는 것역시
System.Int32 a;와 같습니다. 다만 System.Int32라고 입력하는 게 귀찮으니까 int라고 입력합니다. (사실 자세한 이야기는 나중에 하겠습니다만) 이처럼 모든 데이터 형식을 객체로 하면 좋겠지만, 실제로는 객체가 아닙니다. int, long, double 이런 형식들은 모두 스택에 데이터를 저장합니다. 단순한 데이터 형식은 객체가 아니기 때문에 메소드를 사용할 수 없습니다. 앗! 그런데 우리는 지금까지 데이터 형식에 대해서 메소드를 사용하지 않았습니까?
int a = 5; Console.WriteLine( a.ToSring() );위와 같이 a.ToString()라고 쓰는 것처럼 메소드를 사용하고 있습니다. 이처럼 메소드를 쓰는 순간에는 스택이 아니라 힙에 있는 a를 사용하게 됩니다. 실제로 int와 같은 형식은 스택에 있다고 하지 않았나요? 그런데 지금 이순간은 어떻게 해서 또 힙에 있다고 얘기하는 걸까요?
System.Object - System.ValueType - System.Int32 - System.Int64 - System.Long - System.DoubleSystem.Int32(int)도 객체 구조로 포함되어 있다고 합니다. 이처럼 모든 데이터 형식을 클래스 계층 구조로 묶으면서 속도도 빠르게 유지할 수 있게 하기 위해 박싱과 언박싱이라는 개념은 중요하다고 할 수 있습니다.
class Class1 { } class Class2 { }우리가 보기에는 같아보이지만 어쨌든 저 둘은 Class1과 Class2로 서로 다른 클래스라고 할 수 있습니다. 그리고 Class1에 대한 인스턴스를 생성한다고 합시다.
Class1 me = new Class1();이렇게 되어 있는 경우, 프로그래머는 me가 Class1인지 Class2인지 모르기 때문에 me가 어떤 클래스에 속하는지 알아 보려고 하려면 어떻게 할까요? 이럴 때 ==를 사용하는 것이 아니라 is를 사용합니다. 다음과 같이 사용하는 것이지요.
if ( me is Class1 ) { Console.WriteLine("me is Class1 !"); }어려운가요? 안 어렵죠? ^^; is 연산자에 대한 알기 쉬운 예제는 다음과 같습니다.
using System; class Class1 { } class Class2 { } public class IsTest { public static void Test (object o) { Class1 a; Class2 b; if (o is Class1) { Console.WriteLine ("o is Class1"); a = (Class1)o; // do something with a } else if (o is Class2) { Console.WriteLine ("o is Class2"); b = (Class2)o; // do something with b } else { Console.WriteLine ("o is neither Class1 nor Class2."); } } public static void Main() { Class1 c1 = new Class1(); Class2 c2 = new Class2(); Test (c1); Test (c2); Test ("a string"); } }출력
o is Class1 o is Class2 o is neither Class1 nor Class2.이번에는 as 연산자에 대해서 살펴봐야 겠군요.
public void SetColor(int Red, int Green, int Blue);보통 컴퓨터에서 자주 사용하는 색상으로는 삼원색인 빨강, 초록, 파랑이 있습니다. 그러니 위와 같이 정의했다고 합시다. 이 경우, 보통 다음과 같이 사용하겠죠?
SetColor(0, 255, 255);하지만 어떤 사람들은 다음과 같이 잘못 사용할 수도 있습니다.
long red, green, blue; SetColor(red, green, blue);이런 경우에는 정수를 사용하는 것이 아니라 문자열을 사용하게 됩니다. 당연히 함수에서는 전달되는 인자들을 검사하거나 적절하게 에러를 처리하는 방법을 사용해야 합니다. 상당히 어려운 문제고 고민되는 문제입니다. 만약 함수에 전달되는 형식을 어떠한 형태인지 미리 검사할 수 있다면 상당히 편하지 않을까요? 이와 같이 미리 형식을 검사할 수 있게 해주는 키워드가 바로 as입니다. 그러니 위에서 했던 함수에 대한 정의는 다음과 같이 변경됩니다.
public void SetColor(int Red as int, int Blue as int, int Green as int);뭐… 별로 좋은 예는 아닌 것 같습니다. -_- 그리고 위와 같이 잘못 전달되는 형식에 대해서는 null로 설정해줍니다. 따라서 함수 내에서는
if ( Red == null ) { Red = 0; }이와 같이 설정을 다시 해주는 방법도 가능합니다. 즉, 전달되는 형태가 int 형이면 상관없지만 int 형이 아닌 경우에는 객체를 null로 설정해주기 때문에 함수가 원하는 데이터 형식만 받을 수 있게 해줄 수 있습니다. 하지만, 여러분이 익숙하지 않기 때문에 잘 사용하지 않을 수도 있겠군요. as에 대한 예제를 하나 더 살펴보는 것으로 as에 대한 설명을 마무리 하도록 하지요.
using System; class MyClass1 { } class MyClass2 { } public class IsTest { public static void Main() { object [] myObjects = new object[6]; myObjects[0] = new MyClass1(); myObjects[1] = new MyClass2(); myObjects[2] = "hello"; myObjects[3] = 123; myObjects[4] = 123.4; myObjects[5] = null; for (int i=0; i이 예제의 실행결과는 다음과 같습니다. 0:not a string 1:not a string 2:"hello" 3:not a string 4:not a string 5:not a string결과에서 볼 수 있는 것처럼 as string으로 정의되어 있기 때문에 형식이 string이 아닌 경우는 모두 null로 설정되는 것을 알 수 있습니다.
ref와 out
지금까지 클래스에서 메소드를 정의했던 방법들은 모두 인자의 값을 복사하는 방법이었습니다. 흔히 함수에 값을 전달하는 방법은 값에 의한 전달과 참조에 의한 전달이라고 얘기합니다. 뭐, 이렇게까지 어려운 용어를 쓰면서 얘기할만한 것은 아닌 것 같습니다만. -_-
어떤 좌표를 이동하는 프로그램을 작성하는 경우를 생각해보죠. 이런 경우에 Move(int x, int y)와 같이 좌표를 넘긴다면 좌표값 x와 y는 이동한 위치가 되어야 할 니다. 예를 들어 점 A가 현재 (10, 10)이라는 위치에 있었는데, Move(20, 30)과 같이 좌표를 이동했다면 점 A의 값은 (20, 30)이 되어야 한다는 것이죠. 지금까지 메소드를 정의했던 방법들은 다음과 같았습니다.int bonus = 100; int paid = 2; public int Result(int bonus, int paid) { return bonus * paid; }이 경우에 처음에 정의된 bonus와 paid의 값은 변경되지 않습니다. 사실 변경될 필요도 없습니다. 하지만 전달되는 bonus와 paid의 값을 변경하고자 Result를 다음과 같이 변경했다고 합시다.public int Result(int bonus, int paid) { bonus = 200; paid = 3; return bonus * paid; }이 경우에 반환되는 결과는 달라지겠지만 bonus는 100이고 paid는 2라는 사실은 전혀 변하지 않는 다는 것을 알 수 있습니다. 따라서 함수 안에서 변경된 내용을 함수 바깥에도 적용할 방법이 필요합니다. 그리고 이러한 방법을 정의한 것이 ref라는 키워드입니다. 위 함수에서 bonus와 paid의 변경된 값을 함수 외부에도 적용하려면 다음과 같이 작성하면 됩니다.public int Result(ref int bonus, ref int paid) { bonus = 200; paid = 3; return bonus * paid; }이제는 이 함수를 실행한 다음에도 bonus와 paid의 값이 변경된다는 것을 알 수 있습니다. 마찬가지로 처음에 좌표가 변경되는 경우에도 x와 y의 값이 변경되는 것입니다. 완전한 Point 클래스를 정의한다면 다음과 같을 겁니다.class Point { private int x; private int y; public void Move(ref int xPosition, ref int yPosition) { this.x = xPosition; this.y = yPosition; } }어려웠나요? 그리 어렵지는 않았을 겁니다.
ref를 쓰는 또 다른 이유를 생각해 봅시다. 제가 지금까지 ref를 쓰는 이유에 대해서 이렇게 얘기했습니다. "함수 내부에서 변경된 값을 함수 외부에도 적용하기 위한 방법"! 하지만, 이 외에도 ref를 사용하는 이유는 한 가지가 더 있습니다.
보통 메소드에서 어떤 값을 계산하는 경우, 하나의 값을 반환합니다. 값을 반환하기 위해 return 키워드를 사용합니다. 하지만 좌표와 같이 x와 y를 모두 반환해야 하는 경우에는 어떨까요? return 하나로는 이 둘을 반환할 수 없습니다. 그런 경우에 사용할 수 있는 방법이 ref입니다. ref를 사용하면 함수가 여러 개의 값을 반환할 수 있게 할 수 있습니다. 사실 이게 유일한 방법은 아닙니다. 좌표와 같이 특별한 경우라면 Point 클래스에 적절한 값을 설정해서 return 키워드를 사용해서 클래스를 반환하는 방법도 사용할 수 있을 테니까요.
ref는 참조로 전달한다는 것을 지정하기 위해서 사용합니다. 하지만, ref에는 한 가지 단점이 있습니다. 전달되는 인자가 초기화되어 있는지까지는 검사하지 않습니다. 다시 번거로워지는 것이지요. 따라서 값을 전달하는 경우가 아니라 단순히 결과를 받기 위해서 사용하는 경우에 대해서는 out이라는 키워드를 사용할 수 있습니다. out 키워드를 사용해서 얻을 수 있는 장점은 알아서 변수를 초기화 해준다는 것이지요. 예를 들어 다음과 같은 코드를 생각해보세요.using System; class AppMain { public static void Main() { AppMain ap = new AppMain(); ap.DoTest(); } public void DoTest() { string str; Console.WriteLine(str); } }DoTest() 메소드에서 볼 수 있는 것처럼 Console.WriteLine(str)이라는 문장에서 에러가 발생하면서 컴파일조차 되지 않는다는 것을 알 수 있을 겁니다. 변수에 대해서 반드시 초기화를 해야 합니다. 따라서 다음과 같이 사용해야 합니다. 그러나 out 키워드를 사용하면 이러한 초기화를 멋지게 할 수 있습니다.using System; public class MyClass { public static int DoTest(out string str) { str = "Hello World"; return -1; } public static void Main() { string myStr; Console.WriteLine(DoTest(out myStr)); Console.WriteLine(myStr); } }실행결과는 다음과 같습니다.-1 Hello World첫번째는 함수의 반환값을 출력한 것이고, 두 번째는 out 키워드를 사용해서 myStr의 결과를 출력한 것입니다. 만약 out 키워드를 제거하면 초기화되지 않은 값을 전달하기 때문에 컴파일이 되지 않습니다. 또한 out 키워드를 사용하지 않으려면 다음과 같이 선언하는 것이 좋습니다.string myStr = String.Empty;그러나 이러한 것들보다는 out 키워드를 사용해서 반드시 변수가 초기화되어 있다는 것을 직접 지정해 줄 수 있고, 값을 전달하는 것이 아니라 값을 전달 받는다는 것을 지정할 수 있기 때문에 out을 사용하는 것이 좋습니다. 사실 몇 만 줄이 넘어가는 소스 코드를 작성하다보면 다른 개발자들이 작성한 것들의 의미를 알아내기가 무척 어렵습니다. 그러나 ref와 out 키워드는 개발자의 의도를 정확하게 알 수 있게 해줍니다.
정리하면 다음과 같습니다.여기서는 RGB 색상을 넘겨주면 그에 해당하는 Green, Yellow, Blue, Purple, PapayaWhip, Boris와 같은 색상 이름을 colorName에 넘겨 주도록 정의했습니다. 다시 설명하지만 ref는 함수 내에 참조를 사용해서 값을 전달할 때 사용하고, out은 참조를 사용하지만 값을 함수에 전달하는 것이 아니라 함수에서 어떤 값을 받아올 때 사용하는 것을 지칭합니다.
- 함수 내에 값을 전달하지만 함수 외부에는 영향을 미치고 싶지 않다면 단순히 메소드를 정의하면 됩니다.
public void SetColor(int red, int green, int blue)
- 함수 내에 값을 전달하지만 함수에서 변경된 사항이 함수 외부에도 적용되기를 원한다면 ref 키워드를 사용해서 인자를 정의합니다.
public void SetColor(ref int red, ref int green, ref int blue)
- 함수 내에 값을 전달하지 않지만 함수에서 변경된 결과를 넘겨 받는 용도로 인자를 사용하고 싶다면 out 키워드를 사용해서 인자를 정의합니다.
public void SetColor(ref int red, ref int green, ref int blue, out string colorName)
휴… 이제 is, as, ref, out에 대한 이야기가 끝난 것 같습니다. 값에 의한 전달이 어쩌구, 참조에 의한 전달이 어쩌구 하는 것쯤은 잊어버립시다. 정작 중요한 것은 위 내용만 알면 됩니다.
자, 제가 메소드에서 하나 이상의 값을 전달하려면 return이 아니라 ref나 out을 사용하는 것이 좋다고 얘기했습니다. C 언어를 생각해 볼까요? C 언어도 마찬가지로 return 문을 사용했습니다. 그리고 하나 이상의 값을 전달하는 경우에는 어떻게 했을까요? C 언어라고 값을 하나만 반환하는 것은 아니겠죠? 분명 하나 이상의 값을 반환해야 하는 경우가 있을테니까요. 네, C 언어에서는 이런 경우에 포인터를 사용해왔습니다. C#에서 ref니 out이니 하는 것들이 사실은 포인터를 포장한 겁니다. 다시 살펴볼까요?public void SetColor(ref int red, ref int green, ref int blue)C 언어라면 아마도 다음과 같이 썼을겁니다.void SetColor(int* red, int* green, int* blue)하하… 크게 어렵지 않죠?
하지만 포인터라고 해도 ref나 out과 같이 어떤 의미를 나눠놓은 것도 아니고, 전달할 인자가 어떤 데이터 형식이어야 한다고 강제할 수 있는 as 같은 키워드를 정의하지도 않습니다. 사실은 조금 모호한 면이 있었던 것이죠.
ref와 out은 모두 C 언어에서 보자면 포인터입니다. 뭐, 오늘은 이 정도로 할까요. 다음 시간에는 XML에 대해서 설명할 겁니다. 글쎄요. 진도와는 별 관계가 없어보이긴 하지만, XML은 닷넷에서 광범위하게 쓰이고 있기 때문에 제가 간단하게나마 설명할 필요가 있습니다.
지금은 여러분이 이 강의를 듣고 따라오기도 바쁘기 때문에 별도로 XML 도서를 구입해서 학습하는 것은 무리가 있다고 생각합니다. 사실 XML 이라는 것은 어떤 언어라고 보기는 어렵기 때문에 코드보다는 주로 개념에 대한 이야기가 주된 내용입니다. 제가 추천할 만한 XML 책으로는 Professional XML Applicatons, Wrox, XML 21일 완성, SAMS, About XML, 영진 정도가 있습니다. 각각의 책들은 장단점이 있습니다. 하지만, 지금 여러분은 강의를 따라오기도 벅차니, 지금은 이런 책들이 있구나라고 생각하시고, 메모해 둔 다음에 필요할 때 이러한 책들을 찾아보시기 바랍니다. 사실 위에서 말한 책들은 자바나 PHP, 파이썬으로 XML을 다루는 법들을 설명하고 있기 때문에 닷넷이나 C#을 하는 여러분에게 권해드릴만한 것은 못됩니다. 이 강좌가 진행중인 동안 닷넷에서 XML을 다루는 것에 대해서 좋은 책이 나와 있다면 제가 소개해 드리도록 하겠습니다.
이번 시간으로 6장까지의 설명이 모두 끝났습니다. 다음 시간까지는 9장까지 읽어보셨으면 합니다.
후기
실제로 많은 분들이 9장까지 진도를 나가지는 못했습니다. 9장까지 읽어보시고, 예제를 모두 해보시기 바랍니다. 그리고 3번째 강의를 끝으로 여러분에게 내드리는 간단한 숙제가 있습니다.
첫 번째 숙제는 Parent 클래스를 만들고 DoSay() 메소드를 정의하는 것입니다. DoSay() 메소드는 "I"m Parent"라고 출력하게 만들고 Parent 클래스를 상속한 Child 클래스를 만들어 봅시다. 그리고 Child 클래스의 DoSay() 메소드는 "I"m Child"라고 출력하게 만들어 보죠.
두 번째 숙제는 Add(int a, int b), AddFloat(float a, float b)와 같이 다형성을 갖는 것들을 모두 Add로 통일해서 사용할 수 있게 해주는 방법이 있다고 강의에서 설명했습니다. 이게 뭐라고 했었죠? 오버라이딩? 오버로딩?
"오버라이딩요" -_- 오버라이딩이라고요? 아니죠. 오버로딩입니다.
제가 뭐라고 했었죠? 오버라이딩은? 오버라이딩은 "기존의 것을 새로운 의미로 대체하는 것"이라고 했습니다. 하지만 여기서는 Add라는 의미 하나로 int 끼리도 더하고 float 끼리도 더할 수 있게 하는 것이라고 했습니다. 즉, "하나가 여러 개의 의미를 갖게 하는 것이라고 얘기했습니다" 여기서는 Add를 사용해서 오버로딩을 해보는 것입니다.
따라서 하나의 클래스를 정의하고 Add 메소드가 정수도 더할 수 있고, 실수도 더할 수 있는 예제를 만들어보세요. 이미 숙제를 해오신 분들의 말에 의하면 이 숙제에 대한 예제가 이 책의 9장에 있다고 하는군요. 흐음… 여러분도 9장을 뒤적이면서 멋진 프로그램을 작성하시기 바랍니다.
그리고 잘 안되는 프로그램이나 자신이 만든 프로그램을 한 번 봐 주십사하는 생각이 들면 언제라도 한빛미디어(webmaster@hanbitbook.co.kr)로 보내주세요. 한빛미디어 편집진들이 제게 여러분의 코드를 보내줄 것이고, 저는 여러분의 코드를 멋지게 검토해서 글에 피드백시키겠습니다.
그럼, 방학이신 분들은 열심히 공부하세요.
이전 글 : PHP로 하는 Gettext
다음 글 : 다중 파일 PHP 스크립트
최신 콘텐츠