[GO 언어 기초] 포인터를 알아보자! ❷ 인스턴스

이 글은 [Must Have Tucker의 Go 언어 프로그래밍(세종도서 선정작)]에서 발췌했습니다.
골든래빗 출판사

 

포인터는 메모리 주소를 값으로 갖는 타입입니다. 포인터를 이용하면 동일한 메모리 공간을 여러 변수가 가리킬 수 있습니다. 본 글에서는 포인터를 사용하는 방법과 인스턴스 개념을 알아봅시다. 총 2편으로 구성했습니다. 1편에서는 포인터의 정의와 사용법을 정리했었죠. 2편에서는 인스턴스를 알아봅시다.

 

인스턴스

인스턴스란 메모리에 할당된 데이터의 실체를 말합니다. 예를 들어 다음 코드는 Data 타입값을 저장할 수 있는 메모리 공간을 할당합니다.

 

 

이렇게 할당된 메모리 공간의 실체를 인스턴스라고 부릅니다.

Data 타입 포인터 변수를 선언하고 data 변수의 주소를 값으로 대입시켜보겠습니다.

 

var data Data
var p *Data = &data

 

Data 타입 포인터 변수 p를 선언하고 data의 주소를 대입했습니다. 이때 포인터 변수 p는 data를 가리킨다고 말합니다. 이때 p가 생성될 때 새로운 Data 인스턴스가 만들어진 게 아닙니다. 기존에 있던 data 인스턴스를 가리킨 겁니다. 즉 만들어진 총 Data 인스턴스 개수는 한 개입니다.

 

 

 

인스턴스를 별도로 생성하지 않고, 곧바로 인스턴스를 생성해 그 주소를 포인터 변수에 초깃값으로 대입하는 코드를 살펴보죠.

 

 

Data 인스턴스를 만들고 그 메모리 주소를 포인터 변수 p가 가리킵니다. 이번에도 인스턴스는 하나만 생성됩니다.

 

 

포인터 변수가 아무리 많아도 인스턴스가 추가로 생성되는 것은 아닙니다.

 

var p1 *Data = &Data{}
var p2 *Data = p1
var p3 *Data = p1

 

Data 인스턴스 하나를 만들고, 포인터 변수 p1, p2, p3가 가리킵니다. 가리키는 포인터 변수 개수는 인스턴스 개수와 무관합니다.

 

 

그럼 다음 코드에서 인스턴스는 몇 개일까요?

 

var data1 Data
var data2 Data = data1
var data3 Data = data1

 

data1, data2, data3 모두 인스턴스입니다. data1값이 data2, data3에 복사되어서 값만 같을 뿐입니다. 그래서 인스턴스 3개가 생성됩니다.

 

 

인스턴스는 데이터의 실체다

인스턴스는 메모리에 존재하는 데이터의 실체입니다. 인스턴스 개념을 잘 이해해야 앞으로 나올 메서드나 인터페이스 개념도 쉽게 이해할 수 있습니다. 포인터를 이용해서 인스턴스에 접근할 수 있습니다. 구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 얘기와 같습니다.

 

new() 내장 함수

앞서 포인터값을 별도의 변수를 선언하지 않고 초기화하는 방법을 봤습니다. new 내장 함수를 이용하면 더 간단히 표현할 수 있습니다.

 

p1 := &Data{} // 1 &를 사용하는 초기화
var p2 = new(Data) // 2 new()를 사용하는 초기화

 

new( ) 내장 함수는 인수로 타입을 받습니다. 타입을 메모리에 할당하고 기본값으로 채워 그 주소를 반환합니다. ❷ new를 이용해서 내부 필드값을 원하는 값으로 초기화할 수는 없습니다. 반면 ❶ 방식은 p1 := &Data{ 3, 4 }처럼 사용자 초기화가 가능합니다.

TIP &Data{} 과 new(Data) 방식 모두 다 자주 사용하는 방식이기 때문에 잘 알아두셔야 합니다.

 

인스턴스는 언제 사라지나

메모리는 무한한 자원이 아닙니다. 만약 메모리에 데이터가 할당만 되고 사라지지 않는다면 프로그램은 금세 메모리가 고갈되어 프로그램이 비정상 종료될 것입니다. 그래서 쓸모없는 데이터를 메모리에서 해제하는 기능이 필요합니다. Go 언어는 가비지 컬렉터Garbage Collector라는 메모리 청소부 기능을 제공합니다. 이 가비지 컬렉터가 일정 간격으로 메모리에서 쓸모없어진 데이터를 청소합니다(B.6절 ‘Go 언어 가비지 컬렉터’ 참조).

그럼 사용되는 데이터인지 아닌지 어떻게 알 수 있을까요? 간단하게 ‘아무도 찾지 않는 데이터는 쓸모없는 데이터이다’라고 볼 수 있습니다. 간단하게 예를 보겠습니다.

 

