티스토리 뷰

이 글에서는 C#에서 구조적인 데이터를 표현하기 위한 클래스의 사용법을 설명하고, 오브젝트와 클래스의 차이에 대해서 알아보도록 하겠습니다. C# 클래스는 필드, 메소드와 public, private의 접근자를 통해서 캡슐화를 지원합니다. new 연산자는 클래스의 객체 생성에 사용이 됩니다. C#에서 레퍼런스 사용법 및 가비지 컬렉션의 역할에 대해서 설명을 할 것입니다. 또한 특정 인스턴스가 아닌 전체 클래스에서 적용이 가능한 정적 멤버에 대해서도 이야기 할 것입니다. C#에서 특정 상수를 사용하기 위한 Const와 readonly 또한 설명할 것입니다. 


1. 구조적 데이터로사용 되는 클래스


언어에 내장되어 있는 기본 데이터 타입을 정의하고 있습니다. 정수형, 불리언과 같은 데이터 타입은 간단한 데이터를 표현하는데 사용됩니다. C#은 복잡한 데이터를 표현하기 위해서 클래스를 제공합니다. 클래스를 통해서, 개발자는 간단한 데이터를 벗어난 데이터 멤버 혹은 필드라 불리는 구조적인 데이터를 생성할 수 있습니다.


// Account.cs

public class Account

{

public int Id;

public decimal Balance;

}


Account는 새로운 데이터 타입입니다. Account 클래스는 Id와 Balance라는 변수를 가지고 있습니다. 



클래스와 오브젝트


클래스는 데이터의 종류 혹은 데이터의 유형을 표현합니다. 이는 int 혹은 Decimal과 같은 데이터 타입을 내부에 가지고 있는 것과 유사합니다. 클래스는 개별 인스턴스를 생성할 수 있는 일종의 템플릿으로 생각할 수 있습니다. 클래스의 인스턴스는 오브젝트라 불립니다. 클래스는 여러 개의 int 데이터 타입 가질 수 있고, Account라는 인스턴스들을 여러 개 가질 수도 있습니다. 예제의 Id와 Balance와 같은 필드는 인스턴스 변수라고 불립니다. 


레퍼런스 


클래스를 사용하여 생성할 수 있는 기본 데이터 타입과 확장 데이터 타입 사이의 기본적인 차이가 있습니다. 기본적인 데이터의 변수를 선언하는 것은 메모리 할당 및 인스턴스를 생성하는 것입니다. 


int x; // 4 bytes의 메모리가 할당 되었습니다. 


클래스 타입의 변수를 선언하는 것은 클래스 타입의 오브젝트에 대한 레퍼런스와 관련된 메모리를 획득하는 것입니다. 메모리가 많이 필요할 것 같은 오브젝트 자신에 대한 메모리는 할당되지 않습니다. 


Account acc; // acc는 Account 오브젝트의 레퍼런스입니다. 이 오브젝트는 아직 존재하지 않습니다. 


오브젝트 초기화 및 사용법


new 연산자를 통해서 오브젝트를 초기화 할 수 있습니다. 


acc = new Account(); // Account 오브젝트는 이제 존재합니다. 


Acc는 이에 대한 레퍼런스입니다. 오브젝트가 생성이 되면, 개발자는 이에 대한 필드와 메소드를 이용하여 동작을 시킬 수 있습니다. 간단한 예제인 Account 클래스는 메소드를 가지고 있지 않고, 2개의 필드만을 가지고 있습니다. 사용자는 ‘.’을 이용하여 필드와 메소드에 접근할 수 있습니다. 


acc.Id = 1;

acc.Balance = // 100; 필드에 대한 값이 적용되었습니다.

Console.WriteLine("Account id {0} has balance {1}", acc.Id, acc.Balance);


아래에는 Account 클래스를 사용하기 위한 완벽한 테스트 프로그램이 있습니다.


// TestAccount.cs

using System;

public class TestAccount

{

public static void Main (string[] args) 

{

Account acc; // acc는 Account 오브젝트의 레퍼런스입니다. , 이 오브젝트는 아직 존재하지 않습니다. 

acc = new Account(); // Account 오브젝트는 이제 존재합니다. Acc는 이에 대한 레퍼런스입니다. 


acc.Id = 1;

acc.Balance = 100; // 필드에 값이 적용되었습니다.  

Console.WriteLine("Account id {0} has balance {1}", acc.Id, acc.Balance); 

}

}


