[열세번째 C#] C#
이제 C#을 본격적으로 사용해 볼 차례입니다. 이 과정을 통해 우리는 C#문법을 통해 code를 작성하는 기초적인 방법부터 다양하게 사용되는 일반적인 용어까지 차례대로 알아볼 것입니다.
1. C# 개요
1) C# 표준
수년간 마이크로소프트는 ECMA 표준 기구에 C#의 여러 버전을 제출해 왔으며 2014년에 C#을 open source화 하였습니다. 관련된 표준문서는 아래 link에서 확인할 수 있으며
https://learn.microsoft.com/en-us/dotnet/csharp/specification/
Specifications - ECMA specification and latest features.
Read the detailed specifications for the C# language and the latest features. The specifications are the definitive source for the behavior of the C# language.
learn.microsoft.com
공개 GitHub Repository를 통해서도 관련정보를 찾아볼 수 있습니다.
| C# 설계 철학 | https://github.com/dotnet/csharplang |
| Compiler 관련 | https://github.com/dotnet/roslyn |
| C# 언어 표준 | https://github.com/dotnet/csharpstandard |
2) C# compiler version의 이해
C#과 Visual Basic.NET의 compiler는 Roslyn이라고 하며 별개의 F# compiler와 함께 .NET SDK의 일부로 배포됩니다. 따라서 아래 표에 나타나는 것처럼 만약 특정 version의 C#을 사용하려면 해당 version에 맞는 .NET SDK가 설치되어 있어야 합니다.
| .NET SDK version | Roslyn version | C# version |
| 1.0.4 | 2.0 ~ 2.2 | 7.0 |
| 1.1.4 | 2.3 ~ 2.4 | 7.1 |
| 2.1.2 | 2.6 ~ 2.7 | 7.2 |
| 2.1.200 | 2.8 ~ 2.10 | 7.3 |
| 3.0 | 3.0 ~ 3.4 | 8.0 |
| 5.0 | 3.8 | 9.0 |
| 6.0 | 4.0 | 10.0 |
| 7.0 | 4.4 | 11.0 |
| 8.0 | 4.8 | 12.0 |
| 9.0 | 4.12 | 13.0 |
Class Library Project를 생성할때 특정 .NET version뿐만 아니라 .NET Standard 역시 설정할 수 있습니다. 이 경우 사용가능한 C# version은 아래와 같습니다.
| .NET Standard | C# |
| 2.0 | 7.3 |
| 2.1 | 8.0 |
3) Project에서 Compiler version을 지정해 사용하기
Visual Studio나 dotnet command line interface는 기본적으로 가장 최신의 C#언어를 사용한다고 가정합니다. 따라서 만약 C#8이나 C#7과 같은 특정한 version을 사용해야 한다면 project file에서 <LangVersion> 설정요소를 추가함으로써 원하는 version을 지정할 수 있습니다.
<LangVersion>8</LangVersion>
이외에 다음과 같은 값을 지정하여 원하는 version을 사용하도록 지정할 수 있습니다.
| 지정값 | 의미 |
| 10, 11, 13 등 | 사용할 compiler version 번호를 직접 지정합니다. |
| latestmajor | 가장 높은 major번호를 기준으로한 version을 사용합니다. 이 경우 예를 들어 8.1 version이 있더라도 8.0 version을 사용하게 됩니다. |
| latest | 가장 높은 major와 minor번호를 기준으로한 version을 사용합니다. 이 경우 예를 들어 8.1 version이 있다면 8.0이 아닌 8.1을 사용하게 됩니다. |
| preview | 가장 최신의 preview version을 사용합니다. 예를 들어 2025년 9월 시점기준 C#14와 .NET10은 preview에 해당하며 해당 version이 설치했다면 preview설정으로 사용할 수 있습니다. 참고로 Preview는 Microsoft가 개발자들로 하여금 feedback을 받기위한 것이므로 production에 적용해서는 안됩니다. 생각보다 많은 bug를 가질 수 있고 Microsoft에 의해 지원되는 제품이 아닙니다. |
4) 더 높은 compiler version으로 전환하기
.NET9은 C#13 compiler와 함께 release 되었지만 그렇다고 해서 C#13 compiler만 사용할 수 있다는 의미는 아닙니다. .NET10이 C#14 compiler와 함께 release 되면 이전에 .NET9를 target으로 하는 project는 아래와 같은 설정을 통해 C#14를 사용할 수 있습니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>14</LangVersion>
</PropertyGroup>
</Project>
.NET9의 경우 매달 patch version으로 실행한다면 2026년 5월까지 지원받을 수 있습니다.
만약 project를 .NET10으로 전환하거나 target으로 설정하여 생성한다면 이때 C#은 14 version이 기본이 되므로 <LangVersion> 설정이 필요하지 않습니다.
일부 C#언어 기능중에는 .NET과 밀접하게 연결된 것이 존재합니다. 그래서 만약 target으로 설정한 .NET보다 Compiler version이 더 높게 설정된 경우 일부 언어의 새로운 기능을 사용하지 못할 수 있습니다. 예를 들어 C#11에서는 required keyword를 사용하는 기능이 생겼는데 이 기능은 .NET7에서만 가능한 attribute를 필요로 하기 때문에 .NET6를 target으로 한 project에서는 사용할 수 없게 됩니다. 만약 실제 project에서 이러한 상황이 발생한다면 compiler는 문제가 되는 code에 대해 경고를 표시할 것입니다.
2. 문법과 용어
한글뿐만 아니라 영어나 일본어, 중국어등 모든 언어는 자신만의 고유한 문법을 가지고 있으며 해당 문법의 규칙을 잘 지켜 표현해야만 해당 언어를 알아고 있는 사람에게 정확한 내용을 전달할 수 있을 것입니다. C#언어도 마찬가지입니다. C#언어를 정확하게 사용하기 위해서는 우선 기본적인 문법규칙과 용어를 알아둘 필요가 있습니다.
1) C# 문법
C#의 전체적인 구조는 구문과 block으로 이루어지며 필요한 경우 code를 설명하기 위한 comment를 붙여줄 수 있습니다.
Comment는 code를 설명하기 위한 주된 방식이긴 하지만 변수나 method의 직관적인 명명방식또한 code자체를 설명하는 하나의 수단이 될 수 있습니다. 또한 Project와 관련된 많은 산출물 역시 application의 구조를 이해하는데 많은 도움을 주는 중요한 자료에 해당합니다.
(1) 구문
C#에서 하나의 문장은 여러 type과 변수 그리고 표현식으로 구성되며 이들은 공백이나 +,-와 같은 연산자로 그 의미가 구분됩니다. 특히 공백의 경우 공백, tab, 개행문자(newline)를 포함합니다. 또한 영어나 한글 그리고 기타 많은 언어에서 끝맺음은 마침표(.)로 나타내지만 C#에서는 semicolon(;)으로 그 의미를 전달할 수 있습니다.
아래 예제는 일반적인 형태의 C#구문을 나타낸 것으로 여기서 int는 type이고 totalAnimal은 변수 'cats + dogs'가 표현식에 해당합니다.
int totalAnimal = cats + dogs;
여기서 표현식은 cats라는 피연산자와 +라는 연산자, 그리고 dogs라는 피연산자로 구성되는데 이때 피연산자와 연산자의 순서는 처리방식과 결과가 달라질 수 있으므로 매우 중요한 부분이라 할 수 있습니다.
(2) 주석
주석은 현재 code가 어떻게 동작하는지를 설명하는 가장 주된 방법으로서 다른 사람(특히 개발자)이 해당 code를 이해하는데 도움을 주는 중요한 요소에 해당합니다. 특히 본인이 만든 code라 하더라도 해당 code의 동작방식을 언제까지나 계속 기억할 수 있다고 자신해서는 안됩니다. 몇 개월이 지나 본인이 만든 code를 수정하기 위해 다시 code를 검토해야 하는 순간이 오면 그때는 완전 백지상태에서 시작하게 되는 경우가 허다하며 이때 주석은 개발당시의 자신을 떠올리게 하는 유일한 수단이 될 수 있습니다.
주석은 2개의 slash문자인 '//'로 시작하며 compiler는 '//'문자뒤에 오는 모든 내용을 해당 line끝까지 무시하고 처리하게 됩니다.
//개와 고양이를 합친 모든 동물의 수를 구함
int totalAnimal = cats + dogs;
만약 여러줄에 걸쳐 주석을 만들어야 한다면 주석의 시작으로 '/*'를 주석의 끝으로 '*/'를 사용할 수 있습니다.
/*
개와 고양이를 합친 모든 동물의 수를 구함
다른 동물은 제외
/*
int totalAnimal = cats + dogs;
compiler는 '/*'로 시작하고 '*/'로 끝나는 모든 내용을 무시하게 되므로 이런 원리는 이용하면 극단적으로 아래와 같이 code의 중간에 주석을 끼워 넣을 수도 있습니다.
int totalAnimal /* 총 동물의 수 */ = cats /* 고양이 */ + dogs /* 개 */;
너무 길거나 장확안 주석은 오히려 code를 이해하는데 방해가 될 수 있기 때문에 여러 방법을 통해 code를 이해할 수 있는 길을 제공해 주는 것이 좋습니다.
이미 작성된 특정 부분을 주석화 하거나 주석된 내용을 주석에서 해제하려면 VS2002에서 Edit -> Advanced -> Comment Selection이나 Uncomment Selection을 선택할 수 있으며 VSCode라면 Edit -> Toggle Line Comment나 Toggle Block Commnet를 선택할 수 있습니다. 또는 이들 공통된 단축 key로 'Ctrl + K / C'나 'Ctrl + K / U'가 존재하므로 단축 key를 통해서도 주석과 주석해제동작을 수행할 수 있습니다.
(3) Block
C#에서 code는 중괄호({})안에 포함되며 이를 하나의 code block으로 취급하여 생정자, class, method등 다양한 곳에서 code의 시작과 끝을 나타낼 수 있습니다. 아래 예제는 Calc라는 class에서 Add method의 구현을 나타내고 있습니다. class와 method에 대해서는 추후에 자세히 알아볼 것이므로 지금은 code의 block이 중괄호를 통해서 구현된다는 점만 기억하시기 바랍니다.
class Calc
{
public int Add(int oper1, int oper2)
{
return oper1 + oper1;
}
}
위와 같은 예제처럼 block은 class나 method를 정의하는 code를 나타내는 용도뿐만 아니라 특정 동작을 수행하는 code를 구분하기 위해 code를 block화 하는 경우도 있습니다.
class Calc
{
public int Add(int oper1, int oper2)
{
{
//oper2가 0인 경우는 제외
if (oper2 == 0)
return 0;
}
return oper1 + oper1;
}
}
참고로 C#에서 괄호의 경우 여는 괄호와 닫는 괄호모두 동일한 들여쓰기 수준에 독립적인 line을 유지하는 것을 권장하고 있습니다.
if (true)
{
//참의 경우 실행
}
하지만 종종 어떤 개발자의 경우는 code가 세로로 늘어지는걸 최대한 줄이기 위해 여는 괄호를 구문이 끝나는 지점이 놓는 경우도 있습니다.
if (true) {
//참의 경우 실행
}
C# compiler는 위의 어떠한 style을 사용해도 이를 허용하므로 원하는 style을 골라 사용하면 됩니다. C#의 공식적인 coding style은 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions
.NET Coding Conventions - C#
Learn about commonly used coding conventions in C#. Coding conventions create a consistent look to the code and facilitate copying, changing, and maintaining the code. This article also includes the docs repo coding guidelines
learn.microsoft.com
(4) code 영역화
원하는 특정 code의 일부를 묶어 이를 원하는 이름으로 영역화할 수 있으며 이를 Region이라 합니다. 또한 이렇게 묶어놓은 code는 필요에 따라 동일한 동작으로 접거나 펼 수 있는데 이러한 방법으로 개발자는 code를 좀 더 효휼적으로 볼 수 있습니다.
code를 Region하려면 묶고자 하는 code의 위에 '#region [명칭]'을 사용하고 code의 맨 아래에는 #endregion을 사용합니다.
#region 수정된 구문
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("안녕, World!");
#endregion
throw new System.Exception(); //추가된 구문
code editor에서는 왼쪽 삼각형모양을 click 하여 아래와 같이 region화 된 code를 접을 수 있습니다.

