티스토리 뷰

프로그래밍

C# Value Type과 Reference Type

merit nada 2021. 2. 12. 17:46

C#을 공부하다 보면 Value Type(값 형식)Reference Type(참조 형식)이라는 것을 듣게 됩니다.

이런 것들을 모르고 코딩해도 대체로 원하는 결과를 얻을 수 있습니다.

 

하지만 가끔가다 코드가 이해하기 힘든 방식으로 동작하거나 생각보다 오버헤드가 큰 경우가 있는데
이런 경우 대부분 Value TypeReference Type에 대한 이해가 부족해서 실수를 했기 때문입니다.

 

ref, out, in이라는 키워드나 boxing, unboxing 개념 그리고 stack, heap에 관련된 부분도 Value Type, Reference Type과 밀접한 관련이 있습니다.

 

따라서 C#을 제대로 배우고 싶다면 Value Type, Reference Type을 아는 것이 필수라고 볼 수 있습니다.

 

그럼 이제부터 Value TypeReference Type에 대해 알아봅시다.

 

Data Types 다이어그램

참고자료: 시작하세요! C# 7.3 프로그래밍

 

일단 C#에 기본적으로 존재하는 타입들을 다이어그램으로 표현해 보았습니다.

*일반적인 클래스 다이어그램처럼 명확한 상속 구조를 표현한 것은 아닙니다.

 

하늘색으로 표현한 부분이 Value Type이고 파란색으로 표현한 부분이 Reference Type입니다.

표를 보면 알 수 있듯이 object, string, System.Array, class로 정의하는 type들이 Reference Type이고

System.ValueType, bool, byte, int, float, char, enum, struct로 정의하는 type들이 Value Type입니다.

코드로 정의한 모든 배열은 int[] a와 같은 것들을 말합니다.

 

몇 가지 혼동될 수 있는 부분을 언급하자면 System.ValueTypeSystem.Array 자체는 abstract class로 정의되어 있기 때문에 이 타입으로 변수(객체)를 만들 수는 없습니다.

일단은 bool, byte, int 등등의 키워드로 만든 type들, enum, struct로 정의한 type들System.ValueType을 상속받은 것처럼 동작한다는 것만 알아두시면 됩니다.

코드에 정의된 모든 배열 역시 마찬가지로 System.Array를 상속받은 것처럼 동작하겠죠.

 

 

구구절절 설명했지만 사실 요약하면 

1. string을 제외한 기본 타입들 + struct + enum = Value Type

2. string + class로 정의한 타입들 + array = Reference Type

이 두 줄로 생각할 수 있습니다.

 

지금까지 Value TypeReference Type이 무엇인지에 대해 알았습니다.

이제부터 Value TypeReference Type의 동작 차이에 대해서 알아보겠습니다.

 

사실 이 동작 차이 덕분에 이해하기 어려운 포인터 따위도 없고

메모리 구조에 신경쓰지 않고 직관적으로 코딩해도 원하는 결과를 낼 수 있는 것입니다.

 

엄밀히 말하면 직관적으로 쉽게 코딩할 수 있도록 하기 위해 Value TypeReference Type을 구분한 것이겠죠.

 

어찌됐든 C#에는 포인터가 없기 때문에 포인터를 몰라도 됩니다.

하지만 Value TypeReference Type을 이해할 때 포인터에서 쓰이는 개념을 알면 이해하기 쉽습니다.

 

코드 예제

먼저 코드를 통해 동작하는 방식을 봅시다.

Person Class

위와 같은 Reference Type의 Person Class가 있습니다.

사용하는 코드

위와 같이 사용하는 코드가 있다고 합시다.

결과가 어떻게 나올까요?

 

처음 출력 두 줄은 

8

200

임을 쉽게 유추할 수 있습니다.

p1 = p2;v1 = v2; 구문을 통해 p1, v1의 값을 덮어 썼기 때문이죠

 

그럼 다음 두 줄은 어떨까요?

