프로그래밍 일기 — C#을 알아보자 2

배우는 자(Learner Of Life)
22 min readJan 6, 2024

--

C#을 더 깊게 파본다.

C#의 더 깊숙히 숨겨진 보물을 파헤친다(1).

지난 주까지 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)하여 어플리케이션을 조금 더 효율적으로 관리할 수 있다.

추가 참고 자료

추상화

인터페이스

정적클래스

확장메서드

부분클래스/메서드

참조:

(1) https://res.cloudinary.com/practicaldev/image/fetch/s--2XdEnCAM--/c_imagga_scale,f_auto,fl_progressive,h_900,q_auto,w_1600/https://raw.githubusercontent.com/sandeepkumar17/td-dev.to/di-collection-posts/assets/blog-cover/c-sharp.png

(2) https://eightify.app/summary/gaming/clean-architecture-mastering-dependency-injection

--

--

배우는 자(Learner Of Life)

배움은 죽을 때까지 끝이 없다. 어쩌면 그게 우리가 살아있다는 증거일지도 모른다. 배움을 멈추는 순간, 혹은 배움의 기회가 더 이상 존재하지 않는 순간, 우리의 삶은 어쩌면 거기서 끝나는 것은 아닐까? 나는 배운다 그러므로 나는 존재한다. 배울 수 있음에, 그래서 살아 있음에 감사한다.