.NET/C#

[C# 14 / .NET 10] C# 14

클리엘 2025. 12. 1. 17:38
728x90

이제 본격적으로 C#에 관한 전반적인 관한 내용을 다뤄보고자 합니다. C#에 대한 여러 기초 사항들을 살펴보고 문법을 사용해 필요한 구문을 작성하며 일반적으로 사용되는 여러 어휘들도 같이 알아볼 것입니다.

 

1. C#의 표준

 

몇 해에 걸쳐 Microsoft는 C#의 몇 가지 Version을 ECMA 표준 기관에 제출하였고 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

 

ECMA표준 이외에 GitHub Repository에서도 C#을 공개적으로 만들기 위한 여러 가지 들을 살펴볼 수 있습니다.

C# 언어 설계 https://github.com/dotnet/csharplang
Compiler 구현 https://github.com/dotnet/roslyn
언어표준 https://github.com/dotnet/csharpstandard

 

2. C# Compiler Version

 

Roslyn은 C#과 VB.NET을 위한 Compiler를 말하며 .NET SDK의 일부분으로 배포되었고 F#에는 적용되지 않습니다. 일반적으로는 가장 최신의 Compiler를 사용하게 되지만 만약 다른 Version의 Compiler를 사용하고자 한다면 그에 해당하는 .NET SDK가 반드시 설치되어 있어야 합니다.

.NET SDK Compiler 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
10.0 5.0 14.0

 

.NET Application을 생성할 때 일반적인 .NET Version말고도 .NET Standard를 지정할 수 있는데 이런 경우 각 .NET Standard의 Version에 따라 기본 C#언어 Version도 달라집니다.

.NET Standard C#
2.0 7.3
2.1 8.0

 

상술했듯이 특정 Compiler Version을 사용하려면 해당 Compiler를 포함하는 .NET SDK가 설치되어 있어야 합니다. 다만 이전 .NET Version을 Target으로 하는 Project를 생성한 경우 필요하다면 최신 Version의 Compiler를 사용하는 것은 가능합니다. 예를 들어 .NET 10 SDK를 설치했다면 .NET 8을 대상으로 하는 Project라도 C# 14의 언어기능을 사용할 수 있습니다.

 

1) 설치된 SDK Version 확인하기

 

현재 PC에서 사용가능한 .NET SDK와 C# Compiler Version을 확인하고자 한다면 아래 명령을 사용할 수 있습니다.

dotnet --version

 

위 결과로 나온 10.0.100을 보면 아직 초기 .NET SDK Version이며 어떠한 Bug수정이나 기능적인 변화가 없음을 알 수 있습니다.

 

2) 원하는 Compiler Version 사용하기

 

일반적인 경우 사용되는 Compiler Version은 현재 설치된 .NET SDK의 Compiler Version이 사용됩니다. 만약 .NET 14 SDK를 설치한 상태에서 이 후 .NET 14.1 SDK를 설치했다면 Compiler 역시 자체 Version으로 교체될 것입니다. 다만 언어 Version의 경우에는 SDK가 정한 기본 Version이 사용될 수 있는데 만약 특정 언어 Version을 사용해야 하는 경우라면 Project file에 아래와 같이 LangVersion요소를 추가해 줘야 합니다.

<LangVersion>14.1</LangVersion>

 

이때 LangVersion에서는 상황에 따라 다음과 같은 값을 지정해 줄 수 있습니다.

<LangVersion> 의미
9, 10, 11, 12, 13, 14..등등 지정한 언어 Version이 사용됩니다.
latestmajor 가장 높은 major 번호의 Version이 사용됩니다.
latest 가장 높은 major와 minor 번호의 Version이 사용됩니다. 만약 2026년에 14.1이 Release된다면 해당 Version이 사용될 것입니다.
preview 가능한한 가장 높은 version의 preview를 사용합니다. 예를 들어 2026년말 .NET 11 Preview 6를 설치했다면 해당 Version이 사용될 것입니다.

 

Microsoft는 아마도 2026년 초에 .NET 11의 첫 번째 Preview version을 C# 15 Compiler와 함께 Release 할 수도 있습니다.  이 시기에 .NET 11 SDK Preview를 설치하게 되면 이를 통해 Project를 생성하고 C# 15의 새로운 기능들을 사용해 볼 수 있습니다.

 

다만 이런 경우 위에서 설명드린 <LangVersion>을 Project file(.csproj) 아래와 같이 설정하여야 합니다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net11.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
</Project>

 

하지만 Preview는 어디까지나 Preview이므로 이를 Product환경에서 사용해서는 안됩니다. Microsoft는 Preview를 개발자를 통해 feedback을 얻기 위한 목적으로 공개할 뿐 공식적인 지원을 제공하지 않습니다.

 

3) Compiler 판올림하기

 

.NET SDK 10을 설치했다면 C# 14 Compiler만을 사용할 수 있지만 다른 Version의 .NET SDK를 추가로 설치한다면 해당 SDK의 C# Compiler 역시 사용할 수 있을 것입니다. 따라서 만약 2026년에 .NET SDK 11이 나오고 이를 설치한다면 C# 14와 15 Compiler를 사용할 수 있는 환경이 갖춰지게 됩니다.

 

새로운 Project를 생성하는 것은 물론이고 기존 Project의 Version을 올리는 것 역시 가능한데 예를 들어 .NET 10을 대상으로 한 Project가 있고 이 상태에서 .NET SDK 11을 설치했다면 Project file의 아래와 같은 설정을 통해 해당 C# Version을 올려줄 수 있습니다.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>15</LangVersion>
  </PropertyGroup>
</Project>

 

예제를 보면 TargetFramework에서 .NET의 Version을 .NET 10로 유지하고 있고, LangVersion을 통해 C#언어의 Version만 15로 설정해 주는 걸 확인할 수 있습니다. .NET 10은 꾸준히 Update를 지속한다면 2028년 11월까지 지원되므로 C#언어의 Version만 변경하고 SDK자체의 Version은 기존상태를 계속 유지할 수 있습니다. 향후 .NET SDK 11을 사용해 Project를 Build 하고자 하는 경우에도 LangVersion설정은 변경해 줄 필요가 없습니다.(.NET SDK 11을 대상으로 하는 신규 Project의 경우에는 당연히 최신 Version이 기본적으로 사용되므로 따로 별도의 필요하지 않습니다.)

일부 C# 언어기능은 .NET SDK의 Library에 의존성을 갖고 있을 수 있습니다. 따라서 위 예제에서처럼 TargetFramework의 값을 이전상태로 유지하면서 LangVersion만 최신 C# 언어 Version으로 변경하는 경우 해당 Version의 새로운 언어기능을 사용하지 못하는 경우가 생길 수 있습니다. 예를 들어 C# 11에서 도임된 required keyword는 .NET 7에서만 가능한 새로운 Attribute를 필요로 하기 때문에 하위 SDK Version인 .NET 6에서는 사용할 수 없습니다. 이런 경우 Compiler는 지원하지 않는 기능을 사용하는 것에 대한 경고 알림을 표시할 수 있습니다.

 

2. C# 문법 익히기

 

C#의 문법과 어휘에 대한 기본을 익혀보기 위해 간단한 예제를 만드는 것으로 시작해 보겠습니다. C#의 문법은 크게 구문과 block으로 나눌 수 있으며 Code에 대한 설명을 위해 주석문을 사용할 수 있습니다.

 

1) 구문

 

우리의 일상적인 언어에서 문장은 여러 단어와 문구로 이루어지며 마침표로 문장의 끝을 맺습니다. 단어가 나열되는 순서 또한 문장에서 중요하게 작용하는데 한국어와 달리 대부분의 Programming언어는 영어권의 영향을 받으므로 영어와 비슷한 순서가 적용됩니다.

 

C#에서 문장의 끝은 semicolon으로 나타내며 단어처럼 여러 Type과 변수, 그리고 표현식으로 이루어집니다. 예를 들어 아래 예제에서 int는 Type이며 i는 변수, '10 + 20'은 표현식에 해당합니다.

int i = 10 + 20;


여기서 표현식은 다시 10과 20이라는 피연산자와 +라는 기호의 연산자로 나누어 볼 수 있습니다. 이때 연산자와 피연산자의 순서는 그 의미와 결과가 달라질 수 있으므로 매우 중요한 부분에 해당합니다.

 

(1) 주석

 

주석은 Code를 설명하기 위한 가장 주된 방법이자 오래전부터 사용되어 온 고전적인 방법으로서 제삼자가 Code를 읽을 때 Code의 흐름을 이해하기 위한 주요 수단으로 활용됩니다. 심지어 Code를 작성한 본인도 시간이 흐르면 자신의 Code를 잊어버리게 되는데 '주석'은 여기서 기억을 상기시키는데 필요한 History로도 사용될 수 있습니다. 자신의 Code를 잊어버린다는 말은 선뜻 이해하기 어려울 수 있지만 Code를 작성한 개발자 본인아라고 하더라도 시간이 지나면 그때의 의도와 기억을 되찾지 못하는 경우가 대부분입니다. 자신의 기억력을 자신하지 마십시오.

주석이 Code를 설명하기 위한 주된 방법이기는 하지만 유일한 방법은 아닙니다. 의미가 부여된 변수나 Method명, 단위 Test작성, Project과정에서의 여러 가지 산출물등 Code를 설명하는 데 사용되는 방법은 여러 가지가 있을 수 있습니다.
주석중에는 slash 3개로 시작하는 XML주석이라는 것이 존재합니다. 이러한 주석은 Code를 설명하기 위한 다양한 방법으로 활용될 수 있는데 XML주석에 관한 자세한 사항은 추후에 알아볼 것입니다.

 

주석중 한 줄 주석은 아래와 같이 slash 2개로 시작하며 Compiler는 //를 만나면 그 줄 전체를 무시하게 됩니다.

//덧셈 계산
int i = 10 + 20;

 

여러 Line에 걸쳐진 주석을 작성하려면 주석의 처음을 '/*'로 시작하고 끝을 '*/'로 끝내야 합니다.

/*
    아래 Code는
    덧셈 계산을 수행합니다.
*/

 

흔한 경우는 아니지만 위와 같은 주석방식은 Code의 가운데에 주석을 끼워 넣는 경우에도 사용될 수 있습니다.

int i = /* 표현식 부분 */ 10 + 20;
Method나 변수의 의미론적인 명명방식과 class의 Encapsulation 등은 그것 자체로 문서화의 역할을 수행할 수 있습니다. 실제 주석을 사용하는 경우에도 너무 많은 주석은 오히려 Code를 이해하는데 방해가 되는 경우도 있으니 적당한 선을 유지하는 것이 좋습니다.

 

설명하는 목적 이외에 Code자체를 주석화하여 작동하지 않도록 하거나 주석된 Code를 다시 원복 시키는 경우도 있습니다. 이럴 때는 주석문으로 지정할 부분을 Code Editor에서 선택한 뒤 VS2026에서 Edit -> Advanced -> Comment Selection을 선택해 주석화 하거나 Uncomment Selection을 선택해 주석을 취소할 수 있습니다. VSCode라면 Edit -> Toggle Line Comment나 Toggle Block Comment로 동일한 동작을 수행할 수 있습니다. Menu사용할 통한 주석동작은 //을 추가하는 것과 //을 제거하는 것이 전부입니다.

 

(2) Block

 

우리말에서 문단은 새로운 행을 기점으로 나타내지만 C#에서는 중괄호({})를 사용한 Code의 Block을 통해 나타냅니다.

 

특히 C#은 뭔가에 대한 시작점을 정의하는 것으로 사용되는데 예를 들어 구조체, Namespace, Class, Method는 물론 foreach와 같은 구문에서도 사용될 수 있습니다. Namespace나 Class, Method에 대한 자세한 사항은 추후에 자세히 알아볼 테지만 이들에 대해 간략히 설명하자면 다음과 같이 정의할 수 있습니다.

Namespace 관련있는 여러 Class 및 Type들을 Group화하기 위해 사용됩니다.
Class Method를 포함해 개체의 Member들을 포함한 것으로 Type을 나타내는데 사용됩니다.
Method 개체가 가질 수 있는 동작을 구현하는데 사용됩니다.

 

VS2026이나 VSCode와 같은 Editor에서는 왼쪽여백에 화살표모양을 Click 함으로써 하나의 Code Block전체를 접거나 펼 수 있는 편리한 기능을 제공하고 있습니다. 화살표가 내려가면 Block이 펼쳐진 상태를 의미하고 오른쪽으로 향하고 있으면 Block이 접혀 있는 상태를 의미하는데 해당 화살표모양은 평소에는 숨겨져 있지만 mouse pointer를 여백에 가져대 대면 자동으로 표시됩니다.

 