결과는

12

200

입니다.

 

p2.strength = 12; 구문에서 수정한 것은 p2의 값인데 어째서 p1.strength 값이 수정된 것일까요?

p1 = p2;v1 = v2; 구문에서 똑같이 =(Assignment) 연산자를 사용했는데 왜 동작하는게 다를까요?

단순히 Value TypeReference Type이냐에 따라 = 연산자가 다르게 작동하는 것일까요?

물론 단순히 다르게 동작한다고 이해하고 넘어가도 되긴 합니다.

하지만 알고 보면 두 개의 동작 차이는 변수의 메모리 구조에서 기인한다고 볼 수 있습니다.

 

Value Type 변수 생성과 대입

먼저 Value Type부터 봅시다.

ValueType

age라는 int 변수를 생성하고 값을 할당합니다.

위처럼 표현해도 되지만 아래처럼 나눠서 표현할 수 있습니다.

ValueType

나눠서 표현했을 때 int age; 라는 구문이 실행된 후의 메모리 구조를 대략적으로 보면 아래와 같습니다.

int age; 메모리 구조

*이미지는 의미를 전달하기 위해 표현한 것이지 실제 메모리 구조와는 다릅니다.

 

int age;라는 구문은 메모리의 어떤 영역에 4바이트(int의 크기)를 사용하겠다고 설정하고

그 곳을 age라는 이름으로 접근할 수 있도록 하는 것입니다.

 

프로그래머는 앞으로 코드에서 age라는 변수를 사용하지만

컴퓨터 입장에서는 age라고 마크된 메모리의 특정 영역(0x12345678)을 뜻하는 거죠.

우리는 아직 age라고 마크만 해두었지 아직 값을 채우지 않았으므로 이상한 값이 들어가 있습니다.

 

age = 10이라는 구문이 실행되고 나면 메모리 구조는 아래와 같습니다.

age = 10 메모리 구조

age라고 명하는 메모리 공간에 10이라는 값을 대입한 것이지요.

 

다음은 더 나아가 int age2 = age; 라는 구문을 분석해봅시다.

int age2 = age 메모리 구조

위와 똑같이 int age2;라는 구문을 통해 메모리 공간을 확보하고

age2 = age;라는 구문을 통해 age2 공간에 age의 값을 대입합니다.

age2는 메모리 공간을 뜻하고 age는 값을 뜻한다는 것은 눈여겨볼만 합니다.

 

어찌됐든 Value Type의 변수를 생성하고 대입하는 과정은 위와 같이 진행됩니다.

이 과정대로 진행해보면 위 예제에서 v1, v2의 결과가 위와 같이 나온 상황이 이해가 갈겁니다.

 

Reference Type 변수 생성과 대입

이제 다음으로 Reference Type 변수의 생성과 대입에 대해 알아봅시다.

결론부터 말하면 Reference Type 변수의 대입(=) Value Type 변수의 대입(=)과 근본적으로 같습니다.

단지 Reference Type 변수에는 좀 다른 과정이 더 있어서 다르게 행동하는 것처럼 보일 뿐입니다.

 

먼저 아래 구문을 분석해봅시다.

Reference Type

person이라는 객체를 생성합니다.

사실 이 구문도 Value Type에서 했던 것과 마찬가지로 아래처럼 나누어 표현할 수 있습니다.

Reference Type
Person person; 메모리 구조

Person person; 구문부터 보면 Value Type 변수를 생성하는 것과 차이가 없습니다.

특정 메모리 공간을 확보하고 그 공간을 person이라고 칭하겠다고 설정하는 것이죠.

차이는 그 다음 구문에서 일어납니다.

person = new Person(); 메모리 구조

*다시 한 번 말씀드리지만 이미지는 의미를 전달하기 위해 표현한 것이지 실제 메모리 구조와는 다릅니다.

person = new Person(); 이라는 구문이 실행되면

