C#

C# 객체지향 문법 [생성자, 종료자, 정적멤버, 인스턴스 멤버, 네임스페이스]

devrabbit22 2025. 5. 13. 01:28

생성자

C# 클래스에는 특별한 용도의 메서드가 있다.

클래스에 생성자 메서드를 추가하면 객체가 '생성'되는 시점에 해당 메서드가 자동으로 호출된다. 

이런 점에서 생성자는 다른 여타 메서드와 구별된다는 특징이 있다.

class 클래스_명
{
	접근_제한자 클래스_명(타입 매개변수명, ....)
    {
    }
}
//생성자는 이름이 클래스명과 동일하며 반환타입을 명시하지 않는다는 점을 제외하면 
//일반 메서드를 정의하는 방법을 따른다.(constructor를 줄여서 ctor라고 부르기도 한다.)

반환 타입이 없고 클래스 이름과 동일한 public Person 메서드를 정의해두면 코드에서 new Person을 실행하는 시점에 해당 메서드의 코드가 실행된다.

-> 생성자에는 말 그대로 객체를 생성하는 시점에 실행되야 할 코드를 담을 수 있다.

 

사실 모든 클래스는 생성자를 가지고 있다. 생성자를 명시적으로 정의하지 않았다면 C# 컴파일러는 일부러 다음과 같은 빈 생성자를 클래스에 집어넣고 컴파일한다.

public Person()
{
}

따라서 new를 실행하면 언제나 해당 객체의 생성자가 함께 실행된다. 아울러 생성자 역시 다른 일반 메서드처럼 매개변수를 가질 수 도 있다. 일반적으로 매개변수를 갖는 생성자를 통해 외부로부터 객체를 초기화하는 값을 입력받는다.

new를 통해 객체가 생성될 때 '영희'라는 값을 생성자의 인자로 전달받는다. 

생성자는 전달받은 값을 멤버 변수인 _name에 보관해두고 이후에 클래스의 다른 멤버 메서드에서 사용한다.

 

매개변수가 하나도 없는 생성자를 기본 생성자(Default constructor)라고 해서 매개변수를 받는 다른 생성자와 구분하기도 한다.

한 가지 주의할 점

개발자가 명시적으로 생성자를 정의한 경우 컴파일러는 기본 생성자를 추가히지 않는다는 것이다.

Person person = new Person("영희");	//이 부분을
Person person = new Person();	//로 객체를 생성하면 컴파일 오류가 발생한다.

 

생성자를 여러개 정의하는 것도 가능하다.

class Book
{
    public string Title;
    public decimal ISBN13;
    public string Author;

    public Book(string title)
    {
        Title = title;
    }
    public Book(string title, decimal isbn13)
    {
        Title = title;
        ISBN13 = isbn13;
    }
    public Book(string title, decimal isbn13, string author)
    {
        Title = title;
        ISBN13 = isbn13;
        Author = author;
    }
}

따라서 개발자는 각 생성자를 상황에 따라 선택해 사용할 수 있다.

namespace ConsoleApp2;

class Program
{
    static void Main(string[] args)
    {
        Book gulliver = new Book("걸리버 여행기");
        Book huckleberry = new Book("허클베리 핀의 모험", 9788952753403m);
        Book alice = new Book("이상한 나라의 앨리스", 9788992632126, "Lewis Carroll");
    }
}
class Book
{
    public string Title;
    public decimal ISBN13;
    public string Author;

    public Book(string title)
    {
        Title = title;
    }
    public Book(string title, decimal isbn13)
    {
        Title = title;
        ISBN13 = isbn13;
    }
    public Book(string title, decimal isbn13, string author)
    {
        Title = title;
        ISBN13 = isbn13;
        Author = author;
    }
}

종료자

해당 객체가 제거되는 시점에 실행될 종료자(finalizer)도 존재한다.

class 클래스_명
{
	~클래스_명()
    {
    	//_...[자원해제를 위한 코드]
     }
}
//종료자는 이름이 ~(틸드:tilde)를 접두사로 쓰는 클래스명과 동일하며 어떤 인자나 반환값도 갖지 않는다.
//종료자가 실행되는 시점은 예측할 수 없다.(finalizer를 줄여서 dtor라고 쓰기도 한다.)

ex) Book 클래스의 종료자를 정의

class Book
{
	public Book()	//생성자
    {
    }
    