2) C# 어휘
C#의 어휘는 keyword, 기호문자, type으로 구성됩니다. 특히 keyword로는 using, namespace, class, static, int, string, double, bool, if, switch, break, while, do, for, foreach, this, true, false 등이 있으며 가장 자주 사용되는 것들이기도 합니다. 기호문자의 경우 ", ', +, -, *, /, %, @, $와 같은 문자가 사용될 수 있습니다.
this keyword는 현재 개체의 instance를 참조하거나 개체의 생성자를 호출하는데 사용되기도 하며 indexer를 정의하는 데에도 사용됩니다. 아직은 생소하게 들릴 수 있겠지만 중요한 건 다양한 목적으로 사용될 수 있다는 것입니다. 향후 예제에도 this를 자주 접하게 될 텐데 한 가지 목적만으로 사용되는 것이 아니므로 혼동하는 일이 없길 바랍니다. 다만 대부분의 경우 구문을 자주 접하면 자연스럽게 익히게 되므로 너무 걱정하지 않아도 됩니다.
C#에서는 다양한 괄호도 사용되는데 괄호의 경우에는 기호에 따라 쓰임새가 아래와 같이 달라지게 됩니다.
| () | 주로 method를 호출하거나 표현식 또는 조건을 정의하며 type을 형변환하는데 사용됩니다. |
| {} | 구문의 block을 정의하거나 개체 또는 collection을 초기화하는데 사용됩니다. |
| [] | Array나 collection안에 있는 item에 접근하거나 특정 code상단에 attrubute를 적용하는데 사용됩니다. |
| <> | C#에서 generic type을 정의하는데 사용되며 HTML이나 XML에서는 각각의 요소를 정의하는데 사용됩니다. |
이 밖에 like, or, nor , record, init등 특정 상황에서 특별하게 사용되는 keyword도 존재합니다.
참고로 C#에서 모든 keyword는 소문자입니다. 그렇다고 해서 개발자 자신만의 type을 정의하는 경우에도 모두 소문자를 사용해서는 안됩니다(가능하긴 하지만). C#11부터 compiler는 이런 경우에 대해 CS8981 경고표시로 이렇게 해서는 안된다는 것을 알려주고 있습니다.