(3) Region

 

특정한 Code의 영역을 묶어 이를 지역화할 수도 있습니다. 이때 의미 있는 이름을 붙여줄 수 있는데 상술했던 Code Block의 접고 펴는 기능은 Region에서도 동일하게 적용됩니다.

 

지역화는 #Region으로 시작해 #endregion으로 끝나며 Editor는 이 사이에 Code를 지역화로 묶어주게 됩니다.

 

 

(4) Block Style

 

C#에서 Block는 여는 중괄호와 닫는 중괄호를 사용하는 괄호 Style을 사용하며 각각 독립적인 Line에 위치하면서 동일한 들여 쓰기 수준을 갖습니다.

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");
}

 

또는 개발자의 Style에 따라 아래와 같이 구문의 정의가 끝나는 지점에 여는 중괄호를 배치하는 약간 다른 Style을 사용하기도 합니다. Method나 Class를 정의하는 경우에는 잘 없지만 하위의 특정 구문을 작성하는 경우 아래의 Style이 많이 사용됩니다.

static void Main(string[] args)
{
    if (1 == 1) {
        Console.WriteLine("Hello, World!");
    }
}

 

어떤 Style을 사용하든 전혀 문제 될 것은 없으므로 선호하는 방식을 선택해 사용하면 됩니다.

Microsoft에서 제시하는 공식적인 Coding Style은 아래 Link에서 찾아볼 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions

 

다만 team단위개발에서 도입하는 특정 Style이 있다면 그걸 따라가는 방식도 필요합니다. 조직의 규칙이 적용되는 경우가 아니라면 어떤 식으로든 일관된 방식을 고수하는 것이 좋습니다.

Microsoft의 공식문서를 살펴보변 대부분 위 규칙 중 전자의 규칙을 사용한다는 것을 알 수 있습니다.

 

(5) 공백을 사용한 Code 서식화하기

 

공백의 개념은 Space를 포함해 Tab이나 개행문자등을 포함합니다. 그리고 Compiler는 이러한 공백문자를 무시하기 때문에 Code작성 시 서식화에 공백을 자유롭게 사용할 수 있습니다.

 

예를 들어 아래 예제에서의 3개 구문은 모두 동일합니다. Code에서 공백을 사용한 Style만 다를 뿐 변수명과 표현식이 모두 동일하기 때문입니다.

int i = 10 + 20;

int i=10+20;

int
i
=
10
+
20
;
위 예제는 모두 동일한 변수명인 i를 사용하고 있기 때문에 변수병을 변경하지 않는 이상 같은 Block안에 해당 구문이 모두 존재하는 것은 불가능합니다.

 

사실상 위 예제에서 공백이 들어가야 할 부분은 int라는 type명과 변수명 안 i사이뿐입니다. 만약 이 부분을 아래와 같이 작성한다면

inti...

 

Compiler는 어디가 Type이고 어디가 변수명인지를 구분할 수 없기 때문에 오류를 발생시킬 것입니다.

 

C#의 공백에 대한 더 자세한 사항은 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/lexical-structure#634-white-space

 

Lexical structure - C# language specification

This chapter explains the lexical grammar, and the syntactic grammar of the C# language.

learn.microsoft.com

 

2) 어휘

 

C#의 어휘는 keyword, 상징문자, type으로 구성됩니다.

 

여기서 keyword라 함은 사전에 특정 용도로 정의된 것을 말하는 것으로 이들 keyword는 변수명이나 Method이름과 같은 용도로는 사용할 수 없습니다. C# Code작성 시 자주 사용되는 keyword로는 using, namespace, class, static, int, string, double, bool, if, switch, break, shile, do , for, foreach, this, true, false 등이 있습니다.

this Keyword는 현재 개체의 instance를 참조하거나 개체의 생성자를 호출하고 indexer를 정의하는 등 여러 가지 용도로 사용될 수 있습니다. 더 자세한 사항은 추후에 자세히 알아볼 것입니다.

 

상징문자의 경우 ", ', +, -, *, /, %, @, $등이 있으며 괄호문자도 포함됩니다.

 

괄호의 경우에는 아래 4가지 문자가 사용되며 각각의 용도가 정해져 있습니다.

() Method를 호출하고 표현식이나 조건을 정의하며 type간 변환을 수행하는데도 사용됩니다.
{} Block을 정의하거나 개체 또는 Collection의 초기화를 수행합니다.
[] Array나 Collection상에서 특정 Item에 접근하거나 Code에 Attribute를 적용하는데도 사용됩니다.
<> Generic에서 Type을 정의하는데 사용되며 XML이나 HTML에서도 사용됩니다. 또한 표현식에서 작다(<), 크다(>)를 나타내는 용도로도 사용됩니다.

 

이외에 특정한 상황에서만 특별한 의미로 사용되는 like, or, not, record, init와 같은 keyword도 존재합니다.

C# keyword는 모두 소문자입니다. 자신만의 Type을 만드는 경우에도 모두 소문자를 사용할 수 있지만 실제로 그렇게 하는 것은 권장하지 않습니다. 아마다 향후에 새롭게 만들어지게 될 keyword 때문에 그런 것으로 보입니다. 실제로 자체 Type에 소문자를 사용하는 경우 Compiler는 'Warning CS8981'경고 Message를 표시할 수 있습니다.

 

Keyword는 이미 예약된 것으로 사용할 수 없지만 꼭 그렇게 해야 한다면 keyword앞에 '@'문자를 접두사로 붙여 사용할 수 있습니다.

int @class = 10;

 

그러나 위와 같은 방식은 code를 읽는 것에도 혼란을 초래할 수 있기 때문에 여전히 권장되지 않습니다.

 

3) Namespace

 

C#에서 namespace는 서로 관련 있는 class, interface, struct, enum, delegate 등을 group 하여 모은 것으로 이를 통해 동일한 명칭에 대한 충돌을 방지하고 code를 더욱 간결하게 작성할 수 있도록 합니다.

 

Namespace는 또한 일종의 type에 대한 주소의 역할을 수행하기도 하는데, 예를 들어 System.Console.WriteLine이라면 Compiler는 WriteLine이라는 method를 System namespace안에 있는 Console type에서 찾게 됩니다. 하지만 WriteLine method를 작성하기 위해 매번 System.Console.WriteLine Code를 작성할 수는 없으므로 .NET 6.0까지는 cs file상단에

using System;

 

같이 namespace를 선언함으로써 compiler에게 자신의 namespace가 존재하지 않는 type에 대해서는 System namespace를 찾아볼 수 있도록 하였습니다. 따라서 Code작성 시

Console.WriteLine(....)

 

만을 작성해 줄 수 있는 것입니다.

위에서 처럼 using System을 사용하는 경우 이를 namespace import라고 표현합니다.

 

(1) 암시적인 전역 namespace import

 

일반적으로 모든 .cs file은 각각 저마다 내부 code에서 필요로 하는 namespace를 import 하기 위한 using구문을 필요로 했습니다. 그런데 System과 System.Linq 같은 어떤 namespace들은 code를 작성하기 위해 필요한 아주 기본적인 namespace들이었고 따라서 project내의 거의 대부분의 cs file이 몇 줄에 걸친 동일한 using문이 작성되어야 했습니다. 심지어 어떤 경우에는 수십 가지의 namespace를 import 하기 위해 그만큼 많은 using문이 작성되는 경우도 있습니다.

 

이러한 문제를 보완하기 위해 C# 10부터는 새로운 keyword를 도입하였고 .NET SDK 6에서는 공통적으로 사용되는 namespace import를 간소화하기 위한 project 설정을 도입하게 되었습니다. 새로운 keyword는 'global using'문으로 해당 keyword를 사용하여 하나의 file에서만 namespace를 import 하면 이 효과가 project 전체 모든 cs file에 적용되기 때문에 각각의 cs file마다 반복적인 using문을 사용할 필요가 없게 되었습니다. 'gloabl using'문을 사용할 수 있는 file에는 제한이 없으므로 극단적으로 Program.cs file에도 사용할 수 있으나 GlobalUsings.cs와 같은 별도의 분리된 file을 생성하여 여기에서 아래와 같이 공통적으로 사용되는 namespace를 import 하는 것이 좋습니다.

global using System;
global using System.Linq;
...

 

.NET 6나 이후를 target으로 하는 모든 project는 C# 10부터의 compiler를 사용하게 됨으로써 기본적으로 obj/Debug/netXX.0 folder안에서 [Project명].GlobalUsings.g.cs file을 생성하게 됩니다. 그리고 이를 통해 System과 같은 일부 namespace의 암시적인 전역 import를 생성하게 됩니다. 이때 대상 namespace는 SDK에 따라 달라질 수 있습니다.

SDK Implicitly imported namespaces
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

 

실제 작성된 import file을 확인해 보기 위해 이전에 만든 Project를 VS2026으로 열어준 뒤 Solution Explorer에서 'Show All Files'button을 눌러 bin과 object folder가 표시되도록 합니다. 그리고 표시된 obj folder에서 Debug->net10.0순서로 열어 GlobalUsings.g.cs file을 찾아 해당 file을 열어봅니다.

GlobalUsings.g.cs file은 file앞에 Project명이 따라옵니다. 따라서 예제의 경우 'ConsoleApp1.GlobalUsings.g.cs' file이 되는데 설명에서는 'ConsoleApp1.'까지를 생략하였습니다.
GlobalUsings.g.cs file명에서 g는 'generated'를 의미합니다.

 

이러한 file은 .NET 6 이후를 대상으로 한 project를 생성하는 경우 compiler에 의해 자동적으로 생성되는 file로서 System을 포함해 몇 가지 기본적인 namespace의 import를 구현하고 있음을 알 수 있습니다.

 

만약 상기 namespace에 더해 추가적으로 import 하거나 필요 없는 namespace를 제거하고자 한다면 Solution Explorer에서 Project명을 double click 하여 Project file을 열고 여기에 아래와 같이 조정하고자 하는 namespace를 용도에 맞게 지정해 주면 됩니다.

예제의 ItemGroup과 비슷하게 ImportGroup이라는 요소도 존재하지만 정확히 ItemGroup을 사용해야 하므로 ImportGroup과 혼동해서는 안됩니다.

 

예제에서 Using Remove는 namespace의 제거를 의미하며, Static은 namespace의 정적 import를, Alias는 별칭을 지정하는 속성입니다.

 

위에서 처럼 필요한 namespace를 지정한 뒤 file을 저장하고 GlobalUsings.g.cs file을 다시 열어보면 아래와 같이 file의 내용이 변경되어 있음을 알 수 있습니다.

// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading.Tasks;
global using Env = System.Environment;
global using static System.Console;

 

위 결과에서 처럼 System.Environment는 Env라는 별칭이 지정되었으므로 아래와 같이 Env별칭을 통해 namespace의 type을 바로 사용할 수 있습니다.

WriteLine(Env.MachineName);

 

WriteLine 역시 상기에서 Sytem.Console을 정적 import 하였으므로 Console을 사용하지 않고 WriteLine method를 호출하는 것이 가능합니다.

 

자동으로 생성되는 file을 사용하기보다 직접 전역 import를 수행하는 file을 만들어서 사용하고자 하는 경우 처럼 필요하다면 암시적인 전역 import기능을 사용하지 않도록 설정할 수도 있는데 이렇게 하려면 Project file에서

<ImplicitUsings>enable</ImplicitUsings>

 

부분을 삭제하거나 혹은 아래와 같이 enable대신 disable로 지정해 주면 됩니다.

<ImplicitUsings>disable</ImplicitUsings>

 

 

4) Method

 

Method는 실질적으로 Application의 동작을 구현하는 부분입니다. 예를 들어 예제를 통해 계속 봐왔던 WriteLine도 Console에 Message를 표시하는 동작을 실행하는 것으로서 우리가 목표로 하는 대부분의 기능이 이러한 Method를 통해 이루어집니다.

 

Method는 호출되는 방식에 따라서도 그 동작이 달라질 수 있습니다. 예를 들어 아래와 같은 경우

Console.WriteLine();
Console.WriteLine("abcdefg");
Console.WriteLine("number : {0}", 100);

 

첫 번째 호출은 빈 공백을 표시하고 두 번째는 'abcdefg'라는 message를 표시하며 세 번째는 'number : 100'이라는 message를 표시합니다. 이렇게 Method를 다양하게 호출하는 것을 overloading이라고 하는데 이와 관련해서는 추후에 자세히 다뤄볼 것입니다.

 