    ~Book()	//종료자
    {
    	//...[자원을 해제한다.]...
    }
}

생성자의 경우 사용자가 명시적으로 new를 통해 객체를 할당하는 시점에 자동으로 호출된다.

종료자는 언제 호출되는가?

C#에는 delete같은 예약어가 없기 때문에 사용자가 객체를 의도적으로 제거하는 기능이 없다.

데이터를 메모리에 할당만 하고 제거하지 않으면 언젠가 주소 공간이 바닥나므로 프로그램이 제대로 동작하지 않게 된다.

CLR에서는 이런 문제를 내부적으로 가비지 수집기(Garbage Collector)라는 개념을 도입해서 해결하고 있다.

C# 프로그램에서 모든 참조형 변수를 생성할 때는 GC가 관여하고, GC는 스스로 적절하다고 판단되는 시점이 오면 관리 힙을 청소하는 작업을 하는데, 이때 어떤 객체가 더는 사용되고 있지 않다면 객체의 데이터를 해제해 버린다.

C#의 참조형 변수가 가리키는 객체는 GC가 호출되야 종료자가 호출된다. 한 가지 확실한 사실은 GC가 불확실산 시점에서 메모리 정리를 한다는 것이다.

종료자를 사용할 때는 정의하기에 앞서 신중하게 고민하고 판단해야 한다.

GC입장에서 일반 참조 객체와는 달리 종료자가 정의된 클래스의 객체를 관리하려면 더 복잡한 과정을 거쳐야 하므로 성능 면에서 부하를 줄 수 있다.

 

이로인한 소멸자의 주의점

  • C#에서는 소멸자가 언제 실행될지 보장할 수 없습니다. (GC의 타이밍에 의존)
  • 성능에 민감한 경우나 명시적 리소스 관리가 필요한 경우, IDisposable 인터페이스를 사용하는 Dispose 패턴을 권장합니다.

정적 멤버, 인스턴스 멤버

어떤 타입을 실체화한 객체를 인스턴스라고 한다. 인스턴스를 달리 표현하면 new 연산자를 거쳐서 메모리에 할당된 객체라고 할 수 있다. 

그 객체와 관련된 멤버를 인스턴스 멤버(instance member)라고 하며, 필드, 메서드, 생성자는 모두 여기에 속한다.

- 필드, 메서드, 생성자가 인스턴스 멤버라는 것을 강조하기 위해 때로는 인스턴스 필드, 인스턴스 메서드, 인스턴스 생성자라고도 표기한다.

때로는 인스턴스와 관계없는 행동을 정의해야 할 때가 있다. 

개별 인스턴스 수준이 아닌 해당 인스턴스 타입 전체에 걸쳐 전역적으로 적용되는 필드, 메서드, 생성자가 필요할 수 있는데, 이러한 멤버를 인스턴스 멤버와 구분해 정적 멤버8static member)라고 한다.

정적 필드

클래스의 객체가 생성될 때마다 횟수를 증가시키는 필드를 정의한다고 가정

인스턴스 필드의 한계

원하는 결과는 해당 클래스에 속한 인스턴스가 생성된 총 횟수를 나타내는 것으로, 마지막 값이 2가 되기를 바랬지만 원하는 값이 나오지 않았다. 인스턴스 필드는 new로 할당받은 객체마다 고유하게 메모리를 확보하기 때문에 클래스 전역적으로 값이 유지되지 않는다. 이 같은 요구 사항을 만족하려면 클래스 단위의 필드를 정의해야하고, 그것이 바로 정적 필드(static field)가 된다.

실행결과에서 보면 정적 필드의 값은 new로 할당된 인스턴스와 상관없이 존재한다. 

정적 필드와 인스턴스 필드의 메모리 표현

정적 필드를 사용하는 전형적인 패턴 가운데 대표적으로 한 가지를 꼽으면 특정 클래스의 인스턴스를 의도적으로 단 한 개만 만들고 싶은 경우이다.

이 경우 클래스 밖에서 해당 클래스의 인스턴스를 만들지 못하게끔 생성자를 private 접근 제한자로 명시하고 단 하나의 인스턴스만 클래스 내부에서 미리 생성해 두는 것으로 원하는 바를 이룰 수 있다.

class Person
{
    static public Person President = new Person("대통령"); //public 정적필드
    string _name;

