C# 이란 무엇인가?
- MS에서 개발한 객체 지향 프로그래밍 언어
- .NET 프레임 워크와 함께 도입되었으며, 안전하고 현대적인 소프트웨어 개발을 위해 설계
- JAVA C++과 유사한 문법 구조를 가짐
struct(구조체) 와 class 의 주요 차이점
- struct는 값 타입으로 stack에 저장, class는 참조 타입으로 heap에 저장
- Value Type : struct, enum, int, float, double, bool, char
- Reference Type : class, interface, delegate, string, array
- struct는 상속 지원을 하지 않음, 하지만 class 는 상속을 지원
- struct는 매개변수 없는 생성자를 가질 수 없는데 반해, class는 매개변수 없는 생성자를 가질 수 있음
char Type / string Type
정의
char
- 16bit 유니코드 문자를 나타내는 값
- 단일 문자만을 나타낼 수 있음
string
- 문자의 시퀀스를 나타내는 참조 타입
- 여러문자로 구성된 텍스트를 나타낼 때 사용
리터럴 표현
char
- 작은 따옴표 (
'
)를 사용하여 리터럴을 정의 - ‘A’, ‘1’, ‘%’
string
- 큰 따옴표 (
"
)를 사용하여 리터럴을 정의 - “Hello”, “C#”
길이
- char : 항상 길이가 1
- string : 0 에서 여러 개의 문자로 구성 됨
불변성
- char : 값 타입으로 한 번 할당되면 변경할 수 없다
- string : 불변 => 문자열을 수정하며 새로운 인스턴스가 생성됨
사용영역
char
- 개별 문자를 처리하거나 분석할 때 사용
- 문자가 숫자인지 알파벳인지 확인할 때 사용될 수 있음
string
- 텍스트 데이터를 저장하고, 저장할 때 사용
- 사용자의 이름, 주소, 메시지 등을 저장하거나 출력할 때 사용
Value Type과 Reference Type이 다른 영역을 사용하는 이유
메모리 할당 및 해제의 효율성
스택 (Stack)
- 스택은 LIFO(Last-In-First-Out) 방식으로 동작하는 데이터 구조입니다.
- 값 타입은 스택에 저장되는데, 이는 함수나 메서드의 호출이 끝나면 자동으로 그 범위(scope) 내의 로컬 변수들이 메모리에서 제거되기 때문입니다.
- 이러한 자동 메모리 관리는 매우 효율적입니다.
힙 (Heap)
- 참조 타입은 힙에 저장되는데, 힙은 메모리의 동적 할당 및 해제를 위한 영역입니다.
- 객체의 생명 주기는 코드의 특정 범위나 함수의 실행 시간과 연관되지 않습니다.
- 따라서, 가비지 컬렉터가 필요하게 되어 이 영역의 메모리를 관리합니다.
메모리 접근의 속도
- 스택 메모리에 접근하는 것은 힙 메모리에 접근하는 것보다 일반적으로 빠릅니다.
- 값 타입이 대체로 작고, 고정된 크기를 가지므로 스택에서의 할당과 접근이 빠르고 예측 가능하게 됩니다.
생명 주기 관리
- 참조 타입은 사용되지 않을 때 가비지 컬렉터에 의해 메모리에서 제거됩니다.
- 이는 참조 타입의 생명 주기가 더 복잡하기 때문입니다.
- 반면, 값 타입의 생명 주기는 해당 변수의 범위와 연관되어 있어 별도의 관리가 필요하지 않습니다.
값의 할당 및 복사
- 값 타입은 변수에 할당될 때 값 자체가 복사됩니다.
- 이러한 특성은 스택의 구조와 잘 맞습니다.
- 반면, 참조 타입은 참조(주소)가 복사되므로 실제 객체의 데이터를 관리하기 위해 힙을 사용합니다.
.NET에서는 값 타입과 참조 타입을 각기 다른 메모리 영역에 저장하도록 설계되었습니다. 이러한 분리는 메모리 관리 및 성능 최적화의 측면에서 중요한 역할을 합니다.
Interface의 역할
- interface는 메서드, 속성, 이벤트, 인덱서 등의 선언만 포함할 수 있으며 구현을 포함할 수 없음
- class나 구조체는 여러 인터페이스는 동시에 구현할 수 있음
- interface는 다중 상속의 문제점을 해결하면서, 여러 가지 기능을 한 class에 추가할 수 있게 도와줍니다.
Garbage Collection 의 역할
- .NET 프레임워크의 일부로, 메모리 관리를 자동화 함
- 더 이상 사용되지 않는 객체를 자동으로 감지하고, 그 메모리를 회수
- 메모리 누수 문제를 피할 수 있음
세대별 GC
0세대 : GC를 한번도 겪지 않은 갓 생성된 객체가 대상
1세대 : GC를 1회 겪은 객체가 대상
2세대 : GC를 2회 이상 겪은 객체가 대상(전체를 의미)`
- 세대가 낮은 메모리부터 메모리 해제를 해준 다음 메모리 컴펙션을 해준다.
- 2세대 GC를 할 시 Full Garbage Collection이라 하고 전체 Heap에 대하여 GC하는 것을 의미한다.
- 세대를 나누는 근거
최근에 생성된 객체일수록 생명주기가 짧을 가능성이높고, 오래된 객체일수록 생명주기가 길 가능성이 높습니다.
최근에 생성된 객체끼리는 서로 연관성이 높을 수 있으며, 비슷한 시점에 자주 액세스 됩니다.
일부분 Heap에 대해 GC를 하는 것이 전체 GC를 하는 것 보다 빠릅니다.
LOH
- Large Object Heap으로 CLR(Common Language Runtime)에서 용량이 큰(83KB이상) 객체에 사용되는 Heap
SOH
- 그 이하 평소에 사용되는 Heap
- LOH는 GC시 2세대로 간주하고, 메모리 해제 후 메모리 컴펙션을 진행하지 않으므로, 메모리 내부 단편화가 발생할 수 있다.
컴팩션 (Compaction)
데이터베이스 컴팩션
- 데이터베이스에서 레코드가 삭제되거나 변경될 때, 해당 데이터베이스 파일에는 여전히 사용되지 않는 공간 (또는 “프래그먼트”)이 남을 수 있음
- 컴팩션은 이러한 프래그먼트를 제거하고 데이터베이스 파일의 크기를 줄이는 과정
힙 메모리 컴팩션 :
- 가비지 컬렉션을 사용하는 프로그래밍 언어에서, 객체가 메모리에서 해제될 때 메모리의 힙 영역에는 사용되지 않는 공간이 남을 수 있음
- 컴팩션은 이러한 공간을 재구성하여 연속적인 메모리 영역을 만드는 과정입니다.
컴팩션은 시스템의 성능을 향상시키고, 사용되지 않는 리소스를 효율적으로 회수하는 데 중요한 역할을 합니다.
Acync / Await의 용도
- C# 에서 비동기 프로그래밍을 지원하기 위해 도입
- async는 비동기적으로 실행될 수 있음을 나타냄
- await는 비동기 작업의 완료를 기다림
- 이를 통해 UI 스레드의 차단을 피하거나 백그라운드 작업을 쉽게 수행할 수 있음
delegate / event 의 차이
delegate
- delegate는 ‘타입’ 으로, 특히 참조 타입
- 특정 시그니처를 갖는 메서드를 참조하는 타입
- delegate는 메서드의 참조를 보관하며, 이를 사용하여 메서드를 호출할 수 있습니다.
- 여러 메소드를 참조할 수 있음 (다중 캐스팅)
event
- event는 delegate 타입의 변수에 특별한 접근 제한을 부여
- 클래스나 구조체 외부에서 직접 접근은 불가
- event는 구독자가 추가되거나 제거될 때만 접근 가능 (+=. -=)
IEnumerable / IQueryable의 차이
정의 및 사용처
IEnumerable
- System.Collection 네임스페이스에 정의, 메모리 내 컬렉션의 데이터를 반복하는 기능을 제공
IQueryable
- System.Linq 네임스페이스에 정의, 데이터베이스와 같은 원격 데이터 소스에 대한 쿼리를 평가하고 최적화하는 기능을 제공
작동 방식
IEnumerable
- LINQ to Objects 쿼리는 메모리에서 실행됩니다. 쿼리는 모든 데이터를 메모리로 가져온 후 처리
IQueryable
- 쿼리가 데이터베이스에 최적화된 형태 (예: SQL)로 변환되어 데이터베이스에서 실행됩니다. 이로 인해 필요한 데이터만 가져오게 됨
지연 로딩 (Lazy Loading)
IEnumerable
- 데이터베이스에서 데이터를 가져온 후 필터링을 수행
IQueryable
- 필터링이 데이터베이스에서 수행되므로 필요한 데이터만 가져옴
확장 메서드
IEnumerable
- System.Linq.Enumerable 클래스에 정의된 메서드에 대한 확장 메서드를 사용
IQueryable
- System.Linq.Queryable 클래스에 정의된 메서드에 대한 확장 메서드를 사용
실행 위치
IEnumerable
- 클라이언트 측에서 실행
IQueryable
- 서버 측 (예: 데이터베이스 서버)에서 실행
적합한 시나리오
IEnumerable
- 메모리 내의 작은 데이터 세트를 처리할 때 적합
IQueryable
- 큰 데이터베이스나 원격 데이터 소스를 사용할 때 적합합니다. 쿼리가 서버 측에서 평가되므로 성능상의 이점
요약하면, IEnumerable
은 메모리 내 데이터의 반복에 중점을 둔 반면, IQueryable
은 원격 데이터 소스에 대한 쿼리를 최적화하는 데 중점을 둡니다.
sealed의 용도
- 더 이상 해당 클래스가 상속 될 수 없도록 봉인
- 상속제한, 버전관리, 성능최적화, 클래스 의도 명확화
ref / out / in 키워드의 차이점
ref
- 매개변수를 참조로 전달하는데 사용
- 매개변수를 사용할 때 해당 변수는 반드시 초기화 되어 있어야 함
- 호출 시 매개변수 앞에도 ref 키워드를 사용해야 함
out
- 메서드의 값을 반환하도록 의도된 매개변수에 사용
- 초기값 제공 불필요
- 매개변수는 메서드 내에서 반드시 값을 할당 받아야 함
- 호출 시 out 키워드 사용해야 함
in
- 7.2 부터 도입
- 읽기전용 참조로 전달 하는데 사용
- 메서드 내에서 매개변수 값을 수정할 수 없음
- 호출시 키워드 사용은 선택사항
C# 8.0에서 소개된 nullable reference types
- C# 8.0에서는 Nullable Reference Types라는 기능을 도입하여 null 관련 버그를 줄이기 위한 강력한 도구를 제공합니다.
- 전통적으로, C#에서의 참조 타입은 null이 될 수 있었고, 이것은 종종 예기치 않은 NullReferenceException의 원인이 되었습니다.
- Nullable Reference Types를 도입함으로써, 개발자는 명시적으로 어떤 참조 타입이 null이 될 수 있는지, 아닌지를 지정할 수 있게 되었습니다.
C#의 메모리 관리 방식
Stack 과 Heap
Stack
- 지역 변수와 메서드 호출 정보와 같은 임시 데이터를 저장하는 메모리 영역
- 메서드가 종료되면 해당 stack frame도 제거 됨
Heap
- 동적으로 할당 되는 객체 (new 객체)를 저장하는 메모리 영역
- 힙에 있는 객체는 GC에 의해 관리 됨
GC (가비지컬렉션)
- .NET에서 자동으로 동작하는 메모리 관리자
- heap 메모리에서 더이상 접근 되지 않는 객체를 식별하고 해제하여 재사용가능하도록 함
- Generation 개념을 사용하여 메모리를 효율적으로 관리 처음 생성된 객체는 0에 위치, 이루 실행될 때마다 살아남는 객체는 다음 세대로 이동
Finalization
- 객체가 메모리에서 해제되기 전에 어떤 마지막 작업을 수행해야 할 경우 정의함
- ’~’ 기호를 사용하여 클래스내에 정의
- Finalizer가 있는 객체는 메모리 해제가 두 번의 GC 사이클이 필요하기 때문에 성능에 영향을 줄 수 있음
IDisposable 패턴
- 인터페이스로 객체가 사용하는 비관리 리소스 (파일핸들, 네트워크 소켓, 데이터베이스 연결)를 명시적으로 해제할 수 있도록 해줌
- Dispose 메서드를 구현하여 리소스를 해제하며, using 문을 사용하여 자동으로 Dispose를 호출할 수 있음
비관리 리소스
- C#과 .NET은 기본적으로 메모리 관리를 자동화 하지만, 외부 라이브러리나 OS리소스와 같은 비관리 리소그는 직접 관리 해야함
- IDisposable 패턴을 통해 제거 되어야 함
finalizer와 destructor의 차이점
- C#에서 “finalizer”와 “destructor”는 종종 혼용되어 사용되는 용어
- 사실, C#에서는 이 두 용어가 같은 개념을 나타내며, 객체가 가비지 컬렉션에 의해 수집되기 전에 실행되는 특별한 메서드를 참조
- 그러나 두 용어 간에는 문맥적인 차이점이 있음
Destructor
- C++에서 오는 용어
- C++에서 소멸자 (destructor)는 객체가 소멸될 때 자동으로 호출되는 메서드
- C#에서는
~
기호를 사용하여 destructor를 정의 - 예:~MyClass() { ... }
- C#에서의 destructor는 내부적으로 finalizer로 구현
Finalizer
- .NET과 C#의 문맥에서 사용되는 용어
- 객체가 가비지 컬렉션에 의해 수집되기 전에 실행되는 코드를 정의
- C#에서 destructor를 작성하면 컴파일러는 그것을 finalizer로 변환하며, 내부적으로
Finalize
메서드를 오버라이드
C#에서의 오버로딩과 오버라이딩의 차이
오버로딩 (Overloading)
- 오버로딩은 하나의 클래스 내에서 같은 이름의 메서드를 여러 번 정의하는 것을 의미
- 매개변수의 유형, 개수, 순서가 다르게 정의되어야 함
- 반환 타입만 다른 경우에는 오버로딩으로 간주되지 않음
- 오버로딩의 주 목적은 동일한 작업을 수행하지만 다른 매개변수 유형이나 개수를 가진 메서드를 제공하는 것
public class MathOperations
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
오버라이딩 (Overriding)
- 파생 클래스에서 기반 클래스의 메서드를 재정의하는 프로세스를 의미
- 기반 클래스의 메서드가
virtual
,abstract
또는override
키워드로 선언되어야 함 - 파생 클래스에서 해당 메서드를 재정의할 때
override
키워드를 사용 - 오버라이딩의 주 목적은 파생 클래스에서 기반 클래스의 행동을 수정하거나 확장하는 것
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("The animal speaks.");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("The dog barks.");
}
}
오버로딩은 같은 이름의 메서드를 다양한 매개변수로 제공하는 것이며, 오버라이딩은 파생 클래스에서 기반 클래스의 메서드 행동을 변경하는 것
C#에서 dynamic 키워드의 용도와 장단점
- C#에서
dynamic
키워드는 .NET 4.0부터 도입 - 런타임에 데이터 타입을 결정하는 기능을 제공
- 컴파일 타임에 변수의 타입이 결정되지 않지만, 런타임에 그 타입을 확인하고 해당 타입의 모든 작업을 수행
용도
런타임에 타입 결정
-
dynamic
변수의 실제 타입은 런타임에 결정되므로, 컴파일 타임에는 해당 변수에 대한 어떠한 연산도 허용COM 및 Reflection
- 예전 COM 인터페이스나 리플렉션을 사용하여 코드를 작성할 때, 타입 정보가 항상 사용 가능하지 않을 수 있습니다. 이러한 경우에
dynamic
을 사용하면 코드가 간결해짐Interop 시나리오
- 다른 언어와의 상호 운용성 (예: Python, JavaScript)에서
dynamic
이 유용할 수 있음 - C# 코드 내에서 동적 언어의 객체를 사용하려면
dynamic
키워드를 사용할 수 있음
장점
- 유연성 : 특정 시나리오에서 코드를 더 간결하고 읽기 쉽게 만들 수 있음
- 간단한 Interop : 다른 동적 언어나 COM 객체와의 상호 운용성을 쉽게 만들어 줌
단점
- 컴파일 타임 검사 누락 :
dynamic
을 사용하면 컴파일러의 타입 검사가 누락, 런타임 오류의 위험이 높아짐 - 성능 오버헤드 :
dynamic
변수의 작업은 런타임에 수행, 일반적인 변수에 비해 추가적인 오버헤드가 발생 - 코드 가독성 : 너무 많은
dynamic
변수를 사용하면 코드의 가독성이 떨어질 수 있으며, 코드의 흐름을 이해하기 어려워짐
결론적으로, dynamic
키워드는 특정 시나리오에서 매우 유용할 수 있지만,
가능하면 정적 타이핑을 선호하고, 꼭 필요한 경우에만 dynamic
을 사용하는 것이 좋음
LINQ (Language Integrated Query)
- LINQ는 .NET 언어에 통합된 쿼리 기능
- LINQ는 데이터에 대한 쿼리를 쓰기 위한 일관된 문법을 제공
- 다양한 데이터 소스 (예: 배열, 컬렉션, 데이터베이스, XML 등)에 대한 쿼리를 작성할 수 있음
- LINQ는 SQL과 유사한 문법을 가지고 있지만, 순수 C# 또는 VB.NET 코드로 작성됨
LINQ의 장점
- 통합된 문법 : 다양한 데이터 소스에 대한 쿼리를 작성할 때 동일한 문법을 사용할 수 있음
- 강력한 필터링 및 정렬 : 복잡한 데이터 변환, 필터링 및 정렬 작업을 간단한 문법으로 수행할 수 있음
- 타입 안전성 : LINQ 쿼리는 컴파일 타임에 타입 검사를 거치므로, 런타임 오류의 위험을 줄일 수 있음
- 읽기 쉬운 코드 : LINQ는 읽기 쉬운 선언적 문법을 사용하므로, 코드의 의도가 명확하게 드러남
- 지연 실행 : LINQ 쿼리는 실행 시점까지 지연될 수 있음, 이를 통해 효율적인 데이터 처리가 가능
LINQ의 단점
- 성능 오버헤드 : 간단한 작업에 대해서는 전통적인 반복문이 LINQ 쿼리보다 빠를 수 있음, 따라서 성능이 중요한 경우에는 LINQ 사용 전/후를 적절히 평가해야함
- 학습 곡선 : LINQ 문법은 다소 낯설 수 있으므로, 새로운 사용자들에게는 초기 학습 곡선이 있을 수 있음
- 디버깅의 어려움 : 복잡한 LINQ 쿼리는 디버깅하기 어려울 수 있음, 특히 지연 실행의 경우, 예상치 못한 시점에서 오류가 발생할 수 있음
Attributes
- 속성은 대괄호 (
[]
) 안에 정의되며, 대상 코드 엔터티 바로 앞에 위치유용한 경우
- 문서화 : 속성을 사용하여 코드의 의도나 사용 방법에 대한 추가 정보를 제공
- 코드 검사 : 컴파일러 또는 코드 분석 도구에 특정 코드 동작을 지시할 수 있음
예를 들어,
NotNull
속성은 특정 매개변수가 null이 아니어야 함을 나타냄 - 런타임 동작 제어 : 리플렉션을 사용하여 속성 정보를 읽고 기반으로 동작을 변경할 수 있음
- 설정 및 구성 : 속성을 사용하여 코드에 대한 설정이나 구성 정보를 제공할 수 있음
- 직렬화 제어 : 어떤 필드나 프로퍼티가 직렬화되거나 직렬화에서 제외되어야 하는지 지시하는 데 속성을 사용할 수 있음
- ORM (Object-Relational Mapping)**: 데이터베이스와 객체 간의 매핑을 정의하는 데 속성을 사용할 수 있음 예를 들어, Entity Framework에서는 데이터베이스의 테이블과 열을 나타내는 클래스와 프로퍼티에 속성을 사용
- 테스트 프레임워크 : 단위 테스트 프레임워크 (예: NUnit, MSTest)에서는 테스트 메서드와 관련된 메타데이터를 제공하기 위해 속성을 사용합니다.
partial
- C#에서 클래스, 구조체, 인터페이스 또는 메서드의 정의를 여러 파일로 분할할 수 있게 해주는 키워드
-
partial
키워드를 사용하면 하나의 타입의 정의를 여러 파일에 걸쳐 나누어 작성할 수 있으며, 컴파일 시점에 이러한 부분들이 하나의 타입으로 결합
MyClass.Part1.cs
partial class MyClass
{
public void Method1()
{
// ... implementation of Method1
}
}
MyClass.Part2.cs
partial class MyClass
{
public void Method2()
{
// ... implementation of Method2
}
}
partial
키워드를 사용할 때 주의할 점은 모든 partial
정의가 동일한 접근 한정자를 가져야 하며,
한정자가 명시되지 않은 경우 기본적으로 private
으로 간주된다는 것
객체지향
SOLID 원칙
단일 책임 원칙 (Single Responsibility Principle, SRP)
- 클래스는 하나의 목적만 가져야 한다.
- 클래스를 변경하는 이유는 오직 하나여야 한다.
개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- 클래스는 확장에는 열려 있어야 하고, 기존 코드 변경에는 닫혀 있어야 한다.
리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 하위 타입은 상위 타입의 객체를 바꾸어 사용될 수 있어야 한다.
인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 클래스는 필요하지 않은 인터페이스에 의존하게 되면 안 된다.
의존관계 역전 원칙 (Dependency Inversion Principle, DIP)
- 구체적인 구현보다는 추상화에 의존해야 한다.
객체 지향 프로그래밍 (OOP) 원칙
캡슐화 (Encapsulation)
- 연관된 변수와 메서드를 하나의 단위로 묶음.
- 클래스의 정보를 외부에서 직접 접근하지 못하게 하여 정보를 은닉한다.
추상화 (Abstraction)
- 객체에서 공통된 부분을 식별하여 일반적인 특성을 정의한다.
상속 (Inheritance)
- 하위 클래스가 상위 클래스의 특성과 기능을 물려받는 것.
다형성 (Polymorphism)
- 동일한 메서드나 함수가 다양한 방식으로 동작하는 것.
- 오버로딩과 오버라이딩을 통해 구현된다.
.Equals() / ==
기본 동작
==
- 기본적으로 값 타입의 경우 값의 동등성을 비교하고, 참조 타입의 경우 두 객체의 참조(메모리 주소)가 동일한지 비교합니다.
.Equals()
- 메서드는
System.Object
클래스에 정의되어 있으며, 기본적으로==
연산자와 동일한 동작을 합니다. - 그러나 많은 클래스들이 이 메서드를 오버라이드하여 값의 동등성을 비교하는 동작을 제공합니다.
오버로드 및 오버라이드
==
- 사용자 정의 타입에서
==
연산자의 동작을 재정의(오버로드)할 수 있습니다. - 이렇게 하면, 사용자 정의 타입에서도 값의 동등성을 비교하도록 만들 수 있습니다.
.Equals()
- 사용자 정의 타입에서 이 메서드를 오버라이드하여 원하는 동등성 비교 로직을 제공할 수 있습니다.
다형성
==
-
==
연산자는 다형성을 지원하지 않습니다. - 즉, 부모 클래스의 참조를 통해 참조되는 자식 클래스의 객체에 대해
==
연산자를 사용하면 부모 클래스에 정의된==
연산자의 동작이 적용됩니다..Equals()
- 이 메서드는 다형성을 지원합니다.
- 따라서, 부모 클래스의 참조를 통해 참조되는 자식 클래스의 객체에 대해
.Equals()
메서드를 호출하면 자식 클래스에서 오버라이드된 메서드의 동작이 적용됩니다.null 비교
==
- 두 참조가 모두 null인 경우에도 동등하다고 판단합니다.
.Equals()
- 호출하는 객체가 null이면 NullReferenceException이 발생합니다.
- 따라서,
.Equals()
를 호출하기 전에 null 검사를 해야 합니다.