5) 식별자

 

식별자라 함은 어떠한 대상의 이름으로서 C#에서는 type, 변수, field, 속성등을 정의할 때 이를 구분하는 특정한 명칭을 부여하게 됩니다.

 

예를 들어 우리가 'Car'라는 type을 만들면 그 안에 'Engine'이라는 field나 속성을 만들 수 있습니다. Airplan, Train과 같은 것들도 마찬가지로 특정한 개체를 참조하기 위한 식별자로 사용될 수 있을 것입니다. 여기서 중요한 것은 C#자체적으로는 어떠한 type도 정의하지 않는다는 것입니다. 단지 int나 string과 같이 특정 type에 대한 별칭의 keyword만 가지고 있을 뿐이며 이 마저도 현재 platform상에서 제공되는 type을 나타내기 위한 것일 뿐입니다.

 

C#이라는 언어는 .NET을 떠나 독립적으로 존재할 수 없는 언어이며 .NET상에서 동작하는 몇 가지 언어 중 하나에 불과합니다. 누군가가 전혀 다른 type을 가진 다른 paltform용으로의 C# compiler를 만들 수도 있겠지만 사실상 C#의 platform은 .NET이라고 할 수 있으며 .NET에서 System.Int32와 같이 수많은 type을 제공하고 있고 C#에서는 이렇게 제공된 System.Int32 type을 참조하기 위해 int라는 별칭을 사용하는 것입니다.

 

Type이라 함은 흔히 class로 오해하기 쉬운데 C#에서 말하는 type은 class뿐만 아니라 struct, enum, interface, delegate 등 다양하게 존재할 수 있습니다. 실제로 C#에서 쓰이는 string은 class에 해당하지만 int는 구조체(struct)에 해당합니다.

 

(1) 관련 예제로 Type 살펴보기

 

간단한 Console app을 통해 C#에서 사용되는 type(method를 포함해)이 얼마나 있는지를 알아보도록 하겠습니다. 아래 예제 code는 내부적으로 사용되는 type을 확인하기 위해 reflection이라는 방법을 사용하고 있는데, 시간이 지나면서 모두 이해할 수 있게 될 것이므로 지금은 code자체가 어떻게 동작하는지 정확하게 이해하지 않아도 됩니다.

 

Console App Project를 생성한 뒤 Program.cs의 모든 구문을 삭제하고 reflection을 사용하기 위한 namespace를 import 합니다.

using System.Reflection;
원한다면 상기 namespace를 global using을 통해 import 할 수도 있겠지만 예제에서는 단 하나의 file에서만 사용할 것이므로 예제와 같이 개별적으로 import 하여 사용할 것입니다.

 

그리고 아래와 같이 구문을 작성하여 각각의 Assembly가 가진 Method와 type의 수를 확인하도록 합니다.

using System.Reflection;

Assembly sampleApp = Assembly.GetEntryAssembly()!;

foreach (AssemblyName assName in sampleApp.GetReferencedAssemblies())
{
    Assembly ass = Assembly.Load(assName);

    int methodCount = 0;

    foreach (TypeInfo t in ass.DefinedTypes)
    {
        methodCount += t.GetMethods().Length;
    }

    Console.WriteLine("{0:N0} types and {1:N0} methods in {2} assembly.", ass.DefinedTypes.Count(), methodCount, assName.Name);
}

 

위와 같이 code를 작성한 뒤 실행하면 현재 동작중인 OS상에서 Application이 실행될 때 가능한 type과 method의 수를 아래와 같이 표시하게 됩니다. OS에 따라 표시되는 값은 약간씩 다를 수 있습니다.

0 types and 0 methods in System.Runtime assembly.
119 types and 1,491 methods in System.Linq assembly.
44 types and 656 methods in System.Console assembly.
위 결과를 보면 System.Runtime은 type과 method가 0인 것을 알 수 있습니다. System.Runtime은 runtime에서 동적으로 type과 method를 결정하는데, 많은 BCL(Base Class Library) assembly들이 reference assembly로만 존재하고 있으며 System.Runtime에서는 metadata만 가지고 있고 실제 구현 type은 다른 곳에 들어 있기 때문입니다.

 

C#에서 가능한 Method와 type이 얼마나 있는지는 가늠할 수 없습니다. Method는 type이 가질 수 있는 member 중 하나에 불과하며 지금도 수많은 개발자가 나름대로의 새로운 type과 member를 정의하고 있기 때문입니다.

3. 변수

 

어떠한 application이라도 원하는 결과를 얻기 위해서는 처리할 data를 필요로 합니다. 계산기 application을 만든다고 가정했을 때 산술계산에 필요한 수를 전달해야 하는데 이것이 바로 data인 것입니다.

 

Data는 대게 사용자가 입력한 것일 수 있고, database나 file과 같은 매체를 통해 전달될 수도 있습니다. 그리고 이렇게 전달된  data를 application에서 다루고자 할 때 임시적으로 변수에 담는 처리를 통해 computer의 memory에 저장하게 됩니다. 저장된 data는 나름대로의 처리과정을 거치게 되고 그 결과 data를 생성하게 되는데 일반적으로 Application이 종료되면 관련 memory전체가 소멸되기 때문에 결과 data는 다시 database나 file 등에 저장하거나 또는 사용자에게 직접 표시하여 처리 결과를 확인할 수 있도록 합니다.

 

변수를 사용할 때 가장 기본적인 질문은 '다루고자 하는 data에서 얼마나 많은 memory공간을 필요로 하는가?'라는 것입니다. 그리고 우리는 변수의 적절한 type을 지정함으로써 이러한 질문에 응답할 수 있습니다. 여기서 흔히 하는 오해중 하나가 '더 적은 memory type이 더 빨리 수행될 것이다.'라고 추측하는 것입니다. 그러나 항상 그런 것만은 아닙니다. int나 double와 같은 일반적인 type은 각자 차지하는 memory공간이 다르지만 그렇다고 더 빨리 수행되지는 않습니다. 64bit OS에서 16bit 값과 64bit 값은 동일한 조건에 놓여있기 때문에 처리 속도에는 차이가 없습니다. 그리고 일반적인 변수는 서로 근접한 memory공간에 할당될 수 있지만 일부 큰 data의 경우 heap구조를 통해 멀리 떨어진 memory영역에 저장될 수 있습니다. 이러한 memory저장방식은 stack과 heap으로 구분될 수 있는데 이에 대해서는 잠시 후 더 자세히 알아볼 것입니다.

 

1) 변수의 Naming규칙과 값할당

 

변수를 선언할 때는 해당 변수에 대한 이름을 지정해야 합니다. 변수뿐만 아니라 type, method 등 거의 모든 부분에서 개발자의 작명감각이 필요한데, 이때 이름은 직관적이어야 하며 특정한 의미를 연상할 수 있어야 합니다. 또한 다음과 같은 규칙이 존재하므로 가급적이면 해당 규칙을 준수하여 이름을 붙여주는 것이 좋습니다.

규칙방식 설명
Camel case 소문자로 시작해 단어 사이의 구분을 대문자로 지정합니다. userName, age, addressOfBase와 같은 형태가 될 수 있으며 지역변수와 private field에 사용합니다.
Pascal case Camel case와 달리 대문자로 시작합니다. UserName, Age, AddressOfBase와 같은 형태가 될 수 있으며 type과 type의 member(method와 같은), 그리고 private이 아닌 모든 field에 사용합니다.

 

어떤 경우에는 _age와 같이 private field에 _(underscore) 문자를 접두사로 붙여주는 경우도 있는데 private은 외부에 노출되지 않기 때문에 '_'사용여부에 관해서는 별다른 규칙이 존재하지 않습니다.

Team단위나 조직별로 별도의 naming규칙을 적용할 수도 있습니다. 중요한 건 어떤 규칙이든 일관성 있게 적용해야 한다는 것이며 이를 통해 code를 이해하기에 쉬운 접근성을 제공해 줄 수 있습니다.

 

아래 예제에서는 지역변수를 선언하고 해당 변수에 값을 할당하고 있습니다. 값의 할당은 '='문자를 사용하며 참고로 변수자체의 이름을 표현할 때는 nameof 연산자를 사용할 수 있습니다.

int userAge = 50;
Console.WriteLine($"변수 nameof(userAge)의 값은 {userAge}입니다.");
nameof 연산자는 C# 6부터 도입된 것이며 C# 12부터는 정적 context의 instance data에도 사용할 수 있습니다. instance와 static에 관해서는 추후에 자세히 알아볼 것입니다. 또한 'nameof(List<>)'와 같이 bound 되지 않은 generic type에도 사용할 수 있게 되었습니다.

 

2) Literal

 

변수에 값을 할당할 때는 대게 고정된 값을 의미하는 literal값을 할당합니다. 이때 data type에 따라 할당 가능한 literal값의 표기방법이 달라질 수 있습니다. 정수라면 10, 20과 같은 값을 할당할 수 있지만 문자열이라면 "abc"나 "가나다라"와 같이 값을 할당해야 합니다. C#언어의 literal에 관한 더 자세한 사항은 아래 link를 참고해 주시기 바랍니다.


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

 

3) 문자/문자열

 

하나의 문자를 저장하는 경우라면 char type을 사용할 수 있습니다.

대게 하나의 문자를 하나의 char type으로 나타낼 수 있다고 생각하기 쉬운데, 꼭 그런 것만은 아닙니다. 영문자나 숫자의 경우 별 문제가 없고 다른 곳에서의 char에 대한 설명도 편의상 '1개의 문자는 1개의 char를 사용한다.'라고 하지만, 이집트 상형문자의 경우 해당 문자를 표현하기 위해 2개의 char type(이런 것을 surrogate pair라고 합니다.)을 필요로 합니다. 이러한 방식은 일부 아시아국가에서의 문자에서도 마찬가지니 주의하시기 바랍니다.

 

단일 한 개의 문자를 literal표 표현하려면 홑따옴표를 사용해야 합니다.

char c = 'a';

 

반면 문자가 하나이상 존재하는 '문자열'은 큰따옴표를 사용해 표현하며 string type을 사용해야 합니다.

string userName = "kim sang hyun";
string doubleA = new ('a', 2); //2개의 a문자 표현
string emoji = char.ConvertFromUtf32(0x1F600);

 

Windows Termial에서는 emoji를 표현할 수 있는 기능이 있는데 이 기능을 통해 emoji를 표현하려면 UTF-8로 encoding을 수행한 뒤 char.ConvertFromUtf32 method를 사용해야 합니다. 이러한 방식은 Emoji뿐만 아니라 한글과 같은 일부 아시아국가의 고유문자를 나타내기 위한 방법이기도 합니다.

Console.OutputEncoding = System.Text.Encoding.UTF8;
string emoji = char.ConvertFromUtf32(0x1F603); //Grinning Face with Big Eyes
Console.WriteLine(emoji);

 

(1) ESC(Escape) 문자 다루기


C# 13부터 ESC문자(Unicode U+001B)는 '\e'확장문자열로 나타낼 수 있습니다.

char esc = '\e';

 

C# 13 이전이라면 ESC문자를 \u001b로 표현해야 합니다. 어떤 경우에는 \x1b로 나타내는 경우가 있었으나 1b 뒤에 오는 16진수 문자가 확장문자열(Escape sequence)로 잘못 해석될 수 있기 때문에 사용을 권장하지 않습니다.

 

(2) Verbatim string

 

Escape문자는 programming에서 문자열을 처리할 때 확장문자열(Escape sequence)을 도입하기 위해 사용되는 특수한 문자입니다. 확장문자열은 문자열에 직접적으로 삽입이 불가능하거나 그렇게 하기 어려운 문자를 표현할 수 있게 하는 것으로 backslash(\) 문자로 시작하여 처리하고자 하는 문자열값을 지정합니다.

 

예를 들어 문자열에서 tab문자를 표현하고자 하는 경우 아래와 같이 문자열을 구성하여 변수에 저장할 수 있습니다.

string tabInName = "Kim\tLee";

 

그런데 만약 아래와 같이 file의 경로를 지정하는 경우 특정 folder의 이름이 공교롭게도 t로 시작하게 된다면 어떻게 될까?

string path = "C:\source\test\mycs.cs";

 

