06 BCL(Base Class Library)
닷넷은 C#과 같은 언어로 만들어진 프로그램에서 운영체제와 연동할 수 있게 관련 기능을 모아 BCL(Console 타입, 소켓, 스레드, 파일, 레지스트리 등)에 담았다.
BCL은 운영체제 중계역할 뿐 만 아니라, 프로그램의 '처리' 에 해당하는 과정에서 자주 사용되는 것을 함께 포함시켰다.(Math 클래스라이브러리 [자연로그, 코사인 등 수학 개념 내포]
닷넷 버전 올라 갈 수록 BCL 기능도 추가 되고 있다.
6.1 시간
6.1.1 System.DateTime
DateTime now = DateTime.Now;
Console.WriteLine(now);
DateTime dayForChildren = new DateTime(now.Year, 5, 5);
Console.WriteLine(dayForChildren);
#출력
2025-03-30 오전 8:57:32
2025-05-05 오전 12:00:00
1초는 1000 밀리 초인데 이 보다 더 정밀도를 높이고자하면 Ticks 속성을 이용하자. 이 값은 1년 1월 1일 12시 자정을 기준으로 현재까지 100나노초 간격으로 흐른 숫자값이다.
1밀리초의 1/10,000 에 해당하는 정밀도를 보인다.
예제 6.1 DateTime을 이용한 시간차 계산
DateTime before = DateTime.Now;
Sum();
DateTime after = DateTime.Now;
long gap = after.Ticks - before.Ticks;
Console.WriteLine("Total Ticks: " + gap);
Console.WriteLine("Millisecond: : " + gap / 10000);
Console.WriteLine("Second: " + (gap / 10000) / 1000);
long Sum()
{
long sum = 0;
for (int idx = 0; idx < 10000000; idx++)
{
sum += idx;
}
return sum;
}
#출력
Total Ticks: 154921
Millisecond: : 15
Second: 0
닷넷의 기준 시간인 1년 1월 1일과 자바의 기준 시간은 1970년 1월 1일 기준으로
//자바코드
System.println(System.currentTimeMillis());
Console.WriteLine(DateTime.UtcNow.Ticks / 10000);
닷넷에서 구한 값과 정상 비교 하려면 아래와 같이 1970년에 해당하는 고정 밀리초 값을 빼야한다.
long javaMiullis = (DateTime.UtcNow.Ticks - 621355968000000000) / 10000;
6.1.2 System.TimeSapn
DateTime 타입에 대해 사칙 연산 중 '빼기'만 허용 된다.
빼기의 연산 결괏 값은 2개의 DateTime 사이의 시간 간격을 나타내는 TimeSpan으로 나온다.
DateTime endOfYear = new(DateTime.Now.Year, 12, 31);
DateTime now = DateTime.Now;
Console.WriteLine($"오늘날짜: {now}");
TimeSpan gap = endOfYear - now;
Console.WriteLine($"올해의 남은 날짜: {gap.TotalDays}");
#출력
오늘날짜: 2025-03-30 오전 9:19:39
올해의 남은 날짜: 275.61135319320255
TotalDays 뿐만 아니라 TotalHours, TotalMilliseconds, TotalMinutes, TotalSecondes 등의 속성을 통해 시간 간격을 알 수 있다.
6.1.3 System.Diagnostics.Stopwatch
닷넷에서는 더 정확한 시간차 계산 위해 Stopwatch 타입을 제공한다.
using System.Diagnostics;
Stopwatch st = new();
st.Start();
Sum();
st.Stop();
//st.ElapsedTicks 속석은 구간 사이에 흐른 타이머의 틱(tick) 수
Console.WriteLine("Total Ticks: " + st.ElapsedTicks);
Console.WriteLine("Millisecond: : " + st.ElapsedMilliseconds);
Console.WriteLine("Second: " + st.ElapsedMilliseconds / 1000);
//Stop.Frequency 속성이 초당 흐른 틱수를 반환하기에, ElapsedTicks에 대해 나눠 주면 초 단위의 시간 잴 수 있음
Console.WriteLine("Second: " + st.ElapsedTicks / Stopwatch.Frequency);
long Sum()
{
long sum = 0;
for (int idx = 0; idx < 10000000; idx++)
{
sum += idx;
}
return sum;
}
#출력
Total Ticks: 50726
Millisecond: : 5
Second: 0
Second: 0
Stopwatch 타입은 코드의 특정 구간에 대한 성능 측정 시 자주 사용.
6.2 문자열 처리
표 6.2 string 타입의 멤버
멤버 | 유형 | 설명 |
Contains | 인스턴스 메서드 | 인자로 전달된 문자열을 포함하고 있는지 여부를 true/false로 반환 |
EndsWith | 인스턴스 메서드 | 인자로 전달된 문자열로 끝나는지 여부를 true/false로 반환 |
Format | 정적 메서드 | 형식에 맞는 문자열을 생성해 반환 |
GetHashCode | 인스턴스 메서드 | 문자열의 해시값 반환 |
IndexOf | 인스턴스 메서드 | 문자 또는 문자열을 포함하는 경우 그 위치를 반환하고 없으면 -1 반환 |
Split | 인스턴스 메서드 | 주어진 문자 또는 문자열을 구분자로 나눈 문자열의 배열을 반환 |
StartsWith | 인스턴스 메서드 | 인자로 전달된 문자열로 시작하는지 여부를 true/false로 반환 |
Substring | 인스턴스 메서드 | 시작과 길이에 해당하는 만큼의 문자열 반환 |
ToLower | 인스턴스 메서드 | 문자열을 소문자로 변환해서 반환 |
ToUpper | 인스턴스 메서드 | 문자열을 대문자로 변환해서 반환 |
Trim | 인스턴스 메서드 | 문자열의 앞뒤에 주어진 문자가 있는 경우 삭제한 문자열을 반환, 문자가 지정되지 않으면 기본적으로 공백 문자를 제거해서 반환 |
Length | 인스턴스 속성 | 문자열의 길이를 정수로 반환 |
!= | 정적 연산자 | 문자열이 같지 않다면 true 반환 |
== | 정적 연산자 | 문자열 같다면 true 반환 |
인덱서 [] | 인스턴스 속성 | 주어진 정수 위치에 해당하는 문자를 반환 |
Split
string test = "Asazx as";
var tt = test.Split('a');
foreach (var t in tt) Console.WriteLine(t);
#출력
As
zx
s
string txt = "Hello World";
Console.WriteLine(txt + " EndsWith(\"orld\"): " + txt.EndsWith("orld")); // True
Console.WriteLine();
Console.WriteLine(txt + " IndexOf(\"World\"): " + txt.IndexOf("World")); // 6
Console.WriteLine(txt + " IndexOf(\"Halo\"): " + txt.IndexOf("Halo")); // -1
Console.WriteLine();
Console.WriteLine(txt + " StartsWith(\"Hello\"): " + txt.StartsWith("Hello")); // True
Console.WriteLine(txt + " StartsWith(\"ello\"): " + txt.StartsWith("ello")); // False
Console.WriteLine();
Console.WriteLine(txt + " ToLower(): " + txt.ToLower()); // hello world
Console.WriteLine(txt + " ToUpper(): " + txt.ToUpper()); // HELLO WORLD
Console.WriteLine();
Console.WriteLine(txt + " Substring(1): " + txt.Substring(1)); //ello World
Console.WriteLine(txt + " Substring(2, 3): " + txt.Substring(2, 3)); // llo
Console.WriteLine();
Console.WriteLine(txt + " Trim('H'): " + txt.Trim('H')); // ello World
Console.WriteLine(txt + " Trim('d'): " + txt.Trim('d')); // Hello Worl
Console.WriteLine(txt+ " Trim('H', 'd'): " + txt.Trim('H','d')); // ello Worl
Console.WriteLine("Hello == World: " + ("Hello" == "World")); // False
Console.WriteLine("Hello != HELLO: " + ("Hello" != "HELLO")); // True
대소문자 구분 오버로드 버전을 제공하는 메서드로 EndsWith, IndexOf, StartsWith이 있으며 StringComparison 열거형 인자를 추가로 받을 수 있는데 이 인자를 생략 시 대소문자 구분을 하고 대소문자 구분을 하고 싶지 않다면 StringComparision.OrdinalIgnoreCase 인자를 함께 전달하면 된다.
string txt = "Hello World";
Console.WriteLine(txt + " EndsWith(\"WORLD\"): " + txt.EndsWith("WORLD", StringComparison.OrdinalIgnoreCase)); // True
Console.WriteLine();
Console.WriteLine(txt + " IndexOf(\"WORLD\"): " + txt.IndexOf("WORLD", StringComparison.OrdinalIgnoreCase)); //6
Console.WriteLine();
Console.WriteLine(txt + " StartsWith(\"WORLD\"): " + txt.StartsWith("HELLo", StringComparison.OrdinalIgnoreCase)); // True
'==' 비교 연산자는 대소문자 무시기능 없지만, Equals 메서드는 있다.
string txt = "Hello";
Console.WriteLine(txt + " == HELLO: " + txt.Equals("HELLO", StringComparison.OrdinalIgnoreCase)); // True
Format 메서드는 c# 6.0에 추가된 문법으로 주된 기능은 인자를 형식 문자열에 포함된 번호와 맞춰서 치환하는 기능.
string txt = "Hello {0}: {1}";
string output = string.Format(txt, "World", "Anderson");
Console.WriteLine(output);
# 출력
Hello World: Anderson
string txt = "{0, -10} * {1} == {2, 10}";
string output = string.Format(txt, 5, 6, 5*6);
Console.WriteLine(output);
#출력
5 * 6 == 30
{0} 번째의 인자 값이 10개의 공백 - 좌측 정렬
{2} 번째 인자 값이 10개의 공백 - 우측 정렬
타입 | 유형 | 의미 | Format 예제 | 한글 윈도우 출력 |
숫자형 | C | 통화 | "{0:C}", -123 | - ₩ 123 |
D | 10 진수 | "{0:D}", -123 | -123 | |
E | 공학 | "{0:E}", -123.45f | -1.234500E + 002 | |
F | 고정 소수점 | "{0:F}", -123.45f | -123.45 | |
G | 일반(기본값) | "{0:G}", -123 | -123 | |
N | 숫자 | "{0:N}", -123 | -123.00 | |
P | 백분율 | "{0:P}", -123.45f | -12,345.00 % | |
R | 반올림 숫자 | "{0:R}", -123.45f | -123.45 | |
X | 16진수 | "{0:X}", -123 | FFFFFF85 | |
날짜형 | DateTime now = DateTime.Now | |||
d | 단축 날짜 | "{0:d}", now | 2023-02-13 | |
D | 상세 날짜 | "{0:D}", now | 2023년 2월 13일 수요일 | |
t | 단축 시간 | "{0:t}", now | 오후 1:27 | |
T | 상세 시간 | "{0:T}", now | 오후 1:27:52 | |
f | 전체 날짜/단축시간 | "{0:f}", now | 2023년 2월 13일 수요일 오후 1:27 | |
F | 전체날짜/상세시간 | "{0:F}", now | 2023년 2월 13일 수요일 오후 1:27:52 | |
g | 일반날짜/단축시간 | "{0:g}", now | 2023-02-13 오후 1:27 | |
G | 일반날짜/상세시간 | "{0:G}", now | 2023-02-13 오후 1:27:52 | |
M | 달 | "{0:M}", now | 2월 13일 | |
Y | 년 | "{0:Y}", now | 2023년 2월 |
string txt = "날짜: {0,-20:D}, 판매 수량: {1, 15:N}";
Console.WriteLine(txt, DateTime.Now, 267);
# 출력
날짜: 2025년 3월 31일 월요일 , 판매 수량: 267.00
6.2.2 System.Text.StringBuilder
string 타입은 불변 객체(immutable object)이기 때문에 string에 대한 모든 변환은 새로운 메모리 할당을 발생시킨다.
string txt = "Hello World";
string lwrText = txt.ToLower();
txt 변수는 힙에 있는 "Hello World"를 가리키며, ToLower 메서드 호출 시 txt 변수에 담긴 문자열이 소문자로 변경되는 것이 아니라 원문 통째로 복사된 다음 그것이 소문자로 변경되는 절차를 거친다.
예제 6.4 비효율적인 문자열 연산
using System.Diagnostics;
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
string txt = "Hello World";
for (int idx = 0; idx < 300000; ++idx)
{
txt += "1";
}
sw.Stop();
Console.WriteLine(sw.ToString());
#출력
00:00:06.9236317
1. 힙에 "Hello World" 문자열을 담은 공간 할당
2. 스택에 있는 txt 변수에 1번 과정에서 할당된 힙 주소 저장
3. txt + "1" 동작을 수행하기 위해 txt.Length + "1".Length에 해당하는 크기의 메모리 힙 할당, 그 메모리에 txt 변수가 가리키는 힙의 문자열과 "1" 문자열 복사
4. 다시 스택에 있는 txt 변수에 3번 과정에서 새롭게 할당된 힙 주소 저장
5. 3번과 4번의 과정을 300000번 반복
실행시간 6.9초 걸림.
BCL 중 하나인 String Builder 클래스를통한 문제 해결
using System.Diagnostics;
using System.Text;
Stopwatch sw = Stopwatch.StartNew();
sw.Start();
string txt = "Hello World";
StringBuilder sb = new();
sb.Append(txt);
for (int idx = 0; idx < 300000; ++idx)
{
sb.Append("1");
}
string newText = sb.ToString();
sw.Stop();
Console.WriteLine(sw.ToString());
#출력
00:00:00.0007202
1초도 안걸림.
내부 연산과정
1. StringBuilder는 내부적으로 일정한 양의 메모리를 미리 할당.
2. Append 메서드에 들어온 인자를 미리 할당한 메모리에 복사
3. 2번 과정을 300000번 반복. Append로 추가된 문자열이 미리 할당한 메모리 보다 많아지면 새롭게 여유분의 메모리를 할당.
4. ToString 메서드를 호출하면 연속적으로 연결된 하나의 문자열 반환
(주황이에서 로그같은거 남길때 박싱/언박싱(?) 떄매 ToString() 으로 변환해서 하라고 했었던 내용이 있음)
잦은 메모리 할당과 복사가 없어졌기에 성능이 향상된다. 이 때문에 문자열 연결 작업이 많을 때 반드시 StringBuilder를 사용하는 것을 권장한다.
6.2.3 System.Text.Encoding
'A', 'B', 'C' 문자는 시스템에 내장된 폰트 기반 그림에 불과하며 이런 문자는 내부적으로 숫자에 대응된다.
문자가 숫자로 표현되는 것을 인코딩(encoding: 부호화)이라 한다.
다양한 인코딩 방식에 따라 'A'에 문자의 값이 65(ASCII)가 될 수 있고 임의의 값이 될 수 있다.
초기 ASCII 코드는7비트(0 ~ 127)만 사용했기에, 알파벳 대소문자, 숫자, 일부 통신용 제어코드를 포함한 수준에서 결정됐다.
전 세계에서 자국의 언어를 표현하기 위해 코드를 확장하였으며 이에 따라 EUC-KR, CP949, KS_C_5601-1987 등 다양한 인코딩 방식이 나온다.
BCL은 Encoding 타입을 제공한다.
표 6.4 자주 사용되는 Encoding 유형
정적 속성 | 설명 |
ASCII | 7비트 ASCII 문자셋을 위한 인코딩 |
Default | 시스템 기본 문자셋을 위한 인코딩 (한글 윈도우의 경우 ks_c_5601-1987, 영문 윈도우의 경우 iso-8859-1) |
Unicode | 유니코드 문자셋의 UTF-16 인코딩 |
UTF32 | 유니코드 문자셋의 UTF-32 인코딩 |
UTF8 | 유니코드 문자셋의 UTF-8 인코딩 |
using System.Text;
string textData = "Hello World";
byte[] buf = Encoding.UTF8.GetBytes(textData);
// 생략: buf 바이트 배열을 자바 프로그램에 전달
byte[] received = ... // 생략 : 자바 프로그램으로부터 전달 받은 바이트 배열 데이터
string data = Encoding.UTF8.GetString(received);
자바와 C# 간 문자열 교환을 UTF-8 인코딩으로 합의했다고 가정했을 시 상호 주고받은 문자열 데이터를 위와 같이 UTF-8로 변환/복원하는 과정을 거침.
효율상의 이유로 최근 UTF-8 인코딩 방식을 자주 사용.
6.2.4 System.Text.RegularExpressions.Regex
정규 표현식(regular expression)은 문자열 처리에 대한 일반적인 규칙을 표현하는 형식 언어.
^([0-9a-zA-Z]+)@([0-9a-zA-Z]+)(\.[0-9a-zA-Z]+){1,}$
^:문장의 시작이 다음 규칙 만족해야함
([0-9a-zA-Z]+): 영숫자가 1개 이상
@: 반드시 '@'문자가 있음
([0-9a-zA-Z]+): 영숫자가 1개 이상
(\.[0-9a-zA-Z]+): 점(.)과 1개 이상의 영숫자
{1,}: 이전의 규칙이 1번 이상 반복(즉, 점과 1개 이상의 영숫자가 반복)
$: 이전의 규칙을 만족하면서 끝남(즉, 점과 1개 이상의 영숫자가 1번 이상 반복되면서 끝남)
using System.Text.RegularExpressions;
string email = "tester@test.com";
Console.WriteLine(IsEmail(email));
bool IsEmail(string email)
{
Regex regex = new Regex(@"^([0-9a-zA-Z]+)@([0-9a-zA-Z]+)(\.[0-9a-zA-Z]+){1,}$");
return regex.IsMatch(email);
}
#출력
True
* 반드시 @ 문자를 한번 포함
* @ 문자 이전의 문자열에는 문자와 숫자만 허용(특수문자를 포함해선 안됨)
* @ 문자 이후의 문자열에는 문자와 숫자만 허용되지만 반드시 하나 이상의 점(Dot)을 포함
이 규칙에 따라 정규 표현식 코드는 다음과 같다.
Regex 타입에는 패턴 일치를 판단하는 IsMatch 메서드 뿐만 아니라 패턴과 일치하는 문장을 다른 문장으로 치환하는 Replace 메서드도 제공된다.
using System.Text.RegularExpressions;
string txt = "Hello, World! Welcome to my world!";
Regex regex = new Regex("world", RegexOptions.IgnoreCase);
string result = regex.Replace(txt, funcMatch);
Console.WriteLine(result);
string funcMatch(Match match)
{
return "Universe";
}
# 출력
Hello, Universe! Welcome to my Universe!
String 타입의 Replace 메서드도 동일한 기능을 제공하므로 간단한 유형의 패턴인 경우 굳이 Regex를 쓸 필요없다.
using System.Text.RegularExpressions;
string txt = "Hello, World! Welcome to my world!";
string result = txt.Replace("world", "Universe", StringComparison.OrdinalIgnoreCase);
Console.WriteLine(result);
#출력
Hello, Universe! Welcome to my Universe!
6.3 직렬화/역직렬화
프로그램에서 다뤄진느 모든 데이터는 엄밀히 말하면 byte 데이터다. string 타입도 C#언어의 소스코드 내에서 다뤄질 때만 그것이 겹따옴표를 가진 문자열로 표현되지만, 파일에 저장되거나 네트워크 선을 타고 이동하는 단위는 byte 데이터다.
Encoding 타입은 string 타입을 바이트 배열로 변환하거나 그 역에 해당하는 작업을 할 수 있었다.
좁은의미에서, 문자열을 일련의 바이트 배열로 변환하는 작업을 직렬화(serialization), 그 바이트로부터 원래의 데이터를 복원하는 작업을 역직렬화(deserialization)라고 한다.
바이트 배열은 직렬화 수단에 불과하다. 데이터를 어떤 것에 보관하고, 그것으로부터 복원만 할 수 있다면 그 모든 작업을 넓은 의미에서 직렬화/역직렬화라고 정의할 수 있다.
직렬화/역질렬화와 관련된 BCL 클래스를 다뤄보자.
6.3.1 System.BitConverter
문자열은 인코딩 방식에 따라 같은 문자열이라도 바이트 배열로의 변환이 달라질 수 있다. 하지만 그 밖의 기본 타입(byte, short, int ...)은 변환 방법이 고정돼 있다. 그래서 BitConverter 타입에서는 GetBytes 메서드를 통해 이러한 기능을 제공한다.
예제 6.7
// 기본 타입을 바이트 배열로 변환
byte[] boolBytes = BitConverter.GetBytes(true);
byte[] shortBytes = BitConverter.GetBytes((short)32000);
byte[] intBytes = BitConverter.GetBytes((1652300));
//16진수 문자열로 표현하는 ToString 메서드 함께제공
Console.WriteLine(BitConverter.ToString(boolBytes));
Console.WriteLine(BitConverter.ToString(shortBytes));
Console.WriteLine(BitConverter.ToString(intBytes));
// 바이트 배열을 기본 타입으로 복원
bool boolResult = BitConverter.ToBoolean(boolBytes, 0);
short shortResult = BitConverter.ToInt16(shortBytes, 0);
int intResult = BitConverter.ToInt32(intBytes, 0);
Console.WriteLine(boolResult);
Console.WriteLine(shortResult);
Console.WriteLine(intResult);
#출력
01
00 - 7D
4C - 36 - 19 - 00
True
32000
1652300
32000
-> 2바이트 2진수: 0111 1101 0000 0000
-> 2바이트 16진수: 7D 00
1652300
-> 2진수: 0000 0000 0001 1101 0011 0110 0100 1100
-> 16진수: 00 19 36 4C
BitConverter로 변환된 바이트 배열이 순서가 거꾸로 됀 이유는 리틀엔디언으로 표현했기 떄문이다.(예제 6.7)
차례대로 표현한 방식을 빅 엔디언이라한다.
인텔 호환 cpu에서는 모두 리틀 엔디언을 사용하는 반면 RISC 프로세서 계열에서는 빅 엔디언을 사용한다.
2바이트 이상으로 표현되는 short, ushort, int, uint, long, ulong, float, double에서는 엔디언 정렬에 주의하여 데이터를 확인하자.
byte[] buf = new byte[4];
buf[0] = 0x4c;
buf[1] = 0x36;
buf[2] = 0x19;
buf[3] = 0x00;
int result = BitConverter.ToInt32(buf, 0);
Console.WriteLine(result);
#출력
1652300
데이터가 바이트로 표현된 것을 '2진 데이터(바이너리 데이터: binary data)' 라 한다.
숫자 1652300이 바이트로 0x4c, 0x36, 0x19, 0x00으로 바뀌는 것을 직렬화, 복원하면 역직렬화가 된다.
숫자 뿐만 아니라 문자열도 직렬화/역직렬화가 가능하다.
int n = 1652300;
string text = n.ToString(); // 숫자 1652300을 문자열로 직렬화
int result = int.Parse(text); // 문자열로부터 숫자를 역직렬화해서 복원
직렬화 수단이 바이트 배열이 아닌 문자열이 된 것이다.
6.3.2 System.IO.MemoryStream
Stream 타입은 일련의 바이트를 일관성 있게 다루는공통 기반을 제공해주며 MemoryStream은 Stream 추상 클래스를 상속받은 타입이다.
Stream은 '바이트 데이터의 흐름'을의미한다.
Stream은 데이터를 쓰거나 읽는 작업을 순서대로 하는 것이 기본 정책이다.
MemoryStream 타입은 이름 그대로 메모리에 바이트 데이터를 순서대로 읽고 쓰는 작업을 수행하는 클래스다.
이를 이용해 데이터를 메모리에 직렬화/역직렬화하는 것이 가능하다.
예제 6.8 MemoryStream 사용 예
//short 와 int 데이터를 순서대로 MemoryStream에 직렬화
byte[] shortBytes = BitConverter.GetBytes((short)32000); // byte[2]
byte[] intBytes = BitConverter.GetBytes(1652300); // byte[4]
MemoryStream ms = new();
//직렬화
ms.Write(shortBytes, 0, shortBytes.Length); // 0->2
ms.Write(intBytes, 0, intBytes.Length); // 2->6
ms.Position = 0; // ms 위치 범위 0~6 (2(short) + 4(int))
//MemoryStream으로부터 short 데이터 역직렬화
byte[] outBytes = new byte[2];
ms.Read(outBytes, 0, 2); // ms.Position = 2로 이동(0->2)
int shortResult = BitConverter.ToInt16(outBytes, 0);
Console.WriteLine(shortResult); // 출력 결과 32000
//이어서 int 데이터를 역직렬화
outBytes = new byte[4];
ms.Read(outBytes, 0, 4); // ms.Position = 6으로 이동(2->6)
int intRsult = BitConverter.ToInt32(outBytes, 0);
Console.WriteLine(intRsult); // 출력 결과 1652300
예제 6.8의 역직렬화 부분을 ToArray를 사용해 다음과 같이 구현 가능하다.
//short 와 int 데이터를 순서대로 MemoryStream에 직렬화
byte[] shortBytes = BitConverter.GetBytes((short)32000); // byte[2]
byte[] intBytes = BitConverter.GetBytes(1652300); // byte[4]
MemoryStream ms = new();
ms.Write(shortBytes, 0, shortBytes.Length);
ms.Write(intBytes, 0, intBytes.Length);
byte[] buf = ms.ToArray(); // MemoryStream에 담긴 바이트 배열을 반환
// 바이트 배열로부터 short 데이터를 역직렬화
int shortResult = BitConverter.ToInt16(buf, 0);
Console.WriteLine(shortResult); // 출력: 32000
// 이어서 int 데이터 역직렬화
int intResult = BitConverter.ToInt32(buf, 2);
Console.WriteLine(intResult); // 출력: 1652300
즉 byte[] outBytes 선언과 ms.Read 구문을 byte[] buf = ms.ToArray()대체
BitConverter.ToInt32의 두 번째 인자에 2를 지정했다. Stream은 읽을 때마다 자동으로 Position이 이동하여 항상 0을 주면 됐지만, byte 배열에는 Position의 기능이 없으므로 ToInt32 메서드가 취해야할 바이트의 위치를 직접 알려줘야 한다.
6.3.3 System.IO.StreamWriter / System.IO.StreamReader
Stream에 문자열 데이터를 쓰기 전에 반드시 Encoding 타입을 이용해 바이트 배열로 변환해야한다.
using System.Text;
MemoryStream ms = new();
byte[] buf = Encoding.UTF8.GetBytes("Hello World");
ms.Write(buf, 0, buf.Length);
문자열 쓸때마다 매번 이런 식의 변환 과정을 거치는 것을 해소하고자 마이크로소프트에서는 문자열 데이터를 Stream에 쉽게 쓸 수 있는 용도로 StreamWriter 타입의 BCL을 제공한다.
예제 6.9 StreamWriter 사용 예
using System.Text;
MemoryStream ms = new();
StreamWriter sw = new StreamWriter(ms, Encoding.UTF8);
sw.WriteLine("Hello World");
sw.WriteLine("Anderson");
sw.WriteLine(32000);
sw.Flush();
StreamWriter 타입은 생성자로 Stream과 문자열 인코딩 방식을 받는다.
이후 Write 계열의 메서드 호출이 되면 인자로 입력된 문자열을 인코딩 방식에 따라 자동으로 바이트 배열로 변환 후 Stream에 쓴다.
인자가 문자열이 아니어도 ToString 메서드를 이용해 문자열을 쓴다. 그래서 32000 -> '32000'을 기록한다.
sw.Flush() 메서드 호출에 유의해야 한다. StreamWriter는 내부적으로 속도 향상을 위한 바이트 배열 버퍼를 갖고 있다. 짧은 데이터를 Write 메서드를 호출 할 때마다 Stream에 쓰는 것은 떄로는 비효율적이기 떄문이다.
StreamWriter는 Write로 들어온 문자열을 내부 버퍼에 보관하여 일정 크기에 다다르면 한꺼번에 Stream으로 쓰기 작업을 한다. Flush 메서드는 그 크기까지 문자열이 채워지지 않아도 현재 보유한 문자열을 무조건 Stream에 쓰는 역할을 한다.
일반적으로 Stream에 써야 할 데이터를 모두 Write로 썼으면 마지막에 한 번 호출한다.
쓰기 작업은 StreamWriter. 읽기 작업은 StreamReader.
using System.Text;
MemoryStream ms = new();
// StreamWrite( 직렬화 )
StreamWriter sw = new StreamWriter(ms, Encoding.UTF8);
sw.WriteLine("Hello World");
sw.WriteLine("Anderson");
sw.WriteLine(32000);
sw.Flush();
// StreamReader ( 역직렬화 )
ms.Position = 0;
StreamReader sr = new(ms, Encoding.UTF8);
string txt = sr.ReadToEnd();
Console.WriteLine(txt);
#출력
Hello World
Anderson
32000
StreamReader의 두번째 인자에 지정된 인코딩은 StreamWriter에 지정한 인코딩과 동일해야함.
한 줄 씩 읽는 ReadLine 메서드도 있다.
6.3.4 System.IO.BinaryWriter / System.IO.BinaryReader
StreamWriter / StreamReader가 Stream에 문자열 데이터를 쓰고 읽는데 편리함을 주는 반면 BinaryWriter / BinaryReader는 Stream에 2진 데이터를 쓰고 읽는 데 특화된 기능을 제공한다.
예제 6.10 BinaryWriter/BinaryReader 사용 예
MemoryStream ms = new();
BinaryWriter bw = new(ms);
bw.Write("Hello World" + Environment.NewLine);
bw.Write("Anderson" + Environment.NewLine);
bw.Write(32000);
bw.Flush();
ms.Position = 0;
BinaryReader br = new(ms);
string first = br.ReadString();
string second = br.ReadString();
int result = br.ReadInt32();
Console.WriteLine($"{first}{second}{result}");
#출력
Hello World
Anderson
32000
StreamWriter와 BinaryWriter 차이는 MemoryStream 에 저장된 바이트 배열 내용을 보면 알 수 있다.
일반적으로는 가독성 있는 데이터를 원할 시 StreamWriter/StreamReader를 사용하지만 가독성이 떨어지더라도 규격이 정해진 데이터를 입출력 할 때는 BinaryWriter/Reader를 사용한다.
6.3.5 System.Xml.Serialization.XmlSerializer
예제 6.11 직렬화 예제 클래스 - Person
class Person
{
public int Age;
public string Name;
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return string.Format($"{Age} {Name}");
}
}
Age 값은 BitConverter 타입 , Name 값은 Encoding 타입을 이용해 각각 바이트 배열로 바꾼 다음 Stream에 쓰면 된다. 복원은 Person 객체를 만든 다음 마찬가지 방법으로 Age, Name 속성에 값을 대입하면 된다.
그러나 마이크로소프트는 이러한 불편한 절차를 제거하고자 별도 직렬화 클래스를 제공한다.
XmlSerializer 타입은 클래스 내용을 XML 문자열로 직렬화 한다.
XmlSerializer 제약조건
- public 접근 제한자의 클래스여야 함.
- 기본 생성자를 포함하고 있어야함
- public 접근 제한자가 적용된 필드만 직렬화/역직렬화 가능
public class Person
{
public int Age;
public string Name;
public Person()
{
}
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return string.Format($"{Age} {Name}");
}
}
XmlSerializer를 위해 예제 6.11의 Person 클래스에 public 접근 제한자 및 기본 생성자를 추가했다.
이후 다음과 같이 XmlSerializer 타입을 사용해 클래스의 정보를 XML 문자열로 바꿀 수 있다.
using System.Xml.Serialization;
MemoryStream ms = new();
XmlSerializer xs = new(typeof(Person));
Person person = new(36, "Anderson");
// MemoryStream에 문자열로 person 객체 직렬화
xs.Serialize(ms, person);
ms.Position = 0;
// MemoryStream로부터 객체를 역직렬화해서 복원
Person clone = xs.Deserialize(ms) as Person;
Console.WriteLine(clone); // 출력 결과 : 36 Anderson
public class Person
{
public int Age;
public string Name;
public Person()
{
}
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return string.Format($"{Age} {Name}");
}
}
XmlSerializer는 기본적으로 UTF-8 인코딩으로 객체를 문자열로 직렬화한다.
MemoryStream의 내용을 문자열로 변환 해 그 내용을 확인 가능하다.
using System;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;
MemoryStream ms = new();
XmlSerializer xs = new(typeof(Person));
Person person = new(36, "Anderson");
// MemoryStream에 문자열로 person 객체 직렬화
xs.Serialize(ms, person);
ms.Position = 0;
// MemoryStream로부터 객체를 역직렬화해서 복원
Person clone = xs.Deserialize(ms) as Person;
//Console.WriteLine(clone); // 출력 결과 : 36 Anderson
byte[] buf = ms.ToArray();
Console.WriteLine(Encoding.UTF8.GetString(buf));
#출력
<? xml version = "1.0" encoding = "utf-8" ?>
< Person xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xmlns: xsd = "http://www.w3.org/2001/XMLSchema" >
< Age > 36 </ Age >
< Name > Anderson </ Name >
</ Person >
public class Person
{
public int Age;
public string Name;
public Person()
{
}
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return string.Format($"{Age} {Name}");
}
}
XmlSerializer의 장점으로 Person 객체의 Age와 Name 값을 쉽게 구별 가능하며 이로 인해 출력 텍스트를 다른 플랫폼의 응용 프로그램과 쉽게 주고 받을 수 있다.
using System.Text;
using System.Xml.Serialization;
string text = @"<?xml version='1.0'?>
<Person xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'>
<Age>27</Age >
<Name>Ted</Name >
</Person>";
// </ Person > 이 처럼 뛰어쓰기 하면 에러남. 뛰어쓰기 없이 해야함.
byte[] buf = Encoding.UTF8.GetBytes(text);
MemoryStream ms = new(buf);
XmlSerializer xs = new(typeof(Person));
Person clone = xs.Deserialize(ms) as Person;
Console.WriteLine(clone); // 출력 결과 : 27 Ted
public class Person
{
public int Age;
public string Name;
public Person()
{
}
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return $"{Age} {Name}";
}
}
XmlSerializer는 상호 운영성이 높은 직렬화 방식이다. 이러한 반대급부로는 Age = 27, Name = "Ted" 데이터를 주고받기 위한 문자열 크기가 무려 176자다.
6.3.6 System.Text.Json.JsonSerializer
Json : JavaScript Object Notation 약어로, JsonSerializer는 자바스크립트의 객체 직렬화 방식을 닷넷에서 동일하게 구현한다.
XmlSerializer는 편리해도 직렬화 문자열이 다소 길어진다는 점을 해결하고자 System.Text.Json.JsonSerializer을 대체하여 사용할 수 있다.
using System.Text;
using System.Text.Json;
using System.Xml.Serialization;
Person person = new(36, "Anderson");
JsonSerializerOptions options = new JsonSerializerOptions { IncludeFields = true };
string text = JsonSerializer.Serialize(person, options);
Console.WriteLine(text);
// 아래 코드는 제네릭 절에서 다룰 예정
Person clone = JsonSerializer.Deserialize<Person>(text, options);
Console.WriteLine(clone);
public class Person
{
public int Age;
public string Name;
public Person()
{
}
public Person(int age, string name)
{
Age = age;
Name = name;
}
public override string ToString()
{
return $"{Age} {Name}";
}
}
#출력
{"Age":36,"Name":"Anderson"}
36 Anderson
XmlSerializer는 문자열 크기가 176 글자였으나, JsonSerializer는 28자에 불과하다.
게다가 형식도 단순하여 닷넷이외의 플랫폼과도 쉽게 데이터를 주고받아 해석할 수 있다.
이러한 장점으로 최근 객체 직렬화를 위한 방법으로 JsonSerializer를 선호한다.
'C#(.Net)' 카테고리의 다른 글
[시작하세요 C# 12 프로그래밍 ] #5 C# 1.0 완성하기 #3 (0) | 2025.03.23 |
---|---|
[시작하세요 C# 12 프로그래밍 ] #5 C# 1.0 완성하기 #2 (0) | 2025.03.22 |
[시작하세요 C# 12 프로그래밍 ] #5 C# 1.0 완성하기 #1 (0) | 2025.03.21 |
[시작하세요 C# 12 프로그래밍 ] #4 CSharp 객체 지향 문법 3 (1) | 2025.03.19 |
Field(클래스 내부 멤버 변수) vs Property(속성) vs Attribute(특성) (0) | 2025.03.18 |