본문 바로가기

프로그램&DB/C#

[C# 강좌] C# 프로그래밍 #08- 클래스

3. 객체 지행 프로그래밍(Object Oriented Programming)

3.1 클래스

3.1.1 C# 프로그램 구조

C# 프로그램은 클래스를 기본 요소로 모든 수행코드가 클래스 내에서 이루어진다. 다음 소스를 보자.

1

2

3

4

5

6

class Program

{

    static void Main(string[] args)

    {

    }

}

 

실제 using 구문과 namespace에 대한 정의 없이 위와 같이만 코드를 쳐도 프로그램이 정상적으로 컴파일 됨을 알 수 있다. 이는 이것만으로도 완벽한 프로그램이 된다는 것을 의미한다. 실제 C#의 응용프로그램은 위와 같이 구성된다. 1번째 라인과 같이 클래스가 하나 만들어지고, 3번째 라인과 같이 실제 프로그램의 진입점인 Main 메서드를 구현하게 된다. 다시 말해 C#에서 프로그램은 Main 메서드에서 시작되고 Main 메서드가 종료됨으로서 프로그램이 종료된다는 것이다. Main 메서드는 string 타입의 배열 args를 가지며, 이는 실행시 입력 받는 외부 파라메터가 된다. 윈도우 응용 프로그램 역시 이와 동일하게 Main 메서드에서 시작하게 된다는 것만 기억하고 클래스에 대해 알아보기로 하겠다.

실제 class 외부에서 선언되고 이용될 수 있는 코드는 클래스와 구조체, 열겨형 멤버 선언과 같이 데이터 구조를 담는 개체밖에는 선언되지 못한다는 것을 알고 넘어가면 된다.

 

3.1.2 클래스

클래스는 C#의 가장 기본적인 데이터 구조이다. 앞에서 보여준 C# 프로그램 구조 소스를 보면 Program 앞에 class라는 키워드로 시작하는 것을 볼 수 있다. 이는 Program이라는 이름을 이용하는 클래스를 선언한다는 것을 의미한다.

C#에서 클래스는 class라는 키워드를 클래스 이름 앞에 붙여 사용하게 된다. 또한, 프로그램 구조상 클래스에서 클래스를 이용하여 프로그램을 만드는 구조를 가진다. 클래스는 다음과 같은 형식으로 선언된다.

/액세스 한정자/ class [클래스 이름]

클래스의 엑세스 한정자 public, private, protected, internal, protected internal 등이 이용되며 이에 대한 설명은 3.1.2.2의 액세스 한정자에서 다시 다루도록 하겠다. C#에서 액세스 한정자를 생략하고 클래스를 만들 경우 기본으로 public 형으로 클래스가 만들어진다.

 

클래스를 이용하는 목적은 개체의 데이터를 추상화하고 특성과 역할을 정의하기 위해 이용된다. 또한 이는 상속이라는 방법을 통해 확장될 수 있다. 자동차를 추상화하는 클래스를 만들어 보기로 하자. 우선 자동차는 엔진, 브레이크, 핸들 등을 가진다고 가정하고 이 자동차의 기능은 출발과 정지가 있다고 가정하자. 여기서 일반적 구성요소는 엔진, 브레이크, 핸들이 되고, 가능한 행위는 출발과 정지로 정의된다는 것을 알 수 있다. 이를 클래스 다이어그램으로 그리면 아래와 같다.

 

실제 이 클래스를 코드로 구현해 보면 다음과 같다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

public class Car

{

    private string Engine;

    private bool EngineBreak;

    private int SteeringWheel;

 

    public Car()

    {

    }

 

    public void doStart()

    {

    }

 

    public void doStop()

    {

    }

}

 

Car라는 클래스를 먼저 만들고 구성 요소를 3, 4, 5번째 줄에 Engine, EngineBreak, SteeringWheel과 같이 선언했다. 기능에 대한 정의를 보면 출발은 doStart라고 11번째 줄에 정의했고, 정지는 15번째 줄에 정의해 놓았다.

이는 간단한 클래스를 정의하고 구현하는 기본적인 과정을 보여주고 있다.

 

 

3.1.2.1 클래스 멤버

클래스는 멤버 변수와 멤버 메서드를 포함한다.

1) 생성자

