ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • golang: 포인터(Pointer)란?
    Back-End/Golang 2024. 3. 30. 12:34
    반응형

    Go 언어에서 포인터는 변수의 메모리 주소를 저장합니다. 포인터를 사용하면, 변수의 메모리 주소를 통해 직접 변수의 값을 읽거나 수정할 수 있습니다. 이는 데이터를 효율적으로 처리할 수 있게 하며, 특히 큰 데이터 구조를 다룰 때나 함수 간에 데이터를 전달할 때 유용합니다.

    포인터의 기본 개념:

    • 포인터 선언: * 키워드를 사용해 포인터 타입을 선언합니다. 예를 들어, var p *int는 정수형 변수의 메모리 주소를 저장할 수 있는 포인터 p를 선언합니다.
    • 주소 연산자 &: 변수 앞에 &를 붙여 해당 변수의 메모리 주소를 얻습니다.
    • 역참조 연산자 *: 포인터 앞에 *를 붙여 포인터가 가리키는 메모리 주소의 실제 값을 얻습니다.

    코드 예시: 포인터 사용하기

    아래 코드는 포인터를 사용하여 변수의 값이 함수에 의해 어떻게 변경될 수 있는지 보여줍니다.

    package main
    
    import "fmt"
    
    // 정수형 변수의 값을 2배로 증가시키는 함수
    func doubleValue(number *int) {
    	*number *= 2 // 역참조를 사용해 포인터가 가리키는 변수의 값을 변경
    }
    
    func main() {
    	value := 10
    	fmt.Println("Original value:", value) // 변경 전 값 출력
    
    	doubleValue(&value) // value 변수의 주소를 doubleValue 함수에 전달
    	fmt.Println("Doubled value:", value)  // 변경 후 값 출력
    }

    이 예제에서 doubleValue 함수는 포인터 *int 타입의 매개변수를 받습니다. 이 함수 내에서, 매개변수로 받은 포인터를 역참조하여 실제 값을 변경합니다. main 함수에서는 value 변수의 주소를 &value를 통해 얻어 doubleValue 함수에 전달합니다. 결과적으로 value의 값이 함수 내에서 변경됩니다.
     
     

    코드 예시: 포인터 리시버를 사용한 메소드

    Go에서 메소드와 함께 포인터를 사용하는 것은 구조체의 메소드를 정의할 때 특히 유용합니다. 포인터 리시버(pointer receiver)를 사용하면 메소드 내에서 호출된 구조체의 필드 값을 변경할 수 있습니다. 이를 통해, 메소드 호출 시 구조체의 복사본을 생성하지 않고, 원본 구조체 자체를 수정할 수 있어 메모리 사용을 최적화하고 성능을 향상시킬 수 있습니다.
    아래의 예시에서는 직원(Employee) 구조체에 대해 두 가지 메소드를 정의합니다. 하나는 포인터 리시버를 사용하여 직원의 연봉을 변경하는 메소드이고, 다른 하나는 값 리시버를 사용하여 직원 정보를 출력하는 메소드입니다.

    package main
    
    import "fmt"
    
    // Employee 구조체 정의
    type Employee struct {
    	Name   string
    	Salary int
    }
    
    // SetSalary 메소드는 포인터 리시버를 사용하여 Employee의 Salary 필드 값을 변경합니다.
    func (e *Employee) SetSalary(newSalary int) {
    	e.Salary = newSalary // 포인터를 통해 Salary 필드 직접 변경
    }
    
    // DisplayInfo 메소드는 값 리시버를 사용하여 Employee의 정보를 출력합니다.
    func (e Employee) DisplayInfo() {
    	fmt.Printf("Name: %s, Salary: %d\n", e.Name, e.Salary)
    }
    
    func main() {
    	// Employee 구조체 인스턴스 생성
    	emp := Employee{Name: "John Doe", Salary: 50000}
    
    	// 값 변경 전 직원 정보 출력
    	emp.DisplayInfo()
    
    	// SetSalary 메소드를 사용하여 Salary 변경
    	emp.SetSalary(60000)
    
    	// 값 변경 후 직원 정보 출력
    	emp.DisplayInfo()
    }

    이 예제에서, SetSalary 메소드는 포인터 리시버 (e *Employee)를 사용하여 구조체의 Salary 필드 값을 변경합니다. 이렇게 포인터 리시버를 사용함으로써, SetSalary 메소드는 원본 Employee 구조체 인스턴스의 Salary 필드를 직접 수정할 수 있습니다. 반면, DisplayInfo 메소드는 값 리시버 (e Employee)를 사용하므로, 메소드 내에서 Employee 구조체의 필드 값을 변경해도 원본 구조체에는 영향을 주지 않습니다.
    포인터 리시버와 값 리시버의 선택은 해당 메소드가 구조체의 상태를 변경할 필요가 있는지 여부, 그리고 메소드 호출 시 구조체의 복사본 생성을 피하고자 하는지 여부에 따라 결정됩니다. 일반적으로, 구조체의 상태를 변경해야 하는 경우 포인터 리시버를 사용하고, 구조체의 상태를 변경할 필요가 없는 읽기 전용 메소드의 경우 값 리시버를 사용합니다.
     

    코드 예시: 구조체 필드로 포인터 사용하기

    구조체 내부에서 포인터를 사용하는 경우는 주로 두 가지 상황에서 발생합니다:

    1. 구조체 필드가 큰 데이터를 포함하고 있어서 복사를 피하고자 할 때: 큰 데이터 구조체의 복사는 성능 저하를 가져올 수 있습니다. 포인터를 사용하면 데이터의 복사본을 만들지 않고 원본 데이터에 대한 참조만을 저장하므로, 메모리 사용량을 줄이고 성능을 개선할 수 있습니다.
    2. 구조체 필드의 값이 변경될 수 있고, 여러 곳에서 그 변경사항을 반영하고자 할 때: 포인터를 사용하면 여러 곳에서 동일한 데이터 구조에 대한 참조를 공유할 수 있기 때문에, 한 곳에서 데이터가 변경되면 그 변경사항이 참조하는 모든 곳에 반영됩니다.

    아래 예제에서는 두 개의 구조체 Person과 Job을 정의하고, Person 구조체 내에서 Job의 포인터를 필드로 사용합니다. 이를 통해, Person 구조체 인스턴스가 자신의 Job 필드를 변경할 때, 해당 Job 인스턴스의 변경사항이 모든 Person 인스턴스에 반영될 수 있도록 합니다.

    package main
    
    import "fmt"
    
    // Job 구조체 정의
    type Job struct {
    	Title  string
    	Salary int
    }
    
    // Person 구조체 정의
    type Person struct {
    	Name string
    	// Job 구조체에 대한 포인터 사용
    	Job *Job
    }
    
    func main() {
    	// Job 인스턴스 생성
    	developerJob := &Job{Title: "Software Developer", Salary: 80000}
    
    	// 두 Person 인스턴스 생성, 같은 Job 인스턴스 참조
    	john := Person{Name: "John", Job: developerJob}
    	jane := Person{Name: "Jane", Job: developerJob}
    
    	fmt.Printf("%s's job: %s\n", john.Name, john.Job.Title)
    	fmt.Printf("%s's job: %s\n", jane.Name, jane.Job.Title)
    
    	// 한 Person의 Job 필드 변경
    	john.Job.Salary = 90000
    
    	// 변경사항이 다른 Person에도 반영됨
    	fmt.Printf("After raise, %s's salary: %d\n", john.Name, john.Job.Salary)
    	fmt.Printf("After raise, %s's salary: %d\n", jane.Name, jane.Job.Salary)
    }

    이 예제에서, developerJob은 Job 타입의 인스턴스로, 두 Person 인스턴스인 john과 jane은 이 developerJob 인스턴스의 포인터를 공유합니다. 따라서, john의 Job 필드를 통해 Salary를 변경하면, jane이 참조하는 Salary도 같이 변경됩니다. 이는 두 Person 인스턴스가 동일한 Job 인스턴스를 참조하기 때문입니다.
    구조체 필드에 포인터를 사용하는 방식은 데이터의 일관성을 유지하고, 프로그램 전체에서 데이터를 효율적으로 관리할 수 있게 해줍니다. 하지만, 포인터를 사용할 때는 메모리 누수나 참조 오류 같은 잠재적 문제에 주의해야 합니다.
     

    포인터의 장점:

    • 메모리를 직접 조작하여 효율적으로 데이터를 처리할 수 있습니다.
    • 함수를 통해 큰 데이터 구조를 전달할 때, 데이터의 복사본을 만들지 않고 원본 데이터에 접근할 수 있어 성능을 개선할 수 있습니다.
    • 여러 함수에서 데이터의 원본을 공유하고 변경할 수 있어, 프로그램의 특정 부분에서 일어난 변경사항을 다른 부분에서도 반영할 수 있습니다.

    포인터는 강력한 기능을 제공하지만, 사용 시 주의가 필요합니다. 포인터를 잘못 사용하면 예상치 못한 메모리 영역을 변경할 위험이 있으므로, 프로그램의 안정성과 유지보수성을 위해 신중하게 사용해야 합니다. Go 언어는 다른 언어에 비해 포인터 사용을 보다 안전하게 만들어주는 기능들을 제공하지만, 여전히 포인터를 정확하게 이해하고 사용하는 것이 중요합니다.

    반응형

    'Back-End > Golang' 카테고리의 다른 글

    golang: for-select 패턴  (0) 2024.04.19
    golang: 동시성에서 제한(Confinement)이란?  (0) 2024.04.19
    golang: 컨텍스트(Context)란?  (0) 2024.03.30
    golang: 채널(Channel)이란?  (0) 2024.03.30
    golang: 고루틴(Goroutines)이란?  (0) 2024.03.30
Designed by Tistory.