컴퓨터는 또 다른 공간(예시에서는 0x456789AB)을 확보하고

그 공간에 대한 주소를 person 변수의 값으로 담습니다.

값을 변수 자체에 담는 Value Type과는 차이가 있죠.

 

우리는 person.strength를 통해 특정 값 혹은 메모리 공간에 접근합니다.

person.strength = 10; 혹은 int personStrength = person.strength; 라는 구문을 통해 값을 자유롭게 읽고 씁니다.

하지만 사실 위와 같은 구문은 알고 보면 person이라는 메모리 공간을 한 번 거쳐서 접근하는 것이지요.

 

*C나 C++을 해본 분이라면 이것이 포인터 개념과 매우 유사하다는 것을 알 수 있습니다.
C#은 포인터 없이도 이런 개념을 구현할 수 있도록 한 것입니다.

 

그러면 마지막으로 Person person2 = person; 이라는 구문을 분석해봅시다.

 

Person person2 = person; 메모리 구조

왼쪽은 Person person2; 구문이 실행되었을 때의 상황이고

오른쪽은 person2 = person;을 실행했을 때의 상황입니다.

 

이것을 보면 person2 = person; 구문이 Value Type끼리 대입했을 때와 같은 방식으로 동작한다는 것을 알 수 있습니다.

똑같이 person이 가진 값(0x456789AB)을 person2이 뜻하는 메모리 공간의 값으로 대입하는 것이죠.

 

단, 처음 예제에서처럼 대입 연산자(=)가 다르게 동작하는 것처럼 보이는 이유는 Reference Type의 특징 때문입니다.

person2person가 그 자체로 값이 아니라 주소값이기 때문이죠.

 

처음 예제에서 p1 = p2를 하고나면 p1p2는 결국 같은 Person 객체를 가리키고 있기 때문에

p2.strength 값을 수정했을 때 p1.strength 값도 수정되었던 것입니다.

 

결론

암튼 이 과정을 통해서 Value TypeReference Type의 근본적인 동작 차이를 알게 되었습니다.

 

그렇다면 왜 C#에서는 Value TypeReference Type을 구분했을까요?

제 생각에는  프로그래머가 직관적으로 코딩을 했을 때 문제없이 돌아가도록 하기 위해서 였을 것이라고 생각합니다.

 

int, char, float과 같이 메모리가 작은 변수들은 Value Type 특성으로 조작하는 상황이 많고

상대적으로 큰 메모리를 차지하게될 class 변수들은 Reference Type 특성으로 조작하는 상황이 많을 것이기 때문이죠

물론 큰 메모리를 차지할 수 있는 Value Typestruct가 있긴 하지만요.

 

위에서 설명한 대입 연산자는 어떻게 보면 값을 복사하는 것으로 볼 수 있습니다.

메모리가 작은 변수의 값을 복사하는 것은 비교적 빠르게 수행할 수 있지만

메모리가 큰 변수의 값을 복사하는 것은 상대적으로 오래 걸릴 것입니다.

 

상대적으로 메모리가 클 가능성이 높은 Reference Type 변수의 복사값 복사가 아닌 주소값(4바이트 or 8바이트) 복사를 기본으로 하도록 함으로써

일반적인 방식으로 대입 연산자를 사용했을 때 실질적으로는 작은 메모리를 복사한 것이지만 프로그래머는 전체 값이 복사된 것처럼 코딩할 수 있도록 하는 것이지요.

 

아무튼 이 외에도 Value TypeReference Type 변수는 많은 차이점이 존재합니다.

한 가지 예로 함수에서 paramter로 사용할 때 다르게 동작하는 것처럼 보입니다.

사실 이것도 알고보면 이번 포스팅에서 설명한 차이점으로 인해 발생하는 것입니다.

아쉽게도 이번 포스팅이 꽤 길어졌으므로 Value TypeReference Type을 각각 parameter로 사용할 때의 차이점은

다음 포스팅에서 알아보겠습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함