프로젝트는 비주얼 스튜디오의 소스코드 관리를 위해 도입된 개념이다.
한 프로젝트는 여러 개의 소스코드를 담을 수 있고, 해당 프로젝트를 빌드하면 하나의 exe 또는 DLL 파일이 만들어 진다.
프로젝트를 생성하면 그 프로젝트에서 관리하는 모든 정보를 담는 '프로젝트 파일'이 만들어진다. 프로젝트 파일은 언어마다 확장자가 다르다. C# 언어의 경우 비주얼 스튜디오가 생성하는 프로젝트 파일의 확장자는 'csproj'이다.
프로젝트 이름이 'ConsoleApp1'이라면 프로젝트 파일은 ConsoleApp.csproj가 되고 파일 탐색기를 통해 프로젝트가 있는 디렉토리에서 이 파일을 찾을 수 있다.
프로젝트 파일은 텍스트를 담고 있기 때문에 윈도우의 메모장 등으로 내용을 볼 수 있지만 보통은 비주얼 스튜디오에서 프로젝트 노드를 선택해 편집 창으로 열어 볼 수 있다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
프로젝트 파일의 내용은 XML 형식을 따른다.
비주얼 스튜디오에서 프로젝트의 속성 창을 통해 설정을 바꾸는 경우 변경 사항은 모두 프로젝트 파일에 보관된다.
프로젝트보다 큰 단위가 솔루션이다. 일반적으로 프로그램을 한 개의 프로젝트로 만드는 경우는 거의 없다. 여러 개의 프로젝트가 모여 하나의 솔루션을 구성한다. 비주얼 스튜디오에서 솔루션 탐색기를 통해 보여주는 내용이 바로 솔루션으로 이 역시 파일로 저장된다.
솔루션 탐색기에서 최상단의 솔루션 항목을 대상으로 마우스 오른쪽 버튼을 누르면 [파일 탐색기에서 폴더 열기] 메뉴가 나오고 이를 선택하면 확장자가 sln인 파일이 들어 있는 폴더를 보여준다.
메모장으로 솔루션 파일을 열면 솔루션에 등록된 프로젝트의 경로가 포함된 것을 확인할 수 있다.
솔루션 | 프로젝트 |
오피스(Office) | 워드(word) |
엑셀(excel) | |
파워포인트(powerpoint) | |
아웃룩(outlook) |
솔루션과 프로젝트의 관계
다중 소스코드 파일
여러 개의 소스코드 파일로 하나의 실행 파일을 만드는 것이 가능하다.
콘솔 유형의 프로젝트를 생성하면 기본적으로 Program.cs 파일을 포함하므로 위 코드는 별도로 LogWriter.cs 파일을 프로젝트 디렉토리에 추가한 경우다.
다중 소스코드 파일을 구성하는 데 있어 강제성은 없지만 관례는 있다. 보통 클래스 하나당 파일 하나를 만드는 것을 권장한다. 또한 유사한 기능으로 묶을 수 있는 파일은 폴더를 이용해 정리한다.
라이브러리
프로그래밍 언어에서 라이브러리는 일반적으로 재사용 가능한 단위를 의미한다. 그리고 그것이 파일로 저장될 때는 확장자로 DLL이 붙는다.
.NET Runtime이 설치되면 일부 라이브러리가 함께 컴퓨터에 설치되는데, 바로 이것들을 가리켜 BCL(Base Class Library) 또는 FCL(Framework Class Library)이라고 한다. 이것들은 모두 마이크로소프트에서 미리 만들어둔 라이브러리이다.
이렇게 한번 만들어 둔 라이브러리는 다른 프로그램을 만들 때 쉽게 가져다 쓸 수 있다.
이런 라이브러리를 사용자가 직접 만들 수 있다.
dotnet으로 라이브러리 생성 및 사용
c:\temp> md LogWriter
c:\temp> cd LogWriter
c:\temp\LogWriter> dotnet new classlib
[ 생략: 라이브러리 프로젝트 생성]
// 생성된 파일: Class1. cs, LogWriter.csproj
// 생성된 디렉터리: obj
생성된 파일 및 디렉터리도 콘솔 프로젝트와 유사하지만 Program.cs 파일명 대신 Calss1.cs 파일이 생성된 것과 LogWriter.csproj 파일의 내용이 약간 다르다는 차이가 있다.
이 상태에서 Class1.cs 파일의 이름을 LogWriter.cs로 변경하고 LogWriter클래스의 접근 제한자만 public으로 변경한다.
public class LogWriter
{
public void Write(string txt)
{
Console.WriteLine(txt);
}
}
class의 경우 접근 제한자를 생략하면 기본값이 internal이다.
internal은 같은 어셈블리(exe 또는 DLL) 내에서만 그 기능을 사용할 수 있게 제한하는 역할을 한다. 따라서 LogWriter.cs와 그 타입을 사용하는 Program.cs 파일이 같은 EXE/DLL로 묶일 때는 문제가 안되지만, 별도의 DLL에 담길 때는 다른 EXE/DLL에서 그 기능을 가져다 쓸 수 없다.
따라서 라이브러리에서 특정 기능을 노출하고 싶다면 그것의 접근 제한자를 public으로 바꿔야 한다.
DLL 파일 생성은 콘솔 응용 프로그램을 빌드할 때와 마찬가지로 dotnet build 명령어를 수행하면 된다.
c:\temp\LogWriter> dotnet build
명령어를 실행하면 .\bin\Debug\net8.0\LogWriter.dll 파일이 만들어지고 그 단위로 재 사용이 가능하다.
이렇게 라이브러리를 만들면 이것을 사용할 수 있는데 LogWriter디렉토리가 있는 c:\temp 기준으로 콘솔프로젝트를 생선한다.
c:\temp> md ConsoleApp1
c:\temp> cd ConsoleApp1
c:\temp\ConsoleApp1> dotnet new console
[생략 : 콘솔 프로젝트 생성]
// 생성된 파일 : program.cs, ConsoleApp1.csproj
// 생성된 디렉토리 : obj
새로 생성한 program.cs 파일의 내용을 채워준다. LogWriter.dll 기능을 사용할 때 소스코드 상으로 바뀌는 것은 없다.
이전에 LogWriter.cs 파일을 같은 프로젝트에서 포함하고 있었지만, LogWriter.dll 파일로 분리되어 있으므로 Program.cs가 포함된 프로젝트를 빌드할 때 LogWriter.dll의 위치를 알려야 한다.
program.cs 파일이 속한 Console.App1.csproj 프로젝트 파일에 Reference 항목과 함께 HintPath를 이용해 DLL의 위치를 명시해야한다.
<Project Sdk="Microsoft.Net.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="LogWriter">
<HintPath>..\LogWriter\bin\Debug\net8.0\LogWriter.dll<\HintPath>
<\Reference>
</ItemGroup>
</Project>
dotnet run으로 확인하면 프로그램이 정상적으로 실행된다.
// dotnet run 명령어는 dotnet build 과정을 포함하므로 빌드 후 곧바로 프로그램을 실행
c:\temp\ConsoleApp1> dotnet run
DLL을 다른 프로그램에서 사용하는 것을 일반적으로 '참조(reference)한다' 라고 표현한다.
따라서 위의 경우 ConsoleApp1 프로젝트가 LogWriter.dll을 참조하는 셈이다.
Console 타입이 정의된 System.Console.dll에 대해서는 참조하지 않았는데 어떻게 컴파일 된 것인가?
→ dotnet build 명령어는 csproj 파일의 내용에 따라 미리 정해진 닷넷 어셈블리를 자동으로 참조해주기 때문이다.
히지만 직접 만든 LogWriter.dll 파일은 C# 컴파일러가 알 수 없으니 직접 명시해야 한다는 차이점이 있다.
소스코드 파일을 직접 포함하지 않고 DLL을 만들어서 참조하는 이유?
프로그램의 규모가 커져야만 그 필요성을 체감할 수 있다.
프로그램 하나를 만들기 위해 수백 개의 소스코드 파일이 생성되고 그중에서 수십 개의 파일이 다른 프로그램에서도 재사용할 수 있다는 상황을 가정한다.
다른 프로그램을 만들 때 기존의 프로젝트로부터 수십 개의 파일을 골라서 재사용 하는 것은 불편한 일이다. 그 수십개의 파일을 담은 1개의 DLL 파일을 재사용 하는 것이 더 편리하다. 그 밖에도 컴파일 시간도 문제가 된다.
매번 수십 개의 파일을 함께 컴파일하기보다 이미 컴파일된 DLL파일을 참조하는 것이 개발 생산성 측면에서도 좋다.
비주얼스튜디오에서 라이브러리 생성 및 사용
C# 프로젝트 템플릿
프로젝트 템플릿 이름 | 유형 | 의미 |
Windows Forms 앱 (Windows Forms App) |
EXE | 윈도우 폼 응용 프로그램을 만들기 위한 기본 설정이 포함된 프로젝트를 생성한다. |
WPF 애플리케이션 (WPF Application) |
EXE | Windows Presentation Foundation 응용 프로그램을 위한 템플릿이다. |
콘솔 앱 (Console App) |
EXE | 실행 시 '명령 프롬포트'에서 실행되는 응용 프로그램을 만들도록 옵션이 설정되어 있다. |
클래스 라이브러리 (Class Library) |
DLL | 라이브러리(DLL) 유형의 프로젝트 템플릿 |
각 템플릿은 해당 유형의 응용 프로그램을 만드는 데 필요한 기본 설정을 담고 있다.
결국 프로젝트를 생성하고 나면 동일한 포맷의 csproj 파일로 생성되고 그 안의 옵션 값이 일부 다른 것에 불과하다.
Console App으로 만들어진 프로젝트라도 일부 옵션을 변경해 'Class Library'로 바꿀 수 있다.
비주얼스튜디오에서 DLL을 참조하려면 LogWriter라이브러리를 사용하는 콘솔 앱을 솔루션에 추가한다.
참조방법은 파일참조와 프로젝트 참조로 나뉜다. dotnet으로 라이브러리 생성 및 사용 때 사용한 방법이 파일 참조인데, DLL의 위치를 직접 지정해 빌드에 포함시키는 방식이다.
참조하려는 프로젝트와 참조되는 프로젝트가 같은 솔루션 내에 함께 있다면 프로젝트 참조를 사용할 수 있다.
프로젝트의 하위에 있는 종속성 항목에서 프로젝트 참조 추가를 선택하면 참조 관리자 대화상자에서 솔루션 범주를 선택하면 현재 솔루션에 포함된 프로젝트 목록이 나오고 원하는 프로젝트의 체크박스를 설정하고 참조할 수 있다.
프로젝트 참조를 하면 csproj에 ProjectReference 노드로 기록된다.
프로젝트 참조와 파일 참조는 csproj 파일에 Reference로 기록되느냐 ProjectReference로 기록되느냐의 차이이다.
가능하다면 파일 참조보다 프로젝트 참조를 하는 것이 더 좋다.
프로젝트 참조의 경우 대상 프로젝트의 소스코드 파일이 변경되면 자동으로 프로젝트를 다시 빌드하고 그 결과 파일을 반영해 주기 때문이다.
현재 어떤 프로젝트 또는 DLL을 참조하고 있는지 확인하고 싶다면 솔루션 탐색기의 프로젝트 항목에서 '종속성'을 펼치면 된다.
NuGet 패키지 참조
닷넷은 라이브러리 재사용을 높이기 위해 DLL 파일 및 기타 설명 파일을 추가한 패키지 규약을 만들었다.
이 패키지는 nuget 확장자를 가지며 좀 더 편리하게 재사용할 수 있도록 www.nuget.org 사이트가 운영되고 있다.
특정 기능의 라이브러리가 필요하다면 nuget 사이트에 방문해 검색한 다음 라이선스만 맞다면 누구나 그 패키지를 다운로드해 사용할 수 있다.
nuget패키지를 활용한 간단한 MP3 음악파일 재생 프로그램을 구현
콘솔 프로젝트 생성 후 솔루션 탐색기의 프로젝트 항목 하위에 있는 종속성 노드에서 Nuget 패키지 관리 매뉴를 선택한다.
Nuget 관리자 창에서 찾아보기를 누르고 NAudio를 입력해 설치를 눌러 프로젝트에 패키지 참조를 추가한다.
Nuget의 많은 프로젝트가 오픈 소스이며 그 위치를 프로젝트 URL로 공개하고 있다. NAudio도 경로를 밝혔고 웹 브라우저에서 링크를 방문하면 해당 패키지를 만든 전체 소스코드와 함께 패키지의 사용법까지 찾아볼 수 있다.
NAudio 패키지를 이용한 MP3 재생
using NAudio.Wave;
namespace ConsoleApp1;
internal class Program
{
static void Main(string[] args)
{
string mp3Path = @"C:\temp\suddenly.mp3";
AudioFileReader audioFile = new AudioFileReader(mp3Path);
WaveOutEvent outputDevice = new WaveOutEvent();
outputDevice.Init(audioFile);
outputDevice.Play();
while(outputDevice.PlaybackState == PlaybackState.Playing)
{
Thread.Sleep(1000);
}
}
}
정리
DLL을 직접 참조하는 경우는 점점 줄어들고 있는 추세이다. 팀 내에서 만든 프로젝트라면 당연히 '프로젝트 참조'를 할 것이고, 외부 개발자가 만들었다면 대개의 경우 NuGet에 올라와 있으므로 패키지를 참조하게 된다.
사용자가 만든 라이브러리도 NuGet 사이트에 배포할 수 있다. 또한 팀 내에서만 공유하고 싶은 라이브러리가 있다면 사내에서 직접 전용 NuGet 사이트를 호스팅할 수 있다.
디버그 빌드와 릴리스 빌드
프로그램을 만들다 보면 크게 두 가지 오류를 접할 수 있다.
- 컴파일 시 오류(Compile-time error): 문법 오류(syntax error)이며, 컴파일러의 오류 메시지 내용에 따라 올바른 문법으로 변경하면 해결된다.
- 실행 시 오류(Run-time error): 정상적으로 컴파일된 프로그램이지만 실행되는 시점에 오류가 발생하는 것으로 논리 오류(logical error) 등의 원인으로 발생한다.
프로그램을 컴파일 하는 시점에 발생하는 오류의 경우 근래에는 통합 개발 환경의 도움으로 컴파일하기 전에 이미 편집 창에서 오류를 파악할 수 있다. 그 뿐만 아니라 컴파일 오류가 발생하더라도 문법상으로 오류가 발생한 위치를 쉽게 알 수 있어 원인을 수정하기 쉽다.
문제는 실행 시 오류이다. 실행 시에 발생하는 오류는 대개 예상치 못한 상황이며, 프로그램의 실행에 심각한 영향을 끼칠 수 있다.
실행시 오류가 발생하는 유형
class Program
{
static void Main(string[] args)
{
int [] nArray = new int [] {0, 1, 2, 3, 4};
nArray[5] = 0; //예외 발생
}
}
위의 코드는 5개의 요소가 담긴 배열의 6번째 요소에 접근하려고 해서 System.IndexOutOfRangeException 오류가 발생한다.
이런 오류를 버그라고 하며 해당 버그를 수정하는 작업을 디버깅이라고 한다.
디버깅의 핵심은 버그의 원인을 파악하는 것인데, 이때 원인 파악을 도와주는 용도의 전문적인 프로그램을 디버거라고 한다.
한가지 알아둬야 할 내용이 있는데 작성한 코드가 그대로 기계어로 생성되지 않는다는 점이다.
컴파일러 제작자들은 개발자가 작성한 코드를 가능한 가장 빠른 속도 또는 가장 작은 용량의 프로그램으로 번역하는 최적화 처리를 하기 때문이다.
이런 식으로 최적화를 허용하는 빌드를 릴리스 빌드라고 하고 그렇지 않은 경우를 디버그 빌드라고 한다.
보통 디버그 빌드로 출력한 EXE/DLL은 디버깅을 위한 정보를 함께 포함하고 있기 때문에 프로그램을 개발하는 단계에서 주로 사용한다.
이후 프로그램이 완성되 배포할 때가 되면 성능을 위해 릴리스 빌드를 사용한다.
디버그와 릴리스 빌드는 명령행에서 각각 다음과 같이 수행할 수 있다.
[디버그 빌드]
D:\temp\ConsoleApp1> dotnet build
[릴리스 빌드]
D:\temp\ConsoleApp1> dotnet build -c Release
비주얼 스튜디오에서는 도구 바를 통해 Debug와 Release를 선택한다.
디버그와 릴리스 빌드에 따라 생성되는 파일의 경로가 다르다.
[디버그 빌드]
[현재 프로젝트 디렉토리]...\bin\Debug\net8.0
[릴리스 빌드]
[현재 프로젝트 디렉토리]...\bin\Release\net8.0
DEBUG, TRACE 전처리 상수
디버그 빌드와 릴리스 빌드를 할 때 비주얼 스튜디오에서는 자동으로 관리되는 전처리 상수가 있다.
빌드 옵션 | 전처리 상수 | |
DEBUG | TRACE | |
디버그 | O | O |
릴리스 | X | O |
빌드에 따라 정의되는 기본 전처리 상수 값
차이점은 TRACE 상수는 항상 정의되지만 DEBUG 상수는 오직 디버그 빌드에서만 정의된다.
이런 차이를 이용하면 프로그램을 디버그 빌드로 생성했을 때만 동작하는 코드를 만들 수 있다.
디버그 구성으로 빌드하면 문자열이 출력되지만, 릴리스로 빌드하면 전처리기에 의해 코드가 제거되어 화면에는 아무런 문자열도 출력되지 않는다.
유사한 기능을 Conditional 특성으로 구현할 수 있다. 이 특성은 클래스와 메서드에 적용할 수 있고 적용된 클래스와 메서드를 사용하는 코드는 Conditional 특성의 생성자로 전달된 전처리 상수가 정의되어 있는 경우에만 EXE/DLL 실행 파일에 포함된다.
즉, #if/#endif 전처리 지시자가 필요없다.
ex) Conditional 특성을 이용해 동일한 동작으로 바꿀 수 있다.
위의 코드 역시 디버그 모드로 빌드할 때만 문자열이 출력되고, 릴리스 빌드에서는 Main 메서드 내에서 OutputText 메서드 호출이 제거되어 문자열도 출력되지 않는다.
개발자가 원한다면 DEBUG/TRACE 말고도 직접 전처리 상수를 정의해 이를 Conditional 특성에 전달할 수 있다.
Debug 타입과 Trace 타입
BCL의 하나인 System.Runtime.dll과 System.Diagnostics.TraceSource.dll에는 System.Diagnostics 네임스페이스 아래에 Debug와 Trace 타입이 각각 정의되어 있다.
이 타입들에게는 대표적으로 WriteLine 메서드가 함께 제공된다. 이것들은 모두 각 이름에 해당하는 DEBUG, TRACE 전처리 상수가 Conditional 특성으로 적용되어 있다.
이 내용을 실행하면 '사용자 화면 출력'이라는 문자열만 출력된다. Debug와 Trace의 WriteLine은 어디에 문자열을 전송하는가?
이 결과를 확인하려면 비주얼스튜디오에서 StartDebugging을 이용해 실행해야 한다.
→ 프로그램이 실행되자마자 종료되고 하단의 출력(Output) 창에 Debug와 Trace에서 전송한 결과를 확인할 수 있다.
위 출력은 Debug 빌드로 실행했기 때문에 두 가지 출력 결과가 나왔지만 릴리스 빌드로 실행하면 '디버그 화면 출력 - Trace' 문자열만 출력창에 보인다.
정리
Console.WriteLine은 사용자 프로그램의 콘솔 창에 문자열을 출력하는 반면 Debug와 Trace 타입의 WriteLine은 디버거로 문자열을 전송한다.
사용자의 콘솔 화면을 어지럽히지 않고 내부 프로그램 상태를 추적하기 위한 특별한 목적으로 사용할 수 있다. 즉, 출력 내용이 오직 개발자에게만 의미 있는 것이다.
비주얼스튜디오가 설치되지 않은 컴퓨터에서 Debug/Trace의 WriteLine 출력을 확인하려면?
- 마이크로소프트에서 가볍게 쓸 수 있는 DebugView라는 프로그램을 배포하고 있으니 사용하면 된다.
플랫폼(x86, x64, ARM32, ARM64, AnyCPU) 선택
- 닷넷 런타임이 인텔/AMD CPU와 ARM CPU를 지원하기 때문에 각각 32비트, 64비트 지정을 x86/x64, ARM32/ARM64로 나눌 수 있고 그 외 플랫폼에 상관없는 AnyCPU를 추가로 두고 있다.
- 프로젝트 속성 창에서 빌드 탭의 플랫폼 대상에서 EXE/DLL이 실행될 대상 플랫폼을 선택할 수 있다.
비주얼스튜디오에서 프로젝트를 생성하면 기본값은 AnyCPU다.
플랫폼 | 인텔/AMD CPU | ARM CPU | ||
32비트 운영체제 | 64비트 운영체제 | 32비트 운영체제 | 64비트 운영체제 | |
x86 | 32비트 EXE로 실행 | 32비트 EXE로 실행 | 실행 불가 | |
x64 | 실행 불가 | 64비트 EXE로 실행 | ||
ARM32 | 실행 불가 | 32비트 EXE로 실행 | 32비트 EXE로 실행 | |
ARM64 | 실행 불가 | 64비트 EXE로 실행 | ||
AnyCPU | 32비트 EXE로 실행 | 64비트 EXE로 실행 | 32비트 EXE로 실행 | 64비트 EXE로 실행 |
AnyCPU만 구성하는것이 모두 호환되는데 왜 32비트를 고려하는가?
→ 아직도 수 많은 공개된 DLL이 32비트로만 만들어져 있어 그 DLL을 사용하려면 32비트 프로세스로 실행해야 하기 때문이다.
32비트용 DLL은 64비트 프로세스(EXE)에 사용될 수 없다. 64비트 프로세스에 로드되기 위해서는 반드시 AnyCpu 또는 x64로 빌드되어야 한다.
프로세스가 32비트로 실행되는지 64비트로 실행되는지 파악하려면 Environment 타입의 Is64BitProcess 속성을 이용하면 된다.
Console.WriteLine("64 bit process: " + Environment.Is64BitProcess)
//실행된 EXE가 323비트 프로세스이면 False, 64비트 프로세스이면 True 반환
정리
32비트 네이티브 DLL(ex ActiveX)을 사용하는 경우에 한해 x86 옵션을 사용하고, 그렇지 않은 대부분의 경우에는 기본값인 AnyCPU로 만드는 것을 권장한다.
Reference
시작하세요! C# 12 프로그래밍 기본 문법부터 실전 예제까지
'C#' 카테고리의 다른 글
C# 1.0 - 힙과 스택 (0) | 2025.09.26 |
---|---|
C# 1.0 - 예외 (0) | 2025.09.24 |
C# 1.0 - 연산자 (0) | 2025.09.23 |
C# 1.0 - 문법요소 (0) | 2025.09.21 |
C# 객체지향 문법 [C#의 클래스 확장 - 멤버 유형 확장] (4) | 2025.07.26 |