굉장히 하찮아보이는 메서드 중에 하나인 .ToString() 메서드는, 사실 NUnit에서 굉장히 중요한 역할을 해냅니다. 바로 NUnit GUI를 좀 더 편리하게 쓰는 비법이 여기에 숨어 있습니다. (Java에서도 비슷한 일을 해낼 수 있을 것입니다.)
NUnit GUI는 어셈블리 코드를 호스팅하는 일반 프로세스이기때문에 표준 출력의 내용을 가져올 수 있습니다. 이 점을 효과적으로 활용하려면 Console.WriteLine / Console.Write 메서드의 특성을 잘 알아야 하는데, 지금 말한 두 개의 메서드는 Object 형식을 받으면 자동으로 ToString() 메서드를 호출하여 출력할 내용을 결정합니다.
ToString() 메서드를 구현하면서 사용하면 효과적인 클래스도 있습니다. 바로 StringBuilder 클래스입니다. 표현해야 할 정보가 많아서 문자열이 길어진다고 생각되면 그저 String을 더하기만 하지 말고 StringBuilder를 이용하는 것이 좋습니다. 특히, 이진 데이터의 표현이 필요하다면 StringBuilder는 필수입니다!
만약 이진 데이터를 NUnit GUI를 통하여 출력하길 원한다면 ToString() 메서드를 잘 구현하도록 합니다. 이진 데이터를 프레젠테이션하는 방법은 Programming Windows with C# (Charles Petzold 저, Microsoft Press)의 OLE 관련 Chapter에 서술되어있으며 Code Project나 Code Guru에서 찾으셔도 좋습니다.
ToString() 메서드의 구현이 잘 되었다면 테스트 루틴 안에서 Console.WriteLine 이나 Console.Write 메서드를 호출하여 테스트도중 변경되는 변수의 내용이나 데이터, 객체의 내용을 조사할 수 있게됩니다.
보너스 하나 더: ToString() 메서드를 잘 구현해두면 디버거에서도 그 역할을 톡톡히 다 해냅니다. 디버거는 요약 정보를 표현하기 위하여 마찬가지로 ToString() 메서드를 이용합니다. :-)
오늘 강좌는 Boxing과 Unboxing이라는 개념에 대해서 살펴볼 까 합니다. 이것은 일전의 "형 변환을 기반으로 하는 리플렉션"과 관련성이 있는 내용입니다. 그리하여 실질적인 내용을 따져보면 System.ValueType에 관한 특수한 리플렉션 기술이 Boxing과 Unboxing에 해당됩니다.
System.ValueType 형식은 메타 데이터 기반이 아닌 "낮은 수준"의 데이터 형식들의 추상 형식입니다. 여기로부터 파생되었다고 알려지는 주요 데이터 형식으로는, System.Byte, System.Int16, System.Int32, System.Int64, System.Float, System.Double 등이 있습니다. 하지만 이러한 데이터 형식들은 메타 데이터 기반이 아닙니다. 정확한 풀이로는 힙에 할당되며 크기 또한 자유자재로 변형될 수 있는 형식들입니다. 메타 데이터로 표기하는 것은 엄청난 오버로드를 수반하게 되므로 부적절합니다. 하지만 .NET Framework는 이를 매끄럽게 처리하게 되어있는데 그것이 Boxing과 Unboxing이라 불리우는 기술입니다.
Boxing과 Unboxing의 의미 풀이부터 해보도록 합니다. 두 단어 모두 Box 라는 키워드를 포함하는데, 쉽게 생각할 수 있습니다. 힙에 할당된 연속적인 데이터를 편리하게 관리하기 위하여 포장을 해놓는다 라는 의미에서 Box를 떠올리면 쉽습니다. 즉, 힙에 할당된 연속적인 데이터를 메타 데이터가 이해할 수 있는 형태로 포장해준다는 의미입니다. 그러면 실제 코딩을 살펴보도록 하지요.
int i = 123;
object o = (object)i;
위의 두 코드를 살펴보면, 사실 아무런 의미는 없습니다. 하지만 이것이 Boxing의 대표적인 예입니다. 이러한 코딩이 가능함으로서 얻을 수 있는 이점이 대단히 큽니다. Boxing을 가장 잘 활용하는 예는 ADO .NET 관련 클래스들입니다. 데이터 베이스 시스템이 질의에 대한 결과로 반환하는 데이터 형식을 한꺼번에 다루기 위한 방법으로 Boxing과 Unboxing을 복합적으로 활용합니다.
i 라는 변수에 123이라는 정수 값을 대입했습니다. 그 다음, o 라는 하나의 추상 객체를 선언하여 형식 변환을 취하였습니다. 코드 상에서는 분명히 형식 변환으로 표기되었습니다만 사실 이 둘은 형식 변환이 일어날 수 없는 관계입니다. object는 메타 데이터로 표기되는 데이터만을 다룰 수 있지만 int는 힙에 할당되는 연속적인 데이터 스트림입니다. 서로 관계가 없지만 이것이 가능했던 이유가 바로 Boxing입니다.
int x = (int)o; // Okay
long y = (int)o; // Okay
short z = (int)o; // Error!
x는 i와 같은 int 형식이고, y는 int 형식보다 큰 범위의 수를 다룰 수 있는 long 형식이며, z는 int 형식보다는 작은 범위의 수를 다루는 short 형식입니다. 세 동작 모두 object 형식을 int 형식으로 바꾸는 동시에 Unboxing을 수행하게 되었습니다. 즉, object 형식으로 포장된 데이터의 원래 내용물을 대입한 것입니다. 하지만 x, y와는 다르게 z는 컴파일 오류를 낼 것입니다.
x는 손실 변환이 일어나지 않았으며 원래의 형식 그대로를 수용하였습니다. y는 원래의 i가 요구하던 정수 범위보다 더 큰 정수 범위를 지원하게 되어 확장 변환이 일어났습니다. 이 경우 두 가지 의미로 해석이 가능한데, 말 그대로 가능성을 위하여 예약된 확장 변환일 수 있지만 반대로 불필요한 공간이 더 많이 할당된 오버헤드 변환이기도 합니다. 하지만 z는 컴파일 오류를 냅니다. 손실 변환으로 다루어질 수도 있겠지만 Unboxing의 정의에 의하면 원래 가지고 있던 데이터 형식보다 범위가 더 적어졌으므로 메모리 구조와는 일치하지 않는 것으로 해석됩니다.
int a = o as int; // Error
형 변환에 사용하는 as 키워드로 변환을 시도해 보았습니다. 하지만 오류가 나게됩니다. 왜 일까요? as 형식으로 형변환이 가능하다는 것은 메타 데이터에 한정된 내용입니다. 따라서, System.ValueType으로부터 상속받은 모든 형태의 값 형식에서는 as로 형변환을 하거나 as로 Unboxing되지 않게 되었습니다.
as를 사용하고자 하였던 의도가 예외를 Throw 하지 않고 null을 대입하려 했던 것이었다면 null을 대입하지 않는 대신 다음과 같은 방법으로 처리하는게 좋습니다.
또 다시 긴 시간 동안 공백이 있었군요. 그 사이에는 기말고사 시즌이라 공부에 전념하게 되어 카페를 자주 방문하지는 못했습니다. 아직 이틀이 더 남았지만 힘든 시험은 거의 다 지나가서 잠시 여유를 가지고 강좌를 올립니다. ^^
System.Object 클래스는 닷넷 세계에서 가장 최상위 클래스이며 모든 개체들은 이 클래스로 핸들링하는 것이 가능하다고 말씀드린 적이 있었습니다. System.Object의 모든 멤버를 정확히 구현하면 닷넷에서 가장 사용하기 쉬운 클래스가 됩니다. 그리고 몇 가지 멤버 함수를 더 구현하면서 확장된 기능도 추가할 수 있습니다.
1. 해시 테이블의 ID로 사용할 난수를 생성하기: GetHashCode()
해시 테이블의 정의에 의하면, 해시 테이블의 모든 원소 (Atom)들은 각자의 고유한 Identity를 가지고 있어서 어떤 개체를 무작위로 정확히 검색하는 것이 가능합니다. 이 Identity를 생성하도록 해주는 것이 System.Object.GetHashCode() 라는 함수입니다. 이것을 커스터마이징해서 더욱 더 고유한 Identity를 만들 수 있습니다.
Step A. 멤버 변수로 System.Guid 개체를 선언하고 Guid.NewGuid()를 써서 초기화한다.
Step B. GetHashCode()를 override 키워드를 써서 재정의한다.
Step C. Guid 개체의 GetHashCode를 비롯, 사용하는 멤버 변수에서 모두 GetHashCode()를 호출하여 각 값을 ^ 연산 처리한다.
Step D. 나온 값을 반환한다.
위와 같이 구현하였을 경우 Guid (Global Unique Identity)의 특성과 유동적으로 변형되는 각 멤버 함수의 값의 조합에 의하여 고유한 Identity를 가지게 됩니다.
2. 특정 개체의 동등성을 확인하기 위한 함수: Equals()
이 메서드를 직접 호출하지 않는다 하더라도 == 연산자를 씀으로서 이 메서드를 호출하게 됩니다. 진정으로 어떤 개체가 같다는 의미를 부여하기 위하여 Equals() 함수를 직접 구현하여 사용자가 의도하는 대로 비교를 하도록 처리하는 것이 좋습니다.
Equals() 함수는 두 번 써주는 것이 좋습니다. 첫 번째는 System.Object로 부터 override하는 버전, 두 번째는 새롭게 Equals 함수를 정의하는 것으로 합니다.
A. 첫 번째 함수는 다음과 같이 작성한다.
return (obj is [형식명]) ? this.Equals(obj as [형식명]) : base.Equals(obj);
B. 두 번째 함수에서는 각 멤버들을 비교하여 참/거짓 여부를 반환한다. 단, 주의할 것은 1번 섹션의 Guid는 비교 대상에서 빼야 한다는 점이다.
3. 특정 개체에 대한 상세한 정보를 출력하기: ToString()
이 메서드를 구현하지는 않더라도 개략적인 정보 하나 정도는 자동으로 추출됩니다. 현재 클래스의 전체 경로가 나오는데, 기왕이면 이것보다는 특정 개체에 대한 구체적인 정보를 표현하는 것이 더 보기 좋을 것입니다. 이것은 직접 작성하실 수 있을것입니다.
4. 리플렉션이 아닌 특별한 형변환을 가능하게 하기: implicit operator 또는 explicit operator
선언한 클래스에서 해당하는 형식의 멤버가 하나만 존재하고, 이것을 외부에 노출시키기로 결정하고 프로퍼티까지 선언하였다고 가정할 때, 좀 더 편리하게 하기 위하여 implicit operator 또는 explicit operator를 선언하면 프로퍼티를 참조하는 대신 캐스팅 연산자를 쓰거나 혹은 바로 대입하는 것이 가능합니다. (명시적인 캐스팅 연산자를 사용하는 것이 혼란을 줄일 수는 있겠습니다.)
// [반환할 형식] abcd = [선언한 클래스 형식명] test;
// 캐스팅 연산자 없이 개체를 특정 형식의 변수에 직접 대입하면 호출됩니다. (암묵적 형변환)
public static implicit operator [반환할 형식] ([선언한 클래스 형식명] obj)
{
return obj.m_test;
}
// [반환할 형식] abcd = ([선언한 클래스 형식명])test;
// 캐스팅 연산자를 써줌으로서 명시적인 형변환을 할 수 있습니다.
public static explicit operator [반환할 형식] ([선언한 클래스 형식명] obj)
{
return obj.m_test2;
}
// [선언할 클래스 형식명] value = new [선언할 클래스 형식명]();
// value = [값];
// 프로퍼티의 Setter를 대신할 수 있습니다.
public static implicit operator [선언한 클래스 형식명] ([받아들일 형식] obj)
당신의 의견을 작성해 주세요.