    private Person(string name) //private 인스턴스 생성자
    {
        _name = name;
    }
    public void DisplayName()   //public 인스턴스 메서드
    {
        Console.WriteLine(_name);
    }
}

클래스를 이렇게 정의해 두면 외부에서는 newPerson(.....) 구문을 사용할 수 없다.

하지만 내부에서는 이미 정적 필드에 인스턴스를 생성해둿기 때문에 이후 Person.President와 같은 방법으로만 해당 객체를 사용할 수 있다.

        Person.President.DisplayName(); //정적 필드로 단일 인스턴스 접근
        Person person1 = new Person("홍길동"); //생성자가 private이기 때문에 오류 발생

이렇게 인스턴스가 단 하나만 존재하는 타입을 특별하게 싱글턴(singleton) 클래스라고 한다.

단일 시스템 자원을 책임지는 타입이 필요할 때 Singleton class를 만들어 다른 클래스에 기능을 노출하는 용도로 사용한다.

정적 메서드

정적 메서드(static method)는 일반 메서드에  static 예약어를 붙여서 정의한다. 정적 메서드 역시 new로 객체를 생성하는 것과 무관하게 사용할 수 있으므로 [클래스이름].[정적메서드] 형태로 호출할 수 있다.

정적 메서드 안에서는 인스턴스 멤버에 접근할 수 없다는 특징이 있다. 이는 정적 메서드가 new로 할당된 객체가 없는 상태에서도 호출되는 메서드라는 점을 생각하면 쉽게 이해 가능하다.

class Program
{
    static void Main(string[] args)
    {
        //문자열 출력 메서드
        Console.WriteLine("Hello World");   //Console 타입에 정의된 정적 메서드 사용
    }
}

Console.WriteLine은 다름 아닌 Console 클래스에 정의된 WrieLine 정적 메서드를 가리킨다.

Main 메서드도 정적 메서드에 속하지만 C#에서 다소 특별한 대우를 받기 때문에 좀 더 부가적인 설명이 필요하다.

Main 메서드

프로그램은 CPU에 의해 순차적으로 실행되는 특징이 있다.

C#으로 만든 프로그램 역시 순서대로 명령어가 실행되는데, 가장 처음 실행되는 명령어는 무엇인가?

다른 말로 진입점(entry point)이라고도 하는데, C#은 다음과 같은 약속을 따르는 메서드를 최초로 실행될 메서드라고 규정

  1. 메서드 이름은 반드시 Main이고,
  2. 정적 메서드여야 하고,
  3. Main 메서드가 정의된 클래스의 이름은 제한이 없다. 하지만 2개 이상의 클래스에서 Main 메서드를 정의하고 있다면 C# 컴파일러에게 클래스를 지정해야 한다.
  4. Main 메서드의 반환 값은 void 또는 int만 허용된다.
  5. Main 메서드의 매개변수는 없거나 string 배열만 허용된다.

이 규칙을 만족하는 메서드를 정의하면 C# 컴파일러는 자동으로 그 메서드를 시작점으로 선택해 EXE 파일을 생성한다.

 

특이하게도 Main 메서드 역시 반환값과 인자값을 지정할 수 있는데, 어떤 의미가 있는가?

우선 반환값은 대개 EXE 프로그램의 실행 결과에 대한 오류 여부를 판단하는데 사용된다.

ex) 기본 예제코드를 단순하게 0을 반환하는 프로그램을 만들고

class Program
{
	static int Main(string[] args)
    {
    	return 0;
    }
}

명령행에서 해당 프로그램을 실행하면 %ERRORLEVEL%이라는 특수한 변숫값을 통해 반환값을 알아낼 수 있다.

[윈도우 환경]
C:\temp> ConsoleApp1.exe

c:\temp> echo %ERRORLEVEL%
0
[리눅스 환경]
$ %dotnet ./ConsoleApp1.dll

$ echo $?
0

Main 메서드에서 0이외의 다른 값을 반환하면 %ERRORLEVEL% 값 역시 그에 따라 바뀐다. 일반적으로 프로그램이 정상적으로 실행되 종료하면 0을 반환하고, 오류가 발생한 경우 오류의 종류에 따라 숫자값을 정해서 반환한다.

Main 메서드의 인자로 허용되는 string 배열은 어떻게 사용되는가?

명령행에서 EXE 프로그램을 실행할 때 함께 입력되는 문자열을 공백으로 구분해 차례대로 배열에 담아 활용하는 것이 가능