C# keyword를 변수명으로 사용할 수는 없습니다. 그러나 변태처럼 꼭 그렇게 하고 싶다면 '@'문자를 접두사로 붙여줘야 합니다.
int @int = 10;
3) Namespace
Namespace에 관해서는 이미 아래 글에서 한번 언급한바 있습니다.
[.NET/C#] - [열세번째 C#] C# 세계에 온걸 환영합니다!
위의 글에서는 전체적인 진행을 위해 잠시 다뤄본 것으로 namespace에 관해서는 약간만 더 깊이 있게 알아둘 필요가 있습니다.
Namespace는 일종의 주소와 같습니다. 누군가에게 대한민국 국회의 주소를 알려주기 위해 '서울특별시 영등포구 의사당대로 1 (여의도동)'이라고 할 수 있는 것처럼 C#에서 Namespace는 type의 주소와 같아서 'System.Console.WriteLine'라고 하면 이것은 compiler에게 'WriteLine이라는 method는 System namespace에 있는 Console이라는 type에서 찾아야 합니다.'라고 말해주는 것과 같습니다.
하지만 모든 type을 사용할때마다 해당 type이 속한 namespace를 붙여주기는 매우 번거롭고 또한 code가 장황해질 수 있기 때문에 .NET 6.0이전에 Namespace는 기본적으로 해당 file상단에 아래와 같이 namespace를 선언하도록 하여 namespace가 붙어있지 않은 type이 있는 경우 import 된 namespace에서 해당 type을 찾을 수 있도록 하였습니다.
using System;
...
Console.WriteLine("...");
(1) Namespace의 암시적인 전역 import
일반적으로 모든 cs file에서는 그에 필요한 Namespace를 각각의 file마다 일일이 using문을 사용하여 import해야 했습니다. 그런데 System과 같은 일부 Namespace는 거의 모든 file에서 기본적으로 필요로 하는 Namespace이다 보니 아래의 구문처럼 몇 개의 Namespace를 file마다 항상 import 하는 것으로 시작하도록 반복해야 했습니다.
using System;
using System.Linq;
using System.Collections.Generic;
뿐만 아니라 위 3개의 정도의 Namespace import는 기본이고 Web Service와 같은 Project에서는 경우에 따라 십여개가 넘는 Namespace를 import 해야 하는 경우도 허다했습니다.
이러한 문제를 해결하기 위해 .NET SDK 6 이후부터는 Project template에 Namespace를 import 하기 위한 새로운 기능을 도입하게 되었으며 C#10에서도 이를 받쳐주기 위해 'global using'이라는 keyword를 사용하게 되었습니다. 이에 각각의 file에서 자기만의 Namespace를 import 해야 했던 한계를 벗아나 이제는 하나의 file에서 모든 cs file에서 필요로 하는 기본적인 Namespace만을 import 할 수 있게 되었으며 이를 전역 Namespace import라고 합니다. 이때 각각의 cs file에서는 자신만이 필요로 하는 Namespace만 개별적으로 Import 할 수 있게 되었습니다.
이론적으로 'global using'을 사용할 file은 특정되지 않았기 때문에 Program.cs와 같은 file에서 선언할 수 있지만 되도록이면 GolbalUsings.cs와 같은 이름의 file을 별도로 생성한 후 해당 file을 전역 import를 위한 전용으로 사용하는 것을 권장하며 위의 Namespace의 경우 아래와 같이 전역 import를 적용할 수 있습니다.
gloabl using System;
global using System.Linq;
global using System.Collections.Generic;
.NET 6 부터 이후를 대상으로 생성하는 모든 Project의 경우에는 위와 같은 전역 import기능을 기본적으로 도입하고 있으므로 Project의 obj\Debug\net9.0 folder안에 [Project명].GlobalUsings.g.cs file을 생성하여 System과 같은 필수적인 Namespace에 전역 import를 암시적으로 적용하고 있습니다. 이때 적용되는 Namespace는 사용하고자 하는 해당 SDK의 유형에 따라 약간씩 달라질 수 있습니다.
| SDK | 전역 import가 적용되는 Namespace |
| Microsoft.NET.Sdk | System System.Collections.Generic System.IO System.Linq System.Net.Http System.Threading System.Threading.Tasks |
| Microsoft.NET.Sdk.Web | Microsoft.NET.Sdk 항목 적용 + System.Net.Http.Json Microsoft.AspNetCore.Builder Microsoft.AspNetCore.Hosting Microsoft.AspNetCore.Http Microsoft.AspNetCore.Routing Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging |
| Microsoft.NET.Sdk.Worker | Microsoft.NET.Sdk 항목 적용 + Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging |
VS2022에서 이전에 생성한 예제 project의 전역 import file을 다시 살펴보려면 Solution Explorer에서 'Show All Files' button을 눌러 모든 folder가 Tree에서 나오도록 변경한 뒤 obj -> Debug -> net9.0 folder를 순서대로 열어 CSExam1.GlobalUsings.g.cs file을 찾아보면 됩니다.

'[Project명].GlobalUsings.g.cs'에서 g는 'generated'를 의미하는 것으로 개발자가 직접 추가한 file과 구별되도록 하기 위한 장치입니다.
위 그림에서 보시는 바와 같이 GlobalUsings.g.cs에서는 System이나 System.Collections.Generic같은 일반적인 Namespace를 import 하고 있으며 해당 file은 .NET 6 이후부터 compiler가 Project생성 시 자동적으로 생성한 file입니다.
위와 같이 암시적으로 import되는 Namespace는 필요에 따라 약간씩 조정이 가능하기도 한데 이러한 방법을 사용해 보기 위해 Solution Explorer에서 Project를 double click 해 Project file을 열고 아래와 같이 ItemGroup요소를 추가합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Remove="System.Threading" />
<Using Include="System.Numerics" />
<Using Include="System.Console" Static="true" />
<Using Include="System.Environment" Alias="Env" />
</ItemGroup>
</Project>
<ItemGroup>을 <ImportGroup>으로 오인해서는 안되며 요소의 순서는 문제되지 않습니다.
예상하겠지만 Remove는 삭제를 Include를 특정 Namespace의 추가를 의미합니다. 이때 Static의 여부를 지정할 수 있으며 Alias로 별칭을 명시할 수도 있습니다.
Project file을 닫고 이제 CSExam1.GlobalUsings.g.cs file을 다시 열어보면 아래와 같이 바뀌어 있음을 알 수 있습니다.
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Numerics;
global using global::System.Threading.Tasks;
global using Env = global::System.Environment;
global using static global::System.Console;
이제 Program.cs에서 아래와 같이 code를 작성해 봅니다. WriteLine method의 경우 Console을 붙이지 않고 있으며 Env라는 별칭만으로 Environment class를 사용하고 있는데 그럼에도 불구하고 별다른 오류가 발생하지 않는다는 것을 알 수 있습니다.
using System.Text;
// See https://aka.ms/new-console-template for more information
Console.OutputEncoding = Encoding.UTF8;
WriteLine($"사용자명 : {Env.UserName}");
예제를 실행하면 아래와 같은 결과를 표시할 것입니다.

만약 자동으로 생성되는 것이 아닌 자신이 직접 gloabl import file을 생성하고자 하는 경우나 개별 file마다 모두 Namespace를 import 하는 이전 style을 사용하고자 하는 경우라면 암시적인 전역 import기능은 아예 사용하지 않도록 할 수 있습니다. 방법은 간단한데 그냥 project file에서 <ImplicitUsings> 요소를 삭제하거나 disable로 설정하기만 하면 됩니다.
<ImplicitUsings>disable</ImplicitUsings>
4) Method
Method는 Program의 실질적인 동작을 이루는 부분인데 .NET에서 사용가능한 method는 셀 수 없을 정도로 많습니다. 우리가 예제를 통해 이미 사용해 본 method로는 다음과 같이 Console type의 WriteLine() method가 있습니다.
Console.WriteLine();
Console.WriteLine("Hello C#");
Console.WriteLine("Today : {0}", DateTime.Today);
위 예제를 보면 같은 method라 하더라도 해당 method를 호출하는 방식이 조금씩 다른데 이를 overloading이라고 하며 이러한 호출유형에 따라 method의 동작방식을 바꿀 수 있습니다. Overloading에 관해서는 추후에 자세히 다룰 것입니다.
5) Type
Type은 어떤것에 대한 분류라고 할 수 있습니다. 이를 테면 자동차도 Car라는 하나의 Type으로 분류할 수 있을 것입니다.
자동차에서 가장 중요한 Engine은 해당 Type의 Field나 속성으로 간주할 수 있고 Car에 대한 Instance생성 시 할당하는 이름은 변수라고 할 수 있습니다. 변수나 Field는 추후에 자세히 알아볼 테지만 지금 중요한 건 'C#에서 Type은 어떤 개체를 분류하기 위한 것이다.'라고 하는 것입니다.
C#에서 변수를 선언할때 흔히 사용하는 string이나 int도 Type에 해당하는데 사실 C#은 그 어떤 type도 정의하고 있지 않습니다. string이나 int와 같은 명칭은 얼핏 Type처럼 보이지만 이들은 .NET에서 제공하는 Type을 C#에서 사용하기 위한 별칭에 불과합니다. 즉, C#은 .NET이라는 platform상에서 동작하는 언어일 뿐이며 .NET은 자신의 platform상에서 동작하는 C#과 같은 언어를 위해 System.Int32를 포함한 수천 가지의 type을 제공하고 있는 것입니다. 단지 여기서 C#과 같은 언어는 해당언어에서 연결되고 있는 별칭으로 int와 같은 것을 사용해 제공되는 Type을 사용할 뿐입니다.
C#에서 Type이라 함은 종종 class로 인식되는 경우도 있는데 class뿐만 아니라 struct, enum, interface, delegate와 같은 것들도 모두 Type에 해당합니다. 예를 들어 string은 class지만 int는 구조체(struct)입니다.
(1) Type 및 해당 member(Method) 확안하기
이전에 만든 Console app예제를 통해 해당 예제에서 사용가능한 전체 Type과 해당 Type에 속한 어떤 method가 존재하는지 간단한 code를 작성하여 확인해 보도록 하겠습니다. 예제로 작성한 code는 간단히 refaection이라고 하는 기술을 활용한 것인데 아직은 구체적으로 어떻게 작동하는지 아직 자세히 알아야 할 필요는 없습니다.
CSExam1 project에 있는 Program.cs의 모든 code를 제거하고 Refrection과 관련된 Assembly 및 TypeName Type을 사용할 수 있도록 아래와 같이 System.Reflection Namespace를 import 합니다.
using System.Reflection;
예제에서의 System.Reflection에 대해 global using과 암시적 import기능을 사용할 수 있지만 단지 Program.cs에서만 활용할 뿐이므로 해당 file안에서 필요한 Namespace를 import 할 것입니다.
그런 다음 아래와 같은 구문을 작성합니다. 해당 구문은 compile 된 Console app에서 접근가능한 모든 type을 순회하면서 해당 type의 이름과 type이 가진 method의 수를 표시하도록 합니다.
Assembly? app = Assembly.GetEntryAssembly();
if (app is null)
return;
foreach (AssemblyName assName in app.GetReferencedAssemblies())
{
Assembly ass = Assembly.Load(assName);
int methodCount = 0;
foreach (TypeInfo t in ass.DefinedTypes)
methodCount += t.GetMethods().Length;
WriteLine("The {0} type has {1} types and {2} methods.", assName.Name, ass.DefinedTypes.Count(), methodCount);
}
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제는 예제 Application이 동작할 때 Application안에서 사용가능한 Type의 수와 Method의 수를 표시하고 있습니다. 참고로 Type과 Method의 수는 현재 동작하는 OS에 따라 약간씩 달라질 수 있습니다. 또한 System.Runtime에서 Type과 Method가 전부 0으로 표시되는 건 실제 Type이 아닌 Type-Forwarder만 포함하고 있기 때문입니다. Type-Forwarder는 .NET외부에서 구현되었거나 기타 다른 이유로 구현된 Type을 의미합니다.
예제를 통해 알아본 Type과 Method는 어디까지나 해당 예제 Application에 국한된 것입니다. 만약 여기에 code를 추가해 import 하게 되는 Namespace가 더 많아지게 되면 그만큼의 Type과 Method를 더 많이 표시할 것입니다. 이 처럼 C#에서는 사용가능한 Type과 Method가 상당히 많이 존재하는데, C#을 배운다는 건 다른 의미로 이들 Type과 Method를 배운다는 의미로 받아들여질 수도 있습니다. 그러나 이들 모든 것을 외어야 하는 것은 아니며 C# Programmming에서 일반적으로 사용되는 것들 몇 개만 알고 있으면 그걸로 충분합니다. 다른 Type과 Method를 사용해야 할 때는 그때 사용방법을 배워도 늦지 않습니다. 물론 이미 존재하는 Type을 어떻게 써야 하는지를 아는 것도 중요하긴 하지만 개발자 스스로가 새로운 Type을 마음껏 만들어낼 수 있다는 것을 이해하는 것이 무엇보다 중요합니다. Programming의 확장성은 바로 이러한 부분에서 기여합니다.
3. 변수
모든 Application은 Data를 처리합니다. 반대로 Data를 처리하지 않는 Application은 존재할 수 없습니다. Data라는 것은 보통 File이나 Database 혹은 사용자의 입력으로 Application에 전달될 수 있고 이렇게 전달된 Data는 임시적으로 변수라는 공간에 저장되어 Data를 처리하게 됩니다. 처리된 Data는 다시 File 혹은 Database에 저장되거나 사용자가 볼 수 있게 화면으로 그 결과를 출력할 것입니다.
변수의 사용에 있어서는 가장 중요한 게 '사용하고자 하는 변수가 Memory로부터 얼마만큼의 공간이 할당되어야 하는가?'라는 것입니다. 그리고 이 문제는 해당 변수에 적절한 Type을 부여하는 것으로 해결할 수 있습니다. 예를 들어 int와 double이라는 Type이 있는 상태에서 100이라는 값을 저장해야 한다면 double보다는 int가 훨씬 더 효율적인 선택이 될 것입니다. int는 더 적은 memory공간을 차지하면서 100이라는 값을 저장하기에 충분한 공간을 가지기 때문입니다.
또한 변수는 그 유형에 따라 stack과 heap으로 나뉠 수 있는데 이에 관해서는 곧 자세히 알아볼 것입니다.
1) 변수의 명명규칙과 값의 할당
변수를 선언하려면 해당 변수에 이름을 붙여야 하는데 C#의 식별자와 충돌하지 않는다면 어떤 이름이든 붙여줄 수 있습니다. 하지만 여기에도 다음과 같은 규칙이 존재하는데, 이 규칙은 반드시 따라야 하는 것은 아니지만 일관성이 있는 고품질의 Code를 작성하는데 충분한 도움이 될 수 있으므로 가급적 규칙을 적용해 변수를 명명하는 것이 좋습니다.
| 명명규칙 | 예제 | 사용처 |
| Camel Case | totalCount, userName, taxValue등 | 지역변수, private field |
| Pascal Case(Title case) | AddCount, StackMemory, ParameterMerge등 | Type, Method, private field외 모든 field |
Camel case와 Pascal case의 차이는 첫 문자가 소문자로 시작하는지 대문자로 시작하는지의 차이만 있는 것이며 이후 각 단어의 구문을 대소문자로만 구분하는 것이 기본입니다. 어떤 경우에는 private field의 경우 앞에 _(Under Score) 문자를 앞에 붙여 _totalCount라고 명명하기도 합니다. 보통 private member의 경우 Class밖에서는 노출되지 않기 때문에 특정한 명명규칙이 따로 존재하지 않으므로 어떤 형태로 쓰든 유효하다고 할 수 있습니다.
아래 Code는 명명된 지역변수를 선언하고 여기에 '='문자를 사용해 값을 할당하는 예제를 나타내고 있습니다. 또한 예제에서 사용된 nameof는 연산자로서 변수의 이름을 나타내기 위해 사용된 것입니다.
int totalCount = 10;
Console.WriteLine($"{nameof(totalCount)}의 값은 {totalCount}입니다.");
2) Literal
Literal값은 변수에 값을 할당할 때 사용하는 실제 고정된 값을 의미합니다. 이때 실제고정값을 나타내는 표기법은 변수의 type마다 달라집니다.
C#언어의 명세서에서 Literal은 아래와 같이 정의하고 있습니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#645-literals
Lexical structure - C# language specification
This chapter explains the lexical grammar, and the syntactic grammar of the C# language.
learn.microsoft.com
(1) Text
Text는 문자열과 문자로 나뉠 수 있는데 우선 문자의 경우 char type으로 저장되며 아래와 같이 홑따옴표(')로 나타냅니다.
char a = 'a';
일반적으로 문자는 char type으로 단일문자를 취급할 수 있지만 극히 예외적으로 이러한 규칙을 벗어나는 경우가 있습니다. 예를 들어 Egyptian 상형문자를 나타내려면 2개의 Char값을 사용해야 합니다. 이처럼 아주 특수한 경우도 존재하므로 하나의 Char가 '반드시 하나의 문자'를 나타낸다고 단정해서는 안됩니다.
문자열의 경우는 string type을 사용하며 큰따옴표를 사용해 아래와 같이 Literal값을 표현합니다.
string yourName = "Donald Trump";
① Emoji 표현하기
Emoji는 char type의 ConvertFromUtf32 method를 사용해 다음과 같이 표현합니다.
Console.OutputEncoding = System.Text.Encoding.UTF8;
string emoji = char.ConvertFromUtf32(0x1F600);
Console.WriteLine(emoji);
참고로 실제 위 예제를 확인하려면 compile후 Windows Terminal을 사용해야 합니다.
② 축자 문자열(Verbatim string)
문자열 Literal이 축자로 해석되는 것을 '축자 문자열'이라고 합니다. 축자 문자열에 대해 설명하기 전 우리는 확장문자와 확장열에 대해 먼저 알아두어야 할 필요가 있습니다. 확장문자(escape character)는 문자를 확장열(escape sequence)로 나타내기 위해 사용되는 '\'문자를 의미하여 확장열은 '\'로 시작하는 문자와 조합되어 특수한 의미를 가지게 되는 문자열을 의미합니다.
확장열은 문자열에서 직접적으로 표현하기 불가능하거나 어려운 문자를 나타내기 위한 것인데 tab이나 개행과 같은 것들이 대표적이며 아래와 같이 표현할 수 있습니다.
string tabExam = "korea\tseoul";
여기서 '\t'가 확장열에 해당하며 해당 부분은 문자열에서 compiler에 의해 tab으로 해석됩니다. 하지만 folder나 file의 경로를 표시할 때처럼 실제 확장문자를 그대로 표현해야 하는 경우도 있는데
string path = "C:\test\csexam\test.cs";
이때 '\t'가 tab으로 해석되면 path상에서 문제를 일으킬 수 있습니다. 이런 경우 문자열 앞에 '@'문자를 접두사로 붙여주면 '\'문자는 더 이상 확장문자가 아닌 있는 그대로의 문자로 해석되며 이렇게 만들어진 문자열을 우리는 '축자 문자열(Verbatim string)'이라고 합니다.
string path = @"C:\test\csexam\test.cs";
참고로 C# 13 이후부터는 ESC문자를 '\e'확장열로 표현할 수 있게 되었습니다. C# 13 이전의 version이라면 '\u001b'로 표현됩니다.
char esc = '\e';
char exc = '\u001b'; //C# 13이전
\u001b 대신 \x1b를 사용할 수 있으나 확장열에서 1b부분이 16진수로 취급될 위험성이 있어 권장하지 않는 방법입니다.
③ Raw string literals
C# 11에서 도입된 Raw string literals는 content내에 어떠한 곳에도 확장문자를 사용할 필요 없이 모든 유형의 text를 다룰 수 있는 방법입니다. 특히 XML이나 HTML과 같은 content를 literal로 다뤄야 할 때 아래 예제와 같이 편리하게 사용할 수 있습니다.
string html = """
<html lang="ko">
<head></head>
<body>
Hello
</body>
</html>
""";
Raw string literals은 적용할 문자열의 처음에 3개 이상의 큰따옴표를 붙여주기만 하면 됩니다. 여기서 '3개이상의 쌍따옴표'라는 말에 주목해야 합니다. 만약 대상이 되는 content가 자체적으로 3개의 쌍따옴표를 포함하고 있어야 한다면 이때는 4개의 큰따옴표로 Raw string literals를 표현해야 합니다.
또한 마지막에 붙여준 큰따옴표의 들어쓰기 수준도 봐야하는데 예제에서는 공백4개정도의 들여 쓰기를 적용하고 있습니다. compiler는 이러한 마지막 쌍따옴표의 들여쓰기 수준을 확인한 뒤 content내부의 Raw string literal에서 해당 수준만큼의 들여쓰기를 제거해 줍니다. 그 결과 예제의 문자열은 아래와 같은 결과를 표시할 수 있습니다.
<html lang="ko">
<head></head>
<body>
Hello
</body>
</html>
따라서 아래와 같이 큰따옴표를 왼쪽으로 붙여주게 되면
string html = """
<html lang="ko">
<head></head>
<body>
Hello
</body>
</html>
""";
전체적으로 들여 쓰기가 적용된 결과를 표시하게 될 것입니다.
<html lang="ko">
<head></head>
<body>
Hello
</body>
</html>
④ Raw interpolated string literals
Raw interpolated string literals은 Raw string literals에 중괄호({})를 사용하여 보간문자열을 결합한 것을 말합니다. 이때는 시작하는 큰따옴표 앞에만 '$'문자를 붙여주면 되는데 실제 표현식으로 바뀌어야 할 괄호의 수만큼 '$'문자수를 맞춰서 붙여줘야 합니다.
예를 들어
string html = """
Add Result => "{10+20}"
""";
위 예제의 결과로 'Add Result => "30"'을 나타내고 싶다면 표현식은 중괄호 하나가 사용된 {10+20}에 해당하므로 다음과 같이 처리해야 합니다.
string html = $"""
Add Result => "{10+20}"
""";
이 상태에서 중괄호 하나를 더 붙여 'Add Result => "{30}"'으로 나타내고자 한다면 이때는 중괄호 2개를 더 붙여 총 3개로 만들고 '$'문자는 2개를 붙여 2개의 중괄호가 보간식에 해당함을 compiler에게 알려주면 됩니다.
string html = $$"""
Add Result => "{{{10+20}}}"
""";

위 예제만 보면 언뜻 'dollar문자를 하나만 쓰고 중괄호를 2개를 사용하면 하나가 표현식으로 취급되어 '{30}'이라는 결과를 표시하지 않을까?'라는 의문을 가질 수 있습니다. 하지만 중괄호를 연속해서 2개를 붙여주게 되면 앞의 중괄호가 뒤의 중괄호를 escape 하는 역활을 하는 것으로 인식될 수 있습니다. 즉 중괄호가 보간표현식과 escape하는 2가지 역할사이에서 보호해지기 때문에 그런 문법은 존재할 수 없습니다.
(2) 숫자(Number)
여기서 말하는 숫자는 특정 수식 및 계산에 사용될 수 있는 값을 의미합니다. 변수에 어느 유형(숫자 또는 문자열등의 다른 Type)의 Type으로 만들어야 할지 결정해야 한다면 우선 해당값이 산술적 계산에 사용되는 값인지 또는 숫자 이외에 다른 유형의 문자가 포함될 수 있는지의 여부를 생각해서 결정애야 합니다. 이런 의미에서 보자면 전화번호는 숫자로 취급될 수 없습니다. 일단 전화번호자체를 산출 식에 사용하는 경우는 없을 것이며 경우에 따라 hyphen(-) 같은 특수문자를 포함할 수 있기 때문입니다.
숫자는 12나 32와 같이 자연수는 물론 -12와 같은 음수나 3.2와 같은 실수 역시 숫자가 될 수 있습니다. C#은 음수를 포함한 자연수를 정수로 취급하며 실수는 소수점이하 정밀도에 따라 단정도나 배정도 부동 소수점으로 분류됩니다.
예제를 통해 Number값을 사용해 보기 위해 CSStudy01이 있는 folder에서 CSStudy02 folder를 생성하고 여기에 dotnet commnad line tool을 사용하여 CSStudy02 이름의 Solution을 생성합니다. 그리고 다시 CSStudy02에 CSExam1 이름의 Console App Project를 생성한 뒤 해당 Project를 CSStudy02 Solution에 추가합니다.
CSExam1 Project의 Program.cs에서 기존의 문을 모두 삭제한 뒤 아래 구문을 추가합니다.
int i = -12;
uint ui = 32;
float f = 3.2f;
double d = 3.2;
위 예제는 4가지 유형의 Number변수를 선언하고 있습니다. 여기서 int는 0 값을 포함해 자연수를 저장할 수 있습니다. uint 역시 자연수를 저장할 수 있지만 오로지 양수만 저장할 수 있습니다. 사용하지 않는 음수영역만큼 더 높은 양수의 값을 저장할 수 있는데 이 부분에 대해서는 곧 다시 설명할 것입니다.
float는 단정도 부동 소수점을 의미하는데 값자체를 float으로 인식하려면 값뒤에 'f'문자로 접미사를 붙여줘야 합니다. double은 배정도 부동 소수점입니다. 실수는 기본적으로 double로 해석하므로 이때는 따로 접미사를 붙여줄 필요가 없습니다.
① 범자연수
잘 알고 있겠지만 Computer는 0과 1로만 구성된 2진수 값을 사용합니다. 0부터 9까지 10개의 숫자를 사용하는 10진법과는 꽤 다른 방식이라 할 수 있는데, Computer가 굳이 2진법을 사용하는 이유를 들자면 10진법에 비해 2진법이 Computer가 다루기에 훨씬 단순하기 때문이라 할 수 있습니다.
아래 표는 Computer의 관점에서 10진법의 10이라는 숫자를 저장하면 2진법으로 어떻게 표현될지를 나타낸 것입니다. 해당 표에서 2과 8 부분에 1이 표시되어 있음을 알 수 있는데 이를 계산하면 8 + 2 = 10이 됩니다.
| 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
따라서 10진법에서 10의 숫자는 8bit 기준 2진법으로 00001010이 됩니다.
② digit 분리자 사용
C# 7부터는 숫자단위의 가독성 향상을 위해 밑줄(_)을 사용할 수 있습니다. 예를 들어 1억의 값을 표시하기 위해 해당 수를 100000000과 같이 표시했다면 이를 밑줄을 통해 100_000_000와 같이 구분해 줄 수 있습니다. 이러한 방법은 10 진수값뿐만 아니라 2진수, 16진수와 같은 값에서도 사용할 수 있으며 밑줄(_)에 대한 구분도 백단위가 아닌 원하는 위치 어디서든 사용할 수 있습니다.
③ 2진수 및 16진수 표기
2진수는 0b로 시작하며 16진수는 0x로 시작해 아래와 같이 표시할 수 있습니다.
int i = 100_000_000; //10진수
int b = 0b_0000_1010; //2진수
int h = 0x_0000_000A; //16진수
Console.WriteLine($"{b:N0}"); //2진수 -> 10진수 값
Console.WriteLine($"{h:N0}"); //16진수 -> 10진수 값
(3) 실수
C#에서 실수는 단정도와 배정도 부동 소수점을 사용하는 float과 double type을 통해 사용할 수 있습니다.
부동 소수점 산술에 대해서는 이미 IEEE(institute of Electrical and Electronics Engineers) 표준이 마련되어 있고 대부분의 Programming언어에서 부동 소수점을 구현하는데 이 표준을 따르고 있습니다. 특히 IEE 754는 IEEE에 의해 수립된 부동 소수점에 관한 기술적 표준에 해당합니다.
아래 표에서는 Computer가 이진법을 통해 실수를 표현하는 방식을 나타내고 있습니다. 여기서 8, 4, 1/2, 1/4 부분의 값이 1이므로 8 + 4 + 1/2 + 1/4는 12 3/4가 되고 이 값은 곧 12.75를 의미합니다.
| 32 | 16 | 8 | 4 | 2 | 1 | 1/2 | 1/4 | 1/8 | 1/16 |
| 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 |
위 표에 따라 12.75의 값을 2진수를 나타내면 00001100.1100이 되는데 여기까지만 보면 2진수를 통해 실수를 나타내는 데는 아무런 문제가 없는 것처럼 보이지만 실상은 그렇지 않습니다.
① Code를 통해 Number Type의 Size 알아보기
아래 예제에서는 각 Type에서 저장가능한 값의 범위와 그 크기를 확인하고 있습니다. 이를 위해 일부 Number Type에 존재하는 MinValue과 MaxValue속성을 사용하여 Type의 변수가 저장할 수 있는 최소, 최댓값과 가져오고 있고 sizeof() 연산자를 통해 Type이 Memory에서 사용하는 크기(Byte단위 값) 값도 표시하도록 하고 있습니다.
Console.WriteLine($"int size : {sizeof(int)}(MIN : {int.MinValue:N0}), MAX : {int.MaxValue:N0}");
Console.WriteLine($"double size : {sizeof(double)}(MIN : {double.MinValue:N0}), MAX : {double.MaxValue:N0}");
Console.WriteLine($"decimal size : {sizeof(decimal)}(MIN : {decimal.MinValue:N0}), MAX : {decimal.MaxValue:N0}");
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

결과를 보면 최소와 최댓값의 범위를 다루기 위해 int에서는 4byte를, double은 8byte를, decimal은 16byte가 사용됨을 알 수 있습니다. 여기서 한 가지 주목해야 할 건 decimal이 double보다 훨씬 많은 Memory를 사용하지만 저장가능한 값의 범위는 오히려 더 작다는 것입니다.
② double과 decimal의 차이
double과 decimal값을 예제를 통해 직접 비교해 보도록 하겠습니다. 우선 아래와 같이 double type 2개를 선언하고 각각에 값을 할당합니다. 그리고 이들에 덧셈을 수행한 뒤 그 결과를 예상값과 비교합니다. 참고로 지금은 C#문법을 알아보고자 하는 것이 아니므로 문법에 집중하지 말고 double type에만 집중해야 합니다.
double d1 = 0.1;
double d2 = 0.2;
if ((d1 + d2) == 0.3)
{
Console.WriteLine("Result : 0.3");
}
else
{
Console.WriteLine($"Result : {(d1 + d2)}");
}
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

위 결과와 같이 double에서 0.1, 0.2, 0.3과 같은 실수는 부동 소수점으로서 정확하게 표현할 수 없기 때문에 그 정밀도를 보증하지 않습니다. 그런데 '0.1 + 0.3 == 0.4'처럼 값을 조금 변경해서 시도하면 이번에는 예상했던 방향으로 작동함을 알 수 있습니다. 이렇듯 double은 산술적으로 전혀 동일하지 않음에도 불구하고 현재의 표현식에서 값이 동일하다고 판단하는 부정확한 값이 존재할 수 있습니다. 즉, 어떤 경우는 직접적으로 비교할 수 있지만 또 어떤 경우는 그렇지 않은 것입니다.
만약 위와 같은 상황에서 예상한 값과 일치할 때 항상 true를 반환하는 방식이 적용되어야 한다면 아래와 같이 float Type을 사용해야 합니다. 위와 동일한 조건에서 float Type은 true를 반환할 수 있는데, 왜냐하면 float Type은 double Type보다 정밀도가 떨어지므로 높은 정밀도에서의 비교가 이루어지지 않기 때문입니다.
float f1 = 0.1F;
float f2 = 0.2F;
if ((f1 + f2) == 0.3)
{
Console.WriteLine("Result : 0.3");
}
else
{
Console.WriteLine($"Result : {(f1 + f2)}");
}
결론적으로 double은 두 수의 정밀한 비교가 아닌 정밀도 높은 값이 필요한 경우에만 사용해야 합니다. 예를 들어 어떠한 물체의 크기를 높은 정밀도로 측정하여 그 값을 다루고자 하는 경우가 될 수 있으며 크기를 비교하는 방식에서는 맞지 않을 수 있습니다.
이러한 문제는 위 예제에서 사용된 값이 어떤 식으로 저장되는지를 보면 이해할 수 있습니다. 이전수 표기법을 통해 0.1의 값을 나타내려면 Computer는 1/16에 1, 1/32에 1, 1/256에 1, 1/512와 같이 반복적으로 값을 저장해야 합니다.
| 4 | 2 | 1 | 1/2 | 1/4 | 1/8 | 1/16 | 1/32 | 1/64 | 1/128 | 1/256 | 1/512 |
| 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
double값 비교로 인해 예상치 못한 결과를 만들어낸 가장 대표적인 사례로 1990에서 1991까지 진행된 Gulf전을 들 수 있습니다. Patriot missile system에 double로 비교하는 계산식을 사용했다가 적 missile을 방어하지 못했고 이에 따라 미군 28명이 전사한 사고인데 자세한 내용은 아래 link에서 확인하실 수 있습니다.
https://www-users.cse.umn.edu/~arnold/disasters/patriot.html
이번엔 Type을 decimal로 바꿔 그 결과를 확인해 보겠습니다.
decimal d1 = 0.1M;
decimal d2 = 0.2M;
if ((d1 + d2) == 0.3M)
{
Console.WriteLine("Result : 0.3");
}
else
{
Console.WriteLine($"Result : {(d1 + d2)}");
}
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

decimal Type은 수를 큰 정수로서 저장하고 소수점을 이동시키는 방식이기 때문에 정확도를 더 높일 수 있습니다. 예를 들어 0.1의 경우 소수점이 왼쪽으로 한자리 이동해야 한다는 내용을 가지고 값을 1로 저장합니다. 12.75의 경우도 마찬가지입니다. 12.75는 소수점이 왼쪽으로 2자리 이동되어야 한다는 내용을 가지고 1275의 값으로 저장됩니다.
일반적인 자연수에 대해서는 int를 사용하고 실수를 사용하되 비교하는 경우가 아니라면 double을 사용합니다. 실수의 정확도가 중요한 경우에는 decimal을 사용합니다.
③ 특수값의 사용
float과 double type에는 정적 field로 제공되는 특수한 값이 존재합니다. 그중 NaN은 'not a number'로서 0으로 나눈 결과를 의미하며 Epsilon은 float이나 double에 저장가능한 가장 최소의 양수값을, PositiveInfinity와 NegativeInfinity는 무한한 큰 수와 음수값을 의미합니다. 그리고 이와 같은 특수값을 확인하기 위한 IsInfinity나 IsNaN과 같은 method도 같이 제공하고 있습니다.
아래 예제에서는 위에서 언급한 특수값이 실제 어떻게 표현되는지 확인하고 있습니다.
Console.WriteLine($"{double.Epsilon}");
Console.WriteLine($"{double.Epsilon:N500}");
Console.WriteLine("");
Console.WriteLine($"{double.NaN}");
Console.WriteLine("");
Console.WriteLine($"{double.PositiveInfinity}");
Console.WriteLine("");
Console.WriteLine($"{double.NegativeInfinity}");
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

Epsilon는 수학적 표기법을 통해 5E-324로 표현됩니다.(실제 이보다 약간 더 작은 값입니다.)
NoN은 0을 0으로 나눈 표현식을 통해 생성될 수 있으며 PositiveInfinity와 NegativeInfinity는 옆으로 누 운모양의 8자인 무한대 모양으로 표시되고 양의 실수 또는 음의 실수를 0으로 나눈 표현식을 통해 생성됩니다.
(4) 새로운 Number Type과 unsafe code
.NET5 부터 새롭게 도입된 System.Half는 일반적으로 Memory에서 2byte를 사용해 float이나 double처럼 실수를 저장합니다. .NET7에서 도입된 System.Int128과 System.UInt128는 int와 uint 하고 비슷하지만 부호화된(음수와 양수)것과 부호화되지 않은(0과 양수) integer값을 저장할 수 있으며 16byte의 Memory를 사용합니다.
아래 예제에서는 이들 Type에 대한 값의 범위와 Memory size를 sizeof 연산자를 통해 확인하고 있습니다.
unsafe
{
Console.WriteLine($"Half : {sizeof(Half)} bytes({Half.MinValue:N0} ~ {Half.MaxValue:N0})");
Console.WriteLine($"Int128 : {sizeof(Int128)} bytes({Int128.MinValue:N0} ~ {Int128.MaxValue:N0})");
}
다만 위 code를 compile 하려면 csproj project file에서 다음과 같이 AllowUnsafeBlocks요소를 아래와 같이 추가해야 합니다.
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제를 보면 unsafe code block이 사용되었는데 System.Half와 같은 Type을 사용하는 경우에 필요한 부분입니다. 이들 새로운 Type은 일반적인 연산에서 H/W나 Runtime에서 직접 지원되지 않는 경우가 많고 .NET 내부적으로 bit 연산과 pointer 연산을 통해 처리하는 경우가 있으므로 unsafe를 필요로 합니다.
Code에 unsafe를 사용하면 말 그대로 안전하지 않은 Code가 되는데 이에 관해서는 아래 link에서 좀 더 자세히 확인하실 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code
Unsafe code, pointers to data, and function pointers - C# reference
Learn about unsafe code, pointers, and function pointers. C# requires you to declare an unsafe context to use these features to directly manipulate memory or function pointers (unmanaged delegates).
learn.microsoft.com
(5) bool
bool Type은 true와 false라는 2가지 값만 가지며 다음과 같이 사용할 수 있습니다.
bool b1 = true;
bool b2 = false;
bool Type이 가장 많이 사용되는 곳은 분기나 loop인데 이에 관해서는 추후에 자세히 알아볼 것입니다.
(6) object
object는 매우 특별한 Type으로 어떠한 data유형도 포함할 수 있습니다. Data의 Type을 따로 고민할 필요가 없기 때문에 편리하게 Code를 작성할 수 있지만 성능적인 면에서 불리해지기 때문에 되도록이면 사용을 지향해야 합니다. 하지만 드물게 일부 third-party Library를 사용해야 하는 경우처럼 꼭 필요한 사례도 존재하므로 object가 어떤 건지는 알아둘 필요가 있습니다.
아래 예제는 object Type을 사용하는 일반적인 방법을 나타내고 있습니다.
object age = 34;
object name = "kim";
Console.WriteLine($"{name}의 올해 나이는 {age}세 입니다.");
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

만약 object 변수에 담긴 Data를 일반 변수에서 다뤄야 한다면 아래와 같이 형변환을 먼저 수행해야 합니다.
int age2 = (int)age;
string name2 = (string)name;
Console.WriteLine($"{name2}의 올해 나이는 {age2}세 입니다.");
형변환에 관한 자세한 사항은 추후에 알아볼 것입니다.
앞서 언급한 바와 같이 object Type은 Application의 성능을 저해하는 주요 원인이 될 수 있습니다. 이에 따라 C#의 초기 version을 제외하고 C#2부터는 Generic이라는 기능을 추가하였으며 이를 성능저하 없이 object의 대안으로 사용할 수 있게 되었습니다.
(7) dynamic
dynamic은 C#4에서부터 도입된 것으로 object처럼 어떠한 유형의 data Type도 담아둘 수 있으며 그에 따른 성능적인 문제도 동일하게 안고 있습니다. 다만 object와 다르게 형변환을 하지 않고도 data자체를 그대로 사용할 수 있다는 차이가 있습니다.
아래 예제는 이전 object 예제를 수정한 것으로 dynamic의 일반적인 사용방법을 나타내고 있습니다.
dynamic age = 34;
dynamic name = "kim";
int age2 = age;
string name2 = name;
Console.WriteLine($"{name2}의 올해 나이는 {age2}세 입니다.");
위 예제를 실행하면 이전과 동일한 결과를 표시할 것입니다. 따로 형변환을 수행하지 않았음에도 예외가 발생되지 않았음에 주목하세요. 심지어 해당 Type의 member까지도 아래와 같이 문제없이 사용할 수 있습니다.
int nameLength = name.Length;
다만 dynamic은 code editor상에서 IntelliSense를 통한 자동완성기능을 사용할 수 없습니다. 왜냐하면 compiler는 code의 편집단계에서 해당 data의 Type을 확인하는 것이 아니라 Application이 실행되는 runtime시점에 CLR(Common Language Runtime)에서 Type을 확인하여 처리하기 때문입니다. 따라서 존재하지 않은 속성이나 올바르지 않는 할당을 시도하면 예외를 일으키게 됩니다.
예외라 함은 Application이 실행되는 시점에 발생하는 오류를 의미하는데 이에 관해서는 추후에 자세히 알아볼 것입니다.
dynamic은 대체로 JavaScript나 Python등 .NET에 속하지 않는 언어로 만들어진 class library나 Excel이나 Word와 같이 COM(Component Object Model)과의 상호작용이 필요한 경우에 많이 사용됩니다.
3) 변수 사용하기
Method안에서 선언되는 변수를 '지역변수'라고 하며 오로지 Method가 실행되는 동안에만 유효합니다. 따라서 원칙적으로 Method의 실행이 종료되면 지역변수에 할당된 모든 memory를 해제하게 됩니다. 다만 선언된 지역변수가 값 Type이라면 별 문제가 없지만 참조 Type이랴 면 Garbage collection에서 처리되기를 기다려야 하며 즉시 소거되지 않습니다.
값 Type과 참조 Type의 차이점에 관해서는 추후에 자세히 알아볼 것이며 또한 단 한 번의 Garbage Collection만으로 비관리 Resource를 어떻게 해제할 수 있는지에 대해서도 같이 알아볼 것입니다.
(1) 변수의 Type 지정하기
변수의 Type은 변수명 앞에 위치해야 합니다. 아래 예제는 이를 잘 나타낸 것으로 변수의 Type과 변수명 그리고 해당 변수가 가질 기본값을 할당하고 있습니다.
int sum = 12_345_678;
double tax = 12.5;
decimal price = 1.64M;
string name = "kim";
char key = 'C';
bool b = false;
(2) Type 추론
위 예제에서는 명확하게 변수의 Type이 무엇인지를 int나 string처럼 지정하고 있습니다. 그런데 C# 3부터는 var keyword가 도입되면서 변수를 선언하고 값을 할당할 때 var Type으로 지정할 수 있게 되었으며 Compiler는 변수에 할당한 값을 통해 해당 변수의 Type을 자동으로 추론하여 Type을 지정할 수 있게 되었습니다. 또한 추론된 Type이 지정되는 것은 Compile단계에서 실행되므로 실제 성능에는 영향이 없습니다.
Type추론 시 소수점이 없는 숫자라면 기본적으로 int로 추론됩니다. 하지만 소수점이 없더라도 double이나 decimal로 Type이 사용되어야 한다면 할당값에 아래와 같은 접미사를 붙여워야 합니다.
| 접미사 | Type |
| L | long |
| UL | ulong |
| M | decimal |
| D | double |
| F | float |
소수점이 존재하는 숫자라면 기본적으로 double로 추론되는데 이 경우에도 만약 다른 Type이 사용되어야 한다면 위의 접미사를 값에 붙여줘야 합니다.
값이 큰따옴표로 묶여있는 경우에는 string으로, 홑따옴표라면 char로 추론되며 true나 false로 값이 사용되면 이는 bool Type으로 추론됩니다.
위의 내용을 종합해 이전에 사용된 예제를 다시 작성해 보면 아래와 같이 var를 사용할 수 있습니다.
var sum = 12_345_678;
var tax = 12.5;
var price = 1.64M;
var name = "kim";
var key = 'C';
var b = false;
위의 예제를 VS나 VSCode에서 작성한 후 var keyword에 mouse pointer를 올려두면 각각의 var에서 추론된 Type을 tooltip으 fh 확인해 볼 수 있습니다.