func TestFunc() {
  u := &User{}.     // 1 u 포인터 변수를 선언하고 인스턴스를 생성합니다.
  u.Age = 30
  fmt.Println(u)
}.                  // 2 내부 변수 u는 사라집니다. 더불어 인스턴스도 사라집니다.

 

❶ ****u 포인터 변수를 선언하고 인스턴스를 생성했습니다. 메모리에 User 데이터가 할당됐고 u 포인터 변수가 가리킵니다. 이때 이 인스턴스는 u 포인터 변수로 사용되는 인스턴스이기 때문에 지워지면 안 됩니다.

하지만 TestFunc()이 종료되면 함수 내부 변수 u는 사라져 User 인스턴스를 가리키는 포인터 변수가 없게 됩니다. 이제 User 인스턴스는 쓸모가 없게 됐습니다. 드디어 가비지 컬렉터가 나설 차례입니다. 다음번 청소를 할 때 쓸모 없어진 이 User 인스턴스를 지우게 됩니다.

가비지 컬렉터가 알아서 메모리를 청소해주니 편리합니다. 하지만 세상에 공짜는 없다고 가비지 컬렉터도 공짜가 아닙니다. 메모리는 굉장히 크기 때문에 이 메모리 영역을 모두 검사해서 쓸모없는 데이터를 지워주는 데 성능을 많이 씁니다. 가비지 컬렉터를 사용하면 메모리 관리에서 이득을 보지만 성능에서 손해가 발생하는 거죠. 정리하겠습니다. 다음 네 가지만 기억하세요.

  • 인스턴스는 메모리에 생성된 데이터의 실체입니다.
  • 포인터를 이용해서 인스턴스를 가리키게 할 수 있습니다.
  • 함수호출시포인터인수를통해서인스턴스를입력받고그값을변경할수있게됩니다.
  • 쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 지워줍니다.

 

스택 메모리와 힙 메모리 깊이보기

대부분 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용합니다. 이론상 스택 메모리 영역이 힙 메모리 영역보다 훨씬 효율적이기 때문에 스택 메모리 영역에서 메모리를 할당하는 게 더 좋지만, 스택 메모리는 함수 내부에서만 사용 가능한 영역입니다. 그래서 함수 외부로 공개되는 메모리 공간은 힙 메모리 영역에서 할당합니다. C/C++ 언어에서는 malloc( ) 함수를 직접 호출해서 힙 메모리 공간을 할당합니다. 자바에서는 클래스 타입을 힙에, 기본 타입을 스택에 할당합니다. Go 언어는 탈출 검사escape analysis를 해서 어느 메모리에 할당할 지를 결정합니다.

함수 외부로 공개되는 인스턴스의 경우 함수가 종료되어도 사라지지 않습니다. 예제 코드를 보겠습니다.

 

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func NewUser(name string, age int) *User {
	var u = User{name, age}
	return &u // 1 탈출 분석으로 u 메모리가 사라지지 않음
}

func main() {
	userPointer := NewUser("AAA", 23)

	fmt.Println(userPointer)
}

 

❶ NewUser() 함수에서 선언한 u 변수를 반환했습니다. 함수 내부에서 선언된 변수는 함수가 종료되면 사라집니다. 이 코드는 이미 사라진 메모리를 가리키는 댕글링(dangling) 오류가 발생해야 합니다. 그런데 프로그램이 멀쩡하게 잘 동작하네요?

Go 언어에서는 탈출 검사를 통해서 u 변수의 인스턴스가 함수 외부로 공개되는 것을 분석해내서 u를 스택 메모리가 아닌 힙 메모리에서 할당하게 됩니다. 즉 Go 언어는 어떤 타입이나 메모리 할당 함수에 의해서 스택 메모리를 사용할지 힙 메모리를 사용할지를 결정하는 게 아닙니다. 메모리 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 메모리에 할당할지 힙 메모리에 할당할지 결정합니다.

또 Go 언어에서 스택 메모리는 계속 증가되는 동적 메모리 풀입니다. 일정한 크기를 갖는 C/C++ 언어와 비교해 메모리 효율성이 높고, 재귀 호출 때문에 스택 메모리가 고갈되는 문제도 발생하지 않습니다.

 

핵심 요약

  1. 포인터는 메모리 주소를 값으로 갖는 타입입니다.
  2. &를이용해서데이터의메모리주소를알수있습니다.
  3. 포인터를이용하면메모리주솟값으로메모리를조작할수있습니다.
  4. 인스턴스는 메모리에 있는 데이터 실체이고 포인터로 조작할 수 있습니다.
  5. Go는 탈출 분석을 통해 인스턴스를 스택 메모리에 할당할지 힙 메모리에 할당할지 결정합니다.

WRITER

공봉식

13년 차 게임 서버 프로그래머로 다양한 장르의 온라인 게임을 개발했습니다. 넥슨과 네오위즈를 거쳐서 현재는 EA 캐나다에서 근무 중입니다. 「Tucker Programming」 유튜브 채널을 운영합니다.

1 Comment

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내