생성자는 개체 인스턴스를 만들 때 불려지는 메서드를 말한다. 생성자의 호출은 new 키워드를 이용하여 불려지며, 클래스를 초기화하는 역할을 수행한다. 생성자는 한번만 불려지며 생성자의 이름은 클래스의 이름과 동일하게 만들어야 한다. 또한, 생성자는 목적에 따라 여러가지 형식을 이용할 수 있도록 오버로드 될 수 있다.

1

2

3

4

5

6

7

8

9

10

public class MyClass

{

        public MyClass()

        {

        }

 

        public MyClass(string myString)

        {

        }

}

 

3번째 줄은 파라메터를 갖지 않는 생성자를 구현한 것이며, 7번째 줄은 문자열을 파라메터로 갖는 생성자를 구현한 것이다. 위의 3번째 줄과 7번째 줄에서와 같이 생성자는  오버로드 되어 구현될 수 있으며, 일반적으로 개체를 초기화하는 코드가 들어가게 된다.

일반적으로 클래스는 생성자를 통해 생성되지 않으면 사용할 수 없다.(static 멤버 메서드의 경우 생성자 없이 사용 가능)

 

2) 소멸자

소멸자는 클래스 인스턴스를 소멸하는데 이용된며, 사용자에 의해 호출될 수 없다. 소멸자는 다음과 같이 구현한다.

1

2

3

4

5

6

7

public class MyClass

{

    ~MyClass()

    {

// 명시적인 개체 해제 구문

    }

}

 

3번째 라인은 MyClass라는 클래스의 소멸자를 명시적으로 구현한 예이다. 소멸자는 내부적으로 Finalize 메서드를 호출하게 된다.

소멸자가 포함된 클래스는 Finalize 큐에 올라가게 되고, 소멸자가 호출되면 큐를 처리하기 위한 가비지 수집기가 호출되게 된다.

 

-Dispose 메서드

Dispose 메서드는 IDispose 인터페이스를 구현하는 클래스에 구현된다. Dispose의 역할은 명시적으로 리소스를 해제하는데 이용된다. 가비지 수집기에 의해 리소스가 해제 되길 기다려도 되지만, 응용 프로그램에 큰 부담을 주는 외부 리소스를 이용하는 경우 명시적인 해제를 통해 시스템의 부담을 덜어 줄 수 있다.

 

3) 멤버 변수

C#은 모든 형식들이 개체로 만들어져 제공되며, 개체의 인스턴스로 이용될 수 있다. 멤버 변수란, 클래스에 전역적으로 선언된 변수들을 뜻한다. 멤버 변수들 역시 액세스 제한자를 통해 외부의 개체로부터 접근에 대한 권한을 설정할 수 있다.

다음 소스를 보자.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

using System;

using System.Collections.Generic;

using System.Text;

 

namespace MemberVariable_Sample1

{

    class Program

    {

private MyClass myClass;

 

        static void Main(string[] args)

        {

            new Program().Test();

           

            Console.ReadLine();

        }

 

        private void Test()

        {

            myClass = new MyClass();

 

            Console.WriteLine(myClass.integer1);

            //Console.WriteLine(myClass.integer2);

        }

    }

 

    public class MyClass

    {

        public int integer1 = 10;

        private int integer2 = 10;

 

        public MyClass()

        {

        }

    }

}

 

이 소스는 Program이라는 클래스와 MyClass라는 2개의 클래스를 가지고 있다. 실행은 Main 메서드를 포함하는 클래스에서부터 시작된다는 걸 앞에서 다루었다. 프로그램 클래스의 9번재 줄은 MyClass에 대한 개체 인스턴스를 myClass라는 변수명으로 선언하고 있다. 실제 MyClass라는 인스턴스 변수는 18번째 줄의 Test 함수 안에서 사용되고 있다. 13번째 줄에서 Test라는 메서드를 호출하고 있는데, 메서드명을 바로 써서 호출하지 않고 클래스명에 멤버 참조로 호출하는 이유는 Main메서드가 static 메서드이기 때문이다.