Type추론은 할당된 값으로 추론되므로 int나 bool처럼 값 Type뿐만 아니라 아래와 같이 참조 Type에서도 사용될 수 있습니다.
using System.Data;
using System.Data.SqlClient;
using (var sql = new SqlConnection(ConnectionString))
{
}
var Type이 편리하긴 하지만 일부 개발자는 Type을 바로 알아채기 어렵다는 이유로 var의 사용을 회피하는 경우도 있습니다. 어떤 것이 정답이라고는 할 수 없으며 개인적인 성향이나 Team내의 규칙에 따라 사용방식이 달라질 수 있습니다. 어떤 개발자의 경우에는 변수의 유형이 명확한 경우에만 var를 사용하고 그렇지 않은 경우에는 직접 Type을 지정해 명확하게 표현하기도 합니다. 예를 들어 위 예제의 경우 sql변수가 SqlConnection Type이라는 것은 명확하게 드러나므로 'SqlConnection sql = new SqlConnection(ConnectionString)'형태로 사용하는 것보다 var keyword를 사용하는 편이 훨씬 나을 것입니다.
var Type은 compile단계에서 실제 Type으로 바뀌게 된다는 점이 주목해야 합니다. 이는 dynamic과는 구별되는 것으로 compiler는 dynamic을 변경하지 않습니다.
(3) new keyword
상기 바로 직전의 예제에서는 new keyword를 사용했습니다. new는 참조 Type의 변수를 선언할 때 사용하는 것으로 직접적으로는 참조변수를 위해 Memory를 초기화하고 할당하는 역할을 수행합니다.
값과 참조 Type 및 Memory에 대한 세부사항은 추후에 자세히 알아볼 것이므로 간단히 개념정도만 짚고 넘 거 가고자 합니다.
Type에는 값과 참조라는 2가지 종류가 존재합니다. 값 Type은 비교적 단순하며 변수를 선언하는 즉시 해당 Type이 필요로 하는 공간만큼 Memory가 할당되므로 Memory할당을 위한 new keyword를 필요로 하지 않습니다.(굳이 사용하려면 사용할 수도 있으나 권장하지는 않습니다.)
이에 비해 참조 Type은 조금 더 복잡하며 자신의 상태를 초기화하고 Memory를 할당하기 위한 new keyword를 반드시 사용해야 합니다.
int i;
XmlDocument xmlDocument;
위 예제에서 i는 int Type인데 이는 값 Type에 해당하므로 i변수를 위한 4byte의 Stack Memory를 할당합니다. 그런데 xmlDocument변수는 XmlDocument로 참조 Type인데 new keyword를 사용하지 않고 선언한 하고 있습니다. 이렇게 하면 Stack구조의 Memory에 XmlDocument개체가 Heap에 위치할 값을 가질 수 있는 4byte의 공간만을 할당하게 됩니다. 즉, XmlDocument개체가 들어갈 Heap Memory가 아직 할당되지 않았으므로 이 상태에서 xmlDocument의 값은 null이 됩니다.
int i = 10;
XmlDocument xmlDocument = new XmlDocument();
i변수의 경우 이미 Stack Memory가 할당되어 있으므로 저장할 값을 지정하기만 하면 됩니다. 반면 xmlDocument는 XmlDocument개체를 위한 Heap Memory를 할당하기 위해 new keyword가 사용되었습니다. 이때 참조 Type의 경우 다수의 생성자를 가질 수 있고 그러면 new keyword를 통해 특정 생성자를 호출할 수도 있습니다. 만약 new keyword만 사용한다면 기본 생성자를 호출하게 되고 매개변수를 사용하면 해당 매개변수를 필요로 하는 생성자를 호출하여 지정한 값을 사용해 개체를 초기화하게 됩니다.
생성자에 관한 자세한 사항은 추후에 자세히 알아볼 것입니다.
① target-typed new
C#9부터는 개체의 instance를 생성하기 위한 target-typed new를 사용할 수 있습니다. target-typed new는 참조 Type변수 선언 시 Type을 선언해 놓으면 new를 사용할 때 해당 Type을 반복적으로 명시할 필요가 없도록 해줍니다.
XmlDocument xmlDocument = new();
또한 Type이 field나 속성을 가지고 있고 이를 초기화하여야 한다면 다음과 같이 초기값을 설정해 줄 수도 있습니다.
DateTime dt = new(2025, 10, 15); //본래는 DateTime(2025, 10, 15);
이런 방법은 동일한 Type에 대한 개체를 다수로 가지는 배열이나 Collection에서도 사용될 수 있습니다.
List<DateTime> dts = new()
{
new(2025, 10, 15),
new(2025, 10, 20),
new(2025, 10, 25)
};
Array나 Collection에 관해서는 추후에 자세히 알아볼 것입니다.
위와 같이 target-typed new는 더 적은 code양으로 개체의 instance를 생성할 수 있습니다.
(4) Type의 기본값
string을 제외하고 가장 일반적인 Type은 값 Type이라고 할 수 있는데 default 연산자를 사용해 Type을 매개변수로 전달하면 해당 Type에 대한 기본값을 확인할 수 있으며 기본값 자체를 할당하려면 default keyword를 사용합니다.
//Type의 기본값
Console.WriteLine($"int : {default(int)}");
Console.WriteLine($"bool : {default(bool)}");
Console.WriteLine($"string : {default(string) ?? "NULL"}");
//기본값 설정
int i = default;
bool b = default;
string? s = null;
Console.WriteLine($"i : {i}");
Console.WriteLine($"b : {b}");
Console.WriteLine($"s : {s ?? "NULL"}");
예제에서 사용된?? 연산자는 만약 왼쪽 피연산자의 결과가 NULL이면 오른쪽 값을 대신 사용하라는 의미 합니다. 따라서 예제는 NULL이라는 문구를 표시하게 됩니다. 또한 'string?'에서 ?는 해당 string변수가 명시적으로 NULL이 될 수 있음을 compiler에게 알려주기 위한 것입니다.
new를 사용하지 않으면서 값 Type으로 사용하는 유일한 Type이 string입니다. string은 값 Type처럼 사용하지만 사실은 참조 Type으로서 값 자체가 아닌 값이 저장된 Memory주소를 가집니다. 참조 Type이므로 변수가 어떠한 것도 참조하지 않는다는 것을 나타내는 null을 사용할 수 있으며 null은 참조 Type의 기본값에 해당합니다.
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