위와 같은 상태에서 compile을 시도하면 compiler는 '\test'에서 '\t'를 tab문자료 변환하기에 정상적인 경로값이 되지 않을 것입니다. 뿐만 아니라 \s나 \m 역시 유효한 확장문자열이 아닌 것으로 오류를 표현하기 때문에 compile조차 되지 않습니다. 다시 말해 '\'뒤에 모든 문자열은 확장문자열로 우선처리하도록 하고 있으므로 이 가운에 유효하지 않은 확장문자열이 존재한다면 compiler는 오류를 발생시키는 것입니다.

 

C#에서 사용가능한 확장문자열은 아래 link에서 확인하실 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/strings/#string-escape-sequences

 

Strings - C#

Learn about strings in C# programming. See information on declaring and initializing strings, the immutability of string objects, and string escape sequences.

learn.microsoft.com

 

다만 아래와 같이 문자열 앞에 '@'문자를 접미사로 붙이면 문자열 안에 모든 '\'문자는 확장문자열의 시작이 아닌 문자 그대로 해석하게 되므로 file이나 folder의 경로를 나타내고자 할 때 유용하게 사용할 수 있습니다.

string path = @"C:\source\test\mycs.cs";

 

(3) 원시 문자열 (Raw string literal)

 

C# 11부터 사용되어 온 원시문자열 표현방법은 text를 escape 하지 않고도 문자열 그대로 입력할 수 있어서 문자열 안에 XML이나 HTML, JSON과 같은 contnet를 편리하게 포함시킬 수 있습니다.

 

원시문자열은 아래와 같이 세 개의 큰따옴표를 문자열의 처음과 끝에 붙여주는 방법으로 사용됩니다.

string html = """
    <div>
        <span style="color:red">Hello!</span>
    </div>
    """;

Console.WriteLine(html);

 

원시문자열표현방법에서 기본 세 개의 큰따옴표가 사용되는 이유는 문자열자체적으로 큰따옴표를 포함하는 경우 때문입니다. 만약 문자열내부에서 3개의 큰따옴표가 포함되어야 한다면 해당 문자열 앞뒤로 4개의 큰따옴표로 감싸야 하며 문자열내부에 4개의 쌍따옴표가 포함된다면 5개의 쌍따옴표로 감싸줘야 합니다. 원시문자열은 이와 같은 방법으로 내부에 포함된 큰따옴표수보다 더 많은 큰따옴표를 통해 문자열을 감싸줘야 합니다.

 

또한 위 예제를 보면 문자열 끝에 세 개의 쌍따옴표의 들여쓰기가 내부 content와 동일한 수준을 유지하고 있다는 것을 알 수 있습니다. 이와 같은 방법을 통해 compiler는 모든 문자열앞에 들여쓰기 수준을 마지막 세개의 큰따옴표와 동일하게 유지하도록 처리하게 할 수 있습니다. 따라서 위 예제를 실행하면 내부 html의 앞에 들여 쓰기 수준이 모두 제거된 상태로 다음과 같이 문자열을 값을 표현하게 됩니다.

 

따라서 아래와 같이 문자열 끝에 큰따옴표를 왼쪽에 맞춰 들여 쓰기를 없애면

string html = """
    <div>
        <span style="color:red">Hello!</span>
    </div>
""";

Console.WriteLine(html);

 

그만큼 내부 문자열에 들여 쓰기 수준을 그대로 적용하여 표현할 것입니다.

 

① 원시 보간 문자열(Raw interpolated string literals)

 

원시 문자열 안에서 보간 문자열을 사용하기 위한 방법은 일반 문자열의 경우와 크게 다르지 않습니다. 다만 문자열 시작지점에 보간 문자열의 표현식으로 나타낼 중괄호({})의 수만큼 '$'문자를 지정하고 문자열 안에서 동일한 수의 중괄호를 사용해야 합니다.

 

예를 들어 문자열안에서 하나의 중괄호를 있는 그대로의 일반적인 문자열로 취급해야 한다면 2개의 중괄호를 사용해 보간 문자열의 표현식으로 사용해야 합니다. 따라서 아래와 같이 $문자를 2개 사용하고 문자열 안에서도 보간 문자열을 위해 중괄호를 2개 사용합니다.

string say = "hello!";
string html = $$"""
    <div>
        <span style="color:red">{{say}}</span>
    </div>
""";

Console.WriteLine(html);

 

즉, $문자는 compiler에게 문자열안에서 몇 개의 중괄호가 보간 문자열의 표현식으로 인식되어야 하는가를 알려주는 역할을 하는 것입니다. 이를 바꿔 말하면 중괄호가 사용되지 않는 일반 문자열에서는 $문자를 하나만 붙이고 중괄호 역시 하나만 사용하여 보간 문자열을 나타내도 아무런 문제가 되지 않는다는 것을 의미합니다. 하지만 만약 원시 문자열이 중괄호가 빈번하고 중대한 의미로 사용되는 JSON을 나타내야 하는 경우라면 이와 같은 방법이 매우 중요하고 편리하게 작용할 수 있으므로 꼭 기억하시기 바랍니다.

 

4) 숫자

 

C#에서 숫자라 함은 간단히 더하기와 같은 산술계산에 사용되는 값이라 말할 수 있습니다. 그런 의미에서 전화번호는 숫자가 될 수 없습니다. 예를 들어 변수를 사용할 때 숫자로 된 data가 존재하는 경우 이걸 숫자 Type에 넣을지 문자열 Type에 넣을지 고민된다면 그 값이 산술연산에 사용될 수 있는 값인지, 숫자로 구성된 data에서 서식화를 위해 괄호나 hypen과 같은 특수 문자가 사용되었는지를 명확하게 판단하여 결정해야 합니다.

 

여기서 숫자는 31, 32와 같이 셈이 가능한 범자연수(Whole number)가 될 수 있고 또는 -31처럼 음수(Integer)가 될 수 있으며 1.3처럼 실수(단정도 또는 배정도 부동 소수점)가 될 수도 있습니다.

int num = 31;
uint unum = 31;
float fnum = 1.3f;
double dnum = 1.3;

 

위 예제에서 uint는 unsigned integer라 하여 음수를 사용하지 않는 대신 int보다 더 넓은 범위의 양수를 표현할 수 있습니다. float은 단정도 부동 소수점으로 F 또는 f를 값의 접미사로 붙여 표현합니다. double은 배정도 부동 소수점으로서 실수에서 기본적으로 표현되는 type이므로 실수에 별도의 접미사를 붙이지 않으면 double로 인식합니다.

Whole number는 범자연수라 하여 음수를 제외한 0부터의 숫자를 의미하며 integer는 자연수로 음수를 포함한 숫자를 의미합니다.

 

(1) 범자연수 (Whole numbers)

 

Computer는 모든 정보를 0과 1만으로 구성된 bit값으로 저장하며 0과 1, 단 2개의 숫자만 사용하므로 이를 '2진수(2진법)'라고 표현합니다.

 

아래 표는 10의 값을 computer가 저장할 때 만들어지는 bit를 표현하고 있습니다.

 

위 표에서 8과 2에 1이 표시되어 있음에 주목하시기 바랍니다. 8+2면 10의 값이 성립되는데 결론적으로 2진수 총 8bit로 구성된 00001010은 10진수 10의 값과 같다고 할 수 있습니다.

 

① 숫자 구분자를 사용한 가독성 향상

 

C# 7부터는 underscore(_) 문자를 사용하여 원하는 만큼의 숫자를 분리해 줄 수 있으며 이러한 방법은 2진수나 16진수와 같은 여러 유형의 값을 표현할 때도 적용할 수 있습니다.

 

숫자를 분리하는 데 사용되는 '_'문자는 숫자를 표현할 때 해당 숫자의 어디서든 삽입이 가능한데 예를 들어 1백만의 숫자를 표현하는 경우

1000000

 

위와 같이 표현하는 것보다 천 단위로 분리하여

1_000_000

 

작성하면 가독성면에서 훨씬 도움이 될 것입니다.

 

② 2진수와 16진수 표기법

 

C#에서 2진수 값을 직접적으로 표현하려면 해당 숫자의 앞에 0b를 붙여줄 수 있습니다. 0~9, A~F까지 사용하는 16진수의 경우에는 0x를 사용합니다.

int decimal = 1_000_000; //10진수
int binary = 0b_1111_0100_0010_0100_0000; //2진수
int hexadecimal = 0x_000F_4240; //16진수

Console.WriteLine($"{decimalNotation:N0}");
Console.WriteLine($"{binaryNotation:N0}");
Console.WriteLine($"{hexadecimalNotation:N0}");

Console.WriteLine($"{decimalNotation:X}");
Console.WriteLine($"{binaryNotation:X}");
Console.WriteLine($"{hexadecimalNotation:X}");

 

예제에서 'N0'은 10진수를, X는 16진수를 나타내므로 실행결과는 다음과 같을 것입니다.\

 

(2) 실수

 

C#에서 실수에 사용되는 type 중 float와 double은 각각 단정도 부동 소수점과 배정도 부동 소수점을 사용해 표현합니다.

 

대부분의 Programming언어는 부동 소수점을 다루기 위해 IEEE(Institute of Electrical and Electronics Engineers) 표준을 구현하고 있는데 여기서 IEEE 754가 1985년에 만들어진 부동 소수점을 위한 기술적 표준에 해당합니다.

 

아래 표는 computer에서 12.75라는 실수값을 표현하기 위한 2진수를 나타내고 있는데, 1이 표시된 부분만 모두 계산하면 8 + 4 + 1/2 + 1/4가 되므로 결과는 12.75가 됩니다.

 

12.75 값은 이진수 00001100.1100으로 정확하게 표현할 수 있지만 다른 수는 거의 대부분 정확한 표현이 불가능하며 근사치로만 나타냅니다.

 

① 예제를 통해 number type의 size 알아보기

 

C#에서는 sizeof라는 연산자를 통해 type에서 사용하는 memory의 byte값을 확인할 수 있습니다. 또한 대부분의 number type에서는 자체적으로 해당 type의 변수가 가질 수 있는 최솟값과 최댓값을 표현하는 MinValue와 MaxValue를 속성을 가지고 있는데 이들을 조합하여 다음과 같이 type의 수용범위를 알 수 있는 간단한 예제를 만들어 볼 수 있습니다.

Console.WriteLine($"int : {sizeof(int)} bytes, range : {int.MinValue:N0} to {int.MaxValue:N0}.");
Console.WriteLine();
Console.WriteLine($"double : {sizeof(double)} bytes, range {double.MinValue:N0} to {double.MaxValue:N0}.");
Console.WriteLine();
Console.WriteLine($"decimal : {sizeof(decimal)} bytes, range {decimal.MinValue:N0} to {decimal.MaxValue:N0}.");

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

결과를 보면 int는 4byte에 음수부터 양수까지 2십억 정도의 값을 가질 수 있고 double은 8byte에 int보다 더 많은 값을 가질 수 있습니다. Decimal은 16byte를 사용하므로 double보다 더 많은 memory를 사용하지만 표현가능한 값의 범위는 double보다 더 작음을 알 수 있습니다.

 

그렇다면 왜 decimal은 double보다 memory는 더 많이 사용하면서도 값의 범위는 오히려 더 작은 걸까?

 

이 질문에 대한 답을 알아보기 위해 우선 아래와 같은 에제를 작성해 보았습니다. 예제는 2개의 double변수를 선언하고 이들에 +연산자를 적용한 뒤 그 결괏값을 비교하고 있습니다.

double d1 = 0.1;
double d2 = 0.2;

double r = d1 + d2;

if (r == 0.3)
    Console.WriteLine("result : 0.3");
else
    Console.WriteLine($"result : {r}");

 

위 에제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

예제의 결과에서 보듯 double은 정확성을 기대할 수 없습니다. 0.1과 0.2, 0.3과 같은 값은 부동 소수점에서는 문자 그대로의 정확한 표현이 불가능하기 때문입니다.

 

그러나 예제와 약간 다르게 0.1 + 0.3의 식을 대신 사용해 보면 이번에는 0.4라는 정확한 값이 산출됨을 알 수 있습니다. double type에서 실제로는 수학적으로 같지 않음에도 불구하고 현재 표현식에서 정확히 동일한 값으로 간주되는 일부 부정확한 값이 존재할 수 있습니다. 결론은 너무나 모호하지만 일부는 정확한 비교가 가능하나 일부는 그렇지 않다는 것입니다.

 

대신 float type으로 실수를 다루는 경우에는 위 예제와 같은 비교에서 더 정확한 결과를 낼 수 있습니다. 그 이유는 double type보다 정확도가 더 떨어지기 때문입니다.