아래는 프로그램 실행에 대한 결과입니다. 


Account id 1 has balance 100


오브젝트 레퍼런스 할당하기





그림은 acc 오브젝트 레퍼런스와 할당 이후의 데이터가 적용된 것을 보여줍니다.


acc.Id = 1;

acc.Balance = 100; // Fields have now been assigned


 

아래 그림에서 보여주는 것처럼 두번째 오브젝트에 대한 레퍼런스를 가진 할당된 오브젝트 변수를 생각해 봅시다. 


Account acct = new Account();

acc2.Id = 2;

acc2.Balance = 200;


 


오브젝트 변수에 할당을 하게 되면 레퍼런스만 할당이 됩니다. 데이터 복사가 이뤄지지는 않습니다. 아래 그림은 할당 이후의 각각의 오브젝트 레퍼런스와 데이터에 대해서 보여줍니다. 


acc2 = acc; // acc2 now refers to same object acc does


 




가비지 컬렉션 


레퍼런스 할당 및 해제를 통해서 오브젝트는 빈 레퍼런스를 가질 수도 있습니다. 이런 오브젝트는 컴퓨터에서 메모리를 많이 사용하고 있지만, 레퍼런스를 가지고 있지는 않습니다. 위의 그림의 Id가 2인 account는 레퍼런스를 잃어버린 데이터 입니다. CLR은 자동적으로 레퍼런스가 없는 오브젝트의 메모리를 회수합니다. 이 프로세스는 가비지 컬력션이라 알려져 있습니다. 가비지 컬렉션은 조금의 실행시간을 요구합니다. 그러나 이 부분은 프로그래머에게 메모리 릭과 같은 에러를 피하는데 큰 편의성을 제공합니다. 


2. 메소드


일반적으로 클래스는 데이터뿐만 아니라 일종의 행위에 대한 부분도 정의를 할 수 있습니다. 클래스는 데이터와 행위를 하나의 엔티티로 캡슐화 합니다. 메소드는 아래와 같은 구성을 가집니다.


  • 접근자: public 또는 private는 사용
  • 반환 타입: 메소드가 반환하는 데이터가 없으면 void로 선언할 수도 있음
  • 메소드 이름: C#에서 식별이 가능한 이름이어야 함
  • 매개변수 목록: 지정 괄호 안에 메소드로 전달하기 위한 데이터(데이터가 없으면 비어 있을 수 있음)
  • 함수 내용: 중괄호에 둘러싸여있고, C# 코드로 만들어져 메소드 내용을 실행


public void Deposit(decimal amount)

{

balance += amount;

}


이 예제의 반환 타입은 없습니다(void). 메소드 이름은 Deposit이며 매개변수 목록은 하나의 decimal 타입으로 이루어져있습니다. 함수 내용은 멤버 변수 balance에 입력된 값을 더하는 코드가 하나로 이루어져 있습니다. 


Public 과 Private


C# 클래스의 필드와 메소드는 public 또는 private으로 정의할 수 있습니다. 일반적으로 필드는 대게 private으로 선업됩니다. Private로 선언된 필드는 클래스 안에서만 접근이 가능하고, 밖에서는 불가능 합니다. 


public class Account