4. Console app
Console app은 Text기반에 명령형 기반 Program으로 .NET의 가장 기본적인 Project유형이며 지금까지의 예제도 모두 Console을 바탕으로 만들어 보았습니다. 화려한 GUI는 없지만 그만큼 단순한 입력과 출력만을 가지기에 비교적 빠른 속도로 app을 실행하여 원하는 처리를 수행할 수 있으며 Main함수로의 매개변수전달을 통해 세부적인 동작을 제어할 수 있습니다.
1) 화면출력
Application의 주된 동작은 입력과 출력이며 이런 동작은 Console app에서도 마찬가지입니다. 지금까지 예제에서는 사용자에게 Message를 표시하기 위해 WriteLine Method를 사용해 왔었는데 이 Method는 Line의 끝에서 자동으로 줄 넘김을 수행합니다. 만약 줄 넘김 동작 없이 Message만 표시하려면 Write Method를 사용해야 합니다. 따라서 다음과 같이 Write Method를 호출하면
Console.Write("안녕 ");
Console.Write("하세요. ");
Console.Write("반갑습니다.");
다음과 같은 결과를 표시할 것입니다.

즉, 줄 넘김 동작이 수행되지 않기 때문에 하나의 Line안에 지정한 Message를 모두 표시하게 됩니다. 반면 위 예제에서 WriteLine으로 대신 사용하게 되면
Console.WriteLine("안녕");
Console.WriteLine("하세요.");
Console.WriteLine("반갑습니다.");
예상한 대로 아래와 같은 결과를 표시하게 됩니다.