float f1 = 0.1f;
float f2 = 0.2f;

float r = f1 + f2;

if (r == 0.3f)
    Console.WriteLine("result : 0.3");
else
    Console.WriteLine($"result : {r}");

 

따라서 값자체의 비교보다는 표현의 정밀도가 더 중요하다면 double을 사용하고 값의 비교가 더 중요한 경우에는 float을 사용해야 합니다. 중요한 건 double에서 절대로 '=='비교연산자를 통해 값을 비교해서는 안된다는 것입니다.

 

Double을 사용한 위 에제에서는 computer가 실수값을 어떻게 저장하는지를 보면 왜 예상한 것과 다른 결과를 내는지 알 수 있습니다. 2진수에서 0.1이라는 값을 표현하기 위해 double에서는 아래표에서와 같이 1/16, 1/32, 1/256, 1/512... 등등에 계속해서 1이라는 값을 저장해야 합니다. 따라서 결국 0.1 값은 2진법으로 0.00011001100110011... 이 되는 것입니다.

 

아래 예제는 double예제에서 data type을 decimal로 변경한 것입니다.

decimal d1 = 0.1M;
decimal d2 = 0.2M;

decimal r = d1 + d2;

if (r == 0.3M)
    Console.WriteLine("result : 0.3");
else
    Console.WriteLine($"result : {r}");

 

위 예제를 실행해 보면 이전과 달리 r변수의 값이 0.3을 가지게 된다는 것을 알 수 있습니다.

 

Decimal의 실수저장방식은 의외로 단순한데, 모든 수를 정수형태로 저장한 뒤 소수 부분의 숫자만 이동시키는 방식을 따르고 있습니다. 예를 들어 0.1의 값은 1로 저장되며 소수점 1자리만큼 이동시켜 0.1을 표현합니다. 12.75도 마찬가지인데 12.75를 1275로 저장한 다음 소수점 자리만큼 뒤의 75를 이동시켜 12.75로 표현합니다. 이러한 저장 방식은 실수 전체를 큰 범위의 정수형태로 저장하게 되므로 많은 정수 부분의 공간을 필요로 하게 됩니다. 따라서 double보다 더 많은 memory공간을 필요로 하게 되는 것입니다. 하지만 이러한 방식은 double에서 거의 무한적으로 반복되는 정밀도를 따라가기에는 한계가 있기 때문에 당연히 소수의 정밀도 면에서는 double을 따라갈 수 없습니다.

일반적인 자연수는 int를 사용합니다. 다른 값과의 정확한 비교가 필요 없으면 double을 사용합니다. double은 정확힌 비교가 아닌 비교대상보다 더 큰가, 작은 가를 판단하기에 좋지만 값의 정확한 비교에는 적절하지 않습니다.

 

② 기타 실수 관련 상수와 method

 

float과 double은 특별한 경우에 사용가능한 몇 가지 유용한 상수를 갖고 있습니다. 대표적으로 NaN은 not a number로서 0에서 0으로 나눈 결과를 표현하는 것이며 Epsilon은 float과 double에서 저장가능한 가장 작은 양수값을 나타냅니다. PositiveInfinity와 NegativeInfinity는 무한대의 양의실수와 음수값을 표현하며 IsInfinity와 IsNaN과 같이 이들 값을 확인할 수 있는 method도 존재합니다.

 

아래 예제는 이와 같은 상수를 활용한 것으로 해당 상수값이 어떻게 표현되는지 알 수 있습니다.

Console.WriteLine($"double.Epsilon: {double.Epsilon}");
Console.WriteLine($"double.Epsilon(decimal places-324): {double.Epsilon:N324}");
Console.WriteLine($"double.Epsilon(decimal places-330): {double.Epsilon:N330}");
Console.WriteLine();
Console.WriteLine($"double.NaN: {double.NaN}");
Console.WriteLine();
Console.WriteLine($"double.PositiveInfinity: {double.PositiveInfinity}");
Console.WriteLine($"double.PositiveInfinity: {double.NegativeInfinity}");

 

PositiveInfinity와 NegativeInfinity는 옆으로 누 운모양의 8을 표시하는데 각각 양의 실수와 음의 실수를 0으로 나눈 표현식에서 생성될 수 있습니다. Epsilon은 5E-324보다 약간 작은 값으로 아래 link에서의 표시법으로 표현됩니다. https://en.wikipedia.org/wiki/Scientific_notation

 

3) 신규 number type과 unsafe code

 

System.Half type은 .NET 5에서 도입된 것으로 float이나 double처럼 실수를 저장하지만 32bit의 float이나 64bit의 double과 다르게 2byte의 memory만을 사용함으로써 그만큼 memory를 절약하고 cache의 효율성을 증대시킬 수 있습니다. 다만 값의 범위는 더 작아지므로 대규모 data를 처리하는 데는 주의해야 합니다. System.Int128과 System.UInt128은 .NET 7에서 도입되었는데 int와 uint같이 음수와 양수모두를 저장하거나 0을 포함한 양수만을 저장합니다. 차이점은 더 많은 memory를 사용하기에 더 큰 정수 범위를 표현할 수 있습니다.

 

아래 예제는 위의 type의 표현범위를 알아보기 위한 것으로 내부에서 sizeof 연산자를 사용하였습니다.

unsafe
{
    Console.WriteLine($"Half : {sizeof(Half)} bytes, range {Half.MinValue:N0} ~ {Half.MaxValue:N0}");
    Console.WriteLine($"Int128 : {sizeof(Int128)} bytes, range {Int128.MinValue:N0} ~ {Int128.MaxValue:N0}.");
}

 

sizeof 연산자를 위와 같이 사용함으로 인해 code를 unsafe를 통해 구현해하며 이를 build 하려면 Project의 설정에 아래와 같이 AllowUnsafeBlocks를 추가해야 합니다.

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>

 

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

sizeof 연산자는 int나 byte와 같이 일반적으로 사용되는 type을 제외하곤 unsafe code block을 사용해야 합니다. sizeof 연산자에 관한 더 자세한 사항은 아래 link를 참고하시기 바랍니다.

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/sizeof

 

sizeof operator - determine the storage needs for a type - C# reference

Learn about the C# `sizeof` operator that returns the memory amount occupied by a variable of a given type.

learn.microsoft.com

 

다만 unsafe code block을 사용하면 그만큼 안정성을 검증할 수 없습니다. 이에 관한 자세한 사항은 아래 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

 

4) bool

 

bool은 true와 false값 중 하나만을 가질 수 있는 type으로서 아래와 같이 사용됩니다.

bool b1 = true;
bool b2 = false;

 

bool type자체는 조건을 판단하는데 가장 많이 사용되는 type으로서 분기문이나 loop문에 응용됩니다. 분기문 및 loop문에 관한 자세한 사항은 추후에 자세히 알아볼 것입니다.

 

5) object

 

C#에서 object type은 모든 type을 저장할 수 있는 아주 특별한 type입니다. 하지만 자칫 이해하기 어려운 code를 만들 수 있고 특히 application의 성능을 떨어뜨리는 주요 원인이 될 수 있으므로 가급적 사용을 피해야 합니다.

 

아래 예제에서는 object에 값을 저장하거나 가져올 때 어떤 형태로 사용될 수 있는지를 알 수 있습니다.

object o1 = 100;
object o2 = "abcdefg";

Console.WriteLine($"{o1} : {o2}");

int i = (int)o1;
string s = (string)o2;

Console.WriteLine($"{i} : {s}");

 

예제를 실행하면 아래와 같은 결과를 표시할 것입니다.

 

예제와 같이 object에서 data를 가져올 때는 본래 해당 data가 어떤 type인지 알 수 없기 때문에 적절한 type으로의 형변환이 필요합니다. 따라서 아래와 같이 code를 작성하면 compile error가 발생할 수 있습니다.

object s1 = "abcdefg";
string s2 = s1.Length; //error : s1을 문자열로 취급하려면 형변환이 필요하다.

 

형변환 관한 자세한 사항은 추후에 살펴볼 것입니다.

 

C#의 초기 version부터 사용되어 온 object는 형변환이 필수적으로 따라와야 하기 때문에 여기에서 발생하는 성능상 부하가 경우에 따라서는 아주 크게 작용할 수 있습니다. C#에서는 이러한 문제를 해결하기 위해 generic을 도입하게 되었는데 generic은 object와는 다르게 충분한 유연성을 제공하면서도 성능저하에 관한 문제를 피할 수 있습니다. Generic에 관한 자세한 사항은 추후에 자세히 알아볼 것입니다.

 

6) dynamic

 

C# 4에서 도입된 dynamic은 object와 같이 모든 type의 data를 담을 수 있는 type이며 또한 비슷하게 성능저하를 일으키는 원인이 될 수 있습니다. object와 비슷하지만 한 가지 다른 차이점이라면 저장된 값의 type을 명시적으로 형변환하지 않고도 그대로 사용할 수 있다는 것입니다.

dynamic s = "abcdefg";

int sLen = s.Length;

Console.WriteLine(sLen);

 

위 예제는 어떠한 compile 오류도 발생시키지 않고 정상적으로 7이라는 결괏값을 표시할 것입니다. 할당된 값자체가 문자열값이며 해당 type은 Length라는 속성을 가지고 있기 때문에 아무런 문제가 되지 않습니다. 여기서 중요한 건 dynamic은 항상 마지막에 할당된 값의 type을 유지한다는 것입니다. 따라서

dynamic s = "abcdefg";
s = 100;

int sLen = s.Length;

Console.WriteLine(sLen);

 

위와 같은 예제는 compile 오류를 발생시킵니다. 마지막으로 할당된 값은 100이며 int type에 해당하므로 Length라는 속성을 가지지 않기 때문입니다. dynamic의 또 한 가지 특징은 code를 작성할 때 IntelliSense의 도움을 받을 수 없다는 것도 있습니다. 왜냐하면 dynamic은 code를 작성하는 중에는 type을 확인하지 않기 때문이며 type의 member 역시 runtime시에 CLR(Common Language Runtime)이 확인하는 과정을 거치게 됩니다. 따라서 만약 runtime에서 해당 member가 확인되지 않으면 예외를 발생시키게 됩니다.

 

예외에 관해서는 추후에 자세히 알아볼 것입니다.

 

Dynamic type은 주로 비 .NET system과의 상호작용을 위해 사용되는 경우가 많습니다. 대표적으로 F#이나 Python, JavaScript를 예로 들 수 있으며 Excel이나 Word와 같이 COM(Component Object Model)과의 상호작용을 위해서도 사용됩니다.

 

(1) Dynamic과 ExpandoObject

 

ExpandoObject는 System.Dynamic namespace안에 존재하는 것으로 runtime에서 임의의 속성을 추가하거나 삭제할 수 있는 dynamic개체이며 추가되는 속성을 위해 내부적으로 dictonary를 사용하여 key와 값을 저장하고 관리합니다. 뿐만 아니라 단순 속성을 벗어나 Action과 같은 delegate를 할당하여 method의 추가를 구현할 수도 있습니다.

 

ExpandoObject는 주로 고정된 형태의 class가 아닌 동적인 동작을 필요로 하는 경우에 유용하게 사용할 수 있습니다. 다만 성능이 중요시되거나 비교적 안전한 code가 필요한 환경이라면 ExpandoObject의 사용을 피하는 것이 좋습니다.

 

아래는 ExpandoObject를 어떠한 형태로 사용할 수 있는지를 확인해 볼 수 있는 간단한 예제입니다.

using System.Dynamic;

dynamic car = new ExpandoObject();

car.MaxSpeed = 200;
car.MaxPassengers = 4;
car.CarType = "Sedan";
Console.WriteLine($"{car.CarType} Maximun Speed : {car.MaxSpeed}");

Console.WriteLine("--------------------------------------------");

var dic = (IDictionary<string, object>)car;
foreach(var item in dic)
{
    Console.WriteLine($"{item.Key} : {item.Value}");
}

 

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

예제에서 사용된 data type cast와 dictonary에 관해서는 추후에 자세히 알아볼 것입니다.

 

7) 변수의 선언

 

지역변수라 함은 method안에서 선언되는 것들을 의미하며 오로지 method가 실행되는 동안에만 유효합니다. 그런데 Method의 실행이 종료되는 순간 memory에 할당된 지역변수는 값 type인 경우 소멸하지만 참조 type인 경우 garbage collection을 대기하게 됩니다. 변수의 값 type과 참조 type의 차이에 대해서는 이제 곧 알아볼 것입니다.

 

(1) 지역변수의 type 지정하기

 