18번째 줄의 Test메서드를 설명하기 앞서 27번째 줄에서부터 구현된 MyClass라는 클래스를 보자. 이 클래스는 생성자와 integer1, integer2라는 2개의 정수형 멤버 변수가 선언되어 있다. 이 변수들은 각각 public private의 액세스 한정자를 가지고 있다.(액세스 한정자에 대해서 상세한 내용은 뒤에서 다루고 있다.) 이와 같이 메서드 내부가 아닌 클래스에서 선언되는 변수를 멤버 변수라고 부른다. 실제 호출되는 구문을 보기 위해 이제 18번째 줄의 Test 메서드를 보면, 우선 MyClass라는 인스턴스를 이용하기 위해 MyClass의 생성자를 호출하고 있다. 그 후 22번째 줄에서 myClass라는 생성된 인스턴스 변수명으로 integer1이라는 정수형 멤버 변수를 가져오고 있다. 23번에 주석 처리된 구문은 integer2 private로 접근이 제한됨으로 실제 여기서는 사용할 수 없고, MyClass 클래스의 integer2의 액세스 제한자를 public로 고치면 사용 가능해진다.

 

4) 메서드

일반적으로 메서드는 다음과 같은 형식으로 선언된다. 메서드는 실제 수행 코드가 들어가 작업을 수행하는 역할을 한다.

/액세스 한정자/ [반환타입] [메서드 이름](매개변수)

 

앞서 만든 Car 클래스를 생각해보자. Car의 기능은 출발과 정지의 2가지 기능이 있었다. 이를 doStart doStop라는 메서드로 만들었던 것이 기억이 날것이다. 여기에서는 이 메서드를 이용해 실제 자동차의 기능을 이용하는 방법에 대해 알아보기로 하겠다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

namespace Method_Sample1

{

    class Program

    {

        Car carClass;

 

        static void Main(string[] args)

        {

            new Program().CarTest();

           

            Console.ReadLine();

        }

 

        private void CarTest()

        {

            carClass = new Car();

 

            carClass.doStart();

        }

    }

 

    public class Car

    {

        private bool IsEngineOn;

        private bool EngineBreak;

        private int SteeringWheel;

       

        public Car()

        {

        }

 

        public void doStart()

        {

            EngineBreak = false;

            IsEngineOn = true;

            Console.WriteLine(EngineStatus(IsEngineOn));

        }

 

        public void doStop()

        {

            EngineBreak = true;

            IsEngineOn = false;

            Console.WriteLine(EngineStatus(IsEngineOn));

        }

 

        private string EngineStatus(bool isRun)

        {

            return "RunAvailable : " + IsEngineOn.ToString();

        }

    }

}

 

16번째 줄을 보면 Car 클래스에 대한 인스턴스를 생성하고 doStart 메서드를 호출하고 있는 것을 볼 수 있다. 호출되는 32번째 줄에 doStart 메서드는 액세스 한정이 public으로 선언되어 있고 리턴 타입이 void로 되어 있다. 실제 doStart 메서드의 내용을 보면 멤버 변수 EngineBreak의 값과 IsEngineOn의 값을 바꾸고 EngineStatus 메서드를 호출해서 리턴값을 출력해주는 것을 볼 수 있다.

46번째 줄의 EngineStatus 메서드를 보자.

 

private string EngineStatus(bool isRun)

{

return "RunAvailable : " + IsEngineOn.ToString();

}

 

EngineStatus 메서드는 bool 타입으로 isRun이라는 이름으로 매개변수를 받는 다는 것을 알 수 있을 것이다. doStart에서 보면 36번째 줄의 EngineStatus(IsEngineOn)와 같이 IsEngineOn을 매개변수로 EngineStatus를 호출하는 것을 볼 수 있다. EngineStatus 메서드는 IsEngineOn의 값을 isRun이라는 변수에 받아 지역 변수를 생성하게 된다.