(1) Numbered Positional Arguments 사용한 서식화
화면출력 시 Message를 서식화 하기 위한 한 가지 방법은 Numbered Positional Arguments를 사용하는 것입니다. 다만 해당 기능은 Write와 WriteLine Method에서 지원되는 것으로 이를 지원하지 않는 Method에서는 string에 있는 Format Method를 사용하여 문자열 매개변수를 서식화할 수 있습니다.
int age = 20;
string name = "kim";
Console.WriteLine(format: "{1}의 나이는 {0}세입니다.", arg0: age, arg1: name);
string note = string.Format(format: "{1}의 나이는 {0}세입니다.", arg0: age, arg1: name);
Console.WriteLine(note);
Console.WriteLine("{0}-{1}-{2}", arg0:"A", arg1:"B", arg2:"C");
Console.WriteLine("{0}-{1}-{2}-{3}", "A", "B", "C", "D");
format안의 문자열에서는 인수번호를 중괄호({})를 사용해 0부터 부여할 수 있고 그러면 인수의 순서에 따라 해당 인수번호로 지정된 값으로 대체되어 표시됩니다.
format과 'arg0, arg1, arg2'라는 인수명은 생략할 수 있습니다. 특히 arg0부터 3까지의 명명방식은 Write, WriteLine, Format Method에서 모두 사용할 수 있으나 인수의 수가 3개 이상을 넘어가면 더 이상 사용할 수 없고 딱히 장점이 있는 것도 아니어서 예제의 마지막 구문처럼 그냥 생략하고 사용할 것을 권장합니다.
(2) 보간 문자열을 사용한 서식화
C#6부터 도입된 보간문자열은 더욱 편리하게 문자열서식화를 구현할 수 있는 기능으로 '$'문자를 접두사로 붙이면 문자열중간에 변수나 표현식을 중괄호로 감싸 해당위치에 결괏값을 표시할 수 있습니다.
보간 문자열을 사용하면 실제 이전의 예제를 아래와 같이 바꿀 수 있습니다.
int age = 20;
string name = "kim";
Console.WriteLine($"{name}의 나이는 {age}세입니다.");
뿐만 아니라 단순 문자열을 표시하는 경우에도 사람이 읽기 편리한 가독성을 제공해 줄 수 있습니다. 예를 들어 C#10 이전에 문자열의 결합은 +연산자를 통해서 구현해야 했지만
string firstName = "junsu";
string lastName = "kim";
string fullName = firstName + " " + lastName;
C#10부터는 아래와 같이 보간문자열을 사용할 수 있습니다.
string fullName = $"{firstName}} {lastName}";
다만 위와 같은 경우는 변하지 않는 문자열상수값에서만 적용할 수 있으며 runtime에서 Type에 대한 변환이 필요한 숫자 Type과 같은 경우에는 사용할 수 없습니다.
(3) 서식문자열
변수나 표현식은 필요한 경우 comma나 colon이후에 서식문자열을 사용하여 서식화를 적용할 수 있습니다.
서식문자열에서 'N0'은 소수점이 없는 천 단위 구분자가 있는 숫자를 의미하며 'C'는 통화형식을 의미합니다. 이때 표시형식은 현재 실행중인 PC의 문화권설정에 따라 달라집니다. 예를 들어 문화권설정이 대한민국이라면 천단위 구분자로 Comma가 사용되며 통화단위로 '₩'문자가 표시될 것입니다.
int price = 200000;
string fruit = "사과";
Console.WriteLine($"{fruit, -10}의 가격은 {price, 10:C}원입니다.");
예제에서는 '{fruit, -10}'과 '{age, 10:C}'부분에 주목해야 합니다. 여기서 Comma기준으로 왼쪽에는 실제 값을 표시할 변수나 표현식이 올 수 있으며 index를 사용하는 경우에는 아래와 같이 index값이 올 수 있습니다.
Console.WriteLine($"{f0, -10}의 가격은 {1, 10:C}원입니다.", fruit, price);
Comma다음에 숫자는 정렬방식을 의미하며 -10이라면 왼쪽기준 10 정도의 정렬공간을, 10이라면 오른쪽기준 10정도의 정렬공간을 확보하게 됩니다. 여기서 Colon다음에 서식문자열을 적용할 수 있는데 첫 번째 중괄호에서는 서식문자열을 사용하지 않았으며 두 번째 중괄호에 통화표시를 지정하는 서식문자열을 사용하였습니다.
위 예제는 다음과 같은 결과를 표시할 합니다.