지금까지 예제를 통해 꾸준히 봐온 바와 같이 변수는 type과 변수명의 방식으로 아래와 같이 선언됩니다.

int age = 20;
double weight = 60.1;
decimal price = 5.99M;
string fruit = "apple";
char alpabet = 'A';
bool isUse = false;

 

만약 변수를 선언만 하고 해당 변수를 사용하는 구문을 따로 작성하지 않는다면 사용하고 있는 editor와 색상 설정에 따라 물결모양 밑줄과 함께 '변수에 값이 할당되었지만 사용되지 않는다.'는 경고를 표시할 수 있습니다.

 

(2) 변수의 type 추론

 

C# 3 이후부터는 변수를 선언할 때 var keyword를 사용할 수 있습니다. Compiler가 var가 사용된 변수를 만나면 할당된 값을 통해 type을 추론하게 되는데 이러한 동작은 compile 할 때 실행되므로 runtime에 미치는 성능적인 영향은 없습니다.

 

type추론에도 일정한 규칙이 존재하는데 var type변수에 숫자값을 할당할 때는 아래와 같이 값뒤에 붙여주는 접미사를 통해 적절한 type을 추론하게 됩니다.

접미사 추론되는 type
L long
UL ulong
M decimal
D double
F float

 

소수점이 없는 정수값을 할당하는 경우에는 int로 추론되며 어떠한 접미사도 없으면서 실수면 double로 추론됩니다. 숫자가 아닌 문자열의 경우 큰따옴표가 사용되면 string으로, 홑따옴표가 사용되면 char type으로 추론되며 true나 false가 사용되면 bool로 추론됩니다.

 

실제 상기 변수선언의 예제를 var를 사용해 변경하면 아래와 같이 할 수 있으며 각 규칙에 따라 type이 추론됩니다.

var age = 20;
var weight = 60.1;
var price = 5.99M;
var fruit = "apple";
var alpabet = 'A';
var isUse = false;

 

사용하는 code editor에 따라 var keyword에 mouse pointer를 올려두면 추론된 type을 표시하기도 합니다.

type추론은 위와 같은 일반적인 변수에만 적용되는 것은 아닙니다. 복합 type에도 적용될 수 있는데, 예를 들어 C# 3 이전에는

XmlDocument xml = new XmlDocument();

 

위와 같이 해야 했지만 var type을 통해 아래와 같이 XmlDocument type에 대한 변수를 선언해 줄 수 있습니다.

var xml = new XmlDocument();
var keyword는 편리하기는 하지만 type을 즉각적으로 파악할 수 없다는 점 때문에 일부 개발자은 var사용을 선호하지 않는 경우도 있습니다. var를 무조건 사용하거나 또는 사용하지 않기로 마음먹기보다는 type이 명확한 경우에는 var를 사용하고 그렇지 않은 경우 메는 해당 type을 명시적으로 지정하여 변수를 선언하는 것도 괜찮은 방법이 될 수 있습니다.

 

간혹 var를 dynamic과 혼동하는 경우가 있는데 dynamic은 dynamic자체를 compiler가 변경하는 경우가 없으며 runtime에서 type이 확인된다는 점에서 var와는 동작방식이 다릅니다.

Visual Studio에서는 var를 실제 type으로 자동적으로 바꿔주는 refactoring기능을 사용할 수 있습니다. 자세한 사항은 아래 link를 통해 확인할 수 있습니다.
https://learn.microsoft.com/en-us/visualstudio/ide/reference/convert-var-to-explicit-type?view=visualstudio
 

Refactor code to replace var with an explicit type - Visual Studio (Windows)

Learn how to use Quick Actions to replace var in a local variable expression with an explicit type.

learn.microsoft.com

 

(3) new keyword

 

지금까지 C# code에 대한 몇 가지 예제에서는 명확한 설명 없이 new keyword를 계속 사용해 왔습니다. new keyword는 기본적으로 memory에 대한 초기화 및 할당에 사용되는 것으로 변수의 참조 type에서 사용됩니다.

 

값 type에서는 memory할당을 위한 new keyword가 사용되지 않지만 literal을 사용해 값을 초기화하기 위한 방법이 마땅치 않은 경우 사용될 수도 있습니다. 그러나 대부분 new keyword를 굳이 사용해야 하는 경우는 없으며 사용한다 하더라도 참조 type에서의 new와 동일한 의미를 가지지는 않습니다.

 

값 type은 변수를 선언할 때 값을 저장하기 위한 stack memory만을 할당합니다. 이러한 방식은 int나 char처럼 변수가 가져야 할 정해진 memory의 크기가 이미 지정되어 있으므로 가능한 방식입니다. 그러나 참조 type은 해당 변수를 위한 memory공간을 어디서부터 어디까지 할당해야 할지 미리 예견할 수 없으므로 heap이라는 특별한 memory공간을 사용해야 하며 stack memory는 실제 data가 저장된 heap의 memory주소를 기억하기 위한 용도로만 사용합니다.

 

참조 type의 경우에도 변수를 선언하는 경우 new를 사용하지 않으면

Car car;

 

heap에 존재하는 Car개체의 memory주소를 저장하기 위한 stack memory만을 확보(memory주소만을 가질 것이므로 32bit의 경우 4byte를 64bit system의 경우 8byte를 확보)하게 되며 아직 개체를 위한 heap memory까지 할당된 상태에 있지는 않습니다. 따라서 아직 값이 지정되지 않았으므로 이때 변수 car는 null상태가 됩니다. int와 같은 값 type에서 값이 할당되지 않은 경우 기본값 0을 가지는 것과는 다른 것입니다.

Car car = new Car();

 

new keyword를 사용하면 이 순간 car개체를 저장하기 위한 heap memory를 할당합니다. 이때부터 car는 더 이상 null이 아닙니다.

 

① 대상형식화된 new(target-typed new)

 

C# 9에서는 개체를 초기화하기 위한 새로운 구문으로 target-typed new를 도입하였습니다. 개체를 초기화할 때 변수의 type을 지정하고 나면 new를 사용할 때 이를 반복할 필요 없이 new keyword만을 사용하여 아래와 같이 개체를 초기화하는 방식입니다.

XmlDocument xml = new();

 

참조 type이 특정 type의 field나 속성인 경우 이에 값을 할당하는 경우에도 type은 추론될 수 있습니다.

Car car = new();
car.ShipDate = new(2025, 12, 26);

class Car
{
    public DateTime ShipDate;
}

 

Visual Studio에서는 target-typed new를 사용하도록 기존 code의 refactoring기능을 가지고 있습니다. 자세한 사항은 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/visualstudio/ide/reference/use-new?view=visualstudio

 

Use new() - Visual Studio (Windows)

Learn how to use `new()` when you can't use `var`.

learn.microsoft.com

 

(4) 값 Type의 기본값

 

string을 제외하고 대부분의 원시 type은 값 type에 해당하며 값 type은 null이 될 수 없으므로 반드시 값을 가지게 됩니다. 변수에 기본값을 확인하기 위해서는 default() 연산자를 사용하여 type을 매개변수로 전달할 수 있고 반대로 기본값을 할당하는 경우에는 default keyword를 사용할 수 있습니다.

? 접미사를 사용하면 값 type인 경우에도 null을 가질 수 있는 상태로 만들 수 있습니다. 이에 관해서는 추후에 자세히 알아볼 테이지만 지금은 기본적으로 값 type은 null을 가지지 않은 것으로 이해해야 합니다.
string type은 new를 사용하지 않아 값 type으로 오인할 수 있지만 엄밀히 말하면 참조 type에 해당합니다. 그러므로 string의 변수는 실제 값을 가지고 있는 것이 아니라 값을 가진 heap memory의 주소값을 가지게 됩니다. string은 참조 type이므로 당연히 null상태가 될 수 있는데 null은 아직 어떤 것도 참조하고 있지 않은 상태임을 나타내며 모든 참조 type의 기본값이기도 합니다.

 

아래 예제는 int와 bool, DateTime과 같은 값 type의 기본값과 string의 기본값을 확인하고 있습니다.

Console.WriteLine($"int default : {default(int)}");
Console.WriteLine($"bool default : {default(bool)}");
Console.WriteLine($"DateTime default : {default(DateTime)}");
Console.WriteLine($"string default : {default(string) ?? "<NULL>"}");

 

 

예제에서 사용된?? 연산자는 피연산자가 null인 경우 오른쪽 값을 대신 반환하도록 하는 연산자입니다. 따라서 참조형인 string은 기본이 null상태이므로 'null'이라는 문자열을 표시하게 됩니다.

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제에서 시간에 대한 기본값은 현재 예제를 실행하는 PC의 문화권에 따라 다른 서식으로 표현될 수 있습니다.

 

아래 예제는 값 type에 기본값을 할당하는 방법을 나타내고 있습니다.

int number = 10;
Console.WriteLine($"current value 1 : {number}");
number = default;
Console.WriteLine($"current value 2 : {number}");

 

값 type에 아무런 값도 지정하지 않으면 기본적으로 기본값이 할당되지만 이를 명시적으로 나타내고자 하는 경우 예제에서와 같이 default keyword를 사용할 수 있습니다.

 

4. Console App

 

흔히 Console App이라 함은 문자기반의 command prompt에서 동작하는 Application을 말합니다. 문자열로 명령을 입력하며 문자열기반의 응답을 받기 때문에 macOS나 Windows의 UI(User Interface)따위는 존재하지 않습니다. 따라서 비교적 단순하며 빠른 응답을 필요로 하는 경우에 종종 사용되는 Application유형이라 할 수 있습니다. 또한 대부분의 경우 이러한 Application의 동작을 제어하기 위해 인수를 전달하는 방법이 많이 사용됩니다.

 

Console App의 한 가지 예로 Command prompt에서 dotnet 명령을 사용하는 경우를 들 수 있는데 새로운 Console App project를 생성하는 경우 기본적으로 folder의 이름을 사용하게 되지만 특정 project의 이름을 지정하고자 한다면 아래와 같이 dotnet명령을 사용할 수 있습니다.

dotnet new console -n myapp

 

1) 화면 출력

 

Console App에서 가장 빈번하게 수행되는 2가지 작업을 꼽으라면 data를 읽고 쓰는 것을 말할 수 있습니다. 우리는 이미 여러 예제에서 화면 출력을 위해 WriteLine method가 사용되는 것을 봐왔습니다. WriteLine method는 인수로 전달된 내용을 화면에 표시한 뒤 자동으로 carriage return을 수행하는데 만약 이러한 동작이 이루어지는 걸 원하지 않는다면 Write method를 대신 사용할 수 있습니다.

Console.Write("A");
Console.Write("B");
Console.Write("C");

 

위의 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

Write method는 자동개행(carriage return)을 수행하지 않으므로 인수의 문자열이 나란히 표시됨을 알 수 있습니다. 반면 WriteLine method를 대신 사용한다면 carriage return동작으로 인해 위와 다른 결과를 표시할 것입니다.

Console.WriteLine("A");
Console.WriteLine("B");
Console.WriteLine("C");

 

2) Numbered positional arguments(번호가 매겨진 위치 인수)를 사용한 서식화

 

Console App에서 서식화된 문자열을 표시하기 위해 사용되는 가장 흔한 방식으로 numbered positional arguments가 있습니다.

 

Numbered positional arguments는 Write나 WriteLine와 같은 method에서 지원하는데 문자열에서는 format method를 대신 사용할 수 있습니다.

 

아래 예제는 상품의 개수별 단가를 표시하는 것으로 상품의 개수와 단가표시를 위해 WriteLine method와 무너져 열변수에서 numbered positional arguments를 사용하고 있습니다.

int goods = 10;
decimal price = 1200;

Console.WriteLine(
    format: "상품 {0}개 : {1}원",
    arg0: goods,
    arg1: goods * price);

string howmuch = string.Format(
    format: "상품 {0}개 : {1}원",
    arg0: goods,
    arg1: goods * price);

Console.WriteLine(howmuch);

 

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

예제에서는 인수를 지정하기 위해 arg0과 arg1이라는 숫자가 매겨진 이름의 인수를 사용하였습니다. 기본적으로 이러한 인수는 arg2까지 3개를 사용할 수 있으며 그 이상의 인수를 지정해야 한다면 아래와 같이 인수명을 생략하고 인수를 지정해야 합니다.

Console.WriteLine(
    "Colors : {0}, {1}, {2}, {3}, {4}",
    "Red", "Blue", "Black", "Yellow", "Green");

 

3) 보간문자열(Interpolated strings)

 

