값 타입과 참조 타입

데이터 항목의 타입은 해당 데이터를 저장하는 데 필요한 메모리 크기(비트 수)와 타입에 포함된 데이터 멤버를 결정합니다. 또한 타입은 객체가 메모리 내 스택(Stack) 혹은 힙(Heap) 어디에 배치될지 정의합니다.

타입은 크게 두 가지로 나뉩니다: 값 타입(Value Types)참조 타입(Reference Types). 두 종류의 객체는 메모리 저장 방식이 완전히 다릅니다.

1. 값 타입은 단 하나의 메모리 구역만 사용하며, 이 공간에 실제 원본 데이터가 직접 저장됩니다.

값 타입 데이터는 스택 Stack에 직접 적재되고 Data 영역에 실제 값이 담깁니다. 예를 들어 int age = 12; 라고 작성하면 숫자 12 자체가 스택에 저장됩니다

2. 참조 타입은 두 개의 독립적인 메모리 영역을 요구합니다:

  • 첫 번째 영역에는 실제 객체 데이터가 저장되며, 이 공간은 언제나 힙에 할당됩니다;
  • 두 번째 영역은 참조 주소로, 힙에 있는 데이터의 위치를 가리킵니다. (스택에는 힙의 메모리 주소가 보관되는데, C 언어 시절에는 포인터라고 불렸습니다)
참조 타입의 본체 데이터는 힙에 저장되고 000001 같은 고유 주소가 부여되며, 스택에는 이 주소값인 참조 Reference가 기록됩니다

위 두 그림은 객체가 다른 객체의 데이터 멤버에 속하지 않은 독립적인 경우에만 적용됩니다.

주의점

참조 타입 객체의 본체 데이터 부분은 그림에서 보듯 항상 힙에 위치합니다.

하지만 값 타입 인스턴스나 참조 타입의 참조 주소는 코드 작성 방식에 따라 스택 또는 힙 어느 곳에나 저장될 수 있습니다.

예시:

Person이라는 참조 타입 클래스가 있다고 가정하고, 안에 값 타입 멤버 한 개와 참조 타입 멤버 한 개가 포함되어 있다고 봅시다.

많은 개발자들이 값 타입 멤버는 스택에, 참조 주소도 스택에, 객체 본체는 힙에 저장된다고 잘못 인지하는데 이 생각은 틀렸습니다.

한 가지 규칙을 기억하세요: 참조 타입 인스턴스의 모든 본체 데이터는 영원히 힙에 할당됩니다. 두 멤버 모두 해당 객체의 본체 데이터에 속하므로 멤버 자체가 값 타입이든 참조 타입이든 전부 힙 내에 함께 저장됩니다.

예제 코드:

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }
}   

class Phone
{
    public long number;
}Code language: JavaScript (javascript)

위 예제를 살펴보면 Person 클래스에는 기본 타입인 age, name 외에 사용자 정의 타입 Phone이 포함되어 있습니다. Phone 타입 자체에도 휴대폰 번호를 저장하는 long 기본 타입 변수가 존재합니다.

독자들은 Person 내부의 int age나 Phone 내부의 long number를 보고 값 타입이니 스택에 저장될 것이라고 오해하기 쉽지만 실제는 그렇지 않습니다. 이들이 내장 기본 타입이라도 사용자 정의 클래스의 멤버 변수로 선언되었다면 객체 인스턴스와 함께 힙에 저장되며 최종 메모리 배치는 다음과 같습니다.

그림에서 확인할 수 있듯 int age와 long number는 내장 값 타입이지만 사용자 정의 타입의 멤버이므로 힙에 저장됩니다. myPhone에는 참조 주소가 담겨있어 참조 타입의 참조 영역도 힙에 위치할 수 있다는 점을 알 수 있습니다.

핵심 사항: 사용자 정의 클래스의 모든 멤버 변수는 값 타입, 참조 타입 구분 없이 힙에 저장됩니다. 위 예시의 myPhone처럼 클래스 내 참조 타입 멤버가 있으면 해당 필드에는 힙에 존재하는 다른 객체의 주소가 기록됩니다.

만약 Phone 클래스 안에 또 다른 사용자 정의 타입이 선언된다면 Phone의 메모리 구역에 또 다른 객체를 가리키는 참조 주소가 추가로 저장됩니다.

프로그램은 Main 함수에서 실행을 시작하며 최상단 참조 객체의 주소는 스택에 저장됩니다. 그 뒤 여러 객체가 연쇄적으로 힙에 연결된 구조를 갖게 됩니다.

참조는 마치 연결 고리처럼 이어집니다

다른 예시

// 참조 타입 클래스
class MyType
{
    public int A;       // 값 타입 멤버, 힙에 할당된 MyType 인스턴스 내부에 내장
    public Person B;    // 참조 타입 멤버, 참조 포인터는 힙에 저장되고 본체 데이터는 별도 힙 공간에 할당
}

class Person
{
    public string Name;
}

void Test()
{
    // 스택에 myObj의 참조 포인터가 저장됨
    MyType myObj = new MyType(); 
    /*
    힙 배치 구조:
    MyType 객체 {
        A: 인라인으로 저장된 int 값
        B: Person을 가리키는 참조 포인터
    }
    별도 힙 공간: Person{ Name: ... }
    */

    // 지역 값 타입 변수, 스택에 직접 할당
    int localNum = 10;
}Code language: PHP (php)

위 규칙은 클래스의 멤버 변수에만 해당하며 클래스 내 메서드에는 적용되지 않습니다. 클래스 메서드 안에서 지역 변수나 참조 객체를 선언한 경우, 지역 값 타입 변수와 참조 객체의 주소는 여전히 스택에 저장됩니다. 아래 코드를 참고하십시오.

아래 예제를 다시 살펴보겠습니다:

using System.ComponentModel.DataAnnotations;

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        Person bill = new Person("Bill", 18);
        bill.Calculate();
    }

}

class Person
{
    private string name;

    private int age;

    private Phone myPhone;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
        this.myPhone = new Phone();
        this.myPhone.number = 12345;
    }

    public int Calculate()
    {
        int one = 12;
        int two = 23;
        Phone myNewPhone = new Phone();
        return one + two;
    }
}   

class Phone
{
    public long number;
}Code language: PHP (php)

이 코드에서는 Person 클래스에 Calculate 메서드를 추가했습니다. 메서드 내부에는 값 타입과 참조 타입 모두의 지역 변수가 선언되어 있고 Main 함수에서 호출합니다. 이 변수들은 Person의 멤버가 아닌 메서드 전용 지역 변수이므로 값 타입은 스택에, 참조 타입의 참조 주소 역시 스택에 저장됩니다.

bill.Calculate(); 구문을 실행할 때 메모리 상태는 다음 그림과 같습니다

그림의 파란색 영역이 실행 중인 메서드 내부 변수의 메모리 저장 위치입니다.

메서드 실행이 완료되면 해당 지역 변수들이 스택에서 꺼내져 메모리 구조가 아래와 같이 변합니다

Calculate 메서드 실행이 끝나고 제어가 Main으로 돌아오면 메서드 안에서 생성된 모든 지역 변수는 스택에서 제거되어 소멸합니다. 다만 myNewPhone의 본체 데이터(그림의 파란 부분)는 힙에 남아있습니다. 스택에서 사라지는 건 참조 주소일 뿐 실제 데이터는 즉시 삭제되지 않고 가비지 컬렉터 GC가 적절한 시점에 회수할 때까지 유지됩니다.

값 타입과 참조 타입

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다