namespace ConsoleApp2;

class Program
{
    static void Main(string[] args)
    {
        if(args.Length <2)
        {
            return;
        }
        Console.WriteLine(args[0]);
        Console.WriteLine(args[1]);
    }
}

이 프로그램을 명령행에서 실행할 때 인자를 지정해서 실행하면 배열의 요소에 차례대로 값이 담겨 있음을 확인할 수 있다.

C:\temp> ConsoleApp2 Hello World
Hello
World

정적 생성자

정적 생성자(static constructor)는 기본 생성자에 static 예약어를 붙인 경우로 클래스에 단 한 개만 존재할 수 있고, 주로 정적 멤버를 초기화하는 기능을 하기 때문에 형식 이니셜라이저(typeinitializer)라고도 한다.

class 클래스_명
{
	static 클래스_명()
    {
    	//단 한 번, 가장 최초로 실행될 초기화 코드
    }
}
//정적 생성자는 단 한개만 정의할 수 있고 매개변수를 포함할 수 없다. 
//참고로 정적 생성자에서 실행되는 코드는 오류를 발생시키지 않도록 주의해야 한다. 
//왜나하면 정적 생성자의 실행이 실패하는 경우 해당 클래스 자체를 전혀 사용할 수 없게 되고, 
//오류의 원인을 찾는 것 또한 쉽지 않기 때문이다.
//(static constructor를 줄여서 cctor라고 부르기도 한다.)

C# 컴파일러는 정적 필드를 초기화하는 코드를 자동으로 정적 생성자로 옮겨서 컴파일한다.

class Person
{
    static public Person President; // = new Person("대통령") chrlghk zhemfmf
                                    // 정적 생성자로 이전해서 컴파일
    public string _name;
    private Person(string name)
    {
        _name = name;
        Console.WriteLine("ctor 실행");
    }
    static Person() //정적 생성자
    {
        President = new Person("대통령"); //정적 필드 초기화
        Console.WriteLine("cctor 실행");
    }
}

- 정적필드에 초기화 코드도 포함되어 있고, 동시에 정적 생성자도 정의해 두었다면 C# 컴파일러는 사용자가 정의한 정적 생성자의 코드와 초기화 코드를 자동으로 병합해서 정의한다. 

- 이 규칙은 인스턴스 필드와 기본 생성자 간에도 동일하게 적용된다.

 

정적 생성자는 클래스의 어떤 멤버든 최초로 접근하는 시점에 단 한번만 실행된다는 점을 기억해야 한다.

정적 멤버를 처음 호출할 경우이거나 인스턴스 생성자를 통해 객체가 만들어지는 시점이 되면 그 어떤 코드보다도 우선적으로 실행된다.

네임스페이스

네임스페이스는 말 그대로 '이름 공간'이라고 번역되는데, 태생 자체는 이름이 중복되어 정의된 것을 구분하려는 의도에서 나온 것이지만, 더 일반적으로는 수많은 클래스를 분류하는 방법으로 사용되고 있다.

 

인간의 언어는 몇몇 단어의 경우 이름은 같지만 의미가 다른 것을 볼 수 있다. 사람의 뇌는 서로 오가는 대화 문맥을 통해 '배'라고 해도 어떤 것을 지칭하는지 판단할 수 있지만, 컴파일러는 그런 상황에서 적절한 의미 선택을 할 수 없으므로 이름 충돌(naming conflict)이라는 오류를 발생시킨다.

이런 문제를 해결하기 위해 개발자가 '문맥'에 해당하는 힌트를 컴파일러에게 줘야 하는데, 이것이 코드의 이름 공간(namespace)이 된다.

즉, 프로그램에서는 두 가지 이름을 중복해서 사용할 수 없다.

namespace MilkyWay
{
	class Earth
    {
    }
}
namespace Andromeda
{
	class Earth
    {
    }
}

namespace로 구분된 블록 내에서는 동일한 이름 공간이 적용된다. MilkyWay 네임스페이스 블록 안에서는 Earth만 단독 사용하는 경우 그것이 MilkyWay가 생략된 것이라고 여긴다.

네입스페이스가 다른 곳에서 클래스를 생성해야 한다면 해당 클래스가 속한 네임스페이스까지 모두 명시해야 한다.

