3주차 강의를 후다닥 듣고 과제를 진행하면서 내가 온전한 이해를 하지 못했다는 사실을 깨달았다.
그냥 그런가보다 하고 넘어가지 못하는 성격이기도 하고, 보다 명확하게 개념을 이해하지 못하면 활용에서도 어려움이 생길 수밖에 없다.
강의가 꽤나 압축적인 내용을 담고 있어서 단순히 듣기만 하는 걸로는 부족한 것 같다.
과제를 진행하면서도 개념을 익힐 수 있지만, 이번에는 궁금한 점이 많아서 제시된 개념들의 주변정보를 추가적으로 공부했다.
[인터페이스]
인터페이스란?
- 클래스가 구현해야 할 멤버들을 정의하는 것. 이 때 어떤 내용을 구현해야 하는지에 대한 정보만 제공할 뿐, 실제 구현을 하지는 않는다.
- 클래스가 인터페이스를 구현할 경우, 모든 인터페이스의 멤버를 구현해야 한다.
- 다중 상속이 가능하다.
- 클래스의 일종은 아니고, 클래스의 제약 조건이라고 이해하면 된다.
여러 클래스에서 유사한 속성을 공유한다고 하면 상속을 떠올리기 쉽다. 하지만 상속은 종속적인 관계를 생성하는 것이기 때문에 잘 생각해서 활용해야 한다.
여러 클래스가 동일한 속성을 공유한다고 판단될 때, 속성을 제공하는 주체와 "is a" 관계라면 상속을, "can do" 관계라면 인터페이스를 활용하는 것이 좋다.
- 예를 들어 Person - Human의 관계를 생각하면 이 둘은 공통된 속성을 아주 많이 공유하게 된다. 둘의 관계를 생각해보면 Person은 Human의 일종이므로 "is a" 관계라 할 수 있고, 상속을 통해 속성을 공유하면 된다. 상속 관계는 종속성을 띄고 복잡한 구조가 될 수 있기 때문에 상속 받은 멤버 외의 멤버를 생성하는 것을 비추한다고 들었다. 추가적인 기능이나 다른 작동을 하도록 만들 필요가 있다면 추상메서드, 가상메서드를 활용하도록
- 반면 Bird와 Plane은 날 수 있다는 공통된 속성을 공유한다. 그런데 이 둘의 관계를 생각해보면, 어느 하나가 다른 하나의 하위 개체로 볼 수는 없으며, 종속성을 띄지 않는다. 이처럼 동일한 기능을 공유하지만 단순히 동일한 기능만 할 뿐 그 외의 연관성을 찾을 수는 없을 때 인터페이스를 활용하는 것이 맞다. 인터페이스는 느슨한 결합을 만들기 때문에 클래스가 독립적인 기능을 갖더라도 문제가 없다.
인터페이스를 사용하는 이유
- 코드의 재사용성 : 인터페이스는 클래스를 객체화 할 때, 참조를 제공하게 된다. 여러 클래스에 동일한 '계약'을 적용하게 되기 때문에, 프로그래밍 패턴, 설계, 일관성, 확장성, 테스트 용이성 등의 관점에서 코드의 재사용성과 유연성을 높이는 데 크게 기여한다.
- 이중 상속 제공
- 유연한 설계 : 인터페이스는 실제로 어떠한 구현을 하는 것이 아니라 어떤 구현이 필요한지만 제시한다. 따라서 클래스와 인터페이스 간에 느슨한 결합이 생기는데, 구현부(클래스)와 제시부(인터페이스)가 강하게 결합되어 있는 것은 아니며, 두 구성요소의 종속성이 최소화된다. (인터페이스의 변동이 생기더라도 클래스 자체의 내용에 영향을 미치지 않게 된다.)
- 다양한 클래스에서 공통적인 기능을 요구할 경우 인터페이스를 통해 동일한 제약을 걸 수 있고, 각 클래스에서 해당 기능을 구현한 멤버가 조금씩 다른 실행을 포함할 경우에도 동일한 이름으로 호출이 가능해진다.
인터페이스의 구현
public class Player : IMovable
{
public void Move(int x, int y)
{
// 플레이어의 이동 구현
}
}
public class Enemy : IMovable
{
public void Move(int x, int y)
{
// 적의 이동 구현
}
}
- 주로 인터페이스의 이름은 interface의 이니셜을 따와서 대문자 I로 시작한다. 인터페이스 내에는 필요한 메서드나 변수 등의 필드의 명을 적어둔다.
- 각 필드에 대한 자세한 정의는 클래스에서 하게 된다. 클래스는 인터페이스를 구현할 때, 상속과 같이 클래스명 옆에 콜론을 활용해서 인터페이스를 구현하고 있음을 표시한다.
클래스에서는 인터페이스에 포함된 모든 멤버에 대한 정의를 내려야 한다.
암시적 구현과 명시적 구현이 있는데, 암시적 구현은 위의 예시와 같이 클래스가 인터페이스를 구현하고 있음을 표시한 후 인터페이스에서 선언된 멤버와 동일한 멤버를 정의하면 된다. 명시적 구현은 클래스 내에서 멤버에 대한 정의를 내릴 때 이름 앞에 인터페이스 명을 써넣는다.
암시적 구현과 명시적 구현은 클래스가 객체화 되어 활용될 때 영향을 미치는데, 객체가 클래스 참조를 사용하면 암시적으로 구현된 요소가 호출이 되고, 객체가 인터페이스를 참조하면 명시적으로 구현된 메서드가 호출된다. (아래 예시 참고)
interface IExample
{
void InterfaceMethod();
}
class MyClass : IExample
{
public void InterfaceMethod()
{
Console.WriteLine("Implicitly implemented method.");
}
void IExample.InterfaceMethod()
{
Console.WriteLine("Explicitly implemented method.");
}
}
var instance = new MyClass();
// 클래스 참조를 통한 호출
instance.InterfaceMethod(); // 출력: Implicitly implemented method.
// 인터페이스 참조를 통한 호출
((IExample)instance).InterfaceMethod(); // 출력: Explicitly implemented method.
참조
여기서 참조에 대한 개념을 한 번 더 짚고 넘어가야 할 것 같아서 알아보았다.
참조란 주로 변수가 객체를 가리킬 때 사용되는 개념이다.
1. List<int[]> MyList = new List<int[]>();
2. int value1 = MyClass.MyMethod(); // MyMethod는 int 값을 반환한다고 가정
3. MyList.Add();
1번 : MyList가 새로운 리스트 객체를 참조한다.
- 이처럼 참조 타입의 값을 저장(주소만)하는 참조 변수를 "참조"라고 부른다.
- 여기서 List<int[]>는 타입(형)을 의미한다. 리스트는 클래스이며, 각각의 클래스는 타입이 될 수 있다.
2번 : value1은 단순히 값 타입의 데이터를 할당받은 변수로, 어떠한 참조를 하고 있지 않다.
- MyClass.MyMethod() 이 부분이 헷갈렸는데, 명확성을 위해 외부 클래스의 멤버를 활용할 경우, 해당 클래스의 이름을 앞에 붙여주어야 한다. 이 때, 이는 단순히 클래스명일 뿐, 참조라고 부르지 않는다. (3번과 같은 상황 때문에 얘의 정체에 대한 혼란이 생겼다.)
3번 : Add() 메서드 앞에 붙은 MyList는 참조라고 부른다.
- 1번에서 선언되었듯이, MyList는 참조변수이다. Add라는 메서드를 호출할 때, 객체에 직접 접근해서 메서드를 활용하는 것이 아니라 참조변수에 할당된 주소값을 따라가서 해당 객체에 접근한다.
과제를 통해 참조의 개념을 이해하려 하다 보니 혼동이 왔다.
내가 초반에는 각 클래스를 Main()메서드 내에서 객체화 하고, 해당 객체 주소를 할당받은 참조변수를 활용해서 메서드나 필드값을 호출했다. 그런데 후반에 클래스 객체를 생성할 필요 없이 클래스 내의 요소들을 정적(static)으로 설정해주면 클래스 명만 붙여서 메서드를 호출할 수 있다는 사실을 깨달아 바꾸었다.
초반의 방식이라면 메서드 앞에 붙은 것은 참조라고 할 수 있지만(3번), 후반의 방식이라면 메서드 앞에는 단순히 클래스명만 명시해준 것(2번)이 된다. 이러한 차이에 의해 클래스의 개념에 대한 혼동까지도 발생했다. 개념들을 하나하나 짚고 넘어갈 필요성을 아주 깊게 깨닫게 해준 사례 중 하나가 참조에 대한 이해였다.
[열거형]
데이터 타입
열거형의 정의에 대해 공부하면서 데이터 타입, 즉 형식에 대한 이해를 새롭게 하게 되었다.
얕은 지식으로 알고 있는 것은 int, float, bool 과 같은 값 형식이 있고, 배열, 클래스, 문자열과 같은 참조 형식이 있다는 것 정도로 알고 있었다.
+ 값 형식은 Stack(스택)이라는 메모리 영역에 값을 직접 저장한다. 참조 형식은 실제 데이터가 힙에 저장된다. (참조 형식의 데이터가 할당된 변수는 데이터의 메모리 주소 = 참조가 저장되는데 이 참조는 스택에 위치)
그런데 enum 타입에 대해 공부하면서 뭔소리지...? 싶은 부분이 많아졌다.
처음에는 타입의 종류가 한정적인 줄 알았다. 그러니까 우리가 흔히 쓰는 int, string, bool 뭐 이런 친구들만 타입인 줄 알았다.
클래스가 타입이라고 하길래 이것도 이해한 줄...알았다. 어떤 클래스를 생성하고자 할 때 앞에 class를 붙이니까...? int 1이라고 하면 1이 int 타입인 것처럼 class MyClass라고 하면 MyClass의 타입은 당연히 클래스겠죠...? 라고만 생각했는데,
MyClass 자체가 하나의 타입으로서 역할한다는 것을 할게 되었다. 이 때문에 클래스를 객체화 할 때 변수 앞에 타입을 지정하는 키워드가 클래스명이었던 것...
이 외에도 타입을 생성하는 경로는 더 있는 거 같지만 지금은 좀 과부하 상태라 일단 필요해지면 찾아보는 걸로...
아무튼 enum은 값 타입의 데이터이다! 라는 것을 알게 되었다. 선언될 때의 모습 때문에 참조 형태라고 착각했는데, 자세한 것은 아래쪽에서 다시 설명
enum ( = 열거형)
열거형은 연관된 명명된 상수들의 집합을 정의하는 타입이다. 좀 더 풀어서 말하면, 어떤 이름들이 붙여진 숫자들을 모아서 특정한 타입이라고 선언해주는 것이다.
enum도 class처럼 새로운 타입을 생성하게 된다.
enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat } 이렇게 정의하면, Days라는 새로운 타입이 생성된 것이며, 각각의 값 (ex. Mon, Wed)의 타입은 Days가 된다. 즉, { } 안의 내용들은 타입의 가능한 값들이 된다.
enum으로 정의된 타입의 값은 실제로는 숫자를 담고 있지만, string 형태로 각 숫자가 명명되어 있기 때문에 가독성이 높다는 특징을 갖는다. 따라서 우리가 보기에는 문자열의 형태지만, 숫자값으로 변환이 가능하기 때문에 로직에서 수적인 계산이 필요하거나, 조건으로 쓰기에 좋다. 특히, switch case문에서 숫자로 케이스를 표현하곤 했는데, 이를 열거형 데이터로 표현하면 보다 직관적으로 조건에 따른 실행을 이해할 수 있게 된다. 따라서 게임 상태, 방향, 아이템 등급 등을 나눌 때 유용하게 쓰일 수 있다.
열거형으로 정의된 타입의 값이 실제로는 상수를 담고 있다고 했는데, 기본적으로는 앞에서부터 0, 1, 2, ... 이런 식으로 값을 갖게 된다.
만약에 특정한 값에 임의로 숫자를 지정할 경우, 그 다음의 값은 +1된 값을 갖게 된다. 따라서 아래의 경우 Value2는 11이라는 값을 갖게 되고, Value4는 21이 된다. 이런식으로 설정된(혹은 암시적으로 설정된) 값은 int 등의 상수 형태로 변환하여 쓸 수 있다.
enum MyEnum
{
Value1 = 10,
Value2,
Value3 = 20,
Value4
}
열거형의 형변환 및 쓰임
MyEnum myEnum = MyEnum.Value1;
int intValue = (int)MyEnum.Value1; // 열거형 값을 정수로 변환
MyEnum enumValue = (MyEnum)intValue; // 정수를 열거형으로 변환
여기서 MyEnum이라는 타입의 값 Value1을 쓸 때, 앞에 MyEnum이라는 타입명을 한 번 더 붙여주는 것을 확인할 수 있다. 이는 클래스의 메서드를 호출할 때 클래스명을 앞에 붙이는 것과 같이 명확성을 높이기 위함이다.
여기서 MyEnum이라는 타입명을 앞에 붙이지 않고 쓸 수 있는 방법도 있는지 알아보던 중, "using static으로 정의하거나, 같은 스코프 내에서 enum이 정의될 경우 가능하다"는 사실을 알게 되었다. 그렇다면 여기서 using static은 뭘까?
이를 이해하기 위해서는 static, using, using static에 대한 전반적인 이해가 필요했다.
static
클래스는 자체로 작동하는 것이 아니라 <객체>를 통해 실체를 얻고 효력을 가진다. 그런데 static은 클래스 내에 정의된 멤버를 인스턴스(객체) 밖으로 빼준다. 즉, 클래스에 의해 생성된 여러 객체, 그리고 클래스 전체에 대해 하나의 공유된 상태나 기능을 갖도록 하는 것이 static이다.
클래스를 붕어빵 틀, 객체를 찍어낸 붕어빵이라고 비교했었는데, 각각의 찍어낸 붕어빵은 독립적으로 움직인다. 따라서 동일한 클래스의 서로 다른 객체는 독립적으로 기능하여 static으로 선언되지 않은 멤버는 객체에 종속되어 있고, 동일한 이름의 메서드나 변수라 할지라도 서로 다른 객체의 것이라면 독립적으로 기능한다.
다시 한 번 정리하면, static으로 선언된 멤버는 객체가 생성이 되었든 말든, 이 객체이든 저 객체이든 상관 없이 클래스에 하나만 존재하게 된다. 이러한 특성으로 인해 외부 클래스에서 정적(static)멤버에 접근하려 할 때, 객체를 생성할 필요가 없게 된다.
using
using static과 비교하기 위해서 개념을 짚고 넘어가야 했다.
using은 컴파일러에게 특정한 지시를 내리는 명령어인 '디렉티브'의 일종이다. using은 네임스페이스를 가져와서 해당 네임스페이스 안에 있는 타입들을 명시적인 네임스페이스 표시 없이 사용할 수 있게 해준다. using은 네임스페이스 단위로만 선언이 가능한데, 스크립트 상단에 using 어쩌구로 표시해준 친구들은 전부 네임스페이스이다. <using System.Collections.Generic;>이런 식으로 연속적인 이름이 보이더라도, 이들은 하위 계층의 네임스페이스일 뿐(네임스페이스도 계층 구조), 클래스가 아니다.
using static ***
using static은 특정 타입의 정적 멤버에게 쉽게 접근할 수 있게 해준다. 즉, 네임스페이스 하위의 클래스와 같은 요소들까지 언급할 수 있게 된다. 이 때, 언급된 요소 전체에 대한 접근이 가능한 것이 아니라, 해당 요소의 정적 멤버에 대한 접근을 허용하는 것이다.
외부 네임스페이스의 어떤 클래스에서 enum을 정의했다고 하자. enum은 기본적으로 static이다. 현재의 네임스페이스에서 이 열거형을 쓰려고 할 때, 이 열거형 타입의 정의가 포함된 클래스와 네임스페이스를 using static 네임스페이스.클래스; 이렇게 표시해주면 해당 enum 타입의 값을 별도의 타입명 명시 없이 활용할 수 있게 된다.
하지만 타입명을 명시하지 않을 경우 읽는 사람 입장에서는 단순히 문자열을 보고 열거형임을 유추하기 어려울 수 있기 때문에, 상황에 맞게 잘 활용해야 한다.
[예외 처리]
구조
try
{
// 예외가 발생할 수 있는 코드
}
catch (ExceptionType1 ex)
{
// ExceptionType1에 해당하는 예외 처리
}
catch (ExceptionType2 ex)
{
// ExceptionType2에 해당하는 예외 처리
}
finally
{
// 예외 발생 여부와 상관없이 항상 실행되는 코드
}
try 구문 내에서 예외 상황이 발생하는 지 확인한다. 이때, 예외의 존재 유무만 확인하는 것이 아니라, 어떠한 예외 상황이 발생하는지 판별한 후 그 에러에 해당하는 객체를 생성한다.
해당 객체와 일치하는 타입을 매개변수로 갖는 catch문을 찾아서 블록의 내용을 실행한다. catch는 매개변수를 갖는데, 이 매개변수는 ex라는 이름으로 블록 내에서 쓰이고, 타입은 특정한 예외상황 클래스가 된다. ExceptionType1과 ExceptionType2가 예외 상황 클래스 명이라고 보면 된다. (아까 정리했듯이 특정한 클래스는 그 자체로 타입이 될 수 있다.) 따라서 해당 객체와 일치하는 타입을 매개변수로 갖는 catch문을 찾는다는 것은, try에서 생성된 예외 상황 객체와 같은 클래스가 적힌 곳으로 찾아간다는 뜻이다.
finally는 예외 발생 여부와 상관 없이 항상 실행된다는 점! 유용하게 쓰일 것 같다. 마치 do while 처럼...
Exception
예외상황이라는 것은, Exception이라는 클래스 아래에, 하위 클래스의 형태로 각각 존재하는 듯하다. 이 클래스에는 예외 상황에 대한 정보가 저장되어 있다.
여기서 말하는 예외 상황은 C#에서 기본적으로 제공하는 (내장되어 있다고 표현하겠다) 예외들을 의미한다. 예를 들어 DivideByZeroException의 경우, 수를 0으로 나누었을 때의 상황에 대한 정보를 담고 있는 하나의 클래스이다.
try구문 내에서 어떠한 예외 상황이 발생하는지 판별한 후 객체를 생성한다고 했는데, 이 때 생성되는 객체는 예외 상황에 해당하는 클래스에 대한 것이다. (발생한 예외 상황이 어떠한 예외인지 판별하는 것은 CLR이라고 불리는 "Common Language Runtime"에서 한다고 한다.
사용자 정의 예외 클래스
내장되어 있는 예외 상황 외에도 프로그램 내에서 자주 발생될 것이라 우려되는 예외가 존재할 수 있다. 이 때 프로그래머가 원하는 '사용자 정의 예외 클래스'를 만들어 활용할 수 있다.
public class NegativeNumberException : Exception
{
public NegativeNumberException(string message) : base(message)
{
}
}
try
{
int number = -10;
if (number < 0)
{
throw new NegativeNumberException("음수는 처리할 수 없습니다.");
}
}
catch (NegativeNumberException ex)
{
Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}
NegativeNumberException은 프로그래머가 직접 예외 상황을 정의한 새로운 클래스이다. 이렇게 사용자 정의 예외 클래스를 만들 때는Exception 클래스를 상속받도록 해야 한다. 이 클래스의 생성자에 대한 구조는 나중에 살펴보고, 먼저 이렇게 생성된 클래스가 try catch 구문에서 어떻게 쓰이는지 살펴보자.
내장되어 있는 예외 상황의 경우에는, try 구문 내에서 CLR에 의해 지금 일어난 예외 상황이 어떠한 예외인지 인식되고 객체가 자동적으로 만들어진다고 했다. 그러나 사용자가 새롭게 정의한 예외 상황은 CLR이 알 수 없다. 따라서, 코드를 작성할 때 try블록 내에 ~한 조건에 걸리면 이러이러한 예외 상황이 발생한 것이니 내가 정의한 예외 클래스를 객체화해라!라는 명령을 넣어줘야 한다.
위 예시의 if문이 여기에 해당한다. number < 0일 경우, 내가 만든 NegativeNumberException이라는 예외 클래스의 매커니즘을 실행하도록 하고 있다. 이 때, throw는 새로운 예외를 발생시킬 때 (현재 해당 클래스의 객체가 생성되어있지 않을 때) 예외 객체를 생성한다.
즉, 사용자 정의 예외 클래스를 활용하고자 하면, 예외 상황에 대한 조건도 명시해야 하고, 그에 따른 실행도 명시해서 예외 객체를 생성할 수 있도록 해야 한다. 이런 방식으로 예외 객체가 생성되면 내장된 예외 상황과 동일한 방식으로 적합한 catch문을 찾아가 내용을 실행하게 된다.
이니셜라이저와 상속 구조
public class NegativeNumberException : Exception
{
public NegativeNumberException(string message) : base(message)
{
}
}
사용자 정의 예외 클래스의 예시에서 이니셜라이저에 대한 이해가 부족해서 따로 찾아보게 되었다. 해당 개념은 상속과 연관이 있는 듯하다.
이 클래스의 생성자를 살펴보면, string 형태의 파라미터를 message라는 이름으로 받고 있다. 그 옆의 base(message)라는 친구는 '이니셜라이저'로, 초기화를 해주는 역할을 하며, 해당 클래스의 상위클래스의 생성자에 값을 전달해서 현재 클래스의 생성자보다 먼저 수행된다고 한다.
조금 더 구체적으로 정리하면, 원래 하위 클래스의 생성자가 호출될 때, 자동으로 그 상위의 생성자가 먼저 호출된다. 만약에 하위의 생성자에 이니셜라이저가 없다면 자동으로 상위의 기본 생성자가 호출이 된다. 상위의 생성자가 파라미터를 받는 형태로 오버로딩 되어 있다고 할 때, 하위 클래스에서 base()의 값을 명시하고 있다면, base()의 값이 상위 생성자에 전달되어 상위 생성자가 먼저 발동되므로, 전달된 값을 활용할 수 있는 파라미터를 가진 오버로딩 된 생성자가 호출이 된다.
public class A
{
public A()
{
Console.WriteLine("A의 기본 생성자 호출");
}
public A(string value)
{
Console.WriteLine($"A의 문자열 파라미터 생성자 호출: {value}");
}
}
public class B : A
{
public B()
{
Console.WriteLine("B의 기본 생성자 호출");
}
public B(string value) : base(value)
{
Console.WriteLine($"B의 문자열 파라미터 생성자 호출: {value}");
}
}
public class C : B
{
public C() : base("C에서 전달")
{
Console.WriteLine("C의 기본 생성자 호출");
}
}
여기서 클래스 C의 인스턴스를 생성하면, C의 상위 클래스인 B, 그 상위 클래스인 A까지 타고 올라가서 A 생성자, B 생성자, C 생성자 순으로 실행된다.
A의 문자열 파라미터 생성자 호출: C에서 전달
B의 문자열 파라미터 생성자 호출: C에서 전달
C의 기본 생성자 호출
실행 결과는 이렇게 되는데,
C 생성자에서는 "C에서 전달"이라는 값을 B 생성자에 전달하고 있으므로 클래스 B에서는 기본 생성자가 아닌 파라미터를 받는 생성자가 호출이 되고, B의 파라미터를 받는 생성자가 입력받은 "C에서 전달"이라는 문자열을 다시 클래스 A의 생성자로 base 형태로 전달하고 있기 때문에 A 생성자 또한 파라미터를 받는 녀석으로 호출이 된다.
[값형과 참조형]
값형과 참조형
데이터 타입에 대해 정리할 때, 값 형식과 참조 형식에 대해 간단히 언급했다.
값 타입은 메서드의 로컬 변수로 선언될 때, 변수가 데이터 값을 직접 저장한다. 참조 타입의 데이터는 변수가 데이터의 메모리 위치(참조)를 저장한다.
따라서 값 형의 데이터를 A라는 변수에 할당한 후, A의 값을 B라는 변수에 할당했을 때, 각각의 데이터는 독립적이라 할 수 있다. 값 형의 데이터는 할당되는 과정에서 실제값 자체가 복사되어 각각의 변수에 들어가기 때문이다.
반면에 참조 타입의 경우에는, 변수가 실제 데이터를 저장하고 있는 것이 아니라, 데이터가 저장된 위치(참조) 정보를 가지고 있는 것이므로, 변수에 저장된 참조(주소)를 통해 실제 데이터에 접근할 수 있도록 해주는 구조이다.
따라서 변수에 참조 타입의 데이터를 할당하더라도, 이 변수에 저장된 값을 다른 변수에 할당을 하게 되면 동일한 주소값이 복사가 된다. 즉, 한 변수를 통해 실제 데이터의 값에 변화를 주면, 다른 변수 입장에서도 저장된 참조를 통해 얻은 데이터가 변하게 된다.
* 잘못 정리한 부분이 있었는데, 문자열(string)도 참조타입이다. 실제로 string 데이터는 .NET 내부적으로 문자들의 배열로 구성되어 있다고 한다. 따라서 불변성을 가지고, 새로운 문자열 조작이 이루어질 때마다 새로운 string 객체가 생성되는 것이다. 값을 바꾸면 새로 생긴 문자열의 참조가 복사되어 저장되고, 원래의 값은 가비지컬렉터에 의해 정리되는 것. 따라서 대량의 문자열 조작 작업이 필요한 경우에는 성능 문제가 발생할 수 있고, 문자열 연결 똔느 조작 작업이 빈번한 경우 "StringBuilder"와 같은 다른 클래스를 사용하는 것이 좋다고 한다.
박싱과 언박싱
박싱과 언박싱은 값형과 참조형 사이의 변환을 뜻한다.
박싱 - 값형을 참조형으로
언박싱 - 참조형을 값형으로
박싱과 언박싱은 .NET에서 성능과 메모리 사용에 영향을 주는 두 가지 주요 연산이다.
1. 메모리 할당: 박싱의 과정을 살펴보면, 기존의 값형이 어찌저찌 참조형의 박스 안으로 들어가는 것이 아니라, 동일한 값에 대한 참조형의 객체가 새롭게 생성된다. 즉, 박싱이 발생할 때마다 새로운 참조 형식의 객체가 힙 메모리에 할당된다. 이는 추가적인 메모리 사용을 초래한다. 특히, 반복문 내에서 박싱이 빈번히 발생할 경우, 이 메모리 사용은 누적되어 상당한 부담이 될 수 있다.
2. 가비지 컬렉션 (GC) 오버헤드: 힙에 할당된 객체들은 가비지 컬렉터에 의해 수거되어야 한다. 박싱이 빈번하게 발생하면, 이로 인해 생성된 객체들도 가비지 컬렉션의 대상이 되므로 GC가 더 자주 발생할 수 있다. 이렇게 되면, GC의 오버헤드가 증가하여 성능 저하가 발생할 수 있다.
3. 값 복사: 1번에서도 정리했지만, 박싱이 일어날 때 값 형식의 데이터는 참조 형식의 객체로 복사된다. 이는 단순히 용량의 문제만 초래하는 것이 아니라, 복사 연산 자체도 CPU의 연산을 필요로 하므로, 성능에 약간의 부담을 줄 수 있다.
4. 언박싱의 오버헤드: 박싱된 객체를 다시 원래의 값 형식으로 변환하기 위해서는 언박싱이 필요한데, 이 과정도 연산이 필요하므로, 박싱과 언박싱이 반복적으로 발생하는 경우 성능 저하의 원인이 될 수 있다.
5. 타입 안전성: 박싱된 객체는 object 타입이므로, 언박싱 시 원래의 타입으로 명시적으로 변환해야 한다. 이 변환 과정에서 잘못된 타입으로 언박싱하려고 시도하면 실행 시간에 오류가 발생한다. 이러한 상황은 컴파일 시간에는 감지되지 않기 때문에 실행 중에 예기치 않은 문제를 발생시킬 수 있다.
* 따라서 성능이 중요한 상황에서는 박싱과 언박싱 연산을 최소화하거나 피하는 것이 좋다고 한다.
[기록]
잘한 점 :
- 깊이 있는 공부 하기
어려웠던 점 :
- 직접 코드를 짜는 시간이 부족했다
되돌아보기 :
- 오늘의 몰입도 : 80점
- 공부할 게 너무 많다,,
- 스스로 찾아보기: 80점
- 내가 이때까지 생성하던 리스트가 리스트 클래스의 객체이며, 내부적으로는 배열의 형태로 값을 저장한다는 것까지 알게 되었다. 평소에 사용하는 것들의 정의나 기능, 특징, 속성 등에 대해 공부하다 보니 시간은 오래 걸렸지만, 이해도가 높아져서 앞으로 더 잘 활용할 수 있을 것 같다.
내일 목표
- 4주차 완
'👾 내일배움캠프 > 🎮 TIL & WIL' 카테고리의 다른 글
내일배움캠프 2주차 WIL - C# 기초 공부 (1) | 2023.08.20 |
---|---|
내일배움캠프 (휴일) TIL - C# 기초 문법 (4) (0) | 2023.08.19 |
내일배움캠프 8일차 TIL - 스네이크 게임 C# (0) | 2023.08.17 |
내일배움캠프 7일차 TIL - C# 기초 문법 (2) (0) | 2023.08.16 |
내일배움캠프 (휴일) TIL - Tic Tac Toe (틱택토 게임) C# (0) | 2023.08.15 |