{

private int id;

private decimal balance;


함수는 public 또는 private으로 선언될 수 있습니다. Public 메소드는 클래스의 밖에서도 호출할 수 있으며, 이는 private 데이터를 계산하고, 조작하는데 사용됩니다. 또한 private 필드에 대한 public 접근자 역할로의 함수로 제공할 수도 있습니다. 


public decimal GetBalance() 

{

return balance;

}


public int GetId() 

{

return id;

}


클래스 내에서 사용을 위해 내부 처리를 위한 Private 메소드를 만들 수도 있습니다. 코드의 여러 부분에 중복되는 것 피하고 필요할 때 마다 호출되는 private 메소드를 만들 수 있습니다. 


추상화


추상화는 불필요한 세부사항을 없애고, 엔티티의 필수적인 요소만을 정의합니다. 계좌에 대해서 여러가지 기능이 존재하지만, 이 글에의 예제에 필요한 기능은 아이디, 잔액, 인출, 입금에 대한 기능입니다. 추상화의 인스턴스는 이러한 공통 요소를 공유합니다. 추상화는 복잡성을 처리하는데 도움이 됩니다. 


캡슐화


추상화의 구현은 다른 시스템이 알 수 없도록 숨겨지거나 캡슐화되어야 합니다. 오브젝트의 내용은 public과 private로 구분할 수 있습니다. Public은 시스템이 알 수 있는 내용입니다. 반면에 private은 public에 대한 부분을 구현합니다. 그림은 Deposit이라는 public 메소드와 Deposit 함수에서 처리하는 private 필드인 balance를 보여주고 있습니다.




 

데이터 자신은 private이고, public 인터페이스를 가진 함수를 통해서 접근이 가능합니다. 이러한 캡슐화는 두 가지 종류의 보호에 대한 기능을 제공합니다. 내부 데이터는 데이터 충돌로부터 보호받습니다. 오브젝트의 사용자는 사용방법의 변경에 대해서 자유롭습니다. 


3. 생성자와 초기화


클래스의 다른 주요 이슈는 초기화입니다. 오브젝트가 생성이 되었을 때, 어떠한 초기값이 인스턴스 데이터에 할당이 될까요? 프로그래밍 내에서 전통적인 문제는 변수를 초기화 하지 않은 것입니다. 프로그램을 실행했을 때 메모리에서 무슨 일이 발생했는지에 따라서 예측하지 못한 결과를 얻을 수도 있습니다. 


C#은 기본적인 초기화를 수행함으로써 이러한 예측하지 못한 행위를 막습니다. 숫자형 데이터의 변수는 0으로 초기화 됩니다. 일반적으로 사용자는 사용자가 원하는 초기화 수행을 원합니다. TestAccount 프로그램의 방법 1과 2가 이에 대한 부분을 수행합니다. 필드가 public으로 선언이 되어 있기 때문이 이러한 방법이 가능하고, 클래스 사용자는 이 필드들을 초기화 할 수 있습니다. 2번째 방법은 클래스 내부에 초기화 코드를 구현하는 것입니다. I


public class Account

{

private int id = 7;

private decimal balance = 700;

public decimal GetBalance()

return balance;

}

public int GetId()

return id;

}

}


위와 같은 방법으로 처리하면, Account은 모든 인스턴스들은 같은 값을 가지며 시작합니다. 사용자는 객체가 생성될 때 사용자가 데이터를 지정할 수 있는 방법으로 적절히 인스턴스의 데이터를 초기화 할 수 있는 방법을 찾고 싶어 합니다. 이 기능은 C#의 생성자에 의해 제공됩니다. 


생성자


생성자를 통해서 사용자는 사용자가 원하는 데로 개별 오브젝트를 초기화 할 수 있습니다. 게다가 인스턴스 데이터 초기화 하는 것은 사용자가 원하는 다른 방법을 통해서 초기화를 할 수도 있습니다. (예: 파일을 통한 초기화) 


생성자는 new 연산자로 오브젝트가 생성이 되었을 때 자동적으로 호출되는 특별한 메소드와 같습니다. 생성자는 반환 타입이 없고, 클래스와 같은 이름으로 구성되며, public 접근자로 사용됩니다. 또한 new 연산자를 통해서 전달되는 매개변수를 가질수도 있습니다.


public Account(int i, decimal bal)

{

id = i;

balance = bal;

}


사용자는 초기화를 위한 new 생성자를 사용하고 파라미터를 통해서 사용자가 원하는 데이터를 전달할 수 있습니다. 


// InitialAccount.cs

using System;

public class InitialAccount

{

public static void main (string[] args)

Account acc1 = new Account(1, 100); 

Console.WriteLine("balance of {0} is {1}", 

accl.Getld(), accl.GetBalance());

Account acc2 = new Account(2, 200); 

Console.WriteLine("balance of {0} is {1}", acc2.GetId(), acc2.GetBalance());


}

}


아래는 예제에 대한 결과입니다. 


balance of 1 is 100

balance of 2 is 200


기본 생성자

 

만약 생성자를 클래스에 정의하지 않았다면, C#은 자동적으로 기본 생성자라 불리는 클래스에 대한 생성자를 만듭니다. 기본 생성자는 매개변수가 없습니다. 기본 생성자는 인스턴스 데이터를 모두 할당합니다. 기본 생성자는 매개변수 없이 new 생성자를 사용하여 오브젝트 인스턴스를 생성할 때 호출이 됩니다. 아래의 코드를 앞에서 보여준 Step2 테스트 프로그램 뒤에 추가하십시오. 


