프로그래밍 일기 — 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)
배우는 자(Learner Of Life)

Written by 배우는 자(Learner Of Life)

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