보간문자열은 C#6부터 도입된 기능으로 사용하고자 하는 문자열의 앞에 아래 예제와 같이 '$'문자를 접두사로 붙이면 문자열 중간에 중괄호를 사용하여 해당 위치에 변수나 표현식의 현재 결괏값을 표현할 수 있습니다.

int goods = 10;
decimal price = 1200;

Console.WriteLine($"상품 수량 : {goods}, 단가 {price}원");

 

일반적인 문자열에서 보간문자열을 사용하면 해당 문자열을 더 쉽게 읽을 수 있다는 장점이 있습니다.

 

또한 이러한 특징의 보간문자열을 사용하면 문자열을 서로 결합하는 경우에도 유용하게 사용할 수 있습니다. 예를 들어 C#10 이전에는 문자열 결합을 위해 + 연산자를 사용하여 결합해야 했지만

string s1 = "abcd";
string s2 = "efgh";

string result = s1 + s2;

Console.WriteLine(result);

 

보간문자열을 사용하면 위의 예제를 아래와 같이 동일하게 사용할 수 있습니다.

string s1 = "abcd";
string s2 = "efgh";

string result = $"{s1}{s2}";

 

4) 서식 문자열

 

변수나 표현식은 comma나 colon을 통해 서식 문자열을 사용함으로써 서식화활 수 있습니다.

 

서식문자열은, 예를 들어 N0은 천 단위 분리와 함께 소수점이 없는 서식을 의미하며 C는 통화를 의미합니다. 통화와 관련된 서식은 현재 PC의 문화권설정에 따라 달라질 수 있습니다.

 

서식문자열을 사용할 때의 형식은 아래와 같습니다.

{ index [, alignment ] [ : formatString ] }

 

위 형식에서 alignment는 특정 문자수만큼의 간격으로 왼쪽 혹은 오른쪽으로 값을 정렬하고자 할 때 사용되며 정수값이 사용됩니다. 이때 정수값이 양수라면 오른쪽 기준 정렬을, 음수라면 왼쪽 기준 정렬을 수행합니다.

Console.WriteLine("Student Table");
Console.WriteLine("{0, -10} {1, -5} {2, 9}", "Name", "Class", "Age");
Console.WriteLine("{0, -8} {1, -5} {2, 5:N0}", "홍길동", "족구", 12);
Console.WriteLine("{0, -8} {1, -5} {2, 5:N0}", "장아영", "야구", 15);
Console.WriteLine("{0, -8} {1, -5} {2, 5:N0}", "황민경", "야구", 9);
Console.WriteLine("{0, -8} {1, -5} {2, 5:N0}", "김필석", "탁구", 11);
Console.WriteLine("{0, -8} {1, -5} {2, 5:N0}", "김동원", "축구", 10);

 

위 예제에서 Name, Class, Age column을 표시할 때 Name은 10자 기준 왼쪽정렬을, Class는 5자 기준 왼쪽정렬을, Age는 9자 기준 오른쪽 정렬을 수행하며 목록에서는 이름에 8 자 기준 왼쪽, 수업에는 5자 기준 왼쪽, 나이 부분에는 오른쪽 5자 기준 정렬을 수행하여 표시하도록 합니다. 이때 나이는 N0을 적용해 소수점 없이 표시하도록 하였으며 그럴 일이 없겠지만 나잇값이 1000이 넘으면 ,를 사용해 천 단위 분리를 적용할 것입니다.

 

5) 숫자 밑 날짜, 시간 서식화

 

숫자의 서식과 관련해서는 사용자 정의 서식 code를 사용함으로써 원하는 대로 표현방식을 지정할 수 있습니다. 아래 표에서는 사용가능한 서식 code와 그 용도를 확인할 수 있습니다.

0 0자리 표시자로 0이 아닌 값이 있다면 해당 값을 표시하지만 그렇지 않다면 0을 대신 표현합니다. 예를 들어 '0000.00'으로 서식화한 경우 '123.4'라는 값이 있으면 '0123.40'으로 표시됩니다.
# 자리 표시자로 0이 아닌 값이 있다면 해당 값을 표시하지만 그렇지 않다면 해당 자리수를 표현하지 않습니다. 예를 들어 '####.##'으로 서식화한 경우 '123.4'라는 값이 있으면 '123.4'로 표시됩니다.
. 소수점 표시자로 숫자에서 소수점을 나타내기 위해 사용됩니다. 문화권의 영향을 받으므로 소수점의 표현형태가 달라질 수 있습니다. 예를 들어 French 문화권이라면 소수점을 ,(comma)로 표시합니다.
, 그룹단위 분리자입니다. 보통 숫자에서 천단위 분리를 위해 많이 사용됩니다. 예를 들어 '0,000'서식일때 '123456789'라는 숫자가 있으면 '123,456,789'로 표시합니다. 또한 1000단위로 수를 나누어 숫자의 규모를 조정하는데에도 사용됩니다. 예를 들어 '0.00,,'으로 서식화한 경우 ,하나당 1000을 나눈다는 의미가 되므로 '1234567'값은 '1.23'으로 표시됩니다.
% Percentage 자리표시자로 값을 100으로 곱하여 뒤에 percentage문자를 붙여줍니다.
\ 확장문자로서 서식code뒤의 문자를 그대로 표시합니다. 예를 들어 '\##,###'으로 서식화한 경우 '1234'값이 있다면 '#1,234'로 표시합니다.
; Selction분리자로서 양수, 음수, zero에 대한 서식문자열의 차이를 정의합니다. 예를 들어 '[0];(0);Zero'로 서식화한 경우 12값은 '[12]'로, -12값은 '(12)'로, 0은 'Zero'로 표시합니다.

 

상기 외에 사용가능한 다른 서식 code도 존재합니다. 모든 서식 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

 

숫자에 대한 서식은 'C'나 'N'과 같은 표준 서식을 사용하여 좀 더 간단하게 적용할 수도 있습니다. 이때도 'C0, N4'와 같이 정밀도를 조정하여 원하는 만큼의 정밀도를 표현할 수 있는데, C와 N에서는 아무런 숫자도 지정하지 않으면 기본값 2가 적용되지만 다른 서식 code의 영향으로 약간씩 달라질 수 있습니다. 특히 D 같은 경우, 기본정밀도가 필요로 하는 최소수이며 E에서는 6이 됩니다.

C, c 통화를 의미하며 문화권의 영향에 따라 다르게 표시될 수 있습니다. 예를 들어 대한민국문화권이라면 123.4값을 '₩123.4'로 표현합니다. 소수점을 무시하고자 하는 경우 'C0'으로 사용할 수 있으며 그 결과 '₩123'로 표시될 것입니다.
N, n 선택적 음수기호와 group화문자를 가진 정수자리수입니다.
D, d 10진수로 선택적 음수기호가 있지만 group화문자는 없는 정수자리수입니다.
B, b 2진수로 13과 같은 정수를 1101로 표현합니다.
X, x 16진수로 255와 같은 정수를 FF로 표현합니다.
E, e 지수표기법으로 1234.567과 같은 수를 1.234567000E+003으로 표현합니다.

 

상기 이외에 전체 표준서식은 아래 link에서 확인할 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings

 

Standard numeric format strings - .NET

In this article, learn to use standard numeric format strings to format common numeric types into text representations in .NET.

learn.microsoft.com

 

단순 숫자뿐 아니라 아래와 같이 날짜와 시간에 대한 사용자 정의 서식 code도 사용할 수 있습니다.

/ 날짜 분리자입니다. 문화권의 영향을 받아 다르게 표시할 수 있는데 예를 들어 미국 문화권이라면 '/'로 대한민국 문화권이라면 '-'문자로 표현됩니다.
\ 확장문자로서 서식code뒤의 문자를 그대로 표시합니다. 예를 들어 'h \h m \m'이라면 오전 10:30분을 '10 h 30 m'으로 표시할 것입니다.
: 시간분리자로서 문화권의 영량을 받아 다르게 표시할 수 있습니다. 대한민국 문화권이라면 ':'이 사용되지만 French라면 '.'이 사용됩니다.
d, dd 날짜 표시자입니다. 기본적으로 1에서 31까지 날짜를 표시하는데 'dd'를 사용한다면 숫자가 하나일때 앞에 0을 부텅 '01'처럼 표현합니다.
ddd, dddd 요일을 표시합니다. 예를 들어 월요일인 경우 'ddd'라면 '월'로 표시하지만 'dddd'라면 '월요일'로 표시합니다.
f, ff, fff 10분의 1초, 백분의 1초(밀리초)를 나타냅니다.
g 'A.D'같은 시대를 표현합니다.
h, hh 시간을 나타냅니다. 1부터 12시를 표현하며 'hh'인 경우 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
H, HH 시간을 나타냅니다. 1부터 24시를 표현하며 'HH'인 경우 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
K 표준 시간대를 나타냅니다. 표준 시간대가 특정되지 않으면 null이 되며 UTC는 Z로 나타냅니다. UTC로 부터 조정된 지역 시간대는 -8:00과 같은 값으로 표현됩니다.
m, mm 0부터 59분까지 붙을 나타내며 'mm'이라면 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
M, MM 1부터 12까지 월을 나타내며 'MM'이라면 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
MMM, MMMM 월을 문자열로 표현합니다. 'MMM'이라면 'Jan'과 같이 표현하지만 'MMMM'이라면 'January'로 나타냅니다. 문화권의 영향을 받으므로 다르게 표현될 수 있습니다.
s, ss 0부터 59까지 초를 나타내며 'ss'라면 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
t, tt 오전, 오후를 나타내며 't'라면 'A'로 표현하지만 'tt'라면 'AM'으로 표현됩니다.
y, yy 0부터 99까지 세기(century)를 표현하며 'yy'라면 숫자가 하나일때 앞에 0을 붙어 '01'과 같이 표현합니다.
yyy 최소 3자리 숫자만큼의 년도를 표시하며 필요로 하는 만큼의 자리수를 가질 수 있습니다. 예를 들어 '1 A.D'는 '100'으로 표현되며 '2026'과 같은 년도표현도 가능합니다.
yyyy, yyyyy 4자리 혹은 5자리 수의 년도를 나타냅니다.
z, zz UTC로 부터의 offset시간을 나타냅니다.
zzz UTC로 부터의 offset 시간과 붙을 나타냅니다. 0으로 자리수를 채우므로 '+05:30'과 같이 나타냅니다.

 

날짜와 시간에 관한 더 자세한 목록은 아래 link에서 확인할 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings

 

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

 

또한 날짜와 시간에도 아래와 같이 표준서식을 적용할 수 있습니다.

d '2026-01-07'와 같이 날짜를 표현합니다. 문화권의 영향을 받으므로 다르게 표현될 수 있습니다.
D '2026년 1월 7일 수요일'와 같이 날짜와 요일을 표현합니다. 문화권의 영향을 받으므로 다르게 표현될 수 있습니다.
f '2026년 1월 7일 수요일 오후 4:28'와 같이 날짜와 요일, 시간을 표현합니다. 문화권에 따라 다르게 표현될 수 있습니다.
F '2026년 1월 7일 수요일 오후 4:31:16'와 같이 날짜와 요일, 시간, 초를 표현합니다. 문화권에 따라 다르게 표현될 수 있습니다.
m, M '1월 7일'과 같이 월과 날짜를 표현합니다. 문화권에 따라 다르게 표현될 수 있습니다.
o, O 날짜와 시간을 '026-01-07T16:32:42.9964891+09:00'과 같이 표준 pattern으로 표현합니다. 다른 쪽으로의 data전달을 위해 날짜와 시간값을 직렬화하는데 유용합니다.
r, R RFC1123 pattern으로 표현합니다.
t 'HH:mm' 형식으로 짧은 시간을 표현합니다. 문화권에 따라 다르게 표현될 수 있습니다.
T 'HH:mm:ss' 형식으로 긴 시간을 표현합니다. 문화권에 따라 다르게 표현될 수 있습니다.
u '2026-01-07 16:38:32Z'와 같이 범용 짧은 날짜/시간 pattern으로 표현합니다.
U '2026년 1월 7일 수요일 오전 7:39:19'와 같이 범용 긴 날짜/시간 pattern으로 표현합니다.

 

날짜와 시간에 관한 더 자세한 표준서식 목록은 아래 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

 

6) Console 사용 간소화하기

 

C#6부터 using문은 namespace를 import 할 뿐만 아니라 정적 class를 import 함으로써 code작성 자체를 간소화하는데도 사용할 수 있습니다. Console class도 마찬가지인데 using문을 사용하면 Console의 입력을 반복할 필요가 없습니다.

 