Account acc3 = new Account(); 

Console.WriteLine("balance of {0} is {1}", acc3.GetId(), acc3.GetBalance());


Step1에서는 에러가 발생하지 않았지만, 여기에서는 아래와 같은 컴파일 에러가 생성이 됩니다.

 

error:

error CS1501: No overload for method 'Account' takes '0' arguments


C#에서는 생성자를 포함하여 모든 함수에 대해 같은 이름을 가지고, 다른 매개 변수를 보유하는 여러 개 함수를 생성하기 위해 함수를 오버로딩할 수 있습니다. 위의 에러는 매개 변수를 가지지 않는 두번째 생성자를 정의함으로서 해결할 수 있습니다. 생성자의 내용은 비어있어도 됩니다. 


public class Account

{

private int id = 7;

private decimal balance = 700;


public Account()

{


}

public Account(int i, decimal bal)

id = i;

balance = bal;

}


만약 test 프로그램을 실행한다면, 2개의 명백한 초기화 가정을 수행하고, 하나의 기본 초기화를 처리합니다. 실행결과는 아래와 같을 것입니다. 


balance of 1 is 100

balance of 2 is 200

balance of 7 is 700


this


때때로 현재 오브젝트 레퍼런스를 접근하는데 사용되는 메소드에 대한 코드가 있으면 편리할 것입니다. C#은 현재 오브젝트 레퍼런스를 접근하기 위해 정의된 특별한 변수인 this 키워드를 정의하고 있습니다. 만약 위와 같은 생성자를 위한 코드를 구현한다고 하였을 때 인스턴스 변수가 아니 파라미터에 대해 다른 이름을 사용한 것을 위의 예제에서 볼 수 있을 것입니다. this 변수를 사용하게 되면 모호함을 없애면서 같은 이름을 사용할 수 있습니다. 아래는 생성자에 대한 대안 코드를 보여줍니다. 


public Account(int id, decimal balance)

{

this.id = id;

this.balance = balance;

}


4. 정적 필드와 메소드


C#에서 필드는 일반적으로 클래스의 각 객체 인스턴스에 대한 고유한 값을 인스턴스마다 할당합니다. 때때로 전체 클래스와 연관된 하나의 값을 가지는 것이 유용할 때도 있습니다. 이러한 필드를 정적 필드라 부릅니다. 인스턴스의 데이터 멤버와 같이 정적 데이터 멤버는 public 또는 private으로 선언될 수 있습니다. public 정적 멤버를 접근하기 위해서는 ‘.’을 사용할 수 있습니다. 그리고 객체 레퍼런스 대신 클래스의 이름을 이용해야 합니다. 


정적 메소드


메소드 또한 static로 선언할 수 있습니다. Static 메소드는 클래스 초기화를 하지 않고 호출이 됩니다. ‘.’를 사용하여 호출이 되며, ‘.’ 이전에 클래스 이름을 사용해야 합니다. 인스턴스 없이 정적 함수에 대한 호출이 가능하지만, 정적 함수는 정적 데이터 멤버만을 사용하고, 인스턴스 데이터 멤버를 사용할 수는 없습니다. 정적 메소드는 public 또는 private로 선언할 수 있습니다. 다른 private 메소드 처럼 private 정적 메소드는 클래스내의 기능을 수행하는 함수로 사용되고, 외부에서는 호출할 수 없습니다. 


정적 생성자 


클래스가 정적 필드와 정적 메소드를 가질 수 있는 것처럼, 정적 생성자 또한 가질 수 있습니다. 모든 오브젝트 인스턴스가 생성이 되기 전에 정적 생성자는 딱 한번 호출이 됩니다. 정적 생성자는 static 을 접두어로 정의된 생성자입니다. 정적 생성자는 매개 변수를 가지지 않으며, 접근자도 없습니다. 프로그램 StaticConstructor는 Accout 클래스에 대한 정적 생성자를 보여줍니다. 모든 생성자는 호출되는 코드 라인만 작성하기 때문에, 다양한 생성자가 호출 될 때 이를 쉽게 이해할 수 있을 것 입니다.


// Account.cs

using System;

public class Account