48번째 줄의 return 이라는 키워드는 함수의 반환타입이 void가 아닌 경우 반듯이 있어야 한다. return이라는 키워드를 통해 반환타입과 동일한 타입의 값을 리턴하게 되는 것이다. 여기서는 “RunAvailable : True”라는 문자열을 리턴하게 된다. 그리고 최종적으로 36번째 줄의 Console.WriteLine에 결과값이 들어가게 된다. 실제 36번째 줄의 출력 구문은 아래와 같이 함수가 리턴 받은 값이 들어가서 출력되게 된다.

Console.WriteLine( “RunAvailable : True”);

 

3.1.2.2 액세스 한정자

액세스 한정자는 클래스의 접근 권한을 컨트롤하기 위해 이용된다. 실제 사용 가능한 형식은 public, private, protected, internal, protected internal 이 있다.

 

1) public private

public private는 가장 대표적으로 이용되는 형식 멤버에 대한 엑세스 한정자이다. 2개의 차이는 public 는 액세스 제한이 한정되지 않는데 반해, private는 액세스가 포함하는 형식으로 제한된다는 데 있다. 다음 소스는 클래스에 대한 접근 제한을 보이고 있다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

using System;

using System.Collections.Generic;

using System.Text;

 

namespace Class_Sample1

{

    class Program

    {

        static void Main(string[] args)

        {

            new MyClass().Method();

 

            Console.ReadLine();

        }

    }

 

    public class MyClass

    {

        public void Method()

        {

            Console.WriteLine("Test");

        }

    }

}

 

17번째 라인을 보면 MyClass라는 클래스를 액세스 한정자 public로 해서 만들고 있는 것을 볼수 있다. 이와 같은 경우 MyClass public으로 액세스 한정되기 때문에 다른 클래스에서 접근이 가능하게 된다. public private로 고치게 되면 액세스 제한 문제로 인해 컴파일 오류가 발생한다.

 

2) protected

protected 액세스 한정자는 멤버 액세스 한정자로 이용된다. 이는 클래스 상속 관계 하에서 상속받는 자식 클래스만이 부모클래스의 멤버에 대한 접근이 가능하도록 만들어 준다. 다음 예제를 보자.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

class Program : Car

{

Program pg;

 

    static void Main(string[] args)

    {

        new Program().CarTest();

        Console.ReadLine();

    }

 

    private void CarTest()

    {

        pg = new Program();

        pg.IsEngineOn = true;

    }

}

 

public class Car

{

    protected bool IsEngineOn;

    protected bool EngineBreak;

    protected int SteeringWheel;

   

    public Car()

    {

    }

 

    public void doStart()

    {

    }

 

    public void doStop()

    {

    }

}

 

1번째 줄을 보면 지금껏 보지 못했던 구문이 나온다. Program 클래스 뒤에 콜론(:)이 나오고 뒤에 Car이라고 되어 있는 것을 볼 수 있을 것이다. 이는 18번째 줄부터 구현되어 Car 클래스를 상속 받아 Program 클래스가 구현된다는 것을 알려주는 구문이다. 상속의 의미는 부모의 멤버 변수와 멤버 메서드를 물려받는 다는 것을 의미한다.(상속에 대해서는 다음에 설명하도록 하겠다.)

18번째 줄부터 구현되어 있는 Car 클래스의 멤버 변수 IsEngineOn, EngineBreak, SteeringWheel 을 보자. 이들에 대한 액세스 제한자는 모두 protected로 선언되어 있는 것을 볼 수 있다. 11번째 줄의 CarTest 메서드는 Program 클래스의 인스턴스를 생성하고 IsEngineOn의 값을 true로 변경하는 코드를 포함하고 있다. 앞서 말한 상속 관계 하에서 Program 클래스는 Car 클래스의 자식 클래스가 되고 protected 로 선언된 멤버는 자식 클래스에서 이용 가능하다고 적었었다. 그 이유로 실제 Car 클래스로부터 상속받게 되는 IsEngineOn이라는 메서드에 대한 접근이 가능하고 값을 수정할 수 있게 된다. 20번째 줄의 IsEngineOn 변수에 대한 액세스 제한자를 private로 고치고 다시 프로그램을 컴파일 해보면 보호 수준 때문에 컴파일 에러가 나는 것을 볼 수 있을 것이다.