'[네임스페이스].[클래스]'와 같은 형식으로 간단하게 지정할 수 있다. 물론 namespace 이름까지도 같다면 다시 이름 충돌이 발생한다. 그래서 일부 언어에서는 namespace로 사용되는 이름을 해당 업체가 소유한 웹 사이트의 도메인 이름으로 사용할 것을 권장해서 충돌 확률을 낮추기도 한다. 

namespace는 그 안에 또 다른 namespace를 중첩하는 것도 가능하기 때문에 이름 충돌로 인한 문제는 발생하더라도 대부분 손쉽게 해결할 수 있다.

현실적으로 보면 네임스페이스가 이름 충돌 때문에 사용되는 경우는 많지 않다. 대신 클래스의 소속을 구분하는데 사용하는 것이 더 일반적이다.

ex) 통신관련 클래스와 파일 조작을 위한 클래스를 만들 경우 네임스페이스 구분

namespace Communication
{
	class Http
    {
    }
    class Ftp
    {
    }
}

namespace Disk.FileSystem
{
	class Partition
    {
    }
}

네임스페이스를 적용해두면 해당 클래스가 어디에 있는지 좀 더 직관적으로 찾을 수 있다. 

하지만 작은 문제가 있다면 위의 클래스를 네임스페이스가 다른 예제코드에서 사용하려면 긴 코드를 작성해야 한다.

class Program
{
	static void Main(string[] args)
    {
    	Communication.Http http = new Communication.Http();
        Disk.FileSystem.Partition p = new Disk.FileSystem.Partition();
    }
}

매번 객체를 생성할 때마다 네임스페이스를 함ㄲ ㅔ지정해야 하는 것은 번거로운 일이다.

이 때문에 C#은 using이라는 예약어를 추가했고, 네임스페이스를 미리 선언해두면 객체를 생성할 때 이를 생략해도 C# 컴파일러가 알아서 객체가 속한 네임스페이스를 찾아내 오류 없이 컴파일한다.

using Communication;
using Disk.FileSystem;

namespace Communication
{
    class Http
    {
    }
    class Ftp
    {
    }
}

namespace Disk.FileSystem
{
    class Partition
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
    	Http http = new Http();
        Partition p = neew Partition();
    }
}

- using 문은 반드시 파일의 첫 부분에 있어야 한다. 어떤 코드도 using문 앞에 와서는 안된다.

//첫 번째 예제
namespace ConsoleApp1;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, World!");
    }
}
//두 번째 예제
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

소스코드 파일에 단 하나의 namespace만 정의한다면 첫 번째 예제 코드처럼 namespace를 블록 없이 정의하는 것이 가능하다.

- namespace를 한 줄에 정의하는 문법은 C# 10부터 지원한다.

- 즉, C# 9 이하의 컴파일러에서 빌드하기 위해서는 두 번째 예제 코드처럼 작성해야 한다. 

즉, Program 클래스는 ConsoleApp1 네임스페이스에 속한 것임을 알 수 있다.

한가지 더 알아야 할점 : Console 타입이 정의된 네임스페이스가 System이라는 점이다. 

namespace ConsoleApp1;

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

굳이 System을 붙이지 않고 소스코드의 상단에 using System; 코드를 추가할 필요가 없던 이유는 .NET 7을 지원하는 프로젝트부터 C# 10 이상의 컴파일러가 기본적으로 System 네임스페이스를 추가해주기 때문이다.

 

-컴파일러가 자동으로 추가해주는 namespace는 System 이외에도 프로젝트 유형에 따라 다양하다.

 

현업에서 개발하다보면 종종 FQDN(Fully Qualified Domain Name)이라는 단어를 듣게 되는데, 이는 업계마다 여러 가지 다른 의미로 사용되는데, C# 프로그래밍에서는 일반적으로 네임스페이스가 생략된 클래스명과 구분해서 클래스명에 네임스페이스까지 함께 지정하는 경우 특별히 FQDN이라고 한다는 점을 기억해야 한다.

ex) Console 클래스의 FQDN은 Systme.Console이다.


Reference

시작하세요! C# 12 프로그래밍 기본 문법부터 실전 예제까지

'C#' 카테고리의 다른 글

C# 객체지향 문법 [상속]  (0) 2025.05.15
C# 객체지향 문법 [캡슐화]  (0) 2025.05.13
C# 객체지향 문법 [클래스, 필드, 메서드]  (0) 2025.05.12
C# [제어문]  (0) 2025.05.09
C# [배열]  (0) 2025.05.07