프로그래밍 일기 — C#을 알아보자 2
C#을 더 깊게 파본다.
지난 주까지 C#의 기초를 닦았다. 그러나 C#에는 아직 내가 충분히 발견하지 못한 보물들이 산적하다. 마치 내가 영어를 수십년간 배워왔어도, 영문 원서를 읽을 때마다 새로운 표현과 단어들을 만나는 것처럼, 컴퓨터의 언어 역시 깊게 들어가자면 그 수면의 깊이는 태평양 한 가운데있는 바다만큼 깊다는 것을 느낀다.
무엇보다도 지난 한 주간, 얼마전 참여한 지난 백엔드 부트캠프 과정에서 배웠던 Java언어의 특징들과 객체지향개념(상속, 클래스, 생성자, 인터페이스, 추상화 등등)에 대해 다시 리뷰해 볼 수 있었고, C#이 정말 많은 패러다임을 Java와 공유한다는 사실을 느꼈다. 정말 흥미로운 것은 변수를 선언하는 것부터 이러한 객체지향 개념들을 활용하는 것 까지 Java와 아직까지는 큰 차이를 느끼지 못했다는 것이다. Java를 배우려는 노력이 결코 헛되지 않은 것 같아서 참 다행이라고 생각했다. 그와 동시에, 내가 아직 이 언어를 충분히 탐험하지 못했기 때문에 차이점을 느끼지 못한 것은 아닌가하는 의심도 들었다.
그렇다면 내가 아직 발견하지 못한 C#의 중요 정보들은 무엇인가? 이번 글에서는 C#의 드넓은 바다를 조금 더 깊이 탐험해보려한다.
C# 심화학습
Polymorphism(다형성)
- 간단하게 말해서, 하나의 작업이 여러 다른 방식으로 수행될 경우를 말한다.
- “여러가지 형태를 갖는 성질"이라는 그리스어에서 유래되었다.
- 부모의 멤버매서드와 같은 이름을 가지면서도, 다른 수와 타입의 매개변수(Parameter)를 갖는 메서드를 생성하는데 활용된다.
- 컴파일 타임 다형성(Compile-time Polymorphism)/ 정적다형성(Static Polymorphism)혹은 런타임다형성(Runtime Polymorphism)/동적다형성(Dynamic Polymorphism)으로 구현된다.
컴파일 타임 다형성
- 컴파일러는 어떤 메서드가 컴파일 타임에서 호출되는지 식별한다.
메서드 오버로딩(Method Overloading): 클래스와 다른 수의 매개변수를 갖거나, 다른 타입의 매개변수를 가지면 동명의 메서드를 만들 수 있다. 메서드 오버로딩은 early binding 혹은 static binding이라고 불리기도한다. 런타임보다 이른 컴파일 타임에서 어떤 메서드가 호출되는지 결정된다. C#에서는 메서드, 생성자, 인덱싱된 속성(indexed properties)들이 오버로드될 수 있는데, 이는 이러한 멤버들이 매개변수만을 갖기 때문이다.
- 생성자 오버로딩(Constructor Overloading)을 통해서도 구현할 수 있다.
메서드 오버라이딩(Method Overriding): 자녀클래스는 부모클래스와 똑같은 메서드를 정의한다. 다만 런타임 다형성을 구현하기 위한 방법이며, 부모클래스가 제공하는 메서드를 구현할 수 있도록 해준다. 부모클래스 메서드와
virtual
키워드, 그리고 자녀클래스 메서드와override
키워드를 사용한다.
// Example 1 - 메서드 오버로딩을 통한 다형성 예제
class ConsolePrinter
{
// 메서드 1
public void Print(string str){
System.Console.WriteLine(str);
}
// 메서드 2
public void Print(string str1, string str2){
System.Console.WriteLine($"{str1}, {str2}");
}
// 메서드 3
public void Print(string str1, string str2, string str3){
System.Console.WriteLine($"{str1}, {str2}, {str3}");
}
}
// Example 2 - 메서드 오버라이딩을 통한 다형성 예제
class Animal
{
// 부모클래스의 메서드
public virtual void MakeSound() // 자녀클래스가 override하도록 허용
{
System.Console.WriteLine("The animal makes a sound");
}
}
class Dog: Animal
{
// 자녀클래스의 메서드
public override void MakeSound() // override를 통해 자녀클래스의 메서드는 다르게 구현될 수 있다.
{
System.Console.WriteLine("The dog barks");
}
}
추상화 클래스(Abstract Class)
- 인스턴스화(instantiate)될 수 없는 클래스이다.
- 자녀클래스로 하여금 상속할 수 있는 부모클래스로써 활용된다.
- 자녀클래스가 가져야할 공통 속성을 정의하기 위해 사용된다.
abstract
키워드를 사용해 생성한다.- 추상화 클래스는 추상화 메서드(내용이 없는 메서드)와 비추상화 메서드(내용이 있는 메서드) 두 형태를 모두 취할 수 있다. 비추상화 메서드는 일반 메서드라 볼 수 있다.
추상화 메서드(Abstract Method)
- 내용이 없는 메서드로써,
abstract
키워드로 정의한다. - 추상화 클래스에서 선언(declared)되었으나, 부모클래스에서 정의(defined)되지 않았으며, 자녀클래스에서 구현(implemented)된 메서드를 말한다.
// Example 1 - 추상화 클래스
// Test.cs(추상화 클래스)
abstract class Test{
// 필드값과 메서드 정의
}
// Program.cs
// 다음과 같은 에러 메시지 표시: // Cannot create an instance of the abstract type or interface 'Test' [coreConsoleProject]csharp(CS0144)
Test obj = new Test(); // 추상메서드는 인스턴스화 불가
// Example 2 - 추상화 클래스
abstract class Test{
// 추상화 메서드
public abstract void display1(); // 반드시 추상화 메서드 내에서만 정의된다.
// 비추상화 메서드
public void display2(){
System.Console.WriteLine("Non-abstract method");
}
}
// Example 3 - 추상화 클래스 & 추상화 메서드 & 메서드 오버라이딩\
// BankAccount.cs(추상화 클래스)
namespace coreObjectOrientedConcepts
{
// 추상화 클래스
public abstract class BankAccount
{
// 비추상화 메서드(추상화 클래스 멤버 메서드)
public void getMessage()
{
System.Console.WriteLine("Welcome to ABC Bank!!");
}
// 추상화 메서드
public abstract void deposit();
public abstract void withdraw();
public abstract void balance();
}
}
// SavingAccount.cs(자녀클래스)
namespace coreObjectOrientedConcepts{
public class SavingAccount: BankAccount
{
// 메서드 오버라이딩
public override void balance()
{
System.Console.WriteLine("Balance in Saving Account.");
}
// 메서드 오버라이딩
public override void deposit()
{
System.Console.WriteLine("Deposit in Saving Account.");
}
// 메서드 오버라이딩
public override void withdraw()
{
System.Console.WriteLine("Withdraw from Saving Account.");
}
}
}
// Program.cs
using coreObjectOrientedConcepts;
// 자녀클래스는 인스턴스화 가능하다.
SavingAccount savingAccount = new SavingAccount();
savingAccount.deposit();
savingAccount.withdraw();
savingAccount.balance();
// 추상화 클래스는 직접 인스턴스화 불가
// BankAccount bankAccount = new BankAccount(); // 에러 출력
// 추상화클래스의 멤버 속성을 상속받은 자녀클래스를 통해 추상화클래스의 멤버 변수 및 메서드 출력 가능
savingAccount.getMessage();
>>
Deposit in Saving Account.
Withdraw from Saving Account.
Balance in Saving Account.
Welcome to ABC Bank!!
인터페이스(Interface)
- 추상화 클래스와 비슷하지만, 모든 메서드가 추상적이라는 차이점이 있다. 즉, 모든 메서드가 Body를 갖지 않는다.
interface
라는 키워드를 사용해 생성한다.- 인터페이스 파일 이름은
I
로 시작한다(예:IBankAccount.cs
). - 인터페이스 내 접근제어자를 사용할 수 없다.
- 인터페이스의 모든 멤버는 기본(default)적으로
public
이며, 추상화(abstract)되어있다. - 인터페이스는 필드값을 갖지 않는다. 데이터에 대한 특정한 구현(implementation)을 표현하는 것이 목적이기 때문이다.
- 클래스가 반드시 수행해야할 것을 정의하며, 어떻게 수행해야할지는 정의하지 않는다.
- 인터페이스는
private
멤버를 가질 수 없다. - 다중상속(multiple inheritance)을 가능하게 해 두 개 이상의 부모클래스로부터 상속을 받을 수 있게 해준다. 클래스만으로는 다중상속을 구현할 수 없다(클래스는 오직 단일상속만 가능).
- 인터페이스는 오직 다른 인터페이스만 확장(extend)할 수 있다. 다른 추상화 메서드로부터 상속받는 개념이 아니다.
- 인터페이스는 추상화 메서드와는 달리 상수(constant)나 정적 멤버(static members or methods)를 가질 수 없다.
- 추상화 메서드는 생성자를 갖지만, 인터페이스는 갖지 않는다.
장점
- loose coupling(유연한 연결)상태를 가능하게하여, 코드의 유지 보수성을 높인다.
- 컴포넌트 기반 프로그래밍이 가능하게한다. 유연한 연결을 통해 한 부분에 대한 수정이 다른 부분에 대한 수정을 필요로하는 것을 막아준다.
- 완전한 추상화(total abstraction)를 가능하게 해준다.
- 다중상속(multiple inheritance)및 다중추상화(multiple abstraction)를 가능하게한다.
- 어플리케이션을 구현하는데 있어 아키텍쳐(architecture)의 개념을 활용할 수 있게 해준다. 예를 들어 의존성주입(Dependency Injection)은 사용되는 특정 구현형태를 알지 못해도, 주석(Annotation)을 활용해 여러 가지의 인터페이스 구현형태를 활용할 수 있게 해준다. 자주 활용되거나 다양한 형태의 구현 방식이 필요할 경우, 쉽게 특정 구현방식을 만들어 낼 수 있다. 하나의 코드에서 모든 것을 처리하는 방식이 아닌 특정 기능을 수행하는 컴포넌트를 분리시켜 활용하는 개념이기에, 유지 보수성을 매우 높인다. 정돈된 클린 아키텍쳐(clean architecture)를 지향하는 방식이므로, 비즈니스 로직을 쉽게 수정할 수 있도록 유연성도 높아진다(참조 (2)).
// 인터페이스 예제
// 기본 문법
interface Rectangle{
// method without body
void calculateArea(); // 모든 메서드는 기본적으로 public abstract이다.
}
// IBankAccount.cs
// 인터페이스 2개를 사용하여 다중추상화(Multiple Abstraction) 구현
namespace coreObjectOrientedConcepts
{
// 인터페이스 1
interface IManageBank
{
void openAccount();
void closeAccount();
}
// 인터페이스 2
internal interface IBankAccount{
void deposit();
void withdraw();
void balance();
}
// 다중상속을 구현한 클래스(인터페이스 1과 2로부터 모두 상속)
public class SavingAcc : IBankAccount, IManageBank
{
// 인터페이스 2의 메서드
public void balance()
{
System.Console.WriteLine("Balance in Saving Account.");
}
// 인터페이스 1의 메서드
public void closeAccount()
{
System.Console.WriteLine("Closing Saving Account");
}
// 인터페이스 2의 메서드
public void deposit()
{
System.Console.WriteLine("Deposit in Saving Account.");
}
// 인터페이스 1의 메서드
public void openAccount()
{
System.Console.WriteLine("Opening Saving Account");
}
// 인터페이스 2의 메서드
public void withdraw()
{
System.Console.WriteLine("Withdraw from Saving Account.");
}
}
}
// Program.cs(메인프로그램에서 자녀클래스 인스턴스화 및 구현된 메서드 호출)
SavingAcc savingAcc = new SavingAcc();
savingAcc.openAccount();
savingAcc.deposit();
savingAcc.withdraw();
savingAcc.balance();
savingAcc.closeAccount();
>>
Opening Saving Account
Deposit in Saving Account.
Withdraw from Saving Account.
Balance in Saving Account.
Closing Saving Account
정적 클래스(Static Class)와 정적 메서드(Static Method)
- 인스턴스화할 수 없는 클래스를 말한다. 따라서 객체로 만들 수 없다.
- 객체를 통해 정적 멤버(static member)들에 접근할 수 없다.
- C# static class는 인스턴스의 생성자를 포함할 수 없다.
static
키워드가 클래스 이름과 접근제어자 사이에 들어간다.
// 정적 클래스
// 기본 문법
static class classname
{
//static data members
//static methods
}
// 예제 1 - Calculator.cs
// 모든 멤버들이 static이다.
public static class Calculator
{
private static int _resultsStorage = 0;
public static string Type = "Arithmetic";
public static int Sum(int num1, int num2)
{
return num1 + num2;
}
public static void Store(int result){
_resultsStorage = result;
}
}
// 예제 2 - Calculate.cs
namespace coreObjectOrientedConcepts
{
static class Calculate
{
// static variables
static int count = 0;
static Calculate()
{
// count 값 초기화
count = 0;
}
// static methods
public static int increment(){
count++; // 메서드 호출시 count 값을 1 증가
return count;
}
public static int decrement(){
count--; // 메서드 호출시 count 값을 1 감소
return count;
}
}
}
// Program.cs
// Calculate calculate = new Calculate(); // 정적 클래스는 인스턴스화 불가(에러 출력)
// Calculate 정적 클래스의 increment 멤버 메서드 호출
System.Console.WriteLine(Calculate.increment());
// Calculate 정적 클래스의 decrement 멤버 메서드 호출
System.Console.WriteLine(Calculate.decrement());
>>
1
0
장점
- 비정적 멤버(non-static member)를 선언하거나 정적 클래스를 인스턴스화할때 에러를 컴파일시 출력하여 트러블슈팅이 용이하다.
- 정적 멤버는 클래스 이름으로 직접 접근이 가능하다.
- 정적 클래스 정의 시
static
키워드가 클래스 키워드에 앞서 선언되므로 명확하게 알 수 있다. - 정적 클래스 멤버는 인스턴스화없이 바로
{클래스 이름}.{멤버 이름}
을 통해 접근 가능하므로 사용이 편리하다.
확장 메서드(Extension Methods)
- 새로운 자녀클래스를 생성하지 않고 기존 클래스에 새로운 메서드를 생성 및 추가하는 개념이다.
- 정적 메서드의 특별한 타입으로써 인스턴스 메서드(instance methods)라고 불리기도한다.
- 미리정의된 클래스(Pre-defined Class)및 유저생성클래스(User-custom class)에 모두 적용할 수 있다.
- 확장 메서드는 정적 메서드(static methods)여야한다. 즉 정적 클래스(Static Class)에서 사용될 수 있다.
- 클래스 이름과 관련된 키워드를 가져야한다.
- 매개변수 리스트에서 클래스 이름은 반드시 첫 번째 매개변수여야하며 이 매개변수는
this
키워드를 갖는다.( 이 키워드를 가진 변수에 묶인다.).
// 확장메서드 예제
// IntExtension.cs
// 확장 함수를 포함하는 정적 클래스 정의
static class IntExtensions // Program.cs에서 사용하려는 정적 클래스가 int이므로 클래스 이름과 관련된 키워드를 갖는다.
{
// 확장 함수 정의
// 확장 함수는 this 키워드를 가진 변수에 묶인다.
public static bool IsGreaterThan(this int i, int value){
// i 값이 value보다 크면 True 아니면 False 리턴
return i > value;
}
}
// Program.cs
// 기본형자료 int(정적 클래스)의 인스턴스 정의
int number = 100;
// int 타입 정적 클래스의 인스턴스에서 확장 함수 호출
bool result = number.IsGreaterThan(1000);
// 결과 출력
System.Console.WriteLine(result);
>>
False
부분 클래스(Partial Class)
- C#만의 고유한 특징이다.
- 클래스, struct, 메섣, 인터페이스가 여러 .cs파일에 별도로 구현될 수 있다.
- 프로그램 컴파일시 여러 .cs 파일의 구현을 컴파일러가 혼합할 수 있다.
partial
키워드가 사용된다.
부분 메서드(Partial Methods)
- 클래스의 한 부분이 메서드의 시그니쳐(signature)를 포함한다.
- 같은 부분 혹은 다른 부분에서 부가적인 구현(optional implementation)이 정의될 수 있다.
- 구현이 정의되지 않으면, 메서드 및 호출은 컴파일 타임에서 삭제된다.
partial
키워드를 사용하며 리턴 타입은void
이다.- 일반 메서드이거나 정적 메서드(static methods)일 수 있다.
// 부분 클래스(Partial Class) & 부분 메서드(Partial Method) 예제
// 기본 문법(syntax) - 부분 클래스
public partial Class_name
{
// code
}
// 기본 문법(syntax) - 부분 메서드
partial void method_name
{
// code
}
// EmployeeProps.cs - Employee의 부분 클래스
namespace coreObjectOrientedConcepts
{
// 부분 클래스 정의
internal partial class Employee
{
public int EmpId;
public string? EmpName; // 부가(optional) string 변수
public partial void DisplayDetails(); // 부분 메서드 정의(시그니처)
}
}
// EmployeeMethods.cs - Employee의 부분 클래스
namespace coreObjectOrientedConcepts
{
// 부분 클래스 정의
internal partial class Employee
{
// 부분 메서드 정의(구현내용)
public partial void DisplayDetails() {
System.Console.WriteLine("Employee Id: " + this.EmpId);
System.Console.WriteLine("Employee Name: " + this.EmpName);
}
}
}
// Program.cs
var employee = new Employee(); // 부분클래스 인스턴스화
System.Console.WriteLine(employee.EmpId); // 부분클래스 변수 출력
System.Console.WriteLine(employee.EmpName); // 부분클래스 부가 변수 출력
employee.DisplayDetails(); // 부분메서드 출력
>>
Employee Id: 0
Employee Name:
장점
- 여러 개발자가 동시에 같은 클래스내에서, 서로 다른 파일에서 작업이 가능하다.
- 예를 들어 UI와 디자인 파일을 분리하여 코드를 읽거나 이해할 수 있다.
- 자동으로 생성된 코드로 작업한다면, Visual Studio에서는 소스파일을 재생성할 필요없이 코드가 클래스에 추가될 수 있다.
- 큰 클래스를 조금 더 작게 압출(compress)하여 어플리케이션을 조금 더 효율적으로 관리할 수 있다.
추가 참고 자료
추상화
- https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract
- https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members
인터페이스
- https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface
- https://www.geeksforgeeks.org/difference-between-abstract-class-and-interface-in-c-sharp/
정적클래스
확장메서드
부분클래스/메서드
참조:
(2) https://eightify.app/summary/gaming/clean-architecture-mastering-dependency-injection