(4) 사용자정의 서식화
아래의 사용자정의 서식 Code를 사용하면 숫자 서식을 원하는 대로 제어할 수 있습니다.
| 서식코드 | 설명 |
| 0 | 0자리 표시자로 0은 표현이 가능한 경우 해당하는 숫자로 대체되지만 그렇지 않으면 0을 유지합니다. 예를 들어 000.00서식에서 12.3값이 사용된다면 0자리 표시자 서식을 통해 012.30으로 표시하게 됩니다. |
| # | 숫자자리 표시자로 #은 표현이 가능한 경우 해당하는 숫자로 대체되지만 그렇지 않으면 표시를 생략합니다. 예를 들어 ###.##서식에서 12.3값이 사용된다면 숫자자리 표시자 서식을 통해 12.3으로 표시하게 됩니다. |
| . | 숫자에서 소수점 위치를 나타냅니다. 현재 문화권을 반영하므로 대한민국이나 US등에서는 .(점)으로, French등에서는 ,(comma)로 표시됩니다. |
| , | 천단위 분리자를 의미합니다. 따라서 0,000서식이라면 1234567값일때 1,234,567로 표시됩니다. 또한 각 comma마다 1,000의 배수로 나누어진 규모를 확장하는데도 사용될 수 있습니다. 예를 들어 0.00,,서식일때 1234567값이 사용되면 2개의 comma로 인해 1,000이 2번 나누어지므로 1.23으로 표시하게 됩니다. |
| % | Percentage 표시자로 값에 100을 곱하고 Percentage 문자를 추가합니다. |
| \ | 확장문자로서 다음에 오는 문자를 서식 Code로 사용하는 대신 문자를 그대로 표시하게 됩니다. 예를 들어 \##,###\#서식인경우 1234값이 사용된다면 #1,234#으로 표시할 것입니다. |
이외에 더 많은 사용자정의 숫자 서식 Code를 확인하려면 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings
Custom numeric format strings - .NET
Learn how to create a custom numeric format string to format numeric data in .NET. A custom numeric format string has one or more custom numeric specifiers.
learn.microsoft.com
상기 서식 Code 외에도 아래와 같은 간소화된 표준 서식 Code를 사용하면 복잡한 서식을 좀 더 간단하게 적용할 수도 있습니다. 또한 숫자에 대한 정말도를 지정하여 원하는 만큼의 정밀도를 표시할 수도 있습니다. 정밀도에 대한 기본값은 2입니다.
| 서식Code | 설명 |
| C / c | 통화서식입니다. 대한민국 문화권인 경우 123.4값이 사용된다면 ₩123.40으로 표시할 것입니다. 만약 C0으로 서식화하여 정밀로를 없애는 경우에는 ₩123으로 결과를 표시할 것입니다. |
| N / n | 숫자서식입니다. 여기에는 선택적 음수표시와 천단위 구분기호가 적용됩니다. |
| D / d | 숫자서식입니다. 여기에는 선택적 음수표시가 적용되지만 천단위 구분기호는 사용되지 않습니다. |
| B / b | 이진수서식이므로 B서식에서 13값이라면 1101로 표시됩니다. 동일한 값에 B8서식을 사용한다면 해당 수만큼 정밀도가 적용되어 00001101로 표시될 것입니다. |
| X / x | 16진수서식이므로 X서식에서 255값이라면 FF로 표시됩니다. 동일한 값에 X4서식을 사용한다면 해당 수만큼 정밀도가 적용되어 00FF로 표시될 것입니다. |
| E / e | 지수표기법을 의미합니다. 예를 들어 E서식에서 1234.567값이 사용된다면 이는 1.234567000E+003로 표시되며 정밀도를 적용한 E2와 같은 서식을 사용한다면 1.23E+003으로 표시될 것입니다. |
이외에 더 많은 표준 숫자 서식 Code를 확인하려면 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings
시용자정의 서식 Code는 숫자뿐 아니라 날짜와 시간에서도 적용됩니다.
| 서식Code | 설명 |
| / | 날짜부분 분리자로서 문좌권에 따라 다르게 표시됩니다. 대한민국이라면 '-'문자가 사용되지만 US라면 '/'문자가 사용됩니다. |
| : | 시간부분 분리자로서 문화권에 따라 다르게 표시됩니다. 대한민국이라면 ':'문자가 사용되지만 다른 문화권에서는 .(점)이 사용되기도 합니다. |
| d, dd | 날짜서식입니다. 1부터 31까지 나타내지만 dd서식인 경우 0자리 표시자를 통해 01부터 31까지 나타냅니다. |
| ddd,ddd | 요일서식입니다. ddd라면 '월 화 수...'처럼 약식으로 표시하지만 ddd라면 '월요일 화요일 수요일...'형태로 표시하게 됩니다. 문화권의 영향을 받으므로 상황에 따라 다르게 표시될 수 있습니다. |
| f,ff,fff | f는 십분의1초, ff는 백분의1초, fff는 밀리초를 의미합니다. |
| g | 서기(A.D)와 같은 시기를 의미합니다. |
| h,hh | 시간을 나타내는 것으로 12시간제를 사용합니다. 이때 h라면 1부터 12까지 hh라면 01부터 12까지로 표시합니다. |
| H,hh | 시간을 나타내는 것으로 24시간제를 사용합니다. 이때 h라면 1부터 23까지 hh라면 01부터 23까지로 표시합니다. |
| K | 표준 시간대정보를 의미하며 표준 시간대가 특정되지 않으면 null, UTC에 대해선 Z, UTC로 부터 조정된 현지시간이라면 -8:00과 같은 값이 표시됩니다. |
| m,mm | 분을 의미하며 m이라면 0부터 59까지, mm이라면 0자리 표시자를 통해 00부터 59까지 표시됩니다. |
| M,MM | 월을 의미하며 M이랴면 1부터 12까지, MM이라면 0자리 표시자를 통해 01부터 12까지 표시됩니다. |
| MMM,MMMM | 월서식을 믜미합니다. 대한민국은 큰 차이가 없으나 문화권에 따라 표시형태가 달라질 수 있습니다. 예를 들어 영어권인 경우 1월이라면 MMM일때 'Jan'으로, MMMM일때는 'January'로 표시됩니다. |
| s,ss | 초단위 서식을 의미합니다. s일때는 0부터 59까지 나타내지만 ss라면 00부터 59까지 표시됩니다. |
| t,tt | AM/PM구분자로 t일때는 첫번째 문자만으로 표시하지만 tt라면 2개의 문자 모두 표시됩니다. |
| y,yy | 년도 서식을 의미합니다. 천체 년도중 뒤의 2자리만 표시하는 방식으로 y라면 0부터 99까지 yy라면 00부터 99까지로 표시합니다. |
| yyy | 3자리로 구성된 최소년도를 의미하며 필요한 만큼 더 많은 자리수를 가질 수 있습니다. 예를 들어 1 A.D는 001이 되며 Rome의 첫번째 침략은 410년입니다. |
| yyyy,yyyyy | 4자리 또는 5자리 년도입니다. |
| z,zz | UTC 기준시간차이를 의미합니다. z는 0자리 표시자를 가지지 않습니다. |
| zzz | UTC 기준시간과 붙의 차이를 의미합니다. |
이외에 날짜/시간에 관한 더 많은 서식 Code를 확인하려면 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-string
Custom date and time format strings - .NET
Learn to use custom date and time format strings to convert DateTime or DateTimeOffset values into text representations, or to parse strings for dates & times.
learn.microsoft.com
숫자와 마찬가지로 날짜로 간단하게 서식을 적용할 수 있는 표준 서식 Code를 가지고 있습니다.
| 서식Code | 설명 |
| d | 일반날짜형식입니다. 문화권에 따라 다르게 표시될 수 있는데 대한민국이라면 yyyy-MM-dd 형식이 되지만 US라면 M/d/yyyy 형식이 됩니다. |
| D | 좀더 긴 pattern의 날짜형식입니다. 문화권에 따라 다르게 표시될 수 있는데 대한민국 문화권이라면 yyyy MMMM dd mmmm형식이 됩니다. |
| f | 짧은 날짜와 시간 형식입니다. 문화권에 따라 다르게 표시될 수 있습니다. |
| F | 긴 날짜와 시간 형식입니다. 문화권에 따라 다르게 표시될 수 있습니다. |
| m,M | 월/일날짜 형식입니다. |
| o,O | 표준 Pattern으로 날짜/시간값을 직렬화하는데 적합합니다. |
| r,R | RFC1123 Pattern입니다. |
| t | 짧은 시간 형식입니다. |
| T | 긴 시간 형식입니다. |
| u | 범용 짧은 날짜/시간 형식입니다. |
| U | 범용 긴 날짜/시간 형식입니다. |
이외에 더 많은 서식 Code는 아래 link에서 확인하실 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings
Standard date and time format strings - .NET
Learn how to use a standard date and time format string to define the text representation of a date and time value in .NET.
learn.microsoft.com
서식 Code는 대체로 다음과 같은 Pattern으로 사용할 수 있습니다.
var ko = CultureInfo.CreateSpecificCulture("ko-KR");
Thread.CurrentThread.CurrentCulture = ko;
Thread.CurrentThread.CurrentUICulture = ko;
DateTime d = DateTime.Now;
Console.WriteLine($"{d:D}");
위 예제는 다음과 같은 결과를 표시할 것입니다.

2) 입력
Console App에서 사용자로부터 특정 입력값을 받으려면 ReadLine Method를 사용합니다. 이 Method가 실행되면 사용자가 특정 text를 입력할 동안 대기하다가 사용자가 Enter key를 누르는 순간 사용자가 입력한 값을 문자열로 반환하게 됩니다.
아래 예제는 사용자에게 이름입력을 요구하고 사용자가 자신의 이름을 입력하면 해당 이름을 사용해 환영 Message를 표시합니다.
Console.WriteLine("이름을 입력해 주세요.");
string? userName = Console.ReadLine();
Console.WriteLine($"{userName}님 환영합니다.!");
.NET6 이후부터는 NULL가능 check기능으로 인해 C# Compiler가 ReadLine() Method에 대해 문자열대신 NULL을 반환할 수 있다는 경고 Message를 표시할 수 있습니다. 하지만 일반적인 경우 ReadLine() Method가 NULL을 반환하는 경우는 없습니다. 심지어 사용자가 아무런 값을 입력하지 않으면 Method는 공백을 반환합니다. 따라서 이러한 경고표시는 무시해도 되며 필요하다면 이에 대한 경고 Message가 표시되지 않도록 제어할 수도 있습니다.
예제에서는 string에 '?'문자를 사용하여 변수가 NULL을 가질 수 있음을 Compiler에게 알려주고 있습니다. 만약 userName변수가 정말 NULL이 된다면 Console.WriteLine에서는 공백으로 표시될 것이며 이에 관한 다른 오류는 발생시키지 않을 것입니다. 또는 아래와 같이 처리할 수도 있는데
string userName = Console.ReadLine()!;
이때 사용된 '!'문자를 null-forgiving연산자라고 하며 이는 ReadLine() Method가 절대로 NULL을 반환하지 않는다는 것을 Compiler에게 알려주는 역할을 하게 됩니다. 물론 이 경우에도 경고 Message는 표시하지 않을 것입니다. 다만 실제로 NULL이 발생했을 경우에 대한 책임은 개발자가에게 있으므로 사용에 주의할 필요는 있습니다.
위 예제는 다음과 같은 결과를 표시할 것입니다.