Public 키워드와의 차이는 public 키워드는 무조건 공개되지만, protected 키워드는 상속받고 있는 자식에게만 공개된다는 것이다.

 

3) internal

액세스가 현재 어셈블리로 제한된다.

 

4) protected internal

현재 어셈블리 또는 포함하는 클래스에서 파생된 형식으로 액세스가 제한된다.

 

3.1.3 중첩 클래스

클래스는 중첩되어 사용될 수 있다. 중첩 클래스의 접근은 액세스 한정자에 따라 제한된다.

아래는 중첩 클래스의 예이다.

1

2

3

4

5

6

7

8

9

public class ParentClass

{

    public class NestedClass

    {

        public NestedClass()

        {

        }

    }

}

 

위의 소스에서 선언된 내부 클래스에 대한 인스턴스 생성은 다음과 같이 할 수 있다.

ParentClass.NestedClass nestedClass = new ParentClass.NestedClass();

 

3.1.4 partial 클래스

클래스나 구조체, 인터페이스를 분할하기 위해서 partial 이라는 키워드를 이용하는 방법을 C#에서는 제공하고 있다. 규모가 큰 프로젝트의 경우 하나의 클래스 크기가 엄청나게 커 질 수 있다. 이와 같은 경우 동일한 성격의 메서드별로 나눠 클래스를 분리하여 관리하거나 여러 사람이 동시에 작업을 진행하기 위해 partial 클래스를 이용할 수 있다.

추상 클래스는 abstract 키워드를 이용해서 만들 수 있으며 그 형식은 다음과 같다.

/액세스 한정자/  partial class [클래스 이름]

 

Windows 응용 프로그램 프로젝트를 생성해 보자. 그러면 다음과 같이 partial 클래스가 생성되는 것을 볼 수 있을 것이다.

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

 

namespace KailPaperManager

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

    }

}

 

생성자를 보면 InitalizeComponent 라는 메서드를 호출하고 있는데, 현재 이 소스에서는 어디에서도 InitalizeComponent라는 메서드를 찾아 볼 수 없을 것이다. 이와 같이 여러 개의 소스에 클래스를 분할하여 사용하는 것을 가능하게 해주는 기능이 partial 클래스이다.

 

 

3.1.3 정적 클래스

정적 클래스는 .NET 2.0에서 추가된 기능으로 클래스 이름 앞에 static라는 한정자를 이용해 구성할 수 있다. 정적 클래스는 static 멤버만 가져야 하며, 생성자를 가질 수 없다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

namespace StaticClass_Sample1

{

    class Program

    {       

        static void Main(string[] args)

        {

            //Car carInstance = new Car();

            Car.doStart();

           

            Console.ReadLine();

        }

    }

 

    public static class Car

    {

        private static bool IsEngineOn;

        private static bool EngineBreak;

        private static int SteeringWheel;

       

        public static void doStart()

        {

            Console.WriteLine("Static Instance");

        }

 

        public static void doStop()

        {

            Console.WriteLine("Static Instance");

        }

    }

}

 

7번째 줄의 주석 처리되어 있는 부분은 Car 클래스에 대한 인스턴스를 생성하는 구문이다. 주석을 해제하면 컴파일 오류가 발생하게 된다. 이유는 정적 클래스는 정적 멤버들로만 구성되며, new 를 통해 인스턴스를 만들 수 없기 때문이다. 정적 클래스 내부에 생성자가 없는데, 정적 클래스는 생성자를 이와 같은 이유로 만들 수 없다. 14번째 줄의 정적 클래스 Car를 보면 모든 멤버들이 static 키워드를 이용해 선언되어 있는 것을 볼 수 있을 것이다.

정적 멤버는 8번째 줄과 같이 클래스 명과 참조로 바로 접근할 수 있다.

 

3.1.4 봉인 클래스(sealed class)

봉인 클래스는 sealed 키워드를 이용해 정의한다. 이와 동일하게 봉인 멤버 역시 정의가 가능하다. 봉인 클래스나 멤버는 상속 구현이 되지 않는다.


/* 출처 */
inobae의 놀이터
http://blog.naver.com/inobae