(1) 단일 file에서 정적 type import 하기

 

예를 들어 Program.cs라면 해당 file의 상단에 아래와 같이 using문을 추가하여 System.Console class에 대한 정적 import를 수행할 수 있습니다.

using static System.Console;

 

그러면 이후부터는 해당 file에서 아래와 같이 WriteLine와 같은 Console method를 Console 없이 호출할 수 있습니다.

WriteLine("...");

 

(2) Project단위 정적 type import 하기

 

정적 import는 단일 file이 아닌 Project단위 모든 file을 위해서도 적용할 수 있습니다.

 

Program.cs에서 이전에 작성한 using문 전체를 삭제하고 Project의 설정 file(csproj)을 열어 <PropertyGroup> 다음에 <ItemGroup>을 추가하고 그 안에 아래와 같이 Using 요소를 추가함으로써 System.Console을 전역적으로 import 할 수 있습니다.

<ItemGroup Label="Console static import">
    <Using Include="System.Console" Static="true" />
</ItemGroup>

 

위 예제에서 ItemGroup안에 있는 label속성은 일부 요소에 대한 주석화가 필요할 때 또는 다수의 이 사용되는 경우 각 의 역할을 명확하게 구분하는데 유용하게 사용할 수 있지만 필수속성은 아닙니다.

 

7) 입력받기

 

화면을 표시하기 위해서는 WriteLine method를 사용하지만 반대로 사용자로부터의 입력은 ReadLine method를 사용하며 이를 통해 입력된 문자열값을 받을 수 있습니다. ReadLine method가 호출되면 사용자가 값을 입력할 때까지 대기상태에 있다가 사용자가 입력을 마치면(원하는 입력 후 Enter key를 누르는 순간) 그동안의 입력한 값을 문자열로 반환합니다.

 

아래 예제는 사용자에게 이름과 나이의 입력을 요청한 뒤 사용자가 값을 입력하고 나면 해당 값을 그대로 화면에 표시하도록 합니다.

Console.WriteLine("이름을 입력하세요.");
string name = Console.ReadLine();
Console.WriteLine("나이를 입력하세요.");
string age = Console.ReadLine();

Console.WriteLine($"귀하의 이름은 {name}이고 나이는 {age}입니다.");
.NET 6부터는 null가능성 확인기능이 기본으로 동작하므로 위 예제라면 ReadLine method가 null을 반환할 수 있다는 compiler의 경고를 표시할 수 있습니다. 그러나 실질적으로 ReadLine method가 null을 반환할 수 있는 상황은 발생하지 않습니다. 사용자가 아무런 값을 입력하지 않는다 하더라도 빈문자열 값을 반환할 뿐 null을 반환하는 경우는 없습니다. 필요하다면 특정한 경우에 이러한 null확인기능이 동작하지 않도록 할 수 있는데 이에 대한 구체적인 방법은 곧 알아볼 것입니다.

 

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다. 결괏값은 입력한 값에 따라 달라집니다.

 

null가능성 경고와 관련하여 예제의 'string name = Console.ReadLine();'부분에서는 아래와 같이 string뒤에 '?'문자를 접미사로 붙여줄 수 있습니다.

string? name = Console.ReadLine();

 

위의 경우는 compiler에게 name변수가 null이 될 수 있음을 알고 있다고 명시함으로써 compiler가 null가능성 경고를 표시하지 않도록 합니다. 하지만 이미 상술했듯이 ReadLine이 null을 반환하는 경우는 없으므로 예제는 아무런 문제 없이 작동할 수 있습니다. 그러나 만약 위 상태에서 name변수의 특정 member에 접근을 시도하게 되면 compiler는 다시 null가능성 경고를 표시할 수 있습니다. 상기 처리는 compiler에게 'name변수가 null이 될 수 있음을 알고 있어. 그러니 경고표시는 하지 말아 줘.'라고 compiler에게 요청하는 것일 뿐 실제 name이 null이 되지 못하도록 하는 조치는 아닙니다. 때문에 여전히 name변수가 null이 될 수 있는 상황에서 member의 접근은 적절한 처리가 아니므로 compiler의 경고를 보게 되는 것입니다. 만약 이와 같은 상황에서 'name변수가 어떠한 경우에도 null이 되지 않는다.'라는 걸 명시하고자 한다면 아래와 같이 '!'문자를 ReadLine method뒤에 붙여줄 수 있습니다.

string name = Console.ReadLine()!;

 

위 예제에서 '!'표시는 compiler에게 'ReadLine method는 절대 null을 반환하지 않을 거야.'라고 알려주는 것입니다. 따라서 '그 결과를 받는 name변수도 null 되는 경우는 없다.'라는 의미가 되고 따라서 compiler는 상기 처리에 대해서 null가능성 경고를 표시하지 않게 됩니다. 이를 'null 허용 연산자'라고 하는데 다만, 정말 ReadLine method가 null을 반환하게 되는 상황 즉, name변수가 null이 되는 상황이 발생하는 경우에 대한 모든 책임은 직접 개발자에게 있음을 알고 있어야 합니다.

 

위 2가지 방법을 통해 compiler가 발생시키는 null가능성 경고를 어떻게 처리할 수 있는지를 간단히 알아봤는데 추후에 null과 관련된 더 자세한 처리방법을 알아볼 것입니다.

ReadLine method 자체가 null을 반환하는 경우는 있을 수 있습니다.

ReadLine method는 표준입력 stream으로부터 입력값을 받게 되는데 상술했듯 사용자가 아무런 값도 입력하지 않고 Enter를 누르게 되면 null대신 빈 문자열값을 반환합니다.

Console App에서 일반적인 경우 ReadLine이 null을 반환하는 건 EOF(end of stream)에 도달한 경우뿐입니다. 그러나 이 경우는 표준입력 stream이 redirect 되거나 EOF에 도달한 경우 또는 EOF를 임의적으로 만들어낼 수 있는 환경에서만 발생가능한 것일 뿐 단순 사용자 입력으로는 '절대!'발생하지 않습니다.

 

(1) key 입력 여부 판별하기

 

ReadKey method를 사용하면 사용자로부터 특정 key의 입력을 판별할 수 있습니다. 해당 method는 사용자의 key입력을 대기하다가 특정 key나 혹은 여러 key를 누르게 되면 그에 해당하는 ConsoleKeyInfo값을 반환합니다.

 

아래 예제는 사용자에게 key의 입력을 요청하고 사용자가 원하는 key입력을 수행하면 그에 해당하는 key의 정보를 표시하도록 합니다.

Console.WriteLine("원하는 key를 눌러주세요.");
ConsoleKeyInfo key = Console.ReadKey();
Console.WriteLine();
Console.WriteLine($"{key.Key} : {key.KeyChar} : {key.Modifiers}");

 

예제를 실행하면 다음과 같은 결과를 표시할 것입니다. 예제에서는 key입력에 Alt + h key를 누른 것입니다.

 

ConsoleKeyInfo에서 key속성은 입력한 key의 값을, KeyChar는 입력한 key의 문자를 의미합니다. 따라서 만약 F3이나 F12와 같은 key를 누른 경우라면 이 값은 나오지 않을 수 있습니다. 마지막으로 Modifiers는 Alt나 Shift key와 같이 다른 key와의 조합을 의미합니다.

VSCode안에서 Console App을 실행하는 경우 일부 key의 입력은 실행 중인 Console App에 도달하기도 전에 VSCode안에서 가로채 VSCode의 특정기능으로 실행될 수 있습니다. 예를 들어 Ctrl + Shift + X key는 VSCode에서 Extensions보기기능에 해당합니다. 따라서 Console App에서 제대로 key의 입력을 test 해 보려면 Console App을 Build 하고 Command Prompt 또는 Terminal에서 Compile 된 exe를 실행해야 합니다.

 

8) 인수 전달하기

 

Console App의 동작은 대게 필요한 인수를 전달함으로써 제어합니다. 예를 들어 dotnet의 명령줄도구에서 특정 template의 project를 생성하려면 아래와 같이 해당 project template의 이름을 전달합니다.

dotnet new Console

 

위와 같은 방식으로 console ppp이 실행되면 사용자가 전달한 인수값을 application안에서 추출해야 할 필요가 있습니다. .NET6 이전에 console app project template은 기본적으로 아래와 같은 code를 생성했는데

using System;

namespace Arguments
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

 

위 code에서 Main method에 있는 'string[] args'가 바로 인수를 저장하고 있는 매개변수에 해당하며 array type이므로 해당 매개변수를 통해 인수값을 순서대로 확인할 수 있습니다.

 

.NET6 이후부터는 top-level programs가 사용되므로 상기 예제와 같이 Program class와 Main method가 노출되지 않습니다. 따라서 얼핏 보면 args인수 역시 존재하지 않는 것처럼 보일 수 있으나 이러한 요소들은 눈에만 보이지 않고 감춰져 있을 뿐 여전히 존재하므로 사용되지 않는다고 오해해서는 안됩니다.

 

명령줄에서 인수는 오로지 공백으로만 분리되며 이렇게 분리된 인수값이 args배열에 순서대로 들어가게 됩니다. 만약 인수값 자체에 공백이 포함되어야 한다면 이때는 홑따옴표나 큰따옴표로 인수값 자체를 감싸서 표현해야 합니다.

 

아래 예제는 top-level programs환경에서 전달된 인수의 개수를 확인하고 있습니다.

Console.WriteLine($"전달된 인수의 수 : {args.Length}");

 

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

 

만약 위 예제를 Visual Studio에서 실행하고자 한다면 인수를 조금 다른 방식으로 지정해야 합니다.

 

Visual Studio의 Solution Explorer에서 project를 mouse오른쪽 click 하고 Properties를 선택합니다. 그런 후 설정화면의 왼쪽에 Debug 항목을 선택하고 'Open debug launch profiles UI'를 선택합니다.

 

'Launch Profiles'화면의 'Command line arguments'부분에 원하는 매개변수를 공백으로 분리하여 순서대로 입력합니다.

 

위와 같이 설정하고 'Launch Profiles'화면을 닫으면 Solution Explorer에는 'Properties'folder와 함께 하위에 launchSettings.json file이 생성되는데, 해당 file을 선택해 열어보면 위에서 설정한 인수값이 그대로 commandLineArgs로 등록되어 있음을 알 수 있습니다.

{
  "profiles": {
    "ConsoleApp1": {
      "commandName": "Project",
      "commandLineArgs": "aaa bbb ccc"
    }
  }
}

 

전달된 인수의 실제 값을 확인하기 위해 Program.cs에서 Console.WriteLine... 다음에 아래와 같은 구문을 추가하고

foreach (string arg in args)
{
    Console.WriteLine(arg);
}

 

다시 예제를 실행시키면 아래와 같은 결과를 표시할 것입니다.

 

(1) 인수 활용하기

 

아래 예제는 Console App을 통해 현재 실행중인 terminal의 전경색과 배경색을 변경하도록 합니다.

 

우선 현재 필요한 인수는 총 2개이므로 만약 사용자가 23개의 인수를 제대로 입력하지 않은 경우 사용자에게 관련 오류를 표시하고 application의 실행을 중지하도록 합니다.

if (args.Length < 2)
{
    Console.WriteLine("프로그램 실행에 필요한 충분한 인수가 지정되지 않았습니다.");
    return;
}

 

만약 인수가 2개나 그 이상입력된 경우라면 첫 번째 인수값부터 읽어 terminal의 전경색과 배경색, cursor의 크기를 설정하도록 합니다. 인수가 2개 이상 지정된 경우라면 그 이상은 어차피 무시할 것이므로 문제 되지 않습니다.

Console.ForegroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), args[0], true);
Console.BackgroundColor = (ConsoleColor)Enum.Parse(typeof(ConsoleColor), args[1], true);
예제에서 사용된 ForegroundColor나 BackgroundColor 외에도 CursorSize와 같이 terminal에 관련된 여러 속성이 제공됩니다. 다만 일부 속성의 경우는 Windows전용이므로 다른 OS에서 다른 속성을 사용하고자 하는 경우 compiler경고가 발생할 수 있고 실제 Application이 실행되는 경우 예외가 발생할 것입니다.
Enum.Parse, typeof와 같은 생소한 구문은 추후에 자세히 알아볼 것입니다.

 

예제를 실행하기 위해 아래와 같이 2개의 인수를 지정하여 실행하면 다음과 같은 결과를 표시할 것입니다.

728x90