개체와 개체 지향 프로그래밍(Object Oriented Programming)
C#은 개체 지향 프로그래밍으로 불리는 캡슐화, 상속, 다형성, 추상화 등의 개념을 제공합니다. 지금까지 배운 클래스와 클래스의 구성 요소들을 개체 지향 프로그래밍 관점으로 정리해보는 시간을 갖겠습니다.
> // 개체 지향 프로그래밍: 캡슐화, 상속, 다형성을 제공하는 프로그래밍 작성 기법
개체 지향 프로그래밍 소개
여러분들이 이 강의를 듣고 있는 시점인 지금, 현업에서 가장 많이 사용되고 있는 프로그램 작성 기법은 개체 지향 프로그래밍(object oriented programming)입니다. 개체 지향 프로그래밍은 OOP로 줄여서 표현합니다. 이번 강의를 통해서 개체 지향 프로그램의 기본적인 기능들을 학습합니다.
개체 지향 프로그래밍의 목적
개체 지향 프로그래밍의 목적은 다음과 같습니다.
- 프로그램을 분석하기 쉬워집니다.
- 프로그램 유지보수가 쉬워집니다.
- 프로그램의 특정 기능을 재사용할 수 있습니다.
물론, 이 내용은 학술적인 내용이지, 개체 지향 프로그래밍이 만능은 아닙니다.
클래스, 개체, 속성, 메서드
C#에서 클래스와 개체는 명사로 표현되며 속성은 명사 또는 형용사 그리고 메서드는 동사의 의미를 가집니다.
개체(Object)
클래스를 사용하여 새로운 형식 정의하고 개체는 데이터와 기능을 숨기는 캡슐화(Encapsulation) 기능을 제공합니다. 개체는 다음과 같은 3가지 개념을 가집니다. 이해를 돕기 위해서 영어 표현을 추가했습니다.
- (is something) 무엇이 됩니다.
- 예) 고객(Customer), 자동차(Car)
- (has data) 데이터를 가집니다.
- 예) 고객 이름(Name), 나이(Age), 주소(Address)
- (performs action) 기능을 수행합니다.
- 예) 고객 이름 변경(ChangeName), 주소 변경(ModifyAddress)
참고: 개체와 객체
박용준 강사는 Object Oriented Programming을 개체 지향 프로그래밍으로 표현합니다. 많은 수의 프로그래밍 환경에서는 객체 지향 프로그래밍이라고 표현하지만, 마이크로소프트는 이를 개체 지향 프로그래밍으로 표현하기를 권장하므로, 이 기준을 따랐습니다.
참고: POCO 클래스
C#에서는 모델 클래스(레코드 클래스, 엔터티 클래스, 데이터 클래스) 등을 POCO(Plain Old CLR Object) 형식으로도 표현합니다. 주로 속성(Property)으로만 이루어져 사용되는 클래스입니다. POCO는 자바 프로그래밍 언어에서는 POJO(Plain Old Java Object)로 표현됩니다.
코드: POCO.cs
public class PetFish
{
public string Name { get; set; }
public int Age { get; set; }
public string Kind { get; set; }
}
현실 세계의 자동차 설계도 및 자동차 개체 흉내내기
현실 세계에서 사용되는 자동차 설계도와 이 설계도로부터 만들어진 자동차 개체를 프로그램 세계에서 사용되는 Car
클래스와 car
개체로 표현하는 과정을 통해서 개체 지향 프로그래밍 관련 기본 용어에 대해서 살펴보겠습니다.
이번 절은 지금까지 배운 내용을 개체 지향 프로그래밍, 클래스, 개체, 속성, 메서드 등으로 구분해보는 시간이기에 가볍게 읽고 넘어가면 됩니다.
개체 지향 프로그래밍
개체 지향 프로그래밍을 얘기할 때 많이 쓰는 비유 중의 하나는 '현실 세계를 프로그래밍 세계로 옮겨 놓는 행위'를 말하곤 합니다. 이러한 기능을 다른 말로 모델링이라고도 합니다.
C#은 현실 세계의 업무를 프로그램화시키는 데 도움을 줍니다. 자동차에 비유하면 현실 세계의 자동차를 프로그램 세계의 Car 클래스로 표현하는 과정을 통해서 개체 지향 프로그래밍에서 사용되는 주요 구성 요소를 살펴보겠습니다.
클래스
클래스를 얘기할 때 제가 가장 많이 비유하는 단어는 설계도입니다. 클래스는 개체(Object), 컨트롤, 컴포넌트 등을 만들어 내는 설계도입니다.
- 클래스 == 설계도
개체
현실 세계에서 어떤 특징(속성)을 가지며 어떤 동작(메서드)을 수행할 수 있는 하나의 단위를 개체로 표현할 수 있습니다. 예들 들어 자동차, 사람, 컴퓨터 등이 있고 다음 문장과 같이 표현할 수도 있습니다. 빨간색 자동차 개체가 좌회전 동작을 한다.
코드에서는 개체를 만들어내는 설계도가 클래스이고 이 클래스로부터 조립된 하나의 물건이 바로 개체입니다.
자동차 설계도(클래스) -> 조립(인스턴스 화) -> 자동차(개체, Object)
마지막으로 클래스는 대문자로 시작하고, 개체는 소문자로 시작하는 걸 기본 원칙으로 합니다.
필드(Field)
필드는 클래스의 부품 역할을 하는 멤버 변수(Member Variable)를 의미합니다. 자동차 세계에서는 자동차의 부품 역할을 하고 자동차 부품들은 차체에 꽁꽁 숨겨져 있습니다. 코드 세계에서는 클래스 내의 전역 변수 역할을 하며 필드를 외부에 공개하고자 할 때는 public
필드 대신 readonly
필드와 const
필드만 허용하기를 권장합니다.
생성자(Constructor)
생성자는 단어 그대로 개체를 생성하는 메서드입니다. 자동차 세계의 자동차 시동 걸기(자동차를 사용하기 바로 직전에 수행할 작업)를 예로 들 수 있으며, 코드 세계에서는 클래스 내에서 가장 먼저 실행되는 메서드입니다. 생성자는 클래스 내의 필드를 초기화하는 역할을 합니다. 하나 이상의 매개변수가 있는 생성자를 만들 수 있으며, 생성자를 사용하는 클래스를 인스턴스 클래스라고도 합니다.
소멸자(Desctructor)
앞에서 소멸자는 우리가 직접 사용할 일이 없다고 이야기했습니다. 자동차의 시동을 끄고 주차 또는 폐차의 개념을 갖는 게 소멸자입니다. 만약 호텔에서 주차 요원이 주차와 시동끄기를 무료로 해준다면 당연히 서비스를 받아야겠지요. 이에 해당하는 프로그래밍 엔진이 GC(가비지 수집기)입니다. 닷넷에는 이처럼 무료로 비싼 서비스를 제공하기에 사용자가 직접 주차나 시동 끄기 또는 폐차 등의 일을 할 필요가 없습니다. 코드 세계에서는 클래스 내에서 가장 마지막에 실행되는 메서드이고, GC(가비지 수집기) 엔진이 대신 수행하기에 클래스에 대한 메모리를 정리하는 등의 마무리 작업이 크게 필요하지 않습니다.
메서드(Method; 함수; Function/Sub Procedure)
메서드는 클래스에서 가장 자주 사용되는 구성 요소로, 클래스의 기능과 동작을 정의합니다. 자동차에서 전진(), 후진(), 좌회전()과 같은 동작이 메서드에 해당합니다. 코드에서는 메서드 오버로드를 통해 하나의 클래스 내에 동일한 이름의 메서드를 여러 개 생성할 수 있으며, 메서드 오버라이드를 통해 부모 클래스의 메서드를 새롭게 재정의할 수 있습니다.
또한, 메서드의 매개변수 전달 방식에는 값 전달이 기본이며, ref
, out
, in
, params
등의 키워드를 활용하여 다양하게 매개변수를 전달할 수 있습니다.
속성(Property)
속성은 자동차의 속성, 특징, 색상, 모양 등을 표현할 수 있습니다. 빨간색 스포츠카 형태로 표현할 수 있는거죠. 코드 세계에서는 외부에 공개(public)하고자 하는 부품(필드)을 나타낼 수도 있고 private
한 필드를 public
한 속성으로 외부에 공개할 때 사용됩니다.
인덱서(Indexer)
내가 만든 개체를 배열 형식으로 사용할 수 있도록 해주는 개념으로 자동차 세계에서의 카테고리/카탈로그 역할을 합니다. 코드에서는 속성을 배열형으로 표시해줍니다. 인덱서는 정수형 인덱스를 사용하는 것과 문자열 인덱스를 사용하는 방식 등으로 구분할 수 있습니다.
대리자(Delegate)
대리자는 단어 그대로 대신해주는 무엇입니다. 위임의 뜻을 가지는 대리자는 자동차 세계에서의 대리 운전에 비유하면 비슷할 수 있습니다. 즉, 내가 직접 운전하는 게 아닌 대리 운전 기사에게 "어디로 가주세요~"라고 미리 요청하면 정해진 순서대로 실행되는 원리와 같습니다. 코드 세계에서는 하나의 이름으로 여러 메서드를 묶어서 실행하는 구조이며, 이벤트를 만들어내기 위한 또 다른 중간 단계로 사용되기도 합니다.
2이벤트(Event)
이벤트는 사건 및 사고를 나타냅니다. 윈도 운영 체제의 여러 폼의 마우스 클릭 이벤트처럼 무엇인가 실행된 결과를 의미합니다. 다음 문장과 같이 비유적으로 표현할 수도 있습니다. “자동차 세계의 빨간색 속성을 가지는 자동차 개체가 과속이라는 메서드 동작 수행 결과 교통사고라는 이벤트가 발생합니다.”
네임스페이스(Namespace)
네임스페이스는 단어 그대로는 이름 공간이지만, 코드 내에서는 클래스 이름의 충돌 방지를 위해서 클래스를 묶어주는 개념으로 사용됩니다. 닷넷의 많은 명령어는 System 네임스페이스에 들어있고 카테고리별로 네임스페이스가 존재합니다.
자동차 세계에서는 자동차의 브랜드로 네임스페이스를 비교할 수 있습니다.
인터페이스(Interface)
인터페이스는 프로그램에 대한 표준 설계 역할을 합니다. 메서드 이름 등을 강제로 정의할 수 있고 큰 프로그램의 골격(뼈대)을 만들어주는 개념입니다. 인터페이스는 닷넷에서 다중 상속이 가능하도록 해주는 개념입니다. 자동차 세계에서는 자동차 회사는 달라도 자동차 주요 부품등은 공통적으로 사용할 수 있는 개념으로 봐도 좋습니다.
특성
특성은 클래스 자체에 대한 설명(메타데이터)을 붙이는 역할을 합니다. 자동차 세계에서는 튜닝과 같이 자동차를 꾸며주는 역할을 합니다.
개체 지향 프로그래밍의 4가지 큰 개념
개체 지향 프로그래밍(OOP)은 4가지 큰 핵심 개념을 가지는데, 추상화(abstraction), 캡슐화(Encapsulation), 상속(inheritance), 다형성(polymorphism)입니다. 캡슐화는 캡슐약처럼 그 안의 내용을 숨기는 것을 말하며, 상속은 부모의 재산을 자식에게 상속하여 자식에게 능력을 주는 것을 의미입니다. 다형성은 변신 로봇처럼 다양한 형태를 가질 수 있는 능력이며, 추상화는 인터페이스 또는 추상 클래스로부터 상속을 받아 구현하는 설계 관련 개념입니다.
캡슐화(Encapsulation)
우리가 지금까지 사용해온 필드는 특별한 경우가 아니면 private
을 사용하여 클래스 안에서만 사용해 왔습니다. 필드는 꽁꽁 숨기는 게 가장 좋은 개념입니다. 즉, 부품 역할을 하는 필드는 가루약을 캡슐에 넣어 관리하는 것처럼 프로그래밍에서도 필드를 꽁꽁 숨기는 개념이 바로 캡슐화입니다. 캡슐화는 구조화된 데이터 개체의 값 또는 상태를 숨겨서 권한이 없으면 접근하지 못하도록 차단하는 데 사용됩니다. 캡슐화의 또 다른 의미로는 연관된 자료 구조와 메서드(함수)를 한 테두리로 묶는 것을 말합니다. 이 강의에서의 원칙 중 하나는 필드는 private
이고 public
으로 외부에 공개할 때는 속성을 사용합니다.
상속(Inheritance)
부모 클래스의 멤버를 자식 클래스에서 재사용하는 개념을 상속이라고 합니다.
다형성(Polymorphism)
특정 클래스의 메서드는 매개 변수에 따라서 여러 가지 다양한 형태를 가질 수 있습니다. 즉 이러한 클래스의 특징을 다형성이라고 합니다.
캡슐화를 사용하여 좀 더 세련된 프로그램 만들기
캡슐화를 사용하여 좀 더 세련된 프로그램을 만들 수 있습니다. 앞서서 필드는 private
으로 해당 클래스 내에서 꽁꽁 숨겨야 한다고 몇 번이나 강조했습니다. 필드는 자동차의 부품 역할을 하는 것이기에 외부에 보이는 것보다는 내부에서만 사용되도록 명시하는 게 가장 좋기 때문입니다. 이를 프로그램 코드에도 적용하면 프로그램을 좀 더 세련되게 표현할 수 있습니다. 이번 예제에서는 필드를 만들고 외부에 공개할 때 public
한 메서드 또는 public
한 속성 등으로 외부에 공개하는 식으로 캡슐화를 구현하는 예제를 만들어 볼 것입니다.
캡슐화 사용에 대한 예제를 다음 내용을 입력한 뒤 실행해 보세요.
코드: EncapsulationNote.cs
//[?] 캡슐화(Encapsulation): 필드(예를들어 자동차 부품...)는 꽁꽁 숨겨라! => private
using System;
namespace EncapsulationNote
{
public class Person
{
//[1] 필드
private string name;
//[2] 메서드: public 메서드 또는 속성으로 외부에 공개
public void SetName(string n) => name = n;
public string GetName() => this.name;
}
class EncapsulationNote
{
static void Main()
{
//[A] person 개체 생성
Person person = new Person();
//[B] Set 메서드로 필드 설정
person.SetName("C#");
//[C] Get 메서드로 필드 공개
Console.WriteLine(person.GetName());
}
}
}
C#
Person
클래스의 인스턴스를 생성한 후에 name
필드에 접근하려 시도하면 name
필드는 private
으로 설정되었기에 다음과 같은 오류가 발생합니다.
> Person person = new Person();
> person.name = "Error";
(1,8): error CS0122: 보호 수준 때문에 'Person.name'에 액세스할 수 없습니다.
외부에서 필드에 접근하지 못하고, 필드의 값에 접근할 때는 public
메서드 또는 속성을 통해서만 접근하게 하여 클래스 내의 필드 값을 보호할 수 있습니다.
다형성 기법을 통한 프로그램의 융통성 높이기
개체 지향 프로그래밍의 다형성을 사용하면 프로그램의 융통성이 높아집니다. 여기서 융통성이라 하면, 한 번 만들어 놓고 여러 경우에 대비해서 처리할 수 있는 기법을 제공한다는 것을 말합니다. 이번에는 다형성을 사용해 보겠습니다.
다음 내용을 입력한 뒤 실행해 보세요.
코드: PolymorphismDemo.cs
// 다형성: 여러가지 형태를 띄다... ->
// 메서드 오버라이드(Override;다시정의/재정의) <-> 오버로드(여러번 정의)
using System;
namespace PolymorphismDemo
{
//[1] Animal 클래스: 추상 클래스 및 기본 클래스
public abstract class Animal
{
// 동물들은 '울다'라는 기능이 있어야 한다고 명세
public abstract string Cry();
}
//[2] Dog 클래스
public class Dog : Animal
{
public override string Cry() => "멍멍멍";
}
//[3] Cat 클래스
public class Cat : Animal
{
public override string Cry() => "야옹";
}
//[4] Trainer 클래스
public class Trainer
{
public void DoCry(Animal animal)
{
// 뭐가 실행? Dog? Cat? => 모른다(컴파일시점),
// 언제? 런타임에 Dog/Cat을 알 수 있다.
Console.WriteLine("{0}", animal.Cry()); // 다형성:dynamic(동적)
}
}
class PolymorphismDemo
{
static void Main(string[] args)
{
//[A] 기본 개체 생성 방법
Console.WriteLine((new Dog()).Cry());
Console.WriteLine((new Cat()).Cry());
//[B] 부모 클래스 변수로 개체 생성
Animal dog = new Dog();
Console.WriteLine(dog.Cry());
Animal cat = new Cat();
Console.WriteLine(cat.Cry());
//[C] 다형성 테스트:
// 동일한 Cry 메서드를 호출하지만,
// 넘겨준 메시지에 따라서 서로 다른 유형의 기능 구현
// 그러한 다형성은
// 메서드 오버라이드(override;재정의/다시정의)를 통해서 구현한다.
Trainer trainer = new Trainer();
trainer.DoCry(new Dog());
trainer.DoCry(new Cat());
}
}
}
멍멍멍
야옹
멍멍멍
야옹
멍멍멍
야옹
①번 코드의 Animal
클래스는 다른 클래스의 부모 클래스 역할을 하는 추상 클래스로 만들었습니다. 추상 클래스 안에 정의된 추상 메서드는 따로 메서드 본문을 작성하지 않고 메서드에 대한 시그니처를 제공하는 목적이 큽니다. 인터페이스와 추상 클래스는 다른 클래스에게 상속을 주어 특정 메서드를 구현하도록 강제하는 역할을 합니다.
④번 코드에서 Trainer
클래스의 DoCry()
메서드는 매개변수로 부모 클래스인 Animal
형식을 받습니다. 부모 클래스 형식으로 매개변수가 설정되면 자식 클래스의 인스턴스를 받을 수 있습니다. 이때 컴파일 시점에서는 어떤 클래스의 인스턴스가 넘어올지 모르므로 실제 코드는 동적으로 런타임할 때 어떤 Cry()
메서드가 실행될지 알 수 있습니다.
이러한 형태가 바로 다형성입니다. 다형성을 사용하면 이처럼 넘어오는 개체를 공통 형식으로 받지만 해당 개체의 메서드를 호출할 수 있는 융통성을 가지게 됩니다.
클래스의 멤버 종합 연습: 자동차 클래스 구현하기
이번에는 자동차 클래스를 만들고 클래스의 주요 멤버를 모두 사용하는 예제를 만들어 보면서 전체 키워드를 정리하는 연습을 해 보겠습니다. 코드에 있는 #region
과 #endregion
은 다음 예제 다음 단락에서 바로 이어서 설명할 것입니다.
일단 다음 내용을 입력한 뒤 실행해 보세요.
코드: CarWorld.cs
using System;
using System.Collections;
/*
*[1] 네임스페이스: 클래스명 충돌 방지
*/
namespace CarWorld
{
//[2] 인터페이스: 표준, 다중상속
interface IStandard { void Run(); }
/// <summary>
/// [3] 클래스: 설계도
/// </summary>
class Car : IStandard
{
#region [4] 필드: Private Member Variables
private string name; // 필드 : 부품
private string[] names; // 배열형 필드
private readonly int _Length; // 읽기전용 필드
#endregion
#region [5] 생성자: Constructors
public Car()
{
this.name = "좋은차"; // 필드를 기본값으로 초기화
}
public Car(string name) // 생성자 : 시동, 필드 초기화
{
this.name = name;
}
public Car(int length)
{
this.Name = "좋은차";
_Length = length; // 읽기전용 필드는 생성자에 의해서 초기화 가능
names = new string[length]; //넘겨온 값으로 요소생성
}
#endregion
#region [6] 메서드: Public Methods
// 메서드 : 기능/동작
public void Run() => Console.WriteLine("{0} 자동차가 달립니다.", name);
#endregion
#region [7] 속성: Public Properties
public string Name // 속성 : private필드->외부공개
{
get { return name; }
set { name = value; }
}
public int Length { get { return _Length; } }
#endregion
#region [8] 소멸자: Destructor
~Car() // 소멸자 : 폐차, 만들어진 객체 소멸될 때
{
Console.WriteLine("{0} 자동차가 폐차됨.", name);
}
#endregion
#region [9] 인덱서: Indexer
public string this[int index] // 인덱서:카탈로그 화
{
get { return names[index]; }
set { names[index] = value; }
}
#endregion
#region [10] 이터레이터: Iterators
public IEnumerator GetEnumerator() // 반복기
{
for (int i = 0; i < _Length; i++)
{
yield return names[i];
}
}
#endregion
#region [11] 대리자: Public Delegates
public delegate void EventHandler(); // 대리자:다중메서드호출
#endregion
#region [12] 이벤트: Public Events
public event EventHandler Click; // 이벤트
#endregion
#region [13] 이벤트 처리기: Event Handlers
public void OnClick() // 이벤트 핸들러
{
if (Click != null)
{
Click();
}
}
#endregion
}
class CarWorld
{
static void Main()
{
//[A] 클래스, 생성자, 메서드 테스트
Car campingCar = new Car("캠핑카");
campingCar.Run(); // 캠핑카 자동차가 달립니다.
//[B] 속성 테스트
Car sportsCar = new Car();
sportsCar.Name = "스포츠카";
sportsCar.Run(); // 스포츠카 자동차가 달립니다.
//[C] 인덱서 테스트
Car cars = new Car(2);
cars[0] = "1번 자동차";
cars[1] = "2번 자동차";
for (int i = 0; i < cars.Length; i++)
{
Console.WriteLine(cars[i]);
}
//[D] 이터레이터 테스트
foreach (string name in cars)
{
Console.WriteLine(name);
}
//[E] 대리자, 이벤트, 이벤트 처리기 테스트
Car btn = new Car("전기자동차");
btn.Click += new Car.EventHandler(btn.Run);
btn.Click += new Car.EventHandler(btn.Run);
btn.OnClick();
}
}
}
캠핑카 자동차가 달립니다.
스포츠카 자동차가 달립니다.
1번 자동차
2번 자동차
1번 자동차
2번 자동차
전기자동차 자동차가 달립니다.
전기자동차 자동차가 달립니다.
전기자동차 자동차가 폐차됨.
캠핑카 자동차가 폐차됨.
좋은차 자동차가 폐차됨.
스포츠카 자동차가 폐차됨.
상당히 긴 이번 예제는 지금까지 배우고 이전 강의에서 정리했던 여러 키워드를 사용했습니다.
전처리기 지시문(Preprocessor Directive)
앞선 예제에서 미리 사용해 본 전처리기 지시문인 #region
과 #endregion
은 프로그램 코드에 주석처럼 입력하여 특별한 효과를 거두는 코드 블록을 말합니다. 이 중에서 우리가 사용할 만한 코드는 긴 코드를 단일 블록으로 표시하고자 할 때 사용하는 #region
과 #endregion
코드를 들 수 있습니다. 이는 프로그램 실행과는 전혀 무관하지만 비주얼 스튜디오와 같은 편집기 내에서는 그 효과를 충분히 거둘 수 있습니다.
다음 그림과 같이 전처리기 지시문 영역(preprocessor directive)은 한 줄로 줄여 표현할 수 있습니다.
그림: 전처리기 지시문으로 코드를 블록으로 관리하기
장 요약
개체 지향 프로그래밍의 목적은 프로그램을 좀 더 세련되게 하고, 사용의 융통성(flexibility, 유연성)을 높이고, 재사용성을 높이는 데 있습니다. 이번 장을 통해서 캡슐화, 상속, 다형성에 대한 그 의미를 예제로 기억해 두고 C# 기초를 넘어서 데스크톱, 웹, 모바일 등으로 학습을 확장해 나가면서 개체 지향 프로그래밍 및 개발 패턴들에 대해 정리해 나가기 바랍니다.