(1) ReadLine Method가 NULL을 반환하는 경우
ReadLine Method는 표준 입력 Stream을 통해 입력을 받아들이며 사용자가 아무것도 입력하지 않고 Enter key를 누르면 ReadLine은 NULL대신 공백을 반환합니다.
일반적인 Console App에서 ReadLine은 EOF(end of stream)에 도달하는 경우에만 NULL을 반환하는데 이 경우는 표준 Console 환경에서 사용자 입력만으로 발생할 수 있는 경우는 아닙니다. 왜냐하면 EOF는 일반적으로 Console이 완전히 닫히거나 Redirection 된 입력이 완전히 종료되면 발생되기 때문입니다.
따라서 아래 Code에서
string? userName = ReadLine();
NULL이 발생하는 경우는 표준 입력 Stream이 Redirect 되거나 EOF에 도달하는 경우 혹은 일부 개발환경에서처럼 EOF 신호를 고의적으로 내는 경우라고 할 수 있습니다.
3) Console 사용 간소화하기
C#6부터 using문은 Namespace를 Import 하는 것 외에 정적 Class를 Import 할 수 있고 이를 통해 Code를 더욱 간소화할 수 있게 되었습니다.
(1) 단일 File에서의 정적 Type Import
지금까지 예제에서 사용한 Console은 System.Console이라는 정적 Type에 해당하며 이를 Import 하면 Console의 Method를 호출할 때 Console입력을 제외할 수 있습니다.
using System;
using static System.Console;
WriteLine("이름을 입력해 주세요.");
string userName = ReadLine();
WriteLine($"{userName}님 환영합니다.!");
예제에서는 using static문을 통해 System.Console Class에 대한 정적 Import를 수행하고 있습니다. 덕분에 WriteLine이나 ReadLine Method를 호출할때 Console을 붙여줄 필요가 없습니다.
(2) Project 전체 File을 위한 정적 Type Import
하나의 File에 대한 정적 Import대신 필요하다면 Project의 모든 File을 위한 전역 Import를 수행할 수도 있습니다.
이를 위해 csproj Project File을 열고 <PropertyGroup> Section다음에 <ItemGroup>을 아래와 같이 추가합니다. 이 작업은 .NET SDK의 암시적 using기능을 사용하여 System.Console에 대한 정적 Import를 전역적으로 선언하도록 합니다.
<ItemGroup Label="정적 Import 예제">
<Using Include="System.Console" Static="true" />
</ItemGroup>
위 예제에서 사용된 Label attribute는 일종의 주석과 같은 기능을 수행하는 것으로 각각의 group에서 명확한 용도를 전달할 수 있습니다. 특히 다수의 <ItemGroup>을 정의하고 Build 설정이나 기타 조건에 따라 특정 항목을 포함하거나 제외해야 할 때 유용하게 사용할 수 있습니다.
Project file에서 전역 Import를 하고 나면 이전의 예제는 using static을 사용하지 않고도 아래와 같이 Code를 작성할 수 있습니다.
using System;
WriteLine("이름을 입력해 주세요.");
string userName = ReadLine();
WriteLine($"{userName}님 환영합니다.!");
4) 사용자의 Key입력 여부 판단하기
사용자의 Key입력은 ReadKey Method를 통해 구현할 수 있습니다. 이 Method가 실행되면 사용자가 Key 또는 조합 Key입력 시까지 대기하다가 사용자의 Key입력이 발생하면 ConsoleKeyInfo값으로 그 결과를 반환하게 됩니다.
아래는 사용자에게 Key입력을 요청하고 입력한 Key의 정보값을 반환하는 예제입니다.
Console.WriteLine("원하는 Key를 입력하세요.");
ConsoleKeyInfo key = Console.ReadKey();
Console.WriteLine($"입력한 Key : {key.Key}, Char값 : {key.KeyChar}, Key조합 : {key.Modifiers}");
예제를 실행한 후 Test를 위해 Shift + K key를 누르게 되면 아래와 같은 결과를 표시할 것입니다.

예제를 다시 실행하고 이번에는 F7 key를 눌러봅니다. 그러면 아래와 같은 결과를 표시힐 것입니다.

참고로 예제에서 Key입력을 Test 하려는 경우 일부 환경에서는 제대로 동작하지 않을 수 있습니다. 예를 들어 예제가 VSCode의 Terminal에서 실행되고 있는 상태라면 Ctrl + Shift + X key입력의 경우 예제 Application이 key입력을 포착하기도 전에 VSCode에서 먼저 Key입력을 가로채 Extentions View를 표시하게 될 것입니다. 정확한 Test를 위해서는 Application을 Windows의 Terminal에서 단독으로 실행할 것을 권장합니다.
5) 인수 전달하기
Console App에서는 필요에 따라 인수를 전달하여 Console App의 동작을 제어할 수 있습니다. 실제 dotnet command line도구에서도 project를 생성할 때 tempate의 이름을 전달하여 이를 통해 생성한 project유형을 지정할 수 있습니다
| dotnet new console |
그렇다면 위와 같이 전달한 인수는 Console App에서 어떻게 받을 수 있을까?
.NET6까지의 생성되는 Console App Project Template은 아래와 같은 기본 Code를 생성했었습니다.
using System;
namespace Arguments
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
여기서 'string[] args'에 주목하시기 바랍니다. 여기에 선언된 array가 Application으로 인수로 전달하기 위해 사용되는 매개변수에 해당합니다. 그런데 .NET6 이후 Console App Project Template은 Program class와 Main method를 감추고 있고 때문에 args array도 볼 수 없게 되었습니다. 그러나 이것은 눈에 보이지 않을 뿐 여전히 존재하고 있는 것이므로 args를 사용하는 데는 아무런 문제가 되지 않습니다.
참고로 commnad line에서의 인수는 오로지 공백만으로 분리됩니다. 따라서 만약 인수자체에 공백이 포함되어야 하는 경우라면 인수전체의 값을 따옴표(')나 큰따옴표(")로 감싸줘야 합니다.
아래 예제는 command line에서 전달된 인수의 값과 인수의 수를 나타내도록 하고 있습니다.
Console.WriteLine($"인수의 수 : {args.Length}");
foreach (var item in args)
{
Console.WriteLine(item.ToString());
}
예제를 test 하기 위해 아래와 같이 Console Application을 실행하면
| dotnet run aaa bbb "ccc eee" |
다음과 같은 결과를 표시할 것입니다.

만약 Console App을 VS2022에서 생성하고 실행하고자 한다면 Solution Explorer에서 Project의 속성으로 들어가 Debug에서 'Open debug launch profiles UI'를 눌러 'Command Line arguments'부분에 아래와 같이 전달할 인수를 입력하면 됩니다.

(1) 인수를 통해 option설정하기
현재 Console화면에서 사용자가 직접 전경색과 배경색, 그리고 Cursor의 Size를 지정할 수 있게 하려면 사용자로부터 3개의 인수값을 전달받아야 할 것입니다. 이를 위해 우선 아래와 같은 구문을 추가합니다.
if (args.Length != 3)
{
Console.WriteLine("설정을 위해서는 3개의 인수가 필요합니다.");
Console.WriteLine("ex) dotnet run red blue 50");
return;
}
화면설정을 위해서는 3개의 인수가 필요한데 위 예제에서는 필요한 매개변수가 제대로 전달되지 않은 경우 사용자에게 안내 Message를 표시하고 Application의 실행을 종료하도록 합니다.
그다음 계속해서 아래 구문을 추가합니다.
Console.ForegroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), args[0], true);
Console.BackgroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), args[1], true);
Console.CursorSize = int.Parse(args[2]);
정확히 3개의 인수가 전달된 경우 처음부터 하나씩 인수값을 추출하여 Console화면에 필요한 설정을 진행합니다. 이때 Console.CursorSize부분에서 Compiler는 해당 속성은 오로지 Windows에서만 적용된다는 '경고'를 표시할 수도 있지만 일단 무시하고 넘어가도록 합니다.
위와 같이 예제가 완성되면 해당 인수를 전달하여 Application이 제대로 작동하는지 확인합니다. VSCode나 VS2022에서 어떻게 인수를 전달할 수 있는지는 이전 설명을 참고해 주시기 바랍니다.

(2) API를 지원하지 않는 Platform에 대한 대처
위 예제를 Build 할 때는 별다른 오류가 발생하지 않았지만 일부 API의 경우 어떤 Platform에서는 동작하지 않을 수 있습니다. 예를 들어 Console.CursorSize속성의 경우 Compiler가 경고한 것처럼 이 속성은 오로지 Windows OS에서만 적용되는 속성입니다.
대부분은 이런 경우에 try ~ catch문을 통해 아래와 같이 처리할 수 있으며
try
{
Console.CursorSize = int.Parse(args[2]);
}
catch (PlatformNotSupportedException ex)
{
Console.WriteLine(ex.Message);
}
Platform별로 처리하고자 한다면 OperatingSystem class를 통해 다음과 같이 구현하는 것도 가능합니다.
if (OperatingSystem.IsWindowsVersionAtLeast(major: 10))
{
// Windows 10 또는 이후 Version에서 실행되는 경우
}
else if (OperatingSystem.IsWindows())
{
// Windows 10 이전 Version에서 실행되는 경우
}
else if (OperatingSystem.IsIOSVersionAtLeast(major: 14, minor: 5))
{
// iOS 14.5 또는 이후 Version에서 실행되는 경우
}
else if (OperatingSystem.IsBrowser())
{
// Browser에서 실행되는 경우
}
위 예제에서와 같이 OperatingSystem class는 각 Platform별로 판단가능한 대부분의 Method를 제공하고 있습니다. 특히 IsBrowser의 경우 Blazor를 통한 작업을 수행할 때 유용하게 사용될 수 있습니다.
또 다른 처리 방법으로는 조건부 Compile문을 사용하는 것입니다. 이 경우에는 #if, #elif, #else, #endif와 같은 전처리 지시자를 아래와 같이 사용해서 구현할 수 있습니다.
#if WINDOWS
// Windows 플랫폼 대상일 때 컴파일되는 코드
// 주: 컴파일 시점에 플랫폼만 결정할 수 있고, Windows 버전(예: 10 이상)은 확인 불가
void PlatformEntry()
{
#if NET7_0_OR_GREATER
// .NET 7 이상에서만 사용하고 싶은 코드
#endif
// Windows 전용 동작
}
#elif IOS
// iOS 플랫폼 대상일 때 컴파일되는 코드
void PlatformEntry()
{
// iOS 전용 동작
}
#elif ANDROID
// Android 플랫폼
void PlatformEntry()
{
}
#elif BROWSER
// Blazor WebAssembly 등 브라우저 환경
void PlatformEntry()
{
}
#elif LINUX
void PlatformEntry()
{
}
#elif OSX
void PlatformEntry()
{
}
#else
// 기타 플랫폼(컴파일되지 않은 경우 대비)
void PlatformEntry()
{
}
#endif
이때 .NET의 대상 Framework에 따라서는 아래 표의 기호를 사용할 수 있습니다.
| .NET | 기호 |
| .NET Standard | NETSTANDARD2_0, NETSTANDARD2_1 등 |
| .NET | NET7_0, NET7_0_ANDROID, NET7_0_IOS, NET7_0_WINDOWS 등 |