{

private int id;

private decimal balance;

private static int nextid = 1;


static Account()

{

Console.WriteLine("Static constructor");


public Account()

Console.WriteLine("Account() constructor");

this.id = nextid++;

}


public Account(decimal balance)

Console.WriteLine("Account(decimal) constructor");

this.id = nextid++;

this.balance = balance;

}


public decimal GetBalance()

return balance;

}


public int GetId()

return id;

}

}


아래는 테스트 프로그램입니다. 


// StaticConstructor.cs

using System;

public class StaticConstructor

{

public static void Main(string[l args)

Account acct, acc2, acc3;

accl = new Account(100); 

acc2 = new Account(200); 

acc3 = new Account();

WriteAccount(accl);

WriteAccount(acc2);

WriteAccount(acc3);

}


private static void WriteAccount(Account acc)

Console.WriteLine("balance of {0} is {1}", 

acc.GetId(), acc.GetBalance());

}

}



아래는 결과입니다.


Static constructor

Account(decimal) constructor

Account(decimal) constructor

Account() constructor

balance of 1 is 100

balance of 2 is 200

balance of 3 is 0


5. 상수와 Readonly 필드


변수가 항상 동일한 값을 가지고 있다고 확신한다면, initializer를 통해서 값을 할당하고, const 명령어를 사용할 수 있습니다. 상수는 자동적으로 static 타입입니다. 이는 클래스 이름을 통해서 외부에서도 접근을 할 수 있습니다. 


런타임 시 한번의 초기화에 대해서 호출이 필요하고 이후에는 그 값이 바뀌지 않는다면, 사용자는 효율성을 얻기 위해 readonly 필드를 사용할 수 있습니다. 이 필드는 인스턴스 멤버 혹은 정적 멤버로 정의될 수 있습니다. 인스턴스 멤버의 경우 이는 일반적인 생성자에서 할당이 될 것이고, 정적 멤버의 경우 정적 생성자에서 할당이 될 것입니다. 


ConstantAccount는 const와 readonly에 대한 사용방법을 보여줍니다. 이 두 가지 방법에서 사용자가 값을 수정하고자 한다면 컴파일 에러를 볼 수 있을 것입니다. 


// Account.cs

public class Account

{

public coast decimal FEE = 5.00m; 

public readonly int FreeChecks;

public Account(int num)

FreeChecks = num;

}

}


아래는 테스트 프로그램입니다. 


// ConstantAccount.cs

using System;

public class ConstantAccount

{

public static void Main (string[] args)

Console.WriteLine("FEE = (0)", Account.FEE); 

// Account.FEE = 7.00m; // illegal

Account acc = new Account(10);

Console.WriteLine("(0) free checks", acc.FreeChecks); 

// acc.FreeChecks = 20; // illegal

}

}


위의 실행에 대한 결과입니다. 


FEE = 5

10 free checks


6. 정리


이 글에서 우리는 구조적인 데이터를 C#의 클래스를 이용하여 표현하는 방법에 대해서 알아보았습니다. 클래스는 개별 인스턴스를 생성할 수 있는 템플릿으로 생각할 수 있습니다. 클래스의 인스턴스는 오브젝트라고 불립니다. 클래스는 필드와 메소드를 통해서 탭슐화를 제공합니다. 일반적으로 필드는 private으로 메소드는 public 으로 선언이 되어 사용됩니다. new 연산자는 클래스를 오브젝트로 초기화기위해 사용됩니다. 기본 데이터에 대한 변수를 정의하면 이에 대한 메모리를 할당하고 인스턴스를 생성합니다. 클래스 타입의 변수를 선언하면 클래스 타입의 오브젝트에 대한 레퍼런스에 대한 메모리만 획득을 합니다. new 연산자를 통해서 호출이 되지 않으면 메모리에 대한 할당은 없습니다. 레퍼런스의 할당을 통해서 오브젝트는 빈 레퍼런스를 가질 수도 있습니다. CLR은 자동적으로 이러한 오브젝트에 대한 메모리를 회수합니다. 이러한 프로세스는 가비지 컬렉션이라고 알려져 있습니다. 오브젝트의 초기화는 생성자에서 수행이 됩니다. 정적 멤버는 특정 인스턴스 뿐만 아니라 전체 전체 클래스에서 적용이 가능합니다. Const와 readonly는 C# 프로그램에서 상수를 정의하는데 사용을 할 수 있습니다. 


이 글이 도움이 되셨나요?

그렇다면 아래의 그림을 